28 | {[...Array(8)].map((_, r) => {
29 | row = orientation === 'black' ? row + 1 : row - 1;
30 |
31 | return (
32 |
33 | {[...Array(8)].map((_, col) => {
34 | let square =
35 | orientation === 'black'
36 | ? alpha[7 - col] + (row - 1)
37 | : alpha[col] + (row + 1);
38 |
39 | if (col !== 0) {
40 | squareColor = squareColor === 'black' ? 'white' : 'black';
41 | }
42 | return children({ square, squareColor, col, row, alpha });
43 | })}
44 |
45 | );
46 | })}
47 |
48 | );
49 | }
50 | }
51 |
52 | export default Row;
53 |
54 | const boardStyles = width => ({
55 | width,
56 | height: width,
57 | cursor: 'default'
58 | });
59 |
60 | const rowStyles = width => ({
61 | display: 'flex',
62 | flexWrap: 'nowrap',
63 | width
64 | });
65 |
--------------------------------------------------------------------------------
/src/Chessboard/__tests__/Notation.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-testing-library';
3 | import 'jest-dom/extend-expect';
4 |
5 | import Chessboard from '../index';
6 |
7 | test('renders notation when notation prop is true', () => {
8 | const { getByTestId } = render(",
35 | "license": "MIT",
36 | "bugs": {
37 | "url": "https://github.com/willb335/chessboardjsx/issues"
38 | },
39 | "homepage": "https://github.com/willb335/chessboardjsx#readme",
40 | "dependencies": {
41 | "deep-diff": "1.0.1",
42 | "lodash.isequal": "4.5.0",
43 | "react-dnd": "2.6.0",
44 | "react-dnd-html5-backend": "4.0.5",
45 | "react-dnd-multi-backend": "3.1.2"
46 | },
47 | "devDependencies": {
48 | "@babel/core": "7.0.0-beta.51",
49 | "@babel/plugin-proposal-object-rest-spread": "7.0.0-beta.51",
50 | "@babel/preset-env": "7.0.0-beta.51",
51 | "@babel/preset-react": "7.0.0-beta.51",
52 | "@babel/register": "7.0.0-beta.51",
53 | "@commitlint/cli": "6.2.0",
54 | "@commitlint/config-conventional": "6.1.3",
55 | "@types/chess.js": "^0.10.0",
56 | "all-contributors-cli": "5.2.0",
57 | "babel-cli": "6.26.0",
58 | "babel-core": "7.0.0-bridge.0",
59 | "babel-eslint": "8.2.3",
60 | "babel-jest": "23.0.1",
61 | "babel-loader": "8.0.0-beta.3",
62 | "babel-plugin-dynamic-import-node": "1.2.0",
63 | "babel-plugin-lodash": "3.3.4",
64 | "babel-plugin-transform-class-properties": "6.24.1",
65 | "babel-plugin-transform-es2015-modules-commonjs": "6.26.2",
66 | "babel-polyfill": "6.26.0",
67 | "chess.js": "0.10.2",
68 | "clean-webpack-plugin": "0.1.19",
69 | "codecov": "3.0.2",
70 | "commitizen": "2.10.1",
71 | "css-loader": "0.28.11",
72 | "cz-conventional-changelog": "2.1.0",
73 | "eslint": "4.19.1",
74 | "eslint-plugin-prettier": "^2.6.0",
75 | "eslint-plugin-react": "^7.8.2",
76 | "file-loader": "1.1.11",
77 | "ghooks": "2.0.4",
78 | "html-webpack-plugin": "3.2.0",
79 | "identity-obj-proxy": "3.0.0",
80 | "jest": "22.4.3",
81 | "jest-dom": "1.3.0",
82 | "kcd-scripts": "0.44.0",
83 | "lodash-webpack-plugin": "0.11.5",
84 | "mini-css-extract-plugin": "0.4.0",
85 | "npm-run-all": "4.1.3",
86 | "prettier": "1.12.1",
87 | "prop-types": "15.6.1",
88 | "react": "^16.4.0",
89 | "react-docgen": "2.20.1",
90 | "react-dom": "^16.4.0",
91 | "react-testing-library": "3.1.6",
92 | "roughjs": "2.2.3",
93 | "semantic-release": "^17.2.3",
94 | "style-loader": "0.21.0",
95 | "travis-deploy-once": "^5.0.0",
96 | "webpack": "4.12.0",
97 | "webpack-bundle-analyzer": "2.13.1",
98 | "webpack-cli": "3.0.3",
99 | "webpack-dev-server": "3.1.4",
100 | "webpack-merge": "4.1.2"
101 | },
102 | "peerDependencies": {
103 | "react": ">= 16.3.2",
104 | "react-dom": ">= 16.3.2"
105 | },
106 | "config": {
107 | "commitizen": {
108 | "path": "cz-conventional-changelog"
109 | },
110 | "ghooks": {
111 | "pre-commit": "npm run validate",
112 | "commit-msg": "npm run commitmsg"
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/Chessboard/Notation.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent, Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const getRow = (orientation, row) =>
5 | orientation === 'white' ? row + 1 : row - 1;
6 |
7 | const getColumn = (orientation, alpha, col) =>
8 | orientation === 'black' ? alpha[7 - col] : alpha[col];
9 |
10 | class Notation extends PureComponent {
11 | static propTypes = {
12 | row: PropTypes.number,
13 | col: PropTypes.number,
14 | alpha: PropTypes.array,
15 | orientation: PropTypes.string,
16 | width: PropTypes.number,
17 | lightSquareStyle: PropTypes.object,
18 | darkSquareStyle: PropTypes.object
19 | };
20 |
21 | render() {
22 | const {
23 | row,
24 | col,
25 | orientation,
26 | lightSquareStyle,
27 | darkSquareStyle
28 | } = this.props;
29 |
30 | const whiteColor = lightSquareStyle.backgroundColor;
31 | const blackColor = darkSquareStyle.backgroundColor;
32 |
33 | const isRow = col === 0;
34 | const isColumn =
35 | (orientation === 'white' && row === 0) ||
36 | (orientation === 'black' && row === 9);
37 | const isBottomLeftSquare = isRow && isColumn;
38 |
39 | if (isBottomLeftSquare) {
40 | return renderBottomLeft(this.props, { whiteColor });
41 | }
42 |
43 | if (isColumn) {
44 | return renderLetters(this.props, {
45 | whiteColor,
46 | blackColor
47 | });
48 | }
49 |
50 | if (isRow) {
51 | return renderNumbers(this.props, {
52 | whiteColor,
53 | blackColor,
54 | isRow,
55 | isBottomLeftSquare
56 | });
57 | }
58 |
59 | return null;
60 | }
61 | }
62 |
63 | export default Notation;
64 |
65 | /* eslint react/prop-types: 0 */
66 | function renderBottomLeft(
67 | { orientation, row, width, alpha, col },
68 | { whiteColor }
69 | ) {
70 | return (
71 |
72 |
80 | {getRow(orientation, row)}
81 |
82 |
90 | {getColumn(orientation, alpha, col)}
91 |
92 |
93 | );
94 | }
95 |
96 | function renderLetters(
97 | { orientation, width, alpha, col },
98 | { whiteColor, blackColor }
99 | ) {
100 | return (
101 |
109 | {getColumn(orientation, alpha, col)}
110 |
111 | );
112 | }
113 |
114 | function renderNumbers(
115 | { orientation, row, width },
116 | { whiteColor, blackColor, isRow, isBottomLeftSquare }
117 | ) {
118 | return (
119 |
134 | {getRow(orientation, row)}
135 |
136 | );
137 | }
138 |
139 | const columnStyle = ({ col, width, blackColor, whiteColor }) => ({
140 | fontSize: width / 48,
141 | color: col % 2 !== 0 ? blackColor : whiteColor
142 | });
143 |
144 | const rowStyle = ({
145 | row,
146 | width,
147 | blackColor,
148 | whiteColor,
149 | orientation,
150 | isBottomLeftSquare,
151 | isRow
152 | }) => {
153 | return {
154 | fontSize: width / 48,
155 | color:
156 | orientation === 'black'
157 | ? isRow && !isBottomLeftSquare && row % 2 === 0
158 | ? blackColor
159 | : whiteColor
160 | : isRow && !isBottomLeftSquare && row % 2 !== 0
161 | ? blackColor
162 | : whiteColor
163 | };
164 | };
165 |
166 | const alphaStyle = width => ({
167 | alignSelf: 'flex-end',
168 | paddingLeft: width / 8 - width / 48
169 | });
170 |
171 | const numericStyle = width => ({
172 | alignSelf: 'flex-start',
173 | paddingRight: width / 8 - width / 48
174 | });
175 |
176 | const notationStyle = {
177 | fontFamily: 'Helvetica Neue',
178 | zIndex: 3,
179 | position: 'absolute'
180 | };
181 |
--------------------------------------------------------------------------------
/src/Chessboard/Square.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { DropTarget } from 'react-dnd';
4 |
5 | import { ItemTypes } from './helpers';
6 |
7 | class Square extends Component {
8 | static propTypes = {
9 | connectDropTarget: PropTypes.func,
10 | width: PropTypes.number,
11 | squareColor: PropTypes.oneOf(['white', 'black']),
12 | children: PropTypes.oneOfType([PropTypes.array, PropTypes.node]),
13 | isOver: PropTypes.bool,
14 | square: PropTypes.string,
15 | setSquareCoordinates: PropTypes.func,
16 | lightSquareStyle: PropTypes.object,
17 | darkSquareStyle: PropTypes.object,
18 | roughSquare: PropTypes.func,
19 | onMouseOverSquare: PropTypes.func,
20 | onMouseOutSquare: PropTypes.func,
21 | dropSquareStyle: PropTypes.object,
22 | screenWidth: PropTypes.number,
23 | screenHeight: PropTypes.number,
24 | squareStyles: PropTypes.object,
25 | onDragOverSquare: PropTypes.func,
26 | onSquareClick: PropTypes.func,
27 | wasSquareClicked: PropTypes.func,
28 | onSquareRightClick: PropTypes.func
29 | };
30 |
31 | componentDidMount() {
32 | const { square, setSquareCoordinates, width, roughSquare } = this.props;
33 | roughSquare({ squareElement: this.roughSquareSvg, squareWidth: width / 8 });
34 |
35 | const { x, y } = this[square].getBoundingClientRect();
36 | setSquareCoordinates(x, y, square);
37 | }
38 |
39 | componentDidUpdate(prevProps) {
40 | const {
41 | screenWidth,
42 | screenHeight,
43 | square,
44 | setSquareCoordinates
45 | } = this.props;
46 |
47 | const didScreenSizeChange =
48 | prevProps.screenWidth !== screenWidth ||
49 | prevProps.screenHeight !== screenHeight;
50 |
51 | if (didScreenSizeChange) {
52 | const { x, y } = this[square].getBoundingClientRect();
53 | setSquareCoordinates(x, y, square);
54 | }
55 | }
56 |
57 | onClick = () => {
58 | this.props.wasSquareClicked(true);
59 | this.props.onSquareClick(this.props.square);
60 | };
61 |
62 | render() {
63 | const {
64 | connectDropTarget,
65 | width,
66 | squareColor,
67 | children,
68 | square,
69 | roughSquare,
70 | onMouseOverSquare,
71 | onMouseOutSquare,
72 | squareStyles,
73 | onDragOverSquare,
74 | onSquareRightClick
75 | } = this.props;
76 |
77 | return connectDropTarget(
78 | (this[square] = ref)}
82 | style={defaultSquareStyle(this.props)}
83 | onMouseOver={() => onMouseOverSquare(square)}
84 | onMouseOut={() => onMouseOutSquare(square)}
85 | onDragEnter={() => onDragOverSquare(square)}
86 | onClick={() => this.onClick()}
87 | onContextMenu={e => {
88 | e.preventDefault();
89 | onSquareRightClick(square);
90 | }}
91 | >
92 |
99 | {roughSquare.length ? (
100 |
101 | {children}
102 | (this.roughSquareSvg = ref)}
109 | />
110 |
111 | ) : (
112 | children
113 | )}
114 |
115 |
116 | );
117 | }
118 | }
119 |
120 | const squareTarget = {
121 | drop(props, monitor) {
122 | return {
123 | target: props.square,
124 | board: props.id,
125 | piece: monitor.getItem().piece,
126 | source: monitor.getItem().source
127 | };
128 | }
129 | };
130 |
131 | function collect(connect, monitor) {
132 | return {
133 | connectDropTarget: connect.dropTarget(),
134 | isOver: monitor.isOver()
135 | };
136 | }
137 |
138 | export default DropTarget(ItemTypes.PIECE, squareTarget, collect)(Square);
139 |
140 | const defaultSquareStyle = props => {
141 | const {
142 | width,
143 | squareColor,
144 | isOver,
145 | darkSquareStyle,
146 | lightSquareStyle,
147 | dropSquareStyle
148 | } = props;
149 |
150 | return {
151 | ...{
152 | ...size(width),
153 | ...center,
154 | ...(squareColor === 'black' ? darkSquareStyle : lightSquareStyle),
155 | ...(isOver && dropSquareStyle)
156 | }
157 | };
158 | };
159 |
160 | const center = {
161 | display: 'flex',
162 | justifyContent: 'center'
163 | };
164 |
165 | const size = width => ({
166 | width: width / 8,
167 | height: width / 8
168 | });
169 |
--------------------------------------------------------------------------------
/src/Chessboard/helpers.js:
--------------------------------------------------------------------------------
1 | import diff from 'deep-diff';
2 |
3 | export const ItemTypes = { PIECE: 'piece' };
4 | export const COLUMNS = 'abcdefgh'.split('');
5 |
6 | export const constructPositionAttributes = (currentPosition, position) => {
7 | const difference = diff(currentPosition, position);
8 | const squaresAffected = difference.length;
9 | const sourceSquare =
10 | difference && difference[1] && difference && difference[1].kind === 'D'
11 | ? difference[1].path && difference[1].path[0]
12 | : difference[0].path && difference[0].path[0];
13 | const targetSquare =
14 | difference && difference[1] && difference && difference[1].kind === 'D'
15 | ? difference[0] && difference[0].path[0]
16 | : difference[1] && difference[1].path[0];
17 | const sourcePiece =
18 | difference && difference[1] && difference && difference[1].kind === 'D'
19 | ? difference[1] && difference[1].lhs
20 | : difference[1] && difference[1].rhs;
21 | return { sourceSquare, targetSquare, sourcePiece, squaresAffected };
22 | };
23 |
24 | function isString(s) {
25 | return typeof s === 'string';
26 | }
27 |
28 | export function fenToObj(fen) {
29 | if (!validFen(fen)) return false;
30 | // cut off any move, castling, etc info from the end
31 | // we're only interested in position information
32 | fen = fen.replace(/ .+$/, '');
33 |
34 | let rows = fen.split('/');
35 | let position = {};
36 |
37 | let currentRow = 8;
38 | for (let i = 0; i < 8; i++) {
39 | let row = rows[i].split('');
40 | let colIdx = 0;
41 |
42 | // loop through each character in the FEN section
43 | for (let j = 0; j < row.length; j++) {
44 | // number / empty squares
45 | if (row[j].search(/[1-8]/) !== -1) {
46 | let numEmptySquares = parseInt(row[j], 10);
47 | colIdx = colIdx + numEmptySquares;
48 | } else {
49 | // piece
50 | let square = COLUMNS[colIdx] + currentRow;
51 | position[square] = fenToPieceCode(row[j]);
52 | colIdx = colIdx + 1;
53 | }
54 | }
55 |
56 | currentRow = currentRow - 1;
57 | }
58 |
59 | return position;
60 | }
61 |
62 | function expandFenEmptySquares(fen) {
63 | return fen
64 | .replace(/8/g, '11111111')
65 | .replace(/7/g, '1111111')
66 | .replace(/6/g, '111111')
67 | .replace(/5/g, '11111')
68 | .replace(/4/g, '1111')
69 | .replace(/3/g, '111')
70 | .replace(/2/g, '11');
71 | }
72 |
73 | export function validFen(fen) {
74 | if (!isString(fen)) return false;
75 |
76 | // cut off any move, castling, etc info from the end
77 | // we're only interested in position information
78 | fen = fen.replace(/ .+$/, '');
79 |
80 | // expand the empty square numbers to just 1s
81 | fen = expandFenEmptySquares(fen);
82 |
83 | // FEN should be 8 sections separated by slashes
84 | let chunks = fen.split('/');
85 | if (chunks.length !== 8) return false;
86 |
87 | // check each section
88 | for (let i = 0; i < 8; i++) {
89 | if (chunks[i].length !== 8 || chunks[i].search(/[^kqrnbpKQRNBP1]/) !== -1) {
90 | return false;
91 | }
92 | }
93 |
94 | return true;
95 | }
96 |
97 | // convert FEN piece code to bP, wK, etc
98 | function fenToPieceCode(piece) {
99 | // black piece
100 | if (piece.toLowerCase() === piece) {
101 | return 'b' + piece.toUpperCase();
102 | }
103 |
104 | // white piece
105 | return 'w' + piece.toUpperCase();
106 | }
107 |
108 | function validSquare(square) {
109 | return isString(square) && square.search(/^[a-h][1-8]$/) !== -1;
110 | }
111 |
112 | function validPieceCode(code) {
113 | return isString(code) && code.search(/^[bw][KQRNBP]$/) !== -1;
114 | }
115 |
116 | export function validPositionObject(pos) {
117 | if (pos === null || typeof pos !== 'object') return false;
118 |
119 | for (let i in pos) {
120 | if (!pos.hasOwnProperty(i)) continue;
121 |
122 | if (!validSquare(i) || !validPieceCode(pos[i])) {
123 | return false;
124 | }
125 | }
126 | return true;
127 | }
128 |
129 | function squeezeFenEmptySquares(fen) {
130 | return fen
131 | .replace(/11111111/g, '8')
132 | .replace(/1111111/g, '7')
133 | .replace(/111111/g, '6')
134 | .replace(/11111/g, '5')
135 | .replace(/1111/g, '4')
136 | .replace(/111/g, '3')
137 | .replace(/11/g, '2');
138 | }
139 |
140 | // convert bP, wK, etc code to FEN structure
141 | function pieceCodeToFen(piece) {
142 | let pieceCodeLetters = piece.split('');
143 |
144 | // white piece
145 | if (pieceCodeLetters[0] === 'w') {
146 | return pieceCodeLetters[1].toUpperCase();
147 | }
148 |
149 | // black piece
150 | return pieceCodeLetters[1].toLowerCase();
151 | }
152 |
153 | // position object to FEN string
154 | // returns false if the obj is not a valid position object
155 | export function objToFen(obj) {
156 | if (!validPositionObject(obj)) return false;
157 |
158 | let fen = '';
159 |
160 | let currentRow = 8;
161 | for (let i = 0; i < 8; i++) {
162 | for (let j = 0; j < 8; j++) {
163 | let square = COLUMNS[j] + currentRow;
164 |
165 | // piece exists
166 | if (obj.hasOwnProperty(square)) {
167 | fen = fen + pieceCodeToFen(obj[square]);
168 | } else {
169 | // empty space
170 | fen = fen + '1';
171 | }
172 | }
173 |
174 | if (i !== 7) {
175 | fen = fen + '/';
176 | }
177 |
178 | currentRow = currentRow - 1;
179 | }
180 |
181 | // squeeze the empty numbers together
182 | fen = squeezeFenEmptySquares(fen);
183 |
184 | return fen;
185 | }
186 |
--------------------------------------------------------------------------------
/src/Chessboard/Board.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import Piece from './Piece';
4 | import Square from './Square';
5 | import Notation from './Notation';
6 | import Chessboard from './index';
7 | import PhantomPiece from './PhantomPiece';
8 | import Row from './Row';
9 |
10 | class Board extends Component {
11 | setSquareCoordinates = (x, y, square) =>
12 | this.setState({ [square]: { x, y } });
13 |
14 | getSquareCoordinates = (sourceSquare, targetSquare) => ({
15 | sourceSquare: this.state[sourceSquare],
16 | targetSquare: this.state[targetSquare]
17 | });
18 |
19 | showPhantom = ({ square, targetSquare, phantomPiece }) => {
20 | const isActivePiece = (square, targetSquare) =>
21 | targetSquare && targetSquare === square;
22 |
23 | return (
24 | phantomPiece &&
25 | phantomPiece[targetSquare] &&
26 | isActivePiece(square, targetSquare)
27 | );
28 | };
29 |
30 | hasPiece = (currentPosition, square) =>
31 | currentPosition &&
32 | Object.keys(currentPosition) &&
33 | Object.keys(currentPosition).includes(square);
34 |
35 | render() {
36 | return (
37 |
38 | {context => {
39 | return (
40 |
46 | {({ square, squareColor, col, row, alpha }) => {
47 | return (
48 |
69 | {this.hasPiece(context.currentPosition, square) ? (
70 |
93 | ) : null}
94 |
95 | {this.showPhantom({
96 | square,
97 | targetSquare: context.targetSquare,
98 | phantomPiece: context.phantomPiece
99 | }) && (
100 |
109 | )}
110 |
111 | {context.showNotation && (
112 |
121 | )}
122 |
123 | );
124 | }}
125 |
126 | );
127 | }}
128 |
129 | );
130 | }
131 | }
132 |
133 | export default Board;
134 |
--------------------------------------------------------------------------------
/src/integrations/WithMoveValidation.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'; // eslint-disable-line no-unused-vars
2 | import PropTypes from 'prop-types';
3 | import Chess from 'chess.js';
4 |
5 | import Chessboard from '../Chessboard';
6 |
7 | class HumanVsHuman extends Component {
8 | static propTypes = { children: PropTypes.func };
9 |
10 | state = {
11 | fen: 'start',
12 | // square styles for active drop squares
13 | dropSquareStyle: {},
14 | // custom square styles
15 | squareStyles: {},
16 | // square with the currently clicked piece
17 | pieceSquare: '',
18 | // currently clicked square
19 | square: '',
20 | history: []
21 | };
22 |
23 | componentDidMount() {
24 | this.game = new Chess();
25 | }
26 |
27 | // keep clicked square style and remove hint squares
28 | removeHighlightSquare = () => {
29 | this.setState(({ pieceSquare, history }) => ({
30 | squareStyles: squareStyling({ pieceSquare, history })
31 | }));
32 | };
33 |
34 | // show possible moves
35 | highlightSquare = (sourceSquare, squaresToHighlight) => {
36 | const highlightStyles = [sourceSquare, ...squaresToHighlight].reduce(
37 | (a, c) => {
38 | return {
39 | ...a,
40 | ...{
41 | [c]: {
42 | background:
43 | 'radial-gradient(circle, #fffc00 36%, transparent 40%)',
44 | borderRadius: '50%'
45 | }
46 | },
47 | ...squareStyling({
48 | history: this.state.history,
49 | pieceSquare: this.state.pieceSquare
50 | })
51 | };
52 | },
53 | {}
54 | );
55 |
56 | this.setState(({ squareStyles }) => ({
57 | squareStyles: { ...squareStyles, ...highlightStyles }
58 | }));
59 | };
60 |
61 | onDrop = ({ sourceSquare, targetSquare }) => {
62 | // see if the move is legal
63 | let move = this.game.move({
64 | from: sourceSquare,
65 | to: targetSquare,
66 | promotion: 'q' // always promote to a queen for example simplicity
67 | });
68 |
69 | // illegal move
70 | if (move === null) return;
71 | this.setState(({ history, pieceSquare }) => ({
72 | fen: this.game.fen(),
73 | history: this.game.history({ verbose: true }),
74 | squareStyles: squareStyling({ pieceSquare, history })
75 | }));
76 | };
77 |
78 | onMouseOverSquare = square => {
79 | // get list of possible moves for this square
80 | let moves = this.game.moves({
81 | square: square,
82 | verbose: true
83 | });
84 |
85 | // exit if there are no moves available for this square
86 | if (moves.length === 0) return;
87 |
88 | let squaresToHighlight = [];
89 | for (var i = 0; i < moves.length; i++) {
90 | squaresToHighlight.push(moves[i].to);
91 | }
92 |
93 | this.highlightSquare(square, squaresToHighlight);
94 | };
95 |
96 | onMouseOutSquare = square => this.removeHighlightSquare(square);
97 |
98 | // central squares get diff dropSquareStyles
99 | onDragOverSquare = square => {
100 | this.setState({
101 | dropSquareStyle:
102 | square === 'e4' || square === 'd4' || square === 'e5' || square === 'd5'
103 | ? { backgroundColor: 'cornFlowerBlue' }
104 | : { boxShadow: 'inset 0 0 1px 4px rgb(255, 255, 0)' }
105 | });
106 | };
107 |
108 | onSquareClick = square => {
109 | this.setState(({ history }) => ({
110 | squareStyles: squareStyling({ pieceSquare: square, history }),
111 | pieceSquare: square
112 | }));
113 |
114 | let move = this.game.move({
115 | from: this.state.pieceSquare,
116 | to: square,
117 | promotion: 'q' // always promote to a queen for example simplicity
118 | });
119 |
120 | // illegal move
121 | if (move === null) return;
122 |
123 | this.setState({
124 | fen: this.game.fen(),
125 | history: this.game.history({ verbose: true }),
126 | pieceSquare: ''
127 | });
128 | };
129 |
130 | onSquareRightClick = square =>
131 | this.setState({
132 | squareStyles: { [square]: { backgroundColor: 'deepPink' } }
133 | });
134 |
135 | render() {
136 | const { fen, dropSquareStyle, squareStyles } = this.state;
137 |
138 | return this.props.children({
139 | squareStyles,
140 | position: fen,
141 | onMouseOverSquare: this.onMouseOverSquare,
142 | onMouseOutSquare: this.onMouseOutSquare,
143 | onDrop: this.onDrop,
144 | dropSquareStyle,
145 | onDragOverSquare: this.onDragOverSquare,
146 | onSquareClick: this.onSquareClick,
147 | onSquareRightClick: this.onSquareRightClick
148 | });
149 | }
150 | }
151 |
152 | export default function WithMoveValidation() {
153 | return (
154 |
155 |
156 | {({
157 | position,
158 | onDrop,
159 | onMouseOverSquare,
160 | onMouseOutSquare,
161 | squareStyles,
162 | dropSquareStyle,
163 | onDragOverSquare,
164 | onSquareClick,
165 | onSquareRightClick
166 | }) => (
167 | (screenWidth < 500 ? 350 : 480)}
170 | position={position}
171 | onDrop={onDrop}
172 | onMouseOverSquare={onMouseOverSquare}
173 | onMouseOutSquare={onMouseOutSquare}
174 | boardStyle={{
175 | borderRadius: '5px',
176 | boxShadow: `0 5px 15px rgba(0, 0, 0, 0.5)`
177 | }}
178 | squareStyles={squareStyles}
179 | dropSquareStyle={dropSquareStyle}
180 | onDragOverSquare={onDragOverSquare}
181 | onSquareClick={onSquareClick}
182 | onSquareRightClick={onSquareRightClick}
183 | />
184 | )}
185 |
186 |
187 | );
188 | }
189 |
190 | const squareStyling = ({ pieceSquare, history }) => {
191 | const sourceSquare = history.length && history[history.length - 1].from;
192 | const targetSquare = history.length && history[history.length - 1].to;
193 |
194 | return {
195 | [pieceSquare]: { backgroundColor: 'rgba(255, 255, 0, 0.4)' },
196 | ...(history.length && {
197 | [sourceSquare]: {
198 | backgroundColor: 'rgba(255, 255, 0, 0.4)'
199 | }
200 | }),
201 | ...(history.length && {
202 | [targetSquare]: {
203 | backgroundColor: 'rgba(255, 255, 0, 0.4)'
204 | }
205 | })
206 | };
207 | };
208 |
--------------------------------------------------------------------------------
/src/integrations/UndoMove.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Chess from 'chess.js';
4 |
5 | import Chessboard from '../Chessboard';
6 |
7 | class HumanVsHuman extends Component {
8 | static propTypes = { children: PropTypes.func };
9 |
10 | state = {
11 | fen: 'start',
12 | // square styles for active drop square
13 | dropSquareStyle: {},
14 | // custom square styles
15 | squareStyles: {},
16 | // square with the currently clicked piece
17 | pieceSquare: '',
18 | // currently clicked square
19 | square: '',
20 | // array of past game moves
21 | history: [],
22 | // undo previous move
23 | undo: false
24 | };
25 |
26 | componentDidMount() {
27 | this.game = new Chess();
28 | }
29 |
30 | // keep clicked square style and remove hint squares
31 | removeHighlightSquare = () => {
32 | this.setState(({ pieceSquare, history }) => ({
33 | squareStyles: squareStyling({ pieceSquare, history })
34 | }));
35 | };
36 |
37 | // show possible moves
38 | highlightSquare = (sourceSquare, squaresToHighlight) => {
39 | const highlightStyles = [sourceSquare, ...squaresToHighlight].reduce(
40 | (a, c) => {
41 | return {
42 | ...a,
43 | ...{
44 | [c]: {
45 | background:
46 | 'radial-gradient(circle, #fffc00 36%, transparent 40%)',
47 | borderRadius: '50%'
48 | }
49 | },
50 | ...squareStyling({
51 | history: this.state.history,
52 | pieceSquare: this.state.pieceSquare
53 | })
54 | };
55 | },
56 | {}
57 | );
58 |
59 | this.setState(({ squareStyles }) => ({
60 | squareStyles: { ...squareStyles, ...highlightStyles }
61 | }));
62 | };
63 |
64 | onDrop = ({ sourceSquare, targetSquare, piece }) => {
65 | // see if the move is legal
66 | let move = this.game.move({
67 | from: sourceSquare,
68 | to: targetSquare,
69 | promotion: 'q' // always promote to a queen for example simplicity
70 | });
71 |
72 | // illegal move
73 | if (move === null) return;
74 | this.setState(
75 | ({ history, pieceSquare }) => {
76 | return {
77 | fen: this.game.fen(),
78 | history: this.game.history({ verbose: true }),
79 | squareStyles: squareStyling({ pieceSquare, history })
80 | };
81 | },
82 | () =>
83 | setTimeout(() => {
84 | const notKnights = piece !== 'wN' && piece !== 'bN';
85 | this.setState(({ history, pieceSquare }) => {
86 | notKnights && this.game.undo();
87 |
88 | return {
89 | fen: this.game.fen(),
90 | undo: notKnights ? true : false,
91 | history: this.game.history({ verbose: true }),
92 | squareStyles: squareStyling({ pieceSquare, history })
93 | };
94 | });
95 | }, 1000)
96 | );
97 | };
98 |
99 | onMouseOverSquare = square => {
100 | // get list of possible moves for this square
101 | let moves = this.game.moves({
102 | square: square,
103 | verbose: true
104 | });
105 |
106 | // exit if there are no moves available for this square
107 | if (moves.length === 0) return;
108 |
109 | let squaresToHighlight = [];
110 | for (var i = 0; i < moves.length; i++) {
111 | squaresToHighlight.push(moves[i].to);
112 | }
113 |
114 | this.highlightSquare(square, squaresToHighlight);
115 | };
116 |
117 | onMouseOutSquare = square => this.removeHighlightSquare(square);
118 |
119 | // central squares get diff dropSquareStyles
120 | onDragOverSquare = square => {
121 | this.setState({
122 | dropSquareStyle:
123 | square === 'e4' || square === 'd4' || square === 'e5' || square === 'd5'
124 | ? { backgroundColor: 'cornFlowerBlue' }
125 | : { boxShadow: 'inset 0 0 1px 4px rgb(255, 255, 0)' }
126 | });
127 | };
128 |
129 | onSquareClick = square => {
130 | this.setState(({ history }) => ({
131 | squareStyles: squareStyling({ pieceSquare: square, history }),
132 | pieceSquare: square
133 | }));
134 |
135 | let move = this.game.move({
136 | from: this.state.pieceSquare,
137 | to: square,
138 | promotion: 'q' // always promote to a queen for example simplicity
139 | });
140 |
141 | // illegal move
142 | if (move === null) return;
143 |
144 | this.setState({
145 | fen: this.game.fen(),
146 | history: this.game.history({ verbose: true }),
147 | pieceSquare: ''
148 | });
149 | };
150 |
151 | onSquareRightClick = square =>
152 | this.setState({
153 | squareStyles: { [square]: { backgroundColor: 'deepPink' } }
154 | });
155 |
156 | render() {
157 | const { fen, dropSquareStyle, squareStyles, undo } = this.state;
158 | return this.props.children({
159 | squareStyles,
160 | position: fen,
161 | onMouseOverSquare: this.onMouseOverSquare,
162 | onMouseOutSquare: this.onMouseOutSquare,
163 | onDrop: this.onDrop,
164 | dropSquareStyle,
165 | onDragOverSquare: this.onDragOverSquare,
166 | onSquareClick: this.onSquareClick,
167 | onSquareRightClick: this.onSquareRightClick,
168 | undo
169 | });
170 | }
171 | }
172 |
173 | export default function WithMoveValidation() {
174 | return (
175 |
176 |
177 | {({
178 | position,
179 | onDrop,
180 | onMouseOverSquare,
181 | onMouseOutSquare,
182 | squareStyles,
183 | dropSquareStyle,
184 | onDragOverSquare,
185 | onSquareClick,
186 | onSquareRightClick,
187 | undo
188 | }) => (
189 | (screenWidth < 500 ? 350 : 480)}
192 | width={320}
193 | position={position}
194 | onDrop={onDrop}
195 | onMouseOverSquare={onMouseOverSquare}
196 | onMouseOutSquare={onMouseOutSquare}
197 | boardStyle={{
198 | borderRadius: '5px',
199 | boxShadow: `0 5px 15px rgba(0, 0, 0, 0.5)`
200 | }}
201 | squareStyles={squareStyles}
202 | dropSquareStyle={dropSquareStyle}
203 | onDragOverSquare={onDragOverSquare}
204 | onSquareClick={onSquareClick}
205 | onSquareRightClick={onSquareRightClick}
206 | undo={undo}
207 | />
208 | )}
209 |
210 |
211 | );
212 | }
213 |
214 | const squareStyling = ({ pieceSquare, history }) => {
215 | const sourceSquare = history.length && history[history.length - 1].from;
216 | const targetSquare = history.length && history[history.length - 1].to;
217 |
218 | return {
219 | [pieceSquare]: { backgroundColor: 'rgba(255, 255, 0, 0.4)' },
220 | ...(history.length && {
221 | [sourceSquare]: {
222 | backgroundColor: 'rgba(255, 255, 0, 0.4)'
223 | }
224 | }),
225 | ...(history.length && {
226 | [targetSquare]: {
227 | backgroundColor: 'rgba(255, 255, 0, 0.4)'
228 | }
229 | })
230 | };
231 | };
232 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # UNMAINTAINED
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | **A chessboard for React inspired by [chessboard.js](https://github.com/oakmac/chessboardjs)**
12 |
13 | [![Build Status][build-badge]][build]
14 | [![Code Coverage][coverage-badge]][coverage]
15 | [![PRs Welcome][prs-badge]][prs]
16 | [![version][version-badge]][package]
17 | [![MIT License][license-badge]][license]
18 | [![Commitizen friendly][commitzen-badge]][commitzen]
19 | [![semantic-release][semantic-release-badge]][semantic-release]
20 |
21 |
22 |
23 |
24 | ## Installation
25 |
26 | This module is distributed via [npm][npm] which is bundled with [node][node] and
27 | should be installed as one of your project's `dependencies`:
28 |
29 | ```
30 | npm install --save chessboardjsx
31 | ```
32 |
33 | The package also depends on [React](https://reactjs.org/). Make sure you have that installed as well.
34 |
35 | ## Usage
36 |
37 | * [Props](https://www.chessboardjsx.com/props): get started with Chessboard.jsx
38 | * [With move validation](https://www.chessboardjsx.com/integrations/move-validation): how to integrate [chess.js](https://github.com/jhlywa/chess.js)
39 | * [Integrating with chess engines](https://www.chessboardjsx.com/integrations/stockfish): how to integrate with Stockfish, the ~~world's strongest~~ [world's second strongest](https://www.chess.com/news/view/google-s-alphazero-destroys-stockfish-in-100-game-match) chess engine
40 | * [Custom](https://www.chessboardjsx.com/custom): customize the board and pieces
41 |
42 | ## Contributing
43 |
44 | Please take a look at CONTRIBUTING.md to find out how to contribute.
45 |
46 | ## What is Chessboard.jsx?
47 |
48 | Chessboard.jsx is a React component with a flexible "just a board" API modeled from [chessboard.js](https://github.com/oakmac/chessboardjs). It's compatible with touch as well as standard HTML5 drag and drop.
49 |
50 | ## What can Chessboard.jsx **not** do?
51 |
52 | The scope of Chessboard.jsx is limited to "just a board." This is intentional and
53 | makes Chessboard.jsx flexible for handling a multitude of chess-related problems.
54 |
55 | Specifically, Chessboard.jsx does not understand anything about how the game of
56 | chess is played: how a knight moves, who's turn is it, is White in check?, etc.
57 |
58 | Fortunately, the powerful [chess.js](https://github.com/jhlywa/chess.js) library deals with exactly this sort of
59 | problem domain and plays nicely with Chessboard.jsx's flexible API.
60 |
61 | Here is a list of things that Chessboard.jsx is **not**:
62 |
63 | * A chess engine
64 | * A legal move validator
65 | * A PGN parser
66 |
67 | Chessboard.jsx is designed to work well with any of those software components, but the idea
68 | behind the library is that the logic that controls the board should be
69 | independent of those other domains.
70 |
71 | ## Contributors
72 |
73 | Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)):
74 |
75 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
76 |
77 |
78 |
79 | | [Will ](https://github.com/willb335) [💻](https://github.com/willb335/chessboardjsx/commits?author=willb335 "Code") [📖](https://github.com/willb335/chessboardjsx/commits?author=willb335 "Documentation") [💡](#example-willb335 "Examples") [⚠️](https://github.com/willb335/chessboardjsx/commits?author=willb335 "Tests") | [Andrew Bashelor ](https://github.com/a-bash) [📖](https://github.com/willb335/chessboardjsx/commits?author=a-bash "Documentation") | [yougotgotyo ](https://chadburg.com/) [🤔](#ideas-yougotgotyo "Ideas, Planning, & Feedback") | [Roger Knapp ](http://csharptest.net) [🤔](#ideas-csharptest "Ideas, Planning, & Feedback") | [Tiago Serafim ](https://github.com/slig) [💻](https://github.com/willb335/chessboardjsx/commits?author=slig "Code") [📖](https://github.com/willb335/chessboardjsx/commits?author=slig "Documentation") | [Kef Schecter ](http://www.furrykef.com/) [🐛](https://github.com/willb335/chessboardjsx/issues?q=author%3Afurrykef "Bug reports") | [Nils-Helge Garli Hegvik ](http://www.lånekalkulatoren.no) [💻](https://github.com/willb335/chessboardjsx/commits?author=nilsga "Code") |
80 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
81 | | [Levi Durfee ](https://levi.lol/) [💻](https://github.com/willb335/chessboardjsx/commits?author=levidurfee "Code") [📖](https://github.com/willb335/chessboardjsx/commits?author=levidurfee "Documentation") | [Chris ](https://github.com/altruisticsoftware) [💻](https://github.com/willb335/chessboardjsx/commits?author=altruisticsoftware "Code") | [Harrison Kerr ](https://hwkerr.github.io/) [💻](https://github.com/willb335/chessboardjsx/commits?author=hwkerr "Code") |
82 |
83 |
84 | ## LICENSE
85 |
86 | MIT
87 |
88 | [npm]: https://www.npmjs.com/
89 | [node]: https://nodejs.org
90 | [build-badge]: https://img.shields.io/travis/willb335/chessboardjsx.svg?style=flat-square
91 | [build]: https://travis-ci.org/willb335/chessboardjsx
92 | [coverage-badge]: https://img.shields.io/codecov/c/github/willb335/chessboardjsx.svg?style=flat-square
93 | [coverage]: https://codecov.io/github/willb335/chessboardjsx
94 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
95 | [prs]: http://makeapullrequest.com
96 | [version-badge]: https://img.shields.io/npm/v/chessboardjsx.svg?style=flat-square
97 | [package]: https://www.npmjs.com/package/chessboardjsx
98 | [license-badge]: https://img.shields.io/npm/l/chessboardjsx.svg?style=flat-square
99 | [license]: https://github.com/willb335/chessboardjsx/blob/master/LICENSE
100 | [commitzen-badge]: https://img.shields.io/badge/commitizen-friendly-brightgreen.svg
101 | [commitzen]: http://commitizen.github.io/cz-cli/
102 | [semantic-release-badge]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
103 | [semantic-release]: https://github.com/semantic-release/semantic-release
104 |
--------------------------------------------------------------------------------
/src/Demo.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import WithMoveValidation from './integrations/WithMoveValidation';
4 | import PlayRandomMoveEngine from './integrations/PlayRandomMoveEngine';
5 | import RandomVsRandomGame from './integrations/RandomVsRandomGame';
6 | import CustomizedBoard from './integrations/CustomizedBoard';
7 | import AllowDragFeature from './integrations/AllowDrag';
8 | import PrestoChangoExample from './integrations/PrestoChango';
9 | import UndoMove from './integrations/UndoMove';
10 | import SpareOnDrop from './integrations/SpareOnDrop';
11 |
12 | class Demo extends Component {
13 | state = {
14 | showCustomizedBoard: false,
15 | showWithMoveValidation: false,
16 | showRandomVsRandomGame: false,
17 | showPlayRandomMoveEngine: false,
18 | showAllowDragFeature: false,
19 | showPrestoChango: false,
20 | showUndoMove: false,
21 | showSpareOnDrop: false
22 | };
23 | render() {
24 | return (
25 |
26 |
27 |
29 | this.setState({
30 | showCustomizedBoard: true,
31 | showWithMoveValidation: false,
32 | showRandomVsRandomGame: false,
33 | showPlayRandomMoveEngine: false,
34 | showAllowDragFeature: false,
35 | showPrestoChango: false,
36 | showUndoMove: false,
37 | showSpareOnDrop: false
38 | })
39 | }
40 | style={{ ...buttonStyle, ...{ backgroundColor: 'orange' } }}
41 | >
42 | Custom Board
43 |
44 |
46 | this.setState({
47 | showCustomizedBoard: false,
48 | showWithMoveValidation: true,
49 | showRandomVsRandomGame: false,
50 | showPlayRandomMoveEngine: false,
51 | showAllowDragFeature: false,
52 | showPrestoChango: false,
53 | showUndoMove: false,
54 | showSpareOnDrop: false
55 | })
56 | }
57 | style={{
58 | ...buttonStyle,
59 | ...{ backgroundColor: 'purple', color: 'white' }
60 | }}
61 | >
62 | With Move Validation
63 |
64 |
66 | this.setState({
67 | showCustomizedBoard: false,
68 | showWithMoveValidation: false,
69 | showRandomVsRandomGame: true,
70 | showPlayRandomMoveEngine: false,
71 | showAllowDragFeature: false,
72 | showPrestoChango: false,
73 | showUndoMove: false,
74 | showSpareOnDrop: false
75 | })
76 | }
77 | style={{ ...buttonStyle, ...{ backgroundColor: 'gold' } }}
78 | >
79 | Random Vs Random
80 |
81 |
83 | this.setState({
84 | showCustomizedBoard: false,
85 | showWithMoveValidation: false,
86 | showRandomVsRandomGame: false,
87 | showPlayRandomMoveEngine: true,
88 | showAllowDragFeature: false,
89 | showPrestoChango: false,
90 | showUndoMove: false,
91 | showSpareOnDrop: false
92 | })
93 | }
94 | style={{ ...buttonStyle, ...{ backgroundColor: 'silver' } }}
95 | >
96 | Play a Random Move Engine
97 |
98 |
100 | this.setState({
101 | showCustomizedBoard: false,
102 | showWithMoveValidation: false,
103 | showRandomVsRandomGame: false,
104 | showPlayRandomMoveEngine: false,
105 | showAllowDragFeature: true,
106 | showPrestoChango: false,
107 | showUndoMove: false,
108 | showSpareOnDrop: false
109 | })
110 | }
111 | style={{ ...buttonStyle, ...{ backgroundColor: 'aqua' } }}
112 | >
113 | Conditionally Disable Drag
114 |
115 |
117 | this.setState({
118 | showCustomizedBoard: false,
119 | showWithMoveValidation: false,
120 | showRandomVsRandomGame: false,
121 | showPlayRandomMoveEngine: false,
122 | showAllowDragFeature: false,
123 | showPrestoChango: true,
124 | showUndoMove: false,
125 | showSpareOnDrop: false
126 | })
127 | }
128 | style={{
129 | ...buttonStyle,
130 | ...{ backgroundColor: 'brown', color: 'white' }
131 | }}
132 | >
133 | Presto Chango
134 |
135 |
137 | this.setState({
138 | showCustomizedBoard: false,
139 | showWithMoveValidation: false,
140 | showRandomVsRandomGame: false,
141 | showPlayRandomMoveEngine: false,
142 | showAllowDragFeature: false,
143 | showPrestoChango: false,
144 | showUndoMove: true,
145 | showSpareOnDrop: false
146 | })
147 | }
148 | style={{ ...buttonStyle, ...{ backgroundColor: 'pink' } }}
149 | >
150 | Undo Move
151 |
152 |
154 | this.setState({
155 | showCustomizedBoard: false,
156 | showWithMoveValidation: false,
157 | showRandomVsRandomGame: false,
158 | showPlayRandomMoveEngine: false,
159 | showAllowDragFeature: false,
160 | showPrestoChango: false,
161 | showUndoMove: false,
162 | showSpareOnDrop: true
163 | })
164 | }
165 | style={{ ...buttonStyle, ...{ backgroundColor: 'orange' } }}
166 | >
167 | Show Spare OnDrop
168 |
169 |
170 |
171 | {this.state.showCustomizedBoard &&
}
172 | {this.state.showWithMoveValidation &&
}
173 | {this.state.showRandomVsRandomGame &&
}
174 | {this.state.showPlayRandomMoveEngine &&
}
175 | {this.state.showAllowDragFeature &&
}
176 | {this.state.showPrestoChango &&
}
177 | {this.state.showUndoMove &&
}
178 | {this.state.showSpareOnDrop &&
}
179 |
180 |
181 | );
182 | }
183 | }
184 |
185 | export default Demo;
186 |
187 | const buttonStyle = { width: 200, height: 100, margin: 30, fontSize: 16 };
188 |
189 | const buttonRow = {
190 | display: 'flex',
191 | justifyContent: 'space-around',
192 | alignItems: 'center',
193 | width: '100vw',
194 | flexWrap: 'wrap'
195 | };
196 |
197 | const boardsContainer = {
198 | display: 'flex',
199 | justifyContent: 'space-around',
200 | alignItems: 'center',
201 | flexWrap: 'wrap',
202 | width: '100vw',
203 | marginTop: 30,
204 | marginBottom: 50
205 | };
206 |
--------------------------------------------------------------------------------
/src/integrations/Stockfish.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'; // eslint-disable-line no-unused-vars
2 | import PropTypes from 'prop-types';
3 | import Chess from 'chess.js';
4 |
5 | const STOCKFISH = window.STOCKFISH;
6 | const game = new Chess();
7 |
8 | class Engine extends Component {
9 | static propTypes = { children: PropTypes.func };
10 |
11 | state = { fen: 'start' };
12 |
13 | onDrop = (source, target) => {
14 | // see if the move is legal
15 | const move = game.move({
16 | from: source,
17 | to: target,
18 | promotion: 'q'
19 | });
20 |
21 | // illegal move
22 | if (move === null) return;
23 |
24 | return new Promise(resolve => {
25 | this.setState({ fen: game.fen() });
26 | resolve();
27 | }).then(() => this.engineGame().prepareMove());
28 | };
29 |
30 | engineGame = options => {
31 | options = options || {};
32 |
33 | /// We can load Stockfish via Web Workers or via STOCKFISH() if loaded from a