├── .gitignore ├── .npmignore ├── .prettierrc ├── .prettierrc.json ├── .storybook ├── main.ts └── preview.ts ├── LICENSE ├── README.md ├── media └── chessboard.png ├── package-lock.json ├── package.json ├── rollup.config.mjs ├── src ├── chessboard │ ├── components │ │ ├── Arrows.tsx │ │ ├── Board.tsx │ │ ├── CustomDragLayer.tsx │ │ ├── DnDRoot.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── Notation.tsx │ │ ├── Piece.tsx │ │ ├── PromotionDialog.tsx │ │ ├── PromotionOption.tsx │ │ ├── SparePiece.tsx │ │ ├── Square.tsx │ │ └── Squares.tsx │ ├── consts.ts │ ├── context │ │ └── chessboard-context.tsx │ ├── functions.ts │ ├── hooks │ │ └── useArrows.ts │ ├── index.tsx │ ├── media │ │ ├── error.tsx │ │ └── pieces.tsx │ └── types │ │ └── index.ts └── index.ts ├── stories ├── Chessboard.stories.tsx ├── StockfishIntegration.mdx ├── StockfishIntegration.stories.tsx ├── media │ ├── 3d-pieces │ │ ├── bB.webp │ │ ├── bK.webp │ │ ├── bN.webp │ │ ├── bP.webp │ │ ├── bQ.webp │ │ ├── bR.webp │ │ ├── wB.webp │ │ ├── wK.webp │ │ ├── wN.webp │ │ ├── wP.webp │ │ ├── wQ.webp │ │ └── wR.webp │ ├── bB.png │ ├── bK.png │ ├── bN.png │ ├── bP.png │ ├── bQ.png │ ├── bR.png │ ├── wB.png │ ├── wK.png │ ├── wN.png │ ├── wP.png │ ├── wQ.png │ ├── wR.png │ └── wood-pattern.png └── stockfish │ ├── engine.ts │ ├── stockfish.js │ ├── stockfish.wasm │ └── stockfish.wasm.js ├── tsconfig.json └── vercel.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /dist 14 | storybook-static 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # local env files 26 | .env.local 27 | .env.development.local 28 | .env.test.local 29 | .env.production.local 30 | 31 | *storybook.log -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .storybook 2 | media 3 | node_modules 4 | src 5 | stories 6 | storybook-static 7 | .gitignore 8 | package-lock.json 9 | rollup.config.js 10 | vercel.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100 3 | } 4 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-vite"; 2 | 3 | const config: StorybookConfig = { 4 | stories: ["../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)", "../stories/**/*.mdx", ], 5 | addons: [ 6 | '@storybook/addon-storysource', 7 | "@storybook/addon-onboarding", 8 | "@storybook/addon-links", 9 | "@storybook/addon-essentials", 10 | "@chromatic-com/storybook", 11 | "@storybook/addon-interactions", 12 | ], 13 | framework: { 14 | name: "@storybook/react-vite", 15 | options: {}, 16 | }, 17 | staticDirs: ["../stories/media", "../stories/stockfish"], 18 | }; 19 | export default config; 20 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/react"; 2 | 3 | const preview: Preview = { 4 | parameters: { 5 | controls: { 6 | matchers: { 7 | color: /(background|color)$/i, 8 | date: /Date$/i, 9 | }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default preview; 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ryan Gregory 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # [react-chessboard](https://react-chessboard.vercel.app/) 4 | 5 | react chessboard 6 | 7 | ## The React Chessboard Library used at [ChessOpenings.co.uk](https://chessopenings.co.uk) 8 | 9 | ### Inspired and adapted from the unmaintained [chessboardjsx](https://github.com/willb335/chessboardjsx) 10 | 11 | [![Pull Requests][prs-badge]][prs] [![Version][version-badge]][version] [![MIT License][license-badge]][license] 12 | 13 |
14 | 15 | ## What is react-chessboard? 16 | 17 | react-chessboard is a React component that provides chessboard functionality to your application. The Chess game logic that controls the board should be independent to the board, using a library such as [Chess.js](https://github.com/jhlywa/chess.js). An example of these two working together is shown [in the example below](#basic-example). For interactive examples visit [https://react-chessboard.vercel.app/](https://react-chessboard.vercel.app/). 18 | 19 | [ChessOpenings.co.uk](https://chessopenings.co.uk) was originally built utilising the [chessboardjsx](https://github.com/willb335/chessboardjsx) library. With [chessboardjsx](https://github.com/willb335/chessboardjsx) being unmaintained, it made it difficult to add functionality or optimise performance, so react-chessboard was made. 20 | 21 | ## Installation 22 | 23 | ``` 24 | npm i react-chessboard 25 | ``` 26 | 27 | ## Examples 28 | 29 | [Storybook](https://react-chessboard.vercel.app/) 30 | 31 | ## Features 32 | 33 | ### Current 34 | 35 | - Accessible Functions 36 | - `chessboardRef.current.clearPremoves();`, takes optional boolean parameter `clearLastPieceColour` to allow/disallow further premoves of the last moved piece color. `Default: true` 37 | - Board Orientation Choice 38 | - Custom Actions 39 | - getPositionObject 40 | - onArrowsChange 41 | - onDragOverSquare 42 | - onMouseOutSquare 43 | - onMouseOverSquare 44 | - onPieceClick 45 | - onPieceDragBegin 46 | - onPieceDragEnd 47 | - onPieceDrop 48 | - onPromotionCheck 49 | - onPromotionPieceSelect 50 | - onSquareClick 51 | - onSquareRightClick 52 | - Customisable Board Styles 53 | - Customisable Pieces 54 | - Customisable Square Styles 55 | - Customisable Notation Styles 56 | - Drag and Drop 57 | - Draw Arrows with Drag or Props 58 | - Mobile Compatibility 59 | - Moving Piece Animations 60 | - Optional Square Coordinates Notation 61 | - Position Control 62 | - Premoves 63 | - Promotion Piece Select 64 | - Responsive Board Width 65 | - TypeScript Support 66 | 67 | ## Usage 68 | 69 | ### Bare Minimum 70 | 71 | ```jsx 72 | import { Chessboard } from "react-chessboard"; 73 | 74 | export default function App() { 75 | return ( 76 |
77 | 78 |
79 | ); 80 | } 81 | ``` 82 | 83 | ### Basic Example 84 | 85 | #### IMPORTANT: Examples use the current stable release of chess.js. As of writing this, chess.js v1.0.0 is still in beta. These examples use chess.js ^0.12.0 86 | 87 | ```jsx 88 | import { useState } from "react"; 89 | import Chess from "chess.js"; 90 | import { Chessboard } from "react-chessboard"; 91 | 92 | export default function PlayRandomMoveEngine() { 93 | const [game, setGame] = useState(new Chess()); 94 | 95 | function makeAMove(move) { 96 | const gameCopy = { ...game }; 97 | const result = gameCopy.move(move); 98 | setGame(gameCopy); 99 | return result; // null if the move was illegal, the move object if the move was legal 100 | } 101 | 102 | function makeRandomMove() { 103 | const possibleMoves = game.moves(); 104 | if (game.game_over() || game.in_draw() || possibleMoves.length === 0) 105 | return; // exit if the game is over 106 | const randomIndex = Math.floor(Math.random() * possibleMoves.length); 107 | makeAMove(possibleMoves[randomIndex]); 108 | } 109 | 110 | function onDrop(sourceSquare, targetSquare) { 111 | const move = makeAMove({ 112 | from: sourceSquare, 113 | to: targetSquare, 114 | promotion: "q", // always promote to a queen for example simplicity 115 | }); 116 | 117 | // illegal move 118 | if (move === null) return false; 119 | setTimeout(makeRandomMove, 200); 120 | return true; 121 | } 122 | 123 | return ; 128 | } 129 | ``` 130 | 131 | ### Advanced Examples 132 | 133 | For more advanced code usage examples, please see example boards shown in [`Storybook`](https://react-chessboard.vercel.app/). 134 | 135 | ### Props 136 | 137 | | Prop | Default Value | Options | Description | 138 | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 139 | | allowDragOutsideBoard | boolean: false | [true, false] | Whether or not to allow pieces to be dragged outside the board. | 140 | | animationDuration | number: 300 | | Time in milliseconds for piece to slide to target square. Only used when the position is programmatically changed. If a new position is set before the animation is complete, the board will cancel the current animation and snap to the new position. | 141 | | areArrowsAllowed | boolean: true | [true, false] | Whether or not arrows can be drawn with right click and dragging. | 142 | | arePiecesDraggable | boolean: true | [true, false] | Whether or not all pieces are draggable. | 143 | | arePremovesAllowed | boolean: false | [true, false] | Whether or not premoves are allowed. | 144 | | autoPromoteToQueen | boolean: false | [true, false] | Whether or not to automatically promote pawn to queen. | 145 | | boardOrientation | string: 'white' | ['white', 'black'] | The orientation of the board, the chosen colour will be at the bottom of the board. | 146 | | boardWidth | number: 560 | | The width of the board in pixels. | 147 | | clearPremovesOnRightClick | boolean: true | [true, false] | If premoves are allowed, whether or not to clear the premove queue on right click. | 148 | | customArrowColor | string: 'rgb(255,170,0)' | rgb or hex string | String with rgb or hex value to colour drawn arrows. | 149 | | customArrows | [Square, Square, string?]\[\] | array of string arrays | Array where each element is a tuple containing two Square values (representing the 'from' and 'to' squares) and an optional third string element for the arrow color e.g. [ ['a3', 'a5', 'red'], ['g1', 'f3'] ]. | 150 | | customBoardStyle | object: {} | inline CSS styling | Custom board style object e.g. { borderRadius: '5px', boxShadow: '0 5px 15px rgba(0, 0, 0, 0.5)'}. | 151 | | customNotationStyle | object: {} | inline CSS styling | Custom notation style object e.g. { fontSize: '12px' }. | 152 | | customDarkSquareStyle | object: { backgroundColor: '#B58863' } | inline CSS styling | Custom dark square style object. | 153 | | customDndBackend | BackendFactory: undefined | | Custom react-dnd backend to use instead of the one provided by react-chessboard. | 154 | | customDndBackendOptions | any: undefined | | Options to use for the given custom react-dnd backend. See customDndBackend. | 155 | | customDropSquareStyle | object: { boxShadow: 'inset 0 0 1px 6px rgba(255,255,255,0.75)' } | inline CSS styling | Custom drop square style object (Square being hovered over with dragged piece). | 156 | | customLightSquareStyle | object: { backgroundColor: '#F0D9B5' } | inline CSS styling | Custom light square style object. | 157 | | customPieces | object: {} | | Custom pieces object where each key must match a corresponding chess piece (wP, wB, wN, wR, wQ, wK, bP, bB, bN, bR, bQ, bK). The value of each piece is a function that takes in some optional arguments to use and must return JSX to render. e.g. { wK: ({ isDragging: boolean, squareWidth: number, square: String}) => jsx }. | 158 | | customPremoveDarkSquareStyle | object: { backgroundColor: '#A42323' } | inline CSS styling | Custom premove dark square style object. | 159 | | customPremoveLightSquareStyle | object: { backgroundColor: '#BD2828' } | inline CSS styling | Custom premove light square style object. | 160 | | customSquare | ElementType: "div" | | Custom renderer for squares. Can also use an html element. | 161 | | customSquareStyles | object: {} | inline CSS styling | Custom styles for all squares. | 162 | | id | number: 0 | [string, number] | Board identifier, necessary if more than one board is mounted for drag and drop. | 163 | | isDraggablePiece | function: ({ piece, sourceSquare }) => true | returns [true, false] | Function called when a piece drag is attempted. Returns if piece is draggable. | 164 | | getPositionObject | function: (currentPosition) => {} | | User function that receives current position object when position changes. | 165 | | onArrowsChange | function: (squares) => {} | | User function is run when arrows are set on the board. | 166 | | onDragOverSquare | function: (square) => {} | | User function that is run when piece is dragged over a square. | 167 | | onMouseOutSquare | function: (square) => {} | | User function that is run when mouse leaves a square. | 168 | | onMouseOverSquare | function: (square) => {} | | User function that is run when mouse is over a square. | 169 | | onPieceClick | function: (piece, square) => {} | | User function that is run when piece is clicked. | 170 | | onPieceDragBegin | function: (piece, sourceSquare) => {} | | User function that is run when piece is grabbed to start dragging. | 171 | | onPieceDragEnd | function: (piece, sourceSquare) => {} | | User function that is run when piece is let go after dragging. | 172 | | onPieceDrop | function: (sourceSquare, targetSquare, piece) => true | returns [true, false] | User function that is run when piece is dropped on a square. Must return whether the move was successful or not. This return value does not control whether or not the piece was placed (as that is controlled by the `position` prop) but instead controls premove logic. | 173 | | onPromotionCheck | function: (sourceSquare, targetSquare, piece) => (((piece === "wP" && sourceSquare[1] === "7" && targetSquare[1] === "8") \|\| (piece === "bP" && sourceSquare[1] === "2" && targetSquare[1] === "1")) && Math.abs(sourceSquare.charCodeAt(0) - targetSquare.charCodeAt(0)) <= 1) | returns [true, false] | User function that is run when piece is dropped. Must return whether the move results in a promotion or not. | 174 | | onPromotionPieceSelect | function: (piece, promoteFromSquare, promoteToSquare) => true | returns [true, false] | User function that is run when a promotion piece is selected. Must return whether the move was successful or not. | 175 | | onSquareClick | function: (square, piece) => {} | | User function that is run when a square is clicked. | 176 | | onSquareRightClick | function: (square) => {} | | User function that is run when a square is right clicked. | 177 | | position | string: 'start' | ['start', FEN string, { e5: 'wK', e4: 'wP', ... }] | FEN string or position object notating where the chess pieces are on the board. Start position can also be notated with the string: 'start'. | 178 | | promotionDialogVariant | string: 'default': | ['default', 'vertical', 'modal'] | Style of promotion dialog. | 179 | | promotionToSquare | string or null | ['a1', 'a2', ..., 'h8', null] | The square to promote a piece to. Must be passed when promotion dialog is manually shown. | 180 | | showBoardNotation | boolean: true | [true, false] | Whether or not to show the file and rank co-ordinates (a..h, 1..8). | 181 | | showPromotionDialog | boolean: false | [true, false] | Whether or not to manually show the promotion dialog. | 182 | | snapToCursor | boolean: true | [true, false] | Whether or not to center dragged pieces on the mouse cursor. | 183 | | | 184 | 185 | ## Contributing 186 | 187 | 1. Fork this repository 188 | 2. Clone your forked repository onto your development machine 189 | ``` 190 | git clone https://github.com/yourUsernameHere/react-chessboard.git 191 | cd react-chessboard 192 | ``` 193 | 3. Create a branch for your PR 194 | ``` 195 | git checkout -b your-branch-name 196 | ``` 197 | 4. Set upstream remote 198 | ``` 199 | git remote add upstream https://github.com/Clariity/react-chessboard.git 200 | ``` 201 | 5. Make your changes 202 | 6. Test your changes by running storybook 203 | ``` 204 | npm run storybook 205 | ``` 206 | 7. Push your changes 207 | ``` 208 | git add . 209 | git commit -m "feature/cool-new-feature" 210 | git push --set-upstream origin your-branch-name 211 | ``` 212 | 8. Create pull request on GitHub 213 | 9. Contribute again 214 | ``` 215 | git checkout main 216 | git pull upstream main 217 | git checkout -b your-new-branch-name 218 | ``` 219 | 220 | ## LICENSE 221 | 222 | MIT 223 | 224 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 225 | [version-badge]: https://img.shields.io/npm/v/react-chessboard.svg?style=flat-square 226 | [license-badge]: https://img.shields.io/npm/l/react-chessboard.svg?style=flat-square 227 | [prs]: https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github 228 | [version]: https://www.npmjs.com/package/react-chessboard 229 | [license]: https://github.com/Clariity/react-chessboard/blob/main/LICENSE 230 | -------------------------------------------------------------------------------- /media/chessboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/media/chessboard.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-chessboard", 3 | "version": "4.7.3", 4 | "description": "The React Chessboard Library used at ChessOpenings.co.uk. Inspired and adapted from the unmaintained Chessboard.jsx.", 5 | "author": "Ryan Gregory ", 6 | "repository": "https://github.com/Clariity/react-chessboard", 7 | "license": "MIT", 8 | "main": "dist/index.js", 9 | "module": "dist/index.esm.js", 10 | "types": "dist/index.d.ts", 11 | "files": [ 12 | "dist" 13 | ], 14 | "scripts": { 15 | "build": "set NODE_ENV=production && rm -rf /dist && rollup -c", 16 | "test": "echo \"Error: no test specified\" && exit 1", 17 | "storybook": "storybook dev -p 6006", 18 | "build-storybook": "storybook build", 19 | "serve-storybook": "serve storybook-static" 20 | }, 21 | "keywords": [ 22 | "react", 23 | "react.js", 24 | "chess", 25 | "chess.js", 26 | "chessboard", 27 | "chess board", 28 | "react-chessboard", 29 | "react chessboard", 30 | "react chess board", 31 | "react-dnd", 32 | "chessboard component", 33 | "drag and drop" 34 | ], 35 | "devDependencies": { 36 | "@babel/core": "^7.20.7", 37 | "@chromatic-com/storybook": "^1.6.1", 38 | "@mdx-js/react": "^3.0.0", 39 | "@rollup/plugin-commonjs": "^24.0.0", 40 | "@rollup/plugin-node-resolve": "^15.0.1", 41 | "@rollup/plugin-terser": "^0.4.3", 42 | "@storybook/addon-essentials": "^8.2.7", 43 | "@storybook/addon-interactions": "^8.2.7", 44 | "@storybook/addon-links": "^8.2.7", 45 | "@storybook/addon-onboarding": "^8.2.7", 46 | "@storybook/addon-storysource": "^8.2.8", 47 | "@storybook/blocks": "^8.2.7", 48 | "@storybook/react": "^8.2.7", 49 | "@storybook/react-vite": "^8.2.7", 50 | "@storybook/test": "^8.2.7", 51 | "@types/node": "^18.11.18", 52 | "@types/react": "^18.3.3", 53 | "@types/react-dom": "^18.3.0", 54 | "babel-loader": "^8.3.0", 55 | "chess.js": "^0.12.0", 56 | "react": "^18.2.0", 57 | "react-dom": "^18.2.0", 58 | "rollup": "^3.9.1", 59 | "rollup-plugin-peer-deps-external": "^2.2.4", 60 | "rollup-plugin-typescript2": "^0.34.1", 61 | "storybook": "^8.2.7", 62 | "typescript": "^4.9.4" 63 | }, 64 | "peerDependencies": { 65 | "react": ">=16.14.0", 66 | "react-dom": ">=16.14.0" 67 | }, 68 | "dependencies": { 69 | "react-dnd": "^16.0.1", 70 | "react-dnd-html5-backend": "^16.0.1", 71 | "react-dnd-touch-backend": "^16.0.1" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import peerDepsExternal from "rollup-plugin-peer-deps-external"; 2 | import resolve from "@rollup/plugin-node-resolve"; 3 | import commonjs from "@rollup/plugin-commonjs"; 4 | import typescript from "rollup-plugin-typescript2"; 5 | 6 | import pkg from "./package.json" assert { type: "json" }; 7 | 8 | export default [ 9 | { 10 | input: "src/index.ts", 11 | output: [ 12 | { 13 | file: pkg.main, 14 | format: "cjs", 15 | exports: "auto", 16 | }, 17 | { 18 | file: pkg.module, 19 | format: "esm", 20 | }, 21 | ], 22 | plugins: [ 23 | peerDepsExternal(), 24 | resolve(), 25 | commonjs(), 26 | typescript({ useTsconfigDeclarationDir: true, clean: true }), 27 | ], 28 | external: ["react", "react-dom"], 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /src/chessboard/components/Arrows.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from "react"; 2 | 3 | import { getRelativeCoords } from "../functions"; 4 | import { useChessboard } from "../context/chessboard-context"; 5 | import { Arrow } from "../types"; 6 | 7 | export const Arrows = () => { 8 | const { 9 | arrows, 10 | newArrow, 11 | boardOrientation, 12 | boardWidth, 13 | 14 | customArrowColor: primaryArrowCollor, 15 | } = useChessboard(); 16 | const arrowsList = [...arrows, newArrow].filter(Boolean) as Arrow[]; 17 | 18 | return ( 19 | 30 | {arrowsList.map((arrow, i) => { 31 | const [arrowStartField, arrowEndField, arrowColor] = arrow; 32 | if (arrowStartField === arrowEndField) return null; 33 | const from = getRelativeCoords( 34 | boardOrientation, 35 | boardWidth, 36 | arrowStartField 37 | ); 38 | const to = getRelativeCoords( 39 | boardOrientation, 40 | boardWidth, 41 | arrowEndField 42 | ); 43 | let ARROW_LENGTH_REDUCER = boardWidth / 32; 44 | 45 | const isArrowActive = i === arrows.length; 46 | // if there are different arrows targeting the same square make their length a bit shorter 47 | if ( 48 | arrows.some( 49 | (restArrow) => 50 | restArrow[0] !== arrowStartField && restArrow[1] === arrowEndField 51 | ) && 52 | !isArrowActive 53 | ) { 54 | ARROW_LENGTH_REDUCER = boardWidth / 16; 55 | } 56 | const dx = to.x - from.x; 57 | const dy = to.y - from.y; 58 | 59 | const r = Math.hypot(dy, dx); 60 | 61 | const end = { 62 | x: from.x + (dx * (r - ARROW_LENGTH_REDUCER)) / r, 63 | y: from.y + (dy * (r - ARROW_LENGTH_REDUCER)) / r, 64 | }; 65 | 66 | return ( 67 | 72 | 80 | 84 | 85 | 97 | 98 | ); 99 | })} 100 | 101 | ); 102 | }; 103 | -------------------------------------------------------------------------------- /src/chessboard/components/Board.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from "react"; 2 | import { Squares } from "./Squares"; 3 | import { Arrows } from "./Arrows"; 4 | import { useChessboard } from "../context/chessboard-context"; 5 | import { PromotionDialog } from "./PromotionDialog"; 6 | import { WhiteKing } from "./ErrorBoundary"; 7 | 8 | export function Board() { 9 | const boardRef = useRef(null); 10 | 11 | const { 12 | boardWidth, 13 | clearCurrentRightClickDown, 14 | onPromotionPieceSelect, 15 | setShowPromoteDialog, 16 | showPromoteDialog, 17 | customBoardStyle, 18 | } = useChessboard(); 19 | 20 | useEffect(() => { 21 | function handleClickOutside(event: MouseEvent) { 22 | if ( 23 | boardRef.current && 24 | !boardRef.current.contains(event.target as Node) 25 | ) { 26 | clearCurrentRightClickDown(); 27 | } 28 | } 29 | 30 | document.addEventListener("mouseup", handleClickOutside); 31 | return () => { 32 | document.removeEventListener("mouseup", handleClickOutside); 33 | }; 34 | }, []); 35 | 36 | return boardWidth ? ( 37 |
38 |
46 | 47 | 48 | 49 | {showPromoteDialog && ( 50 | <> 51 |
{ 53 | setShowPromoteDialog(false); 54 | onPromotionPieceSelect?.(); 55 | }} 56 | style={{ 57 | position: "absolute", 58 | top: "0", 59 | left: "0", 60 | zIndex: "100", 61 | backgroundColor: "rgba(22,21,18,.7)", 62 | width: boardWidth, 63 | height: boardWidth, 64 | }} 65 | /> 66 | 67 | 68 | )} 69 |
70 |
71 | ) : ( 72 | 73 | ); 74 | } 75 | 76 | const boardStyles = (width: number) => ({ 77 | cursor: "default", 78 | height: width, 79 | width, 80 | }); 81 | -------------------------------------------------------------------------------- /src/chessboard/components/CustomDragLayer.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useCallback } from "react"; 2 | import { useDragLayer, XYCoord } from "react-dnd"; 3 | 4 | import { useChessboard } from "../context/chessboard-context"; 5 | import { CustomPieceFn, Piece, Square } from "../types"; 6 | 7 | export type CustomDragLayerProps = { 8 | boardContainer: { left: number; top: number }; 9 | }; 10 | 11 | export function CustomDragLayer({ boardContainer }: CustomDragLayerProps) { 12 | const { boardWidth, chessPieces, id, snapToCursor, allowDragOutsideBoard } = 13 | useChessboard(); 14 | 15 | const collectedProps = useDragLayer((monitor) => ({ 16 | item: monitor.getItem(), 17 | clientOffset: monitor.getClientOffset(), 18 | sourceClientOffset: monitor.getSourceClientOffset(), 19 | isDragging: monitor.isDragging(), 20 | })); 21 | 22 | const { 23 | isDragging, 24 | item, 25 | clientOffset, 26 | sourceClientOffset, 27 | }: { 28 | item: { piece: Piece; square: Square; id: number }; 29 | clientOffset: XYCoord | null; 30 | sourceClientOffset: XYCoord | null; 31 | isDragging: boolean; 32 | } = collectedProps; 33 | 34 | const getItemStyle = useCallback( 35 | (clientOffset: XYCoord | null, sourceClientOffset: XYCoord | null) => { 36 | if (!clientOffset || !sourceClientOffset) return { display: "none" }; 37 | 38 | let { x, y } = snapToCursor ? clientOffset : sourceClientOffset; 39 | const halfSquareWidth = boardWidth / 8 / 2; 40 | if (snapToCursor) { 41 | x -= halfSquareWidth; 42 | y -= halfSquareWidth; 43 | } 44 | 45 | if (!allowDragOutsideBoard) { 46 | const { left, top } = boardContainer; 47 | // half square so the piece reaches the board 48 | const maxLeft = left - halfSquareWidth; 49 | const maxTop = top - halfSquareWidth; 50 | const maxRight = left + boardWidth - halfSquareWidth; 51 | const maxBottom = top + boardWidth - halfSquareWidth; 52 | x = Math.max(maxLeft, Math.min(x, maxRight)); 53 | y = Math.max(maxTop, Math.min(y, maxBottom)); 54 | } 55 | 56 | const transform = `translate(${x}px, ${y}px)`; 57 | 58 | return { 59 | transform, 60 | WebkitTransform: transform, 61 | touchAction: "none", 62 | }; 63 | }, 64 | [boardWidth, allowDragOutsideBoard, snapToCursor, boardContainer] 65 | ); 66 | 67 | return isDragging && item.id === id ? ( 68 |
77 |
78 | {typeof chessPieces[item.piece] === "function" ? ( 79 | (chessPieces[item.piece] as CustomPieceFn)({ 80 | squareWidth: boardWidth / 8, 81 | isDragging: true, 82 | }) 83 | ) : ( 84 | 89 | {chessPieces[item.piece] as ReactNode} 90 | 91 | )} 92 |
93 |
94 | ) : null; 95 | } 96 | -------------------------------------------------------------------------------- /src/chessboard/components/DnDRoot.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useState, 3 | useEffect, 4 | createContext, 5 | useContext, 6 | ReactNode, 7 | FC, 8 | } from "react"; 9 | import { DndProvider } from "react-dnd"; 10 | import { BackendFactory } from "dnd-core"; 11 | import { HTML5Backend } from "react-dnd-html5-backend"; 12 | import { TouchBackend } from "react-dnd-touch-backend"; 13 | import { ChessboardDnDProviderProps } from "../types"; 14 | 15 | const ChessboardDnDContext = createContext({ isCustomDndProviderSet: false }); 16 | 17 | const EmptyProvider: FC<{ children: ReactNode }> = ({ children }) => { 18 | return <>{children}; 19 | }; 20 | 21 | type ChessboardDnDRootProps = { 22 | customDndBackend?: BackendFactory; 23 | customDndBackendOptions: unknown; 24 | children: ReactNode; 25 | }; 26 | 27 | export const ChessboardDnDProvider: FC = ({ 28 | children, 29 | backend, 30 | context, 31 | options, 32 | debugMode, 33 | }) => { 34 | return ( 35 | 36 | 44 | {children} 45 | 46 | 47 | ); 48 | }; 49 | 50 | export const ChessboardDnDRoot: FC = ({ 51 | customDndBackend, 52 | customDndBackendOptions, 53 | children, 54 | }) => { 55 | const [clientWindow, setClientWindow] = useState(); 56 | const [backendSet, setBackendSet] = useState(false); 57 | const [isMobile, setIsMobile] = useState(false); 58 | const { isCustomDndProviderSet } = useContext(ChessboardDnDContext); 59 | 60 | useEffect(() => { 61 | setIsMobile("ontouchstart" in window); 62 | setBackendSet(true); 63 | setClientWindow(window); 64 | }, []); 65 | 66 | // in case we already wrapped `` with `` we don't need to create a new one 67 | const DnDWrapper = isCustomDndProviderSet ? EmptyProvider : DndProvider; 68 | 69 | if (!backendSet) { 70 | return null; 71 | } 72 | 73 | return clientWindow ? ( 74 | 79 | {children} 80 | 81 | ) : ( 82 | <>{children} 83 | ); 84 | }; 85 | -------------------------------------------------------------------------------- /src/chessboard/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from "react"; 2 | 3 | import { errorImage } from "../media/error"; 4 | 5 | type ErrorBoundaryProps = PropsWithChildren; 6 | 7 | export function ErrorBoundary({ children }: ErrorBoundaryProps) { 8 | try { 9 | return <>{children}; 10 | } catch (error) { 11 | console.log(error); 12 | return ; 13 | } 14 | } 15 | 16 | export function WhiteKing({ showError = false }) { 17 | return ( 18 |
26 |
33 | {errorImage.whiteKing} 34 |
35 | {showError &&

Something went wrong

} 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/chessboard/components/Notation.tsx: -------------------------------------------------------------------------------- 1 | import { COLUMNS } from "../consts"; 2 | import { useChessboard } from "../context/chessboard-context"; 3 | 4 | type NotationProps = { 5 | row: number; 6 | col: number; 7 | }; 8 | 9 | export function Notation({ row, col }: NotationProps) { 10 | const { 11 | boardOrientation, 12 | boardWidth, 13 | customDarkSquareStyle, 14 | customLightSquareStyle, 15 | customNotationStyle, 16 | } = useChessboard(); 17 | 18 | const whiteColor = customLightSquareStyle.backgroundColor; 19 | const blackColor = customDarkSquareStyle.backgroundColor; 20 | 21 | const isRow = col === 0; 22 | const isColumn = row === 7; 23 | const isBottomLeftSquare = isRow && isColumn; 24 | 25 | function getRow() { 26 | return boardOrientation === "white" ? 8 - row : row + 1; 27 | } 28 | 29 | function getColumn() { 30 | return boardOrientation === "black" ? COLUMNS[7 - col] : COLUMNS[col]; 31 | } 32 | 33 | function renderBottomLeft() { 34 | return ( 35 | <> 36 |
44 | {getRow()} 45 |
46 |
54 | {getColumn()} 55 |
56 | 57 | ); 58 | } 59 | 60 | function renderLetters() { 61 | return ( 62 |
71 | {getColumn()} 72 |
73 | ); 74 | } 75 | 76 | function renderNumbers() { 77 | return ( 78 |
89 | {getRow()} 90 |
91 | ); 92 | } 93 | 94 | if (isBottomLeftSquare) { 95 | return renderBottomLeft(); 96 | } 97 | 98 | if (isColumn) { 99 | return renderLetters(); 100 | } 101 | 102 | if (isRow) { 103 | return renderNumbers(); 104 | } 105 | 106 | return null; 107 | } 108 | 109 | const alphaStyle = (width: number, customNotationStyle?: Record) => ({ 110 | alignSelf: "flex-end", 111 | paddingLeft: width / 8 - width / 48, 112 | fontSize: width / 48, 113 | ...customNotationStyle 114 | }); 115 | 116 | const numericStyle = (width: number, customNotationStyle?: Record) => ({ 117 | alignSelf: "flex-start", 118 | paddingRight: width / 8 - width / 48, 119 | fontSize: width / 48, 120 | ...customNotationStyle 121 | }); 122 | -------------------------------------------------------------------------------- /src/chessboard/components/Piece.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect, useState } from "react"; 2 | import { useDrag } from "react-dnd"; 3 | import { getEmptyImage } from "react-dnd-html5-backend"; 4 | 5 | import { useChessboard } from "../context/chessboard-context"; 6 | import { Coords, CustomPieceFn, Piece as Pc, Square } from "../types"; 7 | 8 | type PieceProps = { 9 | isPremovedPiece?: boolean; 10 | piece: Pc; 11 | square: Square; 12 | squares: { [square in Square]?: Coords }; 13 | }; 14 | 15 | export function Piece({ 16 | isPremovedPiece = false, 17 | piece, 18 | square, 19 | squares, 20 | }: PieceProps) { 21 | const { 22 | animationDuration, 23 | arePiecesDraggable, 24 | boardWidth, 25 | boardOrientation, 26 | chessPieces, 27 | currentPosition, 28 | deletePieceFromSquare, 29 | dropOffBoardAction, 30 | id, 31 | isDraggablePiece, 32 | isWaitingForAnimation, 33 | onPieceClick, 34 | onPieceDragBegin, 35 | onPieceDragEnd, 36 | onPieceDropOffBoard, 37 | onPromotionCheck, 38 | positionDifferences, 39 | } = useChessboard(); 40 | 41 | const [pieceStyle, setPieceStyle] = useState({ 42 | opacity: 1, 43 | zIndex: 5, 44 | touchAction: "none", 45 | cursor: 46 | arePiecesDraggable && isDraggablePiece({ piece, sourceSquare: square }) 47 | ? "-webkit-grab" 48 | : "default", 49 | }); 50 | 51 | const [{ canDrag, isDragging }, drag, dragPreview] = useDrag( 52 | () => ({ 53 | type: "piece", 54 | item: () => { 55 | onPieceDragBegin(piece, square); 56 | return { piece, square, id }; 57 | }, 58 | end: (item, monitor) => { 59 | onPieceDragEnd(piece, square); 60 | 61 | const wasDropOutsideTheBoard = !monitor.didDrop(); 62 | 63 | if (wasDropOutsideTheBoard) { 64 | if (dropOffBoardAction === "trash") { 65 | deletePieceFromSquare(square); 66 | } 67 | 68 | onPieceDropOffBoard?.(square, piece); 69 | } 70 | }, 71 | collect: (monitor) => ({ 72 | canDrag: isDraggablePiece({ piece, sourceSquare: square }), 73 | isDragging: !!monitor.isDragging(), 74 | }), 75 | }), 76 | [piece, square, currentPosition, id] 77 | ); 78 | 79 | // hide the default preview 80 | dragPreview(getEmptyImage(), { captureDraggingState: true }); 81 | 82 | // hide piece on drag 83 | useEffect(() => { 84 | setPieceStyle((oldPieceStyle) => ({ 85 | ...oldPieceStyle, 86 | opacity: isDragging ? 0 : 1, 87 | })); 88 | }, [isDragging]); 89 | 90 | // new move has come in 91 | // if waiting for animation, then animation has started and we can perform animation 92 | // we need to head towards where we need to go, we are the source, we are heading towards the target 93 | useEffect(() => { 94 | const removedPiece = positionDifferences.removed?.[square]; 95 | // return as null and not loaded yet 96 | if (!positionDifferences.added || !removedPiece) return; 97 | // check if piece matches or if removed piece was a pawn and new square is on 1st or 8th rank (promotion) 98 | const newSquare = ( 99 | Object.entries(positionDifferences.added) as [Square, Pc][] 100 | ).find( 101 | ([s, p]) => 102 | p === removedPiece || onPromotionCheck(square, s, removedPiece) 103 | ); 104 | // we can perform animation if our square was in removed, AND the matching piece is in added AND this isn't a premoved piece 105 | if ( 106 | isWaitingForAnimation && 107 | removedPiece && 108 | newSquare && 109 | !isPremovedPiece 110 | ) { 111 | const sourceSq = square; 112 | const targetSq = newSquare[0]; 113 | if (sourceSq && targetSq) { 114 | const squareWidth = boardWidth / 8; 115 | setPieceStyle((oldPieceStyle) => ({ 116 | ...oldPieceStyle, 117 | transform: `translate(${ 118 | (boardOrientation === "black" ? -1 : 1) * 119 | (targetSq.charCodeAt(0) - sourceSq.charCodeAt(0)) * 120 | squareWidth 121 | }px, ${ 122 | (boardOrientation === "black" ? -1 : 1) * 123 | (Number(sourceSq[1]) - Number(targetSq[1])) * 124 | squareWidth 125 | }px)`, 126 | transition: `transform ${animationDuration}ms`, 127 | zIndex: 6, 128 | })); 129 | } 130 | } 131 | }, [positionDifferences]); 132 | 133 | // translate to their own positions (repaint on undo) 134 | useEffect(() => { 135 | const { sourceSq } = getSingleSquareCoordinates(); 136 | if (sourceSq) { 137 | setPieceStyle((oldPieceStyle) => ({ 138 | ...oldPieceStyle, 139 | transform: `translate(${0}px, ${0}px)`, 140 | transition: `transform ${0}ms`, 141 | })); 142 | } 143 | }, [currentPosition]); 144 | 145 | // update is piece draggable 146 | useEffect(() => { 147 | setPieceStyle((oldPieceStyle) => ({ 148 | ...oldPieceStyle, 149 | cursor: 150 | arePiecesDraggable && isDraggablePiece({ piece, sourceSquare: square }) 151 | ? "-webkit-grab" 152 | : "default", 153 | })); 154 | }, [square, currentPosition, arePiecesDraggable]); 155 | 156 | function getSingleSquareCoordinates() { 157 | return { sourceSq: squares[square] }; 158 | } 159 | 160 | return ( 161 |
onPieceClick(piece, square)} 164 | data-piece={piece} 165 | style={pieceStyle} 166 | > 167 | {typeof chessPieces[piece] === "function" ? ( 168 | (chessPieces[piece] as CustomPieceFn)({ 169 | squareWidth: boardWidth / 8, 170 | isDragging, 171 | square, 172 | }) 173 | ) : ( 174 | 180 | {chessPieces[piece] as ReactNode} 181 | 182 | )} 183 |
184 | ); 185 | } 186 | -------------------------------------------------------------------------------- /src/chessboard/components/PromotionDialog.tsx: -------------------------------------------------------------------------------- 1 | import { useChessboard } from "../context/chessboard-context"; 2 | import { getRelativeCoords } from "../functions"; 3 | import { PromotionPieceOption } from "../types"; 4 | import { PromotionOption } from "./PromotionOption"; 5 | 6 | export function PromotionDialog() { 7 | const { 8 | boardOrientation, 9 | boardWidth, 10 | promotionDialogVariant, 11 | promoteToSquare, 12 | } = useChessboard(); 13 | 14 | const promotePieceColor = promoteToSquare?.[1] === "1" ? "b" : "w"; 15 | const promotionOptions: PromotionPieceOption[] = [ 16 | `${promotePieceColor ?? "w"}Q`, 17 | `${promotePieceColor ?? "w"}R`, 18 | `${promotePieceColor ?? "w"}N`, 19 | `${promotePieceColor ?? "w"}B`, 20 | ]; 21 | 22 | const dialogStyles = { 23 | default: { 24 | display: "grid", 25 | gridTemplateColumns: "1fr 1fr", 26 | transform: `translate(${-boardWidth / 8}px, ${-boardWidth / 8}px)`, 27 | }, 28 | vertical: { 29 | transform: `translate(${-boardWidth / 16}px, ${-boardWidth / 16}px)`, 30 | }, 31 | modal: { 32 | display: "flex", 33 | justifyContent: "center", 34 | alignItems: "center", 35 | transform: `translate(0px, ${(3 * boardWidth) / 8}px)`, 36 | width: "100%", 37 | height: `${boardWidth / 4}px`, 38 | top: 0, 39 | backgroundColor: "white", 40 | left: 0, 41 | }, 42 | }; 43 | 44 | const dialogCoords = getRelativeCoords( 45 | boardOrientation, 46 | boardWidth, 47 | promoteToSquare || "a8" 48 | ); 49 | 50 | return ( 51 |
61 | {promotionOptions.map((option) => ( 62 | 63 | ))} 64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/chessboard/components/PromotionOption.tsx: -------------------------------------------------------------------------------- 1 | import { useState, ReactNode } from "react"; 2 | 3 | import { useChessboard } from "../context/chessboard-context"; 4 | import { CustomPieceFn, PromotionPieceOption } from "../types"; 5 | 6 | type Props = { 7 | option: PromotionPieceOption; 8 | }; 9 | 10 | export function PromotionOption({ option }: Props) { 11 | const [isHover, setIsHover] = useState(false); 12 | 13 | const { 14 | boardWidth, 15 | chessPieces, 16 | customDarkSquareStyle, 17 | customLightSquareStyle, 18 | handleSetPosition, 19 | onPromotionPieceSelect, 20 | promoteFromSquare, 21 | promoteToSquare, 22 | promotionDialogVariant, 23 | } = useChessboard(); 24 | 25 | const backgroundColor = () => { 26 | switch (option[1]) { 27 | case "Q": 28 | return customDarkSquareStyle.backgroundColor; 29 | case "R": 30 | return customLightSquareStyle.backgroundColor; 31 | case "N": 32 | return promotionDialogVariant === "default" 33 | ? customLightSquareStyle.backgroundColor 34 | : customDarkSquareStyle.backgroundColor; 35 | case "B": 36 | return promotionDialogVariant === "default" 37 | ? customDarkSquareStyle.backgroundColor 38 | : customLightSquareStyle.backgroundColor; 39 | } 40 | }; 41 | 42 | return ( 43 |
{ 45 | if ( 46 | onPromotionPieceSelect( 47 | option, 48 | promoteFromSquare ?? undefined, 49 | promoteToSquare ?? undefined 50 | ) 51 | ) 52 | handleSetPosition(promoteFromSquare!, promoteToSquare!, option, true); 53 | }} 54 | onMouseOver={() => setIsHover(true)} 55 | onMouseOut={() => setIsHover(false)} 56 | data-piece={option} 57 | style={{ 58 | cursor: "pointer", 59 | backgroundColor: isHover ? backgroundColor() : `${backgroundColor()}aa`, 60 | borderRadius: "4px", 61 | transition: "all 0.1s ease-out", 62 | }} 63 | > 64 | {typeof chessPieces[option] === "function" ? ( 65 |
71 | {(chessPieces[option] as CustomPieceFn)({ 72 | squareWidth: boardWidth / 8, 73 | isDragging: false, 74 | })} 75 |
76 | ) : ( 77 | 86 | {chessPieces[option] as ReactNode} 87 | 88 | )} 89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /src/chessboard/components/SparePiece.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { useDrag } from "react-dnd"; 3 | import { getEmptyImage } from "react-dnd-html5-backend"; 4 | 5 | import { CustomPieceFn, Piece as Pc } from "../types"; 6 | import { defaultPieces } from "../media/pieces"; 7 | 8 | type PieceProps = { 9 | piece: Pc; 10 | width: number; 11 | customPieceJSX?: CustomPieceFn; 12 | dndId: string; 13 | }; 14 | 15 | export const SparePiece = ({ 16 | piece, 17 | width, 18 | customPieceJSX, 19 | dndId, 20 | }: PieceProps) => { 21 | const renderPiece = customPieceJSX ?? defaultPieces[piece]; 22 | const [{ canDrag, isDragging }, drag, dragPreview] = useDrag( 23 | () => ({ 24 | type: "piece", 25 | item: () => { 26 | return { piece, isSpare: true, id: dndId }; 27 | }, 28 | 29 | collect: (monitor) => ({ 30 | canDrag: true, 31 | isDragging: !!monitor.isDragging(), 32 | }), 33 | }), 34 | [piece, dndId] 35 | ); 36 | 37 | // hide the default preview 38 | dragPreview(getEmptyImage(), { captureDraggingState: true }); 39 | 40 | return ( 41 |
46 | {typeof renderPiece === "function" ? ( 47 | (renderPiece as CustomPieceFn)({ 48 | squareWidth: width, 49 | isDragging, 50 | }) 51 | ) : ( 52 | 53 | {renderPiece as ReactNode} 54 | 55 | )} 56 |
57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /src/chessboard/components/Square.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect, useRef } from "react"; 2 | import { useDrop } from "react-dnd"; 3 | 4 | import { useChessboard } from "../context/chessboard-context"; 5 | import { BoardOrientation, Coords, Piece, Square as Sq } from "../types"; 6 | 7 | type SquareProps = { 8 | children: ReactNode; 9 | setSquares: React.Dispatch>; 10 | square: Sq; 11 | squareColor: "white" | "black"; 12 | squareHasPremove: boolean; 13 | }; 14 | 15 | export function Square({ 16 | square, 17 | squareColor, 18 | setSquares, 19 | squareHasPremove, 20 | children, 21 | }: SquareProps) { 22 | const squareRef = useRef(null); 23 | const { 24 | autoPromoteToQueen, 25 | boardWidth, 26 | boardOrientation, 27 | clearArrows, 28 | currentPosition, 29 | currentRightClickDown, 30 | customBoardStyle, 31 | customDarkSquareStyle, 32 | customDropSquareStyle, 33 | customLightSquareStyle, 34 | customPremoveDarkSquareStyle, 35 | customPremoveLightSquareStyle, 36 | customSquare: CustomSquare, 37 | customSquareStyles, 38 | drawNewArrow, 39 | handleSetPosition, 40 | handleSparePieceDrop, 41 | isWaitingForAnimation, 42 | lastPieceColour, 43 | lastSquareDraggedOver, 44 | onArrowDrawEnd, 45 | onDragOverSquare, 46 | onMouseOutSquare, 47 | onMouseOverSquare, 48 | onPieceDrop, 49 | onPromotionCheck, 50 | onRightClickDown, 51 | onRightClickUp, 52 | onSquareClick, 53 | setLastSquareDraggedOver, 54 | setPromoteFromSquare, 55 | setPromoteToSquare, 56 | setShowPromoteDialog, 57 | } = useChessboard(); 58 | 59 | const [{ isOver }, drop] = useDrop( 60 | () => ({ 61 | accept: "piece", 62 | drop: handleDrop, 63 | collect: (monitor) => ({ 64 | isOver: !!monitor.isOver(), 65 | }), 66 | }), 67 | [ 68 | square, 69 | currentPosition, 70 | onPieceDrop, 71 | isWaitingForAnimation, 72 | lastPieceColour, 73 | ] 74 | ); 75 | 76 | type BoardPiece = { 77 | piece: Piece; 78 | readonly isSpare: false; 79 | square: Sq; 80 | id: number; 81 | }; 82 | type SparePiece = { piece: Piece; readonly isSpare: true; id: number }; 83 | 84 | function handleDrop(item: BoardPiece | SparePiece) { 85 | if (item.isSpare) { 86 | handleSparePieceDrop(item.piece, square); 87 | return; 88 | } 89 | if (onPromotionCheck(item.square, square, item.piece)) { 90 | if (autoPromoteToQueen) { 91 | handleSetPosition( 92 | item.square, 93 | square, 94 | item.piece[0] === "w" ? "wQ" : "bQ" 95 | ); 96 | } else { 97 | setPromoteFromSquare(item.square); 98 | setPromoteToSquare(square); 99 | setShowPromoteDialog(true); 100 | } 101 | } else { 102 | handleSetPosition(item.square, square, item.piece, true); 103 | } 104 | } 105 | 106 | useEffect(() => { 107 | if (squareRef.current) { 108 | const { x, y } = squareRef.current.getBoundingClientRect(); 109 | setSquares((oldSquares) => ({ ...oldSquares, [square]: { x, y } })); 110 | } 111 | }, [boardWidth, boardOrientation]); 112 | 113 | const defaultSquareStyle = { 114 | ...borderRadius(square, boardOrientation, customBoardStyle), 115 | ...(squareColor === "black" 116 | ? customDarkSquareStyle 117 | : customLightSquareStyle), 118 | ...(squareHasPremove && 119 | (squareColor === "black" 120 | ? customPremoveDarkSquareStyle 121 | : customPremoveLightSquareStyle)), 122 | ...(isOver && customDropSquareStyle), 123 | }; 124 | 125 | return ( 126 |
{ 132 | // Handle touch events on tablet and mobile not covered by onMouseOver/onDragEnter 133 | const touchLocation = e.touches[0]; 134 | const touchElement = document.elementsFromPoint( 135 | touchLocation.clientX, 136 | touchLocation.clientY 137 | ); 138 | const draggedOverSquare = touchElement 139 | ?.find((el) => el.getAttribute("data-square")) 140 | ?.getAttribute("data-square") as Sq; 141 | if (draggedOverSquare && draggedOverSquare !== lastSquareDraggedOver) { 142 | setLastSquareDraggedOver(draggedOverSquare); 143 | onDragOverSquare(draggedOverSquare); 144 | } 145 | }} 146 | onMouseOver={(e) => { 147 | // noop if moving from child of square into square. 148 | 149 | if (e.buttons === 2 && currentRightClickDown) { 150 | drawNewArrow(currentRightClickDown, square); 151 | } 152 | 153 | if ( 154 | e.relatedTarget && 155 | e.currentTarget.contains(e.relatedTarget as Node) 156 | ) { 157 | return; 158 | } 159 | 160 | onMouseOverSquare(square); 161 | }} 162 | onMouseOut={(e) => { 163 | // noop if moving from square into a child of square. 164 | if ( 165 | e.relatedTarget && 166 | e.currentTarget.contains(e.relatedTarget as Node) 167 | ) 168 | return; 169 | onMouseOutSquare(square); 170 | }} 171 | onMouseDown={(e) => { 172 | if (e.button === 2) onRightClickDown(square); 173 | }} 174 | onMouseUp={(e) => { 175 | if (e.button === 2) { 176 | if (currentRightClickDown) 177 | onArrowDrawEnd(currentRightClickDown, square); 178 | onRightClickUp(square); 179 | } 180 | }} 181 | onDragEnter={() => onDragOverSquare(square)} 182 | onClick={() => { 183 | const piece = currentPosition[square]; 184 | onSquareClick(square, piece); 185 | clearArrows(); 186 | }} 187 | onContextMenu={(e) => { 188 | e.preventDefault(); 189 | }} 190 | > 191 | {typeof CustomSquare === "string" ? ( 192 | 202 | {children} 203 | 204 | ) : ( 205 | 215 | {children} 216 | 217 | )} 218 |
219 | ); 220 | } 221 | 222 | const center = { 223 | display: "flex", 224 | justifyContent: "center", 225 | }; 226 | 227 | const size = (width: number) => ({ 228 | width: width / 8, 229 | height: width / 8, 230 | }); 231 | 232 | const borderRadius = ( 233 | square: Sq, 234 | boardOrientation: BoardOrientation, 235 | customBoardStyle?: Record 236 | ) => { 237 | if (!customBoardStyle?.borderRadius) return {}; 238 | 239 | if (square === "a1") { 240 | return boardOrientation === "white" 241 | ? { borderBottomLeftRadius: customBoardStyle.borderRadius } 242 | : { borderTopRightRadius: customBoardStyle.borderRadius }; 243 | } 244 | if (square === "a8") { 245 | return boardOrientation === "white" 246 | ? { borderTopLeftRadius: customBoardStyle.borderRadius } 247 | : { borderBottomRightRadius: customBoardStyle.borderRadius }; 248 | } 249 | if (square === "h1") { 250 | return boardOrientation === "white" 251 | ? { borderBottomRightRadius: customBoardStyle.borderRadius } 252 | : { borderTopLeftRadius: customBoardStyle.borderRadius }; 253 | } 254 | if (square === "h8") { 255 | return boardOrientation === "white" 256 | ? { borderTopRightRadius: customBoardStyle.borderRadius } 257 | : { borderBottomLeftRadius: customBoardStyle.borderRadius }; 258 | } 259 | 260 | return {}; 261 | }; 262 | -------------------------------------------------------------------------------- /src/chessboard/components/Squares.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useMemo } from "react"; 2 | import { COLUMNS } from "../consts"; 3 | import { useChessboard } from "../context/chessboard-context"; 4 | import { Coords, Piece as Pc, Square as Sq } from "../types"; 5 | import { Notation } from "./Notation"; 6 | import { Piece } from "./Piece"; 7 | import { Square } from "./Square"; 8 | 9 | // this type shows the exact route of each premoved piece 10 | type PremovesHistory = { 11 | piece: Pc; 12 | premovesRoute: { sourceSq: Sq; targetSq: Sq; index: number }[]; 13 | }[]; 14 | 15 | export function Squares() { 16 | const [squares, setSquares] = useState<{ [square in Sq]?: Coords }>({}); 17 | 18 | const { 19 | arePremovesAllowed, 20 | boardOrientation, 21 | boardWidth, 22 | currentPosition, 23 | id, 24 | premoves, 25 | showBoardNotation, 26 | } = useChessboard(); 27 | 28 | const premovesHistory: PremovesHistory = useMemo(() => { 29 | const result: PremovesHistory = []; 30 | // if premoves aren't allowed, don't waste time on calculations 31 | if (!arePremovesAllowed) return []; 32 | 33 | premoves.forEach((premove, index) => { 34 | const { sourceSq, targetSq, piece } = premove; 35 | 36 | // determine if the premove is made by an already premoved piece 37 | const relatedPremovedPiece = result.find( 38 | (p) => 39 | p.piece === piece && p.premovesRoute.at(-1)?.targetSq === sourceSq 40 | ); 41 | 42 | // if premove has been made by already premoved piece then write the move to its `premovesRoute` field to be able find its final destination later 43 | if (relatedPremovedPiece) { 44 | relatedPremovedPiece.premovesRoute.push({ sourceSq, targetSq, index }); 45 | } 46 | // if premove has been made by standard piece create new object in `premovesHistory` where we will keep its own premoves 47 | else { 48 | result.push({ 49 | piece, 50 | // index is useful for scenarios where two or more pieces are targeting the same square 51 | premovesRoute: [{ sourceSq, targetSq, index }], 52 | }); 53 | } 54 | }); 55 | 56 | return result; 57 | }, [premoves]); 58 | 59 | return ( 60 |
61 | {[...Array(8)].map((_, r) => { 62 | return ( 63 |
71 | {[...Array(8)].map((_, c) => { 72 | const square = 73 | boardOrientation === "black" 74 | ? ((COLUMNS[7 - c] + (r + 1)) as Sq) 75 | : ((COLUMNS[c] + (8 - r)) as Sq); 76 | const squareColor = c % 2 === r % 2 ? "white" : "black"; 77 | const squareHasPremove = premoves.find( 78 | (p) => p.sourceSq === square || p.targetSq === square 79 | ); 80 | 81 | const squareHasPremoveTarget = premovesHistory 82 | .filter( 83 | ({ premovesRoute }) => 84 | premovesRoute.at(-1)?.targetSq === square 85 | ) 86 | //the premoved piece with the higher index will be shown, as it is the latest one 87 | .sort( 88 | (a, b) => 89 | b.premovesRoute.at(-1)?.index! - 90 | a.premovesRoute.at(-1)?.index! 91 | ) 92 | .at(0); 93 | 94 | return ( 95 | 102 | {!squareHasPremove && currentPosition[square] && ( 103 | 108 | )} 109 | {squareHasPremoveTarget && ( 110 | 116 | )} 117 | {showBoardNotation && } 118 | 119 | ); 120 | })} 121 |
122 | ); 123 | })} 124 |
125 | ); 126 | } 127 | -------------------------------------------------------------------------------- /src/chessboard/consts.ts: -------------------------------------------------------------------------------- 1 | import { BoardPosition } from "./types"; 2 | 3 | export const COLUMNS = "abcdefgh".split(""); 4 | 5 | export const START_POSITION_OBJECT: BoardPosition = { 6 | a8: "bR", 7 | b8: "bN", 8 | c8: "bB", 9 | d8: "bQ", 10 | e8: "bK", 11 | f8: "bB", 12 | g8: "bN", 13 | h8: "bR", 14 | a7: "bP", 15 | b7: "bP", 16 | c7: "bP", 17 | d7: "bP", 18 | e7: "bP", 19 | f7: "bP", 20 | g7: "bP", 21 | h7: "bP", 22 | a2: "wP", 23 | b2: "wP", 24 | c2: "wP", 25 | d2: "wP", 26 | e2: "wP", 27 | f2: "wP", 28 | g2: "wP", 29 | h2: "wP", 30 | a1: "wR", 31 | b1: "wN", 32 | c1: "wB", 33 | d1: "wQ", 34 | e1: "wK", 35 | f1: "wB", 36 | g1: "wN", 37 | h1: "wR", 38 | }; 39 | 40 | export const WHITE_COLUMN_VALUES: { [col in string]: number } = { 41 | a: 0, 42 | b: 1, 43 | c: 2, 44 | d: 3, 45 | e: 4, 46 | f: 5, 47 | g: 6, 48 | h: 7, 49 | }; 50 | export const BLACK_COLUMN_VALUES: { [col in string]: number } = { 51 | a: 7, 52 | b: 6, 53 | c: 5, 54 | d: 4, 55 | e: 3, 56 | f: 2, 57 | g: 1, 58 | h: 0, 59 | }; 60 | 61 | export const WHITE_ROWS = [7, 6, 5, 4, 3, 2, 1, 0]; 62 | export const BLACK_ROWS = [0, 1, 2, 3, 4, 5, 6, 7]; 63 | -------------------------------------------------------------------------------- /src/chessboard/context/chessboard-context.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createContext, 3 | forwardRef, 4 | ReactNode, 5 | useContext, 6 | useEffect, 7 | useImperativeHandle, 8 | useRef, 9 | useState, 10 | } from "react"; 11 | 12 | import { defaultPieces } from "../media/pieces"; 13 | import { 14 | convertPositionToObject, 15 | getPositionDifferences, 16 | isDifferentFromStart, 17 | } from "../functions"; 18 | import { 19 | BoardPosition, 20 | ChessboardProps, 21 | CustomPieces, 22 | Piece, 23 | Square, 24 | Arrow, 25 | } from "../types"; 26 | 27 | import { useArrows } from "../hooks/useArrows"; 28 | 29 | interface ChessboardProviderProps extends ChessboardProps { 30 | boardWidth: number; 31 | children: ReactNode; 32 | } 33 | 34 | type Premove = { 35 | sourceSq: Square; 36 | targetSq: Square; 37 | piece: Piece; 38 | }; 39 | 40 | type RequiredChessboardProps = Required; 41 | 42 | interface ChessboardProviderContext { 43 | // Props from user 44 | allowDragOutsideBoard: RequiredChessboardProps["allowDragOutsideBoard"]; 45 | animationDuration: RequiredChessboardProps["animationDuration"]; 46 | arePiecesDraggable: RequiredChessboardProps["arePiecesDraggable"]; 47 | arePremovesAllowed: RequiredChessboardProps["arePremovesAllowed"]; 48 | autoPromoteToQueen: RequiredChessboardProps["autoPromoteToQueen"]; 49 | boardOrientation: RequiredChessboardProps["boardOrientation"]; 50 | boardWidth: RequiredChessboardProps["boardWidth"]; 51 | customArrowColor: RequiredChessboardProps["customArrowColor"]; 52 | customBoardStyle: ChessboardProps["customBoardStyle"]; 53 | customNotationStyle: ChessboardProps["customNotationStyle"]; 54 | customDarkSquareStyle: RequiredChessboardProps["customDarkSquareStyle"]; 55 | customDropSquareStyle: RequiredChessboardProps["customDropSquareStyle"]; 56 | customLightSquareStyle: RequiredChessboardProps["customLightSquareStyle"]; 57 | customPremoveDarkSquareStyle: RequiredChessboardProps["customPremoveDarkSquareStyle"]; 58 | customPremoveLightSquareStyle: RequiredChessboardProps["customPremoveLightSquareStyle"]; 59 | customSquare: RequiredChessboardProps["customSquare"]; 60 | customSquareStyles: ChessboardProps["customSquareStyles"]; 61 | dropOffBoardAction: ChessboardProps["dropOffBoardAction"]; 62 | id: RequiredChessboardProps["id"]; 63 | isDraggablePiece: RequiredChessboardProps["isDraggablePiece"]; 64 | onDragOverSquare: RequiredChessboardProps["onDragOverSquare"]; 65 | onMouseOutSquare: RequiredChessboardProps["onMouseOutSquare"]; 66 | onMouseOverSquare: RequiredChessboardProps["onMouseOverSquare"]; 67 | onPieceClick: RequiredChessboardProps["onPieceClick"]; 68 | onPieceDragBegin: RequiredChessboardProps["onPieceDragBegin"]; 69 | onPieceDragEnd: RequiredChessboardProps["onPieceDragEnd"]; 70 | onPieceDrop: RequiredChessboardProps["onPieceDrop"]; 71 | onPieceDropOffBoard: ChessboardProps["onPieceDropOffBoard"]; 72 | onPromotionCheck: RequiredChessboardProps["onPromotionCheck"]; 73 | onPromotionPieceSelect: RequiredChessboardProps["onPromotionPieceSelect"]; 74 | onSparePieceDrop: ChessboardProps["onSparePieceDrop"]; 75 | onSquareClick: RequiredChessboardProps["onSquareClick"]; 76 | promotionDialogVariant: RequiredChessboardProps["promotionDialogVariant"]; 77 | showBoardNotation: RequiredChessboardProps["showBoardNotation"]; 78 | snapToCursor: RequiredChessboardProps["snapToCursor"]; 79 | 80 | // Exported by context 81 | arrows: Arrow[]; 82 | chessPieces: CustomPieces | Record; 83 | clearArrows: () => void; 84 | clearCurrentRightClickDown: () => void; 85 | currentPosition: BoardPosition; 86 | currentRightClickDown?: Square; 87 | deletePieceFromSquare: (sq: Square) => void; 88 | drawNewArrow: (from: Square, to: Square) => void; 89 | handleSetPosition: ( 90 | sourceSq: Square, 91 | targetSq: Square, 92 | piece: Piece, 93 | wasManualDropOverride?: boolean 94 | ) => void; 95 | handleSparePieceDrop: (piece: Piece, targetSq: Square) => void; 96 | isWaitingForAnimation: boolean; 97 | lastPieceColour: string | undefined; 98 | lastSquareDraggedOver: Square | null; 99 | newArrow?: Arrow; 100 | onArrowDrawEnd: (from: Square, to: Square) => void; 101 | onRightClickDown: (square: Square) => void; 102 | onRightClickUp: (square: Square) => void; 103 | positionDifferences: { added: BoardPosition; removed: BoardPosition }; 104 | premoves: Premove[]; 105 | promoteFromSquare: Square | null; 106 | promoteToSquare: Square | null; 107 | setLastSquareDraggedOver: React.Dispatch>; 108 | setPromoteFromSquare: React.Dispatch>; 109 | setPromoteToSquare: React.Dispatch>; 110 | setShowPromoteDialog: React.Dispatch>; 111 | showPromoteDialog: boolean; 112 | } 113 | 114 | export const ChessboardContext = createContext({} as ChessboardProviderContext); 115 | 116 | export const useChessboard = () => useContext(ChessboardContext); 117 | 118 | export const ChessboardProvider = forwardRef( 119 | ( 120 | { 121 | allowDragOutsideBoard = true, 122 | animationDuration = 300, 123 | areArrowsAllowed = true, 124 | arePiecesDraggable = true, 125 | arePremovesAllowed = false, 126 | autoPromoteToQueen = false, 127 | boardOrientation = "white", 128 | boardWidth, 129 | children, 130 | clearPremovesOnRightClick = true, 131 | customArrows, 132 | customArrowColor = "rgb(255,170,0)", 133 | customBoardStyle, 134 | customNotationStyle, 135 | customDarkSquareStyle = { backgroundColor: "#B58863" }, 136 | customDropSquareStyle = { 137 | boxShadow: "inset 0 0 1px 6px rgba(255,255,255,0.75)", 138 | }, 139 | customLightSquareStyle = { backgroundColor: "#F0D9B5" }, 140 | customPieces, 141 | customPremoveDarkSquareStyle = { backgroundColor: "#A42323" }, 142 | customPremoveLightSquareStyle = { backgroundColor: "#BD2828" }, 143 | customSquare = "div", 144 | customSquareStyles, 145 | dropOffBoardAction = "snapback", 146 | id = 0, 147 | isDraggablePiece = () => true, 148 | getPositionObject = () => {}, 149 | onArrowsChange = () => {}, 150 | onDragOverSquare = () => {}, 151 | onMouseOutSquare = () => {}, 152 | onMouseOverSquare = () => {}, 153 | onPieceClick = () => {}, 154 | onPieceDragBegin = () => {}, 155 | onPieceDragEnd = () => {}, 156 | onPieceDrop = () => true, 157 | onPieceDropOffBoard = () => {}, 158 | onPromotionCheck = (sourceSquare, targetSquare, piece) => { 159 | return ( 160 | ((piece === "wP" && 161 | sourceSquare[1] === "7" && 162 | targetSquare[1] === "8") || 163 | (piece === "bP" && 164 | sourceSquare[1] === "2" && 165 | targetSquare[1] === "1")) && 166 | Math.abs(sourceSquare.charCodeAt(0) - targetSquare.charCodeAt(0)) <= 1 167 | ); 168 | }, 169 | onPromotionPieceSelect = () => true, 170 | onSparePieceDrop = () => true, 171 | onSquareClick = () => {}, 172 | onSquareRightClick = () => {}, 173 | position = "start", 174 | promotionDialogVariant = "default", 175 | promotionToSquare = null, 176 | showBoardNotation = true, 177 | showPromotionDialog = false, 178 | snapToCursor = true, 179 | }: ChessboardProviderProps, 180 | ref 181 | ) => { 182 | // position stored and displayed on board 183 | const [currentPosition, setCurrentPosition] = useState( 184 | convertPositionToObject(position) 185 | ); 186 | 187 | // calculated differences between current and incoming positions 188 | const [positionDifferences, setPositionDifferences] = useState<{ 189 | added: BoardPosition; 190 | removed: BoardPosition; 191 | }>({ removed: {}, added: {} }); 192 | 193 | // colour of last piece moved to determine if premoving 194 | const [lastPieceColour, setLastPieceColour] = 195 | useState(undefined); 196 | 197 | // show / hide promotion dialog 198 | const [showPromoteDialog, setShowPromoteDialog] = useState( 199 | showPromotionDialog && !autoPromoteToQueen 200 | ); 201 | 202 | // which square a pawn is being promoted to 203 | const [promoteFromSquare, setPromoteFromSquare] = 204 | useState(null); 205 | const [promoteToSquare, setPromoteToSquare] = 206 | useState(promotionToSquare); 207 | 208 | // current premoves 209 | const [premoves, setPremoves] = useState([]); 210 | 211 | // ref used to access current value during timeouts (closures) 212 | const premovesRef = useRef(premoves); 213 | 214 | // current right mouse down square 215 | const [currentRightClickDown, setCurrentRightClickDown] = 216 | useState(); 217 | 218 | // chess pieces/styling 219 | const [chessPieces, setChessPieces] = useState({ 220 | ...defaultPieces, 221 | ...customPieces, 222 | }); 223 | 224 | // whether the last move was a manual drop or not 225 | const [wasManualDrop, setWasManualDrop] = useState(false); 226 | 227 | // the most recent timeout whilst waiting for animation to complete 228 | const previousTimeoutRef = useRef(); 229 | 230 | // if currently waiting for an animation to finish 231 | const [isWaitingForAnimation, setIsWaitingForAnimation] = useState(false); 232 | 233 | // last square dragged over for checking in touch events 234 | const [lastSquareDraggedOver, setLastSquareDraggedOver] = 235 | useState(null); 236 | 237 | // open clearPremoves() to allow user to call on undo/reset/whenever 238 | useImperativeHandle(ref, () => ({ 239 | clearPremoves(clearLastPieceColour = true) { 240 | clearPremoves(clearLastPieceColour); 241 | }, 242 | })); 243 | 244 | // handle custom pieces change 245 | useEffect(() => { 246 | setChessPieces({ ...defaultPieces, ...customPieces }); 247 | }, [customPieces]); 248 | 249 | // handle promote changes 250 | useEffect(() => { 251 | setShowPromoteDialog(showPromotionDialog); 252 | setPromoteToSquare(promotionToSquare); 253 | }, [promotionToSquare, showPromotionDialog]); 254 | 255 | // handle external position change 256 | useEffect(() => { 257 | // clear any open promotion dialogs 258 | clearPromotion(); 259 | 260 | const newPosition = convertPositionToObject(position); 261 | const differences = getPositionDifferences(currentPosition, newPosition); 262 | const newPieceColour = 263 | Object.keys(differences.added)?.length <= 2 264 | ? Object.entries(differences.added)?.[0]?.[1][0] 265 | : undefined; 266 | 267 | // external move has come in before animation is over 268 | // cancel animation and immediately update position 269 | if (isWaitingForAnimation) { 270 | setCurrentPosition(newPosition); 271 | setIsWaitingForAnimation(false); 272 | arePremovesAllowed && attemptPremove(newPieceColour); 273 | if (previousTimeoutRef.current) { 274 | clearTimeout(previousTimeoutRef.current); 275 | } 276 | } else { 277 | // move was made using drag and drop 278 | if (wasManualDrop) { 279 | setCurrentPosition(newPosition); 280 | setIsWaitingForAnimation(false); 281 | arePremovesAllowed && attemptPremove(newPieceColour); 282 | } else { 283 | // move was made by external position change 284 | 285 | // if position === start then don't override newPieceColour 286 | // needs isDifferentFromStart in scenario where premoves have been cleared upon board reset but first move is made by computer, the last move colour would need to be updated 287 | if ( 288 | isDifferentFromStart(newPosition) && 289 | lastPieceColour !== undefined 290 | ) { 291 | setLastPieceColour(newPieceColour); 292 | } else if (!isDifferentFromStart(newPosition)) { 293 | // position === start, likely a board reset. set to black to allow black to make premoves on first move 294 | setLastPieceColour("b"); 295 | } else { 296 | setLastPieceColour(undefined); 297 | } 298 | setPositionDifferences(differences); 299 | 300 | // animate external move 301 | setIsWaitingForAnimation(true); 302 | const newTimeout = setTimeout(() => { 303 | setCurrentPosition(newPosition); 304 | setIsWaitingForAnimation(false); 305 | arePremovesAllowed && attemptPremove(newPieceColour); 306 | }, animationDuration); 307 | previousTimeoutRef.current = newTimeout; 308 | } 309 | } 310 | 311 | // reset manual drop, ready for next move to be made by user or external 312 | setWasManualDrop(false); 313 | // inform latest position information 314 | getPositionObject(newPosition); 315 | // clear arrows 316 | clearArrows(); 317 | 318 | // clear timeout on unmount 319 | return () => { 320 | clearTimeout(previousTimeoutRef.current); 321 | }; 322 | }, [position]); 323 | 324 | const { arrows, newArrow, clearArrows, drawNewArrow, onArrowDrawEnd } = 325 | useArrows( 326 | customArrows, 327 | areArrowsAllowed, 328 | onArrowsChange, 329 | customArrowColor 330 | ); 331 | 332 | // handle drop position change 333 | function handleSetPosition( 334 | sourceSq: Square, 335 | targetSq: Square, 336 | piece: Piece, 337 | wasManualDropOverride?: boolean 338 | ) { 339 | // if dropped back down, don't do anything 340 | if (sourceSq === targetSq) { 341 | return; 342 | } 343 | 344 | clearArrows(); 345 | 346 | // if second move is made for same colour, or there are still premoves queued, then this move needs to be added to premove queue instead of played 347 | // premoves length check for colour is added in because white could make 3 premoves, and then black responds to the first move (changing the last piece colour) and then white pre-moves again 348 | if ( 349 | (arePremovesAllowed && isWaitingForAnimation) || 350 | (arePremovesAllowed && 351 | (lastPieceColour === piece[0] || 352 | premovesRef.current.filter((p: Premove) => p.piece[0] === piece[0]) 353 | .length > 0)) 354 | ) { 355 | const oldPremoves: Premove[] = [...premovesRef.current]; 356 | 357 | oldPremoves.push({ sourceSq, targetSq, piece }); 358 | premovesRef.current = oldPremoves; 359 | setPremoves([...oldPremoves]); 360 | clearPromotion(); 361 | return; 362 | } 363 | 364 | // if transitioning, don't allow new drop 365 | if (!arePremovesAllowed && isWaitingForAnimation) return; 366 | 367 | const newOnDropPosition = { ...currentPosition }; 368 | 369 | setWasManualDrop(!!wasManualDropOverride); 370 | setLastPieceColour(piece[0]); 371 | 372 | // if onPieceDrop function provided, execute it, position must be updated externally and captured by useEffect above for this move to show on board 373 | if (onPieceDrop.length) { 374 | const isValidMove = onPieceDrop(sourceSq, targetSq, piece); 375 | if (!isValidMove) { 376 | clearPremoves(); 377 | setWasManualDrop(false); 378 | } 379 | } else { 380 | // delete source piece 381 | delete newOnDropPosition[sourceSq]; 382 | 383 | // add piece in new position 384 | newOnDropPosition[targetSq] = piece; 385 | setCurrentPosition(newOnDropPosition); 386 | } 387 | 388 | clearPromotion(); 389 | 390 | // inform latest position information 391 | getPositionObject(newOnDropPosition); 392 | } 393 | 394 | function deletePieceFromSquare(square: Square) { 395 | const positionCopy = { ...currentPosition }; 396 | 397 | delete positionCopy[square]; 398 | setCurrentPosition(positionCopy); 399 | 400 | // inform latest position information 401 | getPositionObject(positionCopy); 402 | } 403 | function attemptPremove(newPieceColour?: string) { 404 | if (premovesRef.current.length === 0) return; 405 | 406 | // get current value of premove as this is called in a timeout so value may have changed since timeout was set 407 | const premove = premovesRef.current[0]; 408 | 409 | // if premove is a differing colour to last move made, then this move can be made 410 | if ( 411 | premove.piece[0] !== undefined && 412 | premove.piece[0] !== newPieceColour && 413 | onPieceDrop.length 414 | ) { 415 | setLastPieceColour(premove.piece[0]); 416 | setWasManualDrop(true); // pre-move doesn't need animation 417 | const isValidMove = onPieceDrop( 418 | premove.sourceSq, 419 | premove.targetSq, 420 | premove.piece 421 | ); 422 | 423 | // premove was successful and can be removed from queue 424 | if (isValidMove) { 425 | const oldPremoves = [...premovesRef.current]; 426 | oldPremoves.shift(); 427 | premovesRef.current = oldPremoves; 428 | setPremoves([...oldPremoves]); 429 | } else { 430 | // premove wasn't successful, clear premove queue 431 | clearPremoves(); 432 | } 433 | } 434 | } 435 | 436 | function handleSparePieceDrop(piece: Piece, targetSq: Square) { 437 | const isValidDrop = onSparePieceDrop(piece, targetSq); 438 | 439 | if (!isValidDrop) return; 440 | const newOnDropPosition = { ...currentPosition }; 441 | // add piece in new position 442 | newOnDropPosition[targetSq] = piece; 443 | setCurrentPosition(newOnDropPosition); 444 | 445 | // inform latest position information 446 | getPositionObject(newOnDropPosition); 447 | } 448 | 449 | function clearPremoves(clearLastPieceColour = true) { 450 | // don't clear when right clicking to clear, otherwise you won't be able to premove again before next go 451 | if (clearLastPieceColour) setLastPieceColour(undefined); 452 | premovesRef.current = []; 453 | setPremoves([]); 454 | } 455 | 456 | function clearPromotion() { 457 | setPromoteFromSquare(null); 458 | setPromoteToSquare(null); 459 | setShowPromoteDialog(false); 460 | } 461 | 462 | function onRightClickDown(square: Square) { 463 | setCurrentRightClickDown(square); 464 | } 465 | 466 | function onRightClickUp(square: Square) { 467 | if (currentRightClickDown) { 468 | // same square, don't draw an arrow, but do clear premoves and run onSquareRightClick 469 | if (currentRightClickDown === square) { 470 | setCurrentRightClickDown(undefined); 471 | clearPremovesOnRightClick && clearPremoves(false); 472 | onSquareRightClick(square); 473 | return; 474 | } 475 | } else setCurrentRightClickDown(undefined); 476 | } 477 | 478 | function clearCurrentRightClickDown() { 479 | setCurrentRightClickDown(undefined); 480 | } 481 | 482 | const ChessboardProviderContextValue: ChessboardProviderContext = { 483 | allowDragOutsideBoard, 484 | animationDuration, 485 | arePiecesDraggable, 486 | arePremovesAllowed, 487 | arrows, 488 | autoPromoteToQueen, 489 | boardOrientation, 490 | boardWidth, 491 | chessPieces, 492 | clearArrows, 493 | clearCurrentRightClickDown, 494 | currentPosition, 495 | currentRightClickDown, 496 | customArrowColor, 497 | customBoardStyle, 498 | customDarkSquareStyle, 499 | customDropSquareStyle, 500 | customLightSquareStyle, 501 | customNotationStyle, 502 | customPremoveDarkSquareStyle, 503 | customPremoveLightSquareStyle, 504 | customSquare, 505 | customSquareStyles, 506 | deletePieceFromSquare, 507 | drawNewArrow, 508 | dropOffBoardAction, 509 | handleSetPosition, 510 | handleSparePieceDrop, 511 | id, 512 | isDraggablePiece, 513 | isWaitingForAnimation, 514 | lastPieceColour, 515 | lastSquareDraggedOver, 516 | newArrow, 517 | onArrowDrawEnd, 518 | onDragOverSquare, 519 | onMouseOutSquare, 520 | onMouseOverSquare, 521 | onPieceClick, 522 | onPieceDragBegin, 523 | onPieceDragEnd, 524 | onPieceDrop, 525 | onPieceDropOffBoard, 526 | onPromotionCheck, 527 | onPromotionPieceSelect, 528 | onRightClickDown, 529 | onRightClickUp, 530 | onSparePieceDrop, 531 | onSquareClick, 532 | positionDifferences, 533 | premoves, 534 | promoteFromSquare, 535 | promoteToSquare, 536 | promotionDialogVariant, 537 | setLastSquareDraggedOver, 538 | setPromoteFromSquare, 539 | setPromoteToSquare, 540 | setShowPromoteDialog, 541 | showBoardNotation, 542 | showPromoteDialog, 543 | snapToCursor, 544 | }; 545 | 546 | return ( 547 | 548 | {children} 549 | 550 | ); 551 | } 552 | ); 553 | -------------------------------------------------------------------------------- /src/chessboard/functions.ts: -------------------------------------------------------------------------------- 1 | import { BoardOrientation, BoardPosition, Piece, Square } from "./types"; 2 | import { 3 | BLACK_COLUMN_VALUES, 4 | BLACK_ROWS, 5 | COLUMNS, 6 | START_POSITION_OBJECT, 7 | WHITE_COLUMN_VALUES, 8 | WHITE_ROWS, 9 | } from "./consts"; 10 | 11 | /** 12 | * Retrieves the coordinates at the centre of the requested square, relative to the top left of the board (0, 0). 13 | */ 14 | export function getRelativeCoords( 15 | boardOrientation: BoardOrientation, 16 | boardWidth: number, 17 | square: Square 18 | ): { 19 | x: number; 20 | y: number; 21 | } { 22 | const squareWidth = boardWidth / 8; 23 | const columns = 24 | boardOrientation === "white" ? WHITE_COLUMN_VALUES : BLACK_COLUMN_VALUES; 25 | const rows = boardOrientation === "white" ? WHITE_ROWS : BLACK_ROWS; 26 | 27 | const x = columns[square[0]] * squareWidth + squareWidth / 2; 28 | const y = rows[parseInt(square[1], 10) - 1] * squareWidth + squareWidth / 2; 29 | return { x, y }; 30 | } 31 | 32 | /** 33 | * Returns whether the passed position is different from the start position. 34 | */ 35 | export function isDifferentFromStart(newPosition: BoardPosition): boolean { 36 | let isDifferent = false; 37 | 38 | ( 39 | Object.keys(START_POSITION_OBJECT) as Array< 40 | keyof typeof START_POSITION_OBJECT 41 | > 42 | ).forEach((square) => { 43 | if (newPosition[square] !== START_POSITION_OBJECT[square]) 44 | isDifferent = true; 45 | }); 46 | 47 | (Object.keys(newPosition) as Array).forEach( 48 | (square) => { 49 | if (START_POSITION_OBJECT[square] !== newPosition[square]) 50 | isDifferent = true; 51 | } 52 | ); 53 | 54 | return isDifferent; 55 | } 56 | 57 | /** 58 | * Returns what pieces have been added and what pieces have been removed between board positions. 59 | */ 60 | export function getPositionDifferences( 61 | currentPosition: BoardPosition, 62 | newPosition: BoardPosition 63 | ): { 64 | added: BoardPosition; 65 | removed: BoardPosition; 66 | } { 67 | const difference: { added: BoardPosition; removed: BoardPosition } = { 68 | removed: {}, 69 | added: {}, 70 | }; 71 | 72 | // removed from current 73 | (Object.keys(currentPosition) as Array).forEach( 74 | (square) => { 75 | if (newPosition[square] !== currentPosition[square]) 76 | difference.removed[square] = currentPosition[square]; 77 | } 78 | ); 79 | 80 | // added from new 81 | (Object.keys(newPosition) as Array).forEach( 82 | (square) => { 83 | if (currentPosition[square] !== newPosition[square]) 84 | difference.added[square] = newPosition[square]; 85 | } 86 | ); 87 | 88 | return difference; 89 | } 90 | 91 | /** 92 | * Converts a fen string or existing position object to a position object. 93 | */ 94 | export function convertPositionToObject( 95 | position: string | BoardPosition 96 | ): BoardPosition { 97 | if (position === "start") { 98 | return START_POSITION_OBJECT; 99 | } 100 | 101 | if (typeof position === "string") { 102 | // attempt to convert fen to position object 103 | return fenToObj(position); 104 | } 105 | 106 | return position; 107 | } 108 | 109 | /** 110 | * Converts a fen string to a position object. 111 | */ 112 | function fenToObj(fen: string): BoardPosition { 113 | if (!isValidFen(fen)) return {}; 114 | 115 | // cut off any move, castling, etc info from the end. we're only interested in position information 116 | fen = fen.replace(/ .+$/, ""); 117 | const rows = fen.split("/"); 118 | const position: BoardPosition = {}; 119 | let currentRow = 8; 120 | 121 | for (let i = 0; i < 8; i++) { 122 | const row = rows[i].split(""); 123 | let colIdx = 0; 124 | 125 | // loop through each character in the FEN section 126 | for (let j = 0; j < row.length; j++) { 127 | // number / empty squares 128 | if (row[j].search(/[1-8]/) !== -1) { 129 | const numEmptySquares = parseInt(row[j], 10); 130 | colIdx = colIdx + numEmptySquares; 131 | } else { 132 | // piece 133 | const square = COLUMNS[colIdx] + currentRow; 134 | position[square as Square] = fenToPieceCode(row[j]); 135 | colIdx = colIdx + 1; 136 | } 137 | } 138 | currentRow = currentRow - 1; 139 | } 140 | return position; 141 | } 142 | 143 | /** 144 | * Returns whether string is valid fen notation. 145 | */ 146 | function isValidFen(fen: string): boolean { 147 | // cut off any move, castling, etc info from the end. we're only interested in position information 148 | fen = fen.replace(/ .+$/, ""); 149 | 150 | // expand the empty square numbers to just 1s 151 | fen = expandFenEmptySquares(fen); 152 | 153 | // fen should be 8 sections separated by slashes 154 | const chunks = fen.split("/"); 155 | if (chunks.length !== 8) return false; 156 | 157 | // check each section 158 | for (let i = 0; i < 8; i++) { 159 | if (chunks[i].length !== 8 || chunks[i].search(/[^kqrnbpKQRNBP1]/) !== -1) { 160 | return false; 161 | } 162 | } 163 | 164 | return true; 165 | } 166 | 167 | /** 168 | * Expand out fen notation to countable characters for validation 169 | */ 170 | function expandFenEmptySquares(fen: string): string { 171 | return fen 172 | .replace(/8/g, "11111111") 173 | .replace(/7/g, "1111111") 174 | .replace(/6/g, "111111") 175 | .replace(/5/g, "11111") 176 | .replace(/4/g, "1111") 177 | .replace(/3/g, "111") 178 | .replace(/2/g, "11"); 179 | } 180 | 181 | /** 182 | * Convert fen piece code to camel case notation. e.g. bP, wK. 183 | */ 184 | function fenToPieceCode(piece: string): Piece { 185 | // black piece 186 | if (piece.toLowerCase() === piece) { 187 | return ("b" + piece.toUpperCase()) as Piece; 188 | } 189 | // white piece 190 | return ("w" + piece.toUpperCase()) as Piece; 191 | } 192 | -------------------------------------------------------------------------------- /src/chessboard/hooks/useArrows.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { Square, Arrow } from "../types"; 3 | 4 | type Arrows = Arrow[]; 5 | 6 | export const useArrows = ( 7 | customArrows?: Arrows, 8 | areArrowsAllowed: boolean = true, 9 | onArrowsChange?: (arrows: Arrows) => void, 10 | customArrowColor?: string 11 | ) => { 12 | // arrows passed programatically to `ChessBoard` as a react prop 13 | const [customArrowsSet, setCustomArrows] = useState([]); 14 | 15 | // arrows drawn with mouse by user on the board 16 | const [arrows, setArrows] = useState([]); 17 | 18 | // active arrow which user draws while dragging mouse 19 | const [newArrow, setNewArrow] = useState(); 20 | 21 | // handle external `customArrows` props changes 22 | useEffect(() => { 23 | if (Array.isArray(customArrows)) { 24 | // so that custom arrows overwrite temporary arrows 25 | clearArrows(); 26 | setCustomArrows( 27 | //filter out arrows which starts and ends in the same square 28 | customArrows?.filter((arrow) => arrow[0] !== arrow[1]) 29 | ); 30 | } 31 | }, [customArrows]); 32 | 33 | // callback when arrows changed after user interaction 34 | useEffect(() => { 35 | onArrowsChange?.(arrows); 36 | }, [arrows]); 37 | 38 | // function clears all arrows drawed by user 39 | function clearArrows() { 40 | setArrows([]); 41 | setNewArrow(undefined); 42 | } 43 | 44 | const drawNewArrow = (fromSquare: Square, toSquare: Square) => { 45 | if (!areArrowsAllowed) return; 46 | 47 | setNewArrow([fromSquare, toSquare, customArrowColor]); 48 | }; 49 | 50 | const allBoardArrows = [...arrows, ...customArrowsSet]; 51 | 52 | const onArrowDrawEnd = (fromSquare: Square, toSquare: Square) => { 53 | if (fromSquare === toSquare || !areArrowsAllowed) return; 54 | 55 | let arrowsCopy; 56 | const newArrow: Arrow = [fromSquare, toSquare, customArrowColor]; 57 | 58 | const isNewArrowUnique = allBoardArrows.every(([arrowFrom, arrowTo]) => { 59 | return !(arrowFrom === fromSquare && arrowTo === toSquare); 60 | }); 61 | 62 | // add the newArrow to arrows array if it is unique 63 | if (isNewArrowUnique) { 64 | arrowsCopy = [...arrows, newArrow]; 65 | } 66 | // remove it from the board if we already have same arrow in arrows array 67 | else { 68 | arrowsCopy = arrows.filter(([arrowFrom, arrowTo]) => { 69 | return !(arrowFrom === fromSquare && arrowTo === toSquare); 70 | }); 71 | } 72 | 73 | setNewArrow(undefined); 74 | setArrows(arrowsCopy); 75 | }; 76 | 77 | return { 78 | arrows: allBoardArrows, 79 | newArrow, 80 | clearArrows, 81 | drawNewArrow, 82 | setArrows, 83 | onArrowDrawEnd, 84 | }; 85 | }; 86 | -------------------------------------------------------------------------------- /src/chessboard/index.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, useEffect, useMemo, useRef, useState } from "react"; 2 | 3 | import { Board } from "./components/Board"; 4 | import { ChessboardDnDRoot } from "./components/DnDRoot"; 5 | import { ChessboardProps } from "./types"; 6 | import { ChessboardProvider } from "./context/chessboard-context"; 7 | import { CustomDragLayer } from "./components/CustomDragLayer"; 8 | import { ErrorBoundary } from "./components/ErrorBoundary"; 9 | // spare pieces component 10 | // semantic release with github actions 11 | // improved arrows 12 | 13 | // npm publish --tag alpha 14 | // npm publish --dry-run 15 | 16 | // rewrite readme, add link to react-chessboard-svg for simply showing a chess position 17 | // add other things from chessground 18 | // change board orientation to 'w' or 'b'? like used in chess.js? 19 | // Animation on premove? - only set manual drop to false in useEffect if not attempting successful premove 20 | 21 | export type ClearPremoves = { 22 | clearPremoves: (clearLastPieceColour?: boolean) => void; 23 | }; 24 | 25 | export { SparePiece } from "./components/SparePiece"; 26 | export { ChessboardDnDProvider } from "./components/DnDRoot"; 27 | 28 | export const Chessboard = forwardRef( 29 | (props, ref) => { 30 | const { 31 | customDndBackend, 32 | customDndBackendOptions, 33 | onBoardWidthChange, 34 | ...otherProps 35 | } = props; 36 | const [boardWidth, setBoardWidth] = useState(props.boardWidth); 37 | 38 | const boardRef = useRef(null); 39 | const boardContainerRef = useRef(null); 40 | 41 | const [boardContainerPos, setBoardContainerPos] = useState({ 42 | left: 0, 43 | top: 0, 44 | }); 45 | 46 | const metrics = useMemo( 47 | () => boardRef.current?.getBoundingClientRect(), 48 | [boardRef.current] 49 | ); 50 | 51 | useEffect(() => { 52 | boardWidth && onBoardWidthChange?.(boardWidth); 53 | }, [boardWidth]); 54 | 55 | useEffect(() => { 56 | setBoardContainerPos({ 57 | left: metrics?.left ? metrics?.left : 0, 58 | top: metrics?.top ? metrics?.top : 0, 59 | }); 60 | }, [metrics]); 61 | 62 | useEffect(() => { 63 | if (props.boardWidth === undefined && boardRef.current?.offsetWidth) { 64 | const resizeObserver = new ResizeObserver(() => { 65 | setBoardWidth(boardRef.current?.offsetWidth as number); 66 | }); 67 | resizeObserver.observe(boardRef.current); 68 | 69 | return () => { 70 | resizeObserver.disconnect(); 71 | }; 72 | } 73 | }, [boardRef.current]); 74 | 75 | return ( 76 | 77 |
85 |
86 | 90 | {boardWidth && ( 91 | 96 | 97 | 98 | 99 | )} 100 | 101 |
102 | 103 | ); 104 | } 105 | ); 106 | -------------------------------------------------------------------------------- /src/chessboard/media/error.tsx: -------------------------------------------------------------------------------- 1 | export const errorImage = { 2 | whiteKing: ( 3 | 19 | 20 | 27 | 28 | 29 | ), 30 | }; 31 | -------------------------------------------------------------------------------- /src/chessboard/media/pieces.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | // https://commons.wikimedia.org/wiki/Category:SVG_chess_pieces 3 | // By en:User:Cburnett - Own work 4 | // This W3C - unspecified vector image was created with Inkscape., CC BY - SA 3.0, https://commons.wikimedia.org/w/index.php?curid=1499810 5 | 6 | export const defaultPieces: Record = { 7 | wP: ( 8 | 14 | 30 | 31 | ), 32 | wR: ( 33 | 39 | 54 | 58 | 62 | 66 | 67 | 71 | 72 | 76 | 77 | 78 | ), 79 | wN: ( 80 | 86 | 101 | 105 | 109 | 113 | 118 | 119 | 120 | ), 121 | wB: ( 122 | 128 | 143 | 146 | 147 | 148 | 149 | 150 | 154 | 155 | 156 | ), 157 | wQ: ( 158 | 164 | 172 | 173 | 174 | 175 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | ), 187 | wK: ( 188 | 194 | 208 | 212 | 216 | 225 | 229 | 233 | 237 | 241 | 242 | 243 | ), 244 | bP: ( 245 | 251 | 267 | 268 | ), 269 | bR: ( 270 | 276 | 291 | 295 | 299 | 303 | 307 | 311 | 315 | 324 | 333 | 342 | 351 | 360 | 361 | 362 | ), 363 | bN: ( 364 | 370 | 385 | 389 | 393 | 397 | 402 | 406 | 407 | 408 | ), 409 | bB: ( 410 | 416 | 431 | 434 | 435 | 436 | 437 | 438 | 442 | 443 | 444 | ), 445 | bQ: ( 446 | 452 | 461 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | ), 486 | bK: ( 487 | 493 | 507 | 512 | 521 | 525 | 529 | 533 | 537 | 538 | 539 | ), 540 | }; 541 | -------------------------------------------------------------------------------- /src/chessboard/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { FC, ReactElement, ReactNode, Ref, RefObject } from "react"; 2 | import { BackendFactory } from "dnd-core"; 3 | 4 | export type Square = 5 | | "a8" 6 | | "b8" 7 | | "c8" 8 | | "d8" 9 | | "e8" 10 | | "f8" 11 | | "g8" 12 | | "h8" 13 | | "a7" 14 | | "b7" 15 | | "c7" 16 | | "d7" 17 | | "e7" 18 | | "f7" 19 | | "g7" 20 | | "h7" 21 | | "a6" 22 | | "b6" 23 | | "c6" 24 | | "d6" 25 | | "e6" 26 | | "f6" 27 | | "g6" 28 | | "h6" 29 | | "a5" 30 | | "b5" 31 | | "c5" 32 | | "d5" 33 | | "e5" 34 | | "f5" 35 | | "g5" 36 | | "h5" 37 | | "a4" 38 | | "b4" 39 | | "c4" 40 | | "d4" 41 | | "e4" 42 | | "f4" 43 | | "g4" 44 | | "h4" 45 | | "a3" 46 | | "b3" 47 | | "c3" 48 | | "d3" 49 | | "e3" 50 | | "f3" 51 | | "g3" 52 | | "h3" 53 | | "a2" 54 | | "b2" 55 | | "c2" 56 | | "d2" 57 | | "e2" 58 | | "f2" 59 | | "g2" 60 | | "h2" 61 | | "a1" 62 | | "b1" 63 | | "c1" 64 | | "d1" 65 | | "e1" 66 | | "f1" 67 | | "g1" 68 | | "h1"; 69 | 70 | export type Piece = 71 | | "wP" 72 | | "wB" 73 | | "wN" 74 | | "wR" 75 | | "wQ" 76 | | "wK" 77 | | "bP" 78 | | "bB" 79 | | "bN" 80 | | "bR" 81 | | "bQ" 82 | | "bK"; 83 | 84 | export type BoardPosition = { [square in Square]?: Piece }; 85 | 86 | export type PromotionPieceOption = 87 | | "wQ" 88 | | "wR" 89 | | "wN" 90 | | "wB" 91 | | "bQ" 92 | | "bR" 93 | | "bN" 94 | | "bB"; 95 | export type PromotionStyle = "default" | "vertical" | "modal"; 96 | 97 | export type CustomSquareProps = { 98 | children: ReactNode; 99 | // Allow user to specify their outer element 100 | // Opting not to use generics for simplicity 101 | ref: Ref; 102 | square: Square; 103 | squareColor: "white" | "black"; 104 | style: Record; 105 | }; 106 | 107 | export type CustomSquareRenderer = 108 | | FC 109 | | keyof JSX.IntrinsicElements; 110 | 111 | export type CustomPieceFnArgs = { 112 | isDragging: boolean; 113 | squareWidth: number; 114 | square?: Square; 115 | }; 116 | 117 | export type CustomPieceFn = (args: CustomPieceFnArgs) => ReactElement; 118 | 119 | export type CustomPieces = { 120 | [key in Piece]?: CustomPieceFn; 121 | }; 122 | 123 | export type CustomSquareStyles = { 124 | [key in Square]?: Record; 125 | }; 126 | 127 | export type BoardOrientation = "white" | "black"; 128 | 129 | export type DropOffBoardAction = "snapback" | "trash"; 130 | 131 | export type Coords = { x: number; y: number }; 132 | 133 | export type Arrow = [Square, Square, string?]; 134 | 135 | export type ChessboardProps = { 136 | /** 137 | * Whether or not the piece can be dragged outside of the board 138 | * @default true 139 | * */ 140 | allowDragOutsideBoard?: boolean; 141 | /** 142 | * Time in milliseconds for piece to slide to target square. Only used when the position is programmatically changed. If a new position is set before the animation is complete, the board will cancel the current animation and snap to the new position. 143 | * @default 300 144 | */ 145 | animationDuration?: number; 146 | /** 147 | * Whether or not arrows can be drawn with right click and dragging. 148 | * @default true 149 | */ 150 | areArrowsAllowed?: boolean; 151 | /** 152 | * Whether or not all pieces are draggable. 153 | * @default true 154 | */ 155 | arePiecesDraggable?: boolean; 156 | /** 157 | * Whether or not premoves are allowed. 158 | * @default false 159 | */ 160 | arePremovesAllowed?: boolean; 161 | /** 162 | * Whether or not to automatically promote pawn to queen 163 | * @default false 164 | */ 165 | autoPromoteToQueen?: boolean; 166 | /** 167 | * The orientation of the board, the chosen colour will be at the bottom of the board. 168 | * @default white 169 | */ 170 | boardOrientation?: BoardOrientation; 171 | /** 172 | * The width of the board in pixels. 173 | */ 174 | boardWidth?: number; 175 | /** 176 | * If premoves are allowed, whether or not to clear the premove queue on right click. 177 | * @default true 178 | */ 179 | clearPremovesOnRightClick?: boolean; 180 | /** 181 | * Array where each element is a tuple containing two Square values (representing the 'from' and 'to' squares) and an optional third string element for the arrow color 182 | * e.g. [ ['a3', 'a5', 'red'], ['b1, 'd5] ]. 183 | * If third element in array is missing arrow will have `customArrowColor` or default color value 184 | * @default [] 185 | */ 186 | customArrows?: Arrow[]; 187 | /** 188 | * String with rgb or hex value to colour drawn arrows. 189 | * @default rgb(255,170,0) 190 | */ 191 | customArrowColor?: string; 192 | /** 193 | * Custom board style object e.g. { borderRadius: '5px', boxShadow: '0 5px 15px rgba(0, 0, 0, 0.5)'}. 194 | * @default {} 195 | */ 196 | customBoardStyle?: Record; 197 | /** 198 | * Custom notation style object e.g. { fontSize: '12px' } 199 | * @default {} 200 | */ 201 | customNotationStyle?: Record; 202 | /** 203 | * Custom dark square style object. 204 | * @default { backgroundColor: "#B58863" } 205 | */ 206 | customDarkSquareStyle?: Record; 207 | /** 208 | * Custom react-dnd backend to use instead of the one provided by react-chessboard. 209 | */ 210 | customDndBackend?: BackendFactory; 211 | /** 212 | * Options to use for the given custom react-dnd backend. See customDndBackend. 213 | */ 214 | customDndBackendOptions?: unknown; 215 | /** 216 | * Custom drop square style object (Square being hovered over with dragged piece). 217 | * @default { boxShadow: "inset 0 0 1px 6px rgba(255,255,255,0.75)" } 218 | */ 219 | customDropSquareStyle?: Record; 220 | /** 221 | * Custom light square style object. 222 | * @default { backgroundColor: "#F0D9B5" } 223 | */ 224 | customLightSquareStyle?: Record; 225 | /** 226 | * Custom pieces object where each key must match a corresponding chess piece (wP, wB, wN, wR, wQ, wK, bP, bB, bN, bR, bQ, bK). The value of each piece is a function that takes in some optional arguments to use and must return JSX to render. e.g. { wK: ({ isDragging: boolean, squareWidth: number }) => jsx }. 227 | * @default {} 228 | */ 229 | customPieces?: CustomPieces; 230 | /** 231 | * Custom premove dark square style object. 232 | * @default { backgroundColor: "#A42323" } 233 | */ 234 | customPremoveDarkSquareStyle?: Record; 235 | /** 236 | * Custom premove light square style object. 237 | * @default { backgroundColor: "#BD2828" } 238 | */ 239 | customPremoveLightSquareStyle?: Record; 240 | /** 241 | * Custom square renderer for all squares. 242 | * @default div 243 | */ 244 | customSquare?: CustomSquareRenderer; 245 | /** 246 | * Custom styles for all squares. 247 | * @default {} 248 | */ 249 | customSquareStyles?: CustomSquareStyles; 250 | /** 251 | * Action to take when piece is dropped off the board. 252 | * @default snapback 253 | */ 254 | dropOffBoardAction?: DropOffBoardAction; 255 | /** 256 | * User function that is run when piece is dropped off the board. 257 | * @default snapback 258 | */ 259 | onPieceDropOffBoard?: (sourceSquare: Square, piece: Piece) => void; 260 | /** 261 | * Board identifier, necessary if more than one board is mounted for drag and drop. 262 | * @default 0 263 | */ 264 | id?: string | number; 265 | /** 266 | * Function called when a piece drag is attempted. Returns if piece is draggable. 267 | * @default () => true 268 | */ 269 | isDraggablePiece?: (args: { piece: Piece; sourceSquare: Square }) => boolean; 270 | /** 271 | * User function that receives current position object when position changes. 272 | * @default () => {} 273 | */ 274 | getPositionObject?: (currentPosition: BoardPosition) => any; 275 | /** 276 | * User function is run when arrows are set on the board. 277 | * @default () => {} 278 | */ 279 | onArrowsChange?: (squares: Arrow[]) => void; 280 | /** 281 | * Action to take when chessboard width has been changed 282 | * @default false 283 | */ 284 | onBoardWidthChange?: (boardWidth: number) => void; 285 | /** 286 | * User function that is run when piece is dragged over a square. 287 | * @default () => {} 288 | */ 289 | onDragOverSquare?: (square: Square) => any; 290 | /** 291 | * User function that is run when mouse leaves a square. 292 | * @default () => {} 293 | */ 294 | onMouseOutSquare?: (square: Square) => any; 295 | /** 296 | * User function that is run when mouse is over a square. 297 | * @default () => {} 298 | */ 299 | onMouseOverSquare?: (square: Square) => any; 300 | /** 301 | * User function that is run when piece is clicked. 302 | * @default () => {} 303 | */ 304 | onPieceClick?: (piece: Piece, square: Square) => any; 305 | /** 306 | * User function that is run when piece is grabbed to start dragging. 307 | * @default () => {} 308 | */ 309 | onPieceDragBegin?: (piece: Piece, sourceSquare: Square) => any; 310 | /** 311 | * User function that is run when piece is let go after dragging. 312 | * @default () => {} 313 | */ 314 | onPieceDragEnd?: (piece: Piece, sourceSquare: Square) => any; 315 | /** 316 | * User function that is run when piece is dropped on a square. Must return whether the move was successful or not. 317 | * @default () => true 318 | */ 319 | onPieceDrop?: ( 320 | sourceSquare: Square, 321 | targetSquare: Square, 322 | piece: Piece 323 | ) => boolean; 324 | /** 325 | * User function that is run when spare piece is dropped on a square. Must return whether the drop was successful or not. 326 | * @default () => true 327 | */ 328 | onSparePieceDrop?: (piece: Piece, targetSquare: Square) => boolean; 329 | /** 330 | * User function that is run when piece is dropped. Must return whether the move results in a promotion or not. 331 | * @default (sourceSquare, targetSquare, piece) => (((piece === "wP" && sourceSquare[1] === "7" && targetSquare[1] === "8") || 332 | * (piece === "bP" && sourceSquare[1] === "2" && targetSquare[1] === "1")) && 333 | * Math.abs(sourceSquare.charCodeAt(0) - targetSquare.charCodeAt(0)) <= 1) 334 | */ 335 | onPromotionCheck?: ( 336 | sourceSquare: Square, 337 | targetSquare: Square, 338 | piece: Piece 339 | ) => boolean; 340 | /** 341 | * User function that is run when a promotion piece is selected. Must return whether the move was successful or not. 342 | * @default () => true 343 | */ 344 | onPromotionPieceSelect?: ( 345 | piece?: PromotionPieceOption, 346 | promoteFromSquare?: Square, 347 | promoteToSquare?: Square 348 | ) => boolean; 349 | /** 350 | * User function that is run when a square is clicked. 351 | * @default () => {} 352 | */ 353 | onSquareClick?: (square: Square, piece: Piece | undefined) => any; 354 | /** 355 | * User function that is run when a square is right clicked. 356 | * @default () => {} 357 | */ 358 | onSquareRightClick?: (square: Square) => any; 359 | /** 360 | * FEN string or position object notating where the chess pieces are on the board. Start position can also be notated with the string: 'start'. 361 | * @default start 362 | */ 363 | position?: string | BoardPosition; 364 | /** 365 | * Style of promotion dialog. 366 | * @default default 367 | */ 368 | promotionDialogVariant?: PromotionStyle; 369 | /** 370 | * The square to promote a piece to. 371 | * @default null 372 | */ 373 | promotionToSquare?: Square | null; 374 | /** 375 | * RefObject that is sent as forwardRef to chessboard. 376 | */ 377 | ref?: RefObject; 378 | /** 379 | * Whether or not to show the file and rank co-ordinates (a..h, 1..8). 380 | * @default true 381 | */ 382 | showBoardNotation?: boolean; 383 | /** 384 | * Whether or not to show the promotion dialog. 385 | * @default false 386 | */ 387 | showPromotionDialog?: boolean; 388 | /** 389 | * Whether or not to center dragged pieces on the mouse cursor. 390 | * @default true 391 | */ 392 | snapToCursor?: boolean; 393 | }; 394 | 395 | export type ChessboardDnDProviderProps = { 396 | children: ReactNode; 397 | backend?: BackendFactory; 398 | context?: unknown; 399 | options?: unknown; 400 | debugMode?: boolean; 401 | }; 402 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./chessboard"; 2 | -------------------------------------------------------------------------------- /stories/Chessboard.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useEffect, useRef, useState, useMemo } from "react"; 2 | import { Meta } from "@storybook/react"; 3 | import { Chess } from "chess.js"; 4 | 5 | import { 6 | Chessboard, 7 | ClearPremoves, 8 | SparePiece, 9 | ChessboardDnDProvider, 10 | } from "../src"; 11 | import { CustomSquareProps, Piece, Square } from "../src/chessboard/types"; 12 | import Engine from "./stockfish/engine"; 13 | 14 | const buttonStyle = { 15 | cursor: "pointer", 16 | padding: "10px 20px", 17 | margin: "10px 10px 0px 0px", 18 | borderRadius: "6px", 19 | backgroundColor: "#f0d9b5", 20 | border: "none", 21 | boxShadow: "0 2px 5px rgba(0, 0, 0, 0.5)", 22 | }; 23 | 24 | const inputStyle = { 25 | padding: "10px 20px", 26 | margin: "10px 0 10px 0", 27 | borderRadius: "6px", 28 | border: "none", 29 | boxShadow: "0 2px 5px rgba(0, 0, 0, 0.5)", 30 | width: "100%", 31 | }; 32 | 33 | const boardWrapper = { 34 | width: `70vw`, 35 | maxWidth: "70vh", 36 | margin: "3rem auto", 37 | }; 38 | 39 | const meta: Meta = { 40 | title: "Chessboard", 41 | component: Chessboard, 42 | decorators: [ 43 | (Story) => ( 44 |
45 | 46 |
47 | ), 48 | ], 49 | }; 50 | export default meta; 51 | 52 | export const Default = () => { 53 | return ; 54 | }; 55 | 56 | export const PlayVsRandom = () => { 57 | const [game, setGame] = useState(new Chess()); 58 | const [currentTimeout, setCurrentTimeout] = useState(); 59 | 60 | function safeGameMutate(modify) { 61 | setGame((g) => { 62 | const update = { ...g }; 63 | modify(update); 64 | return update; 65 | }); 66 | } 67 | 68 | function makeRandomMove() { 69 | const possibleMoves = game.moves(); 70 | 71 | // exit if the game is over 72 | if (game.game_over() || game.in_draw() || possibleMoves.length === 0) 73 | return; 74 | 75 | const randomIndex = Math.floor(Math.random() * possibleMoves.length); 76 | safeGameMutate((game) => { 77 | game.move(possibleMoves[randomIndex]); 78 | }); 79 | } 80 | 81 | function onDrop(sourceSquare, targetSquare, piece) { 82 | const gameCopy = { ...game }; 83 | const move = gameCopy.move({ 84 | from: sourceSquare, 85 | to: targetSquare, 86 | promotion: piece[1].toLowerCase() ?? "q", 87 | }); 88 | setGame(gameCopy); 89 | 90 | // illegal move 91 | if (move === null) return false; 92 | 93 | // store timeout so it can be cleared on undo/reset so computer doesn't execute move 94 | const newTimeout = setTimeout(makeRandomMove, 200); 95 | setCurrentTimeout(newTimeout); 96 | return true; 97 | } 98 | 99 | return ( 100 |
101 | 110 | 121 | 132 |
133 | ); 134 | }; 135 | 136 | export const PlayVsComputer = () => { 137 | const levels = { 138 | "Easy 🤓": 2, 139 | "Medium 🧐": 8, 140 | "Hard 😵": 18, 141 | }; 142 | const engine = useMemo(() => new Engine(), []); 143 | const game = useMemo(() => new Chess(), []); 144 | 145 | const [gamePosition, setGamePosition] = useState(game.fen()); 146 | const [stockfishLevel, setStockfishLevel] = useState(2); 147 | 148 | function findBestMove() { 149 | engine.evaluatePosition(game.fen(), stockfishLevel); 150 | 151 | engine.onMessage(({ bestMove }) => { 152 | if (bestMove) { 153 | // In latest chess.js versions you can just write ```game.move(bestMove)``` 154 | game.move({ 155 | from: bestMove.substring(0, 2), 156 | to: bestMove.substring(2, 4), 157 | promotion: bestMove.substring(4, 5), 158 | }); 159 | 160 | setGamePosition(game.fen()); 161 | } 162 | }); 163 | } 164 | 165 | function onDrop(sourceSquare, targetSquare, piece) { 166 | const move = game.move({ 167 | from: sourceSquare, 168 | to: targetSquare, 169 | promotion: piece[1].toLowerCase() ?? "q", 170 | }); 171 | setGamePosition(game.fen()); 172 | 173 | // illegal move 174 | if (move === null) return false; 175 | 176 | // exit if the game is over 177 | if (game.game_over() || game.in_draw()) return false; 178 | 179 | findBestMove(); 180 | 181 | return true; 182 | } 183 | 184 | return ( 185 |
186 |
193 | {Object.entries(levels).map(([level, depth]) => ( 194 | 203 | ))} 204 |
205 | 206 | 211 | 212 | 221 | 231 |
232 | ); 233 | }; 234 | 235 | export const ClickToMove = () => { 236 | const [game, setGame] = useState(new Chess()); 237 | const [moveFrom, setMoveFrom] = useState(""); 238 | const [moveTo, setMoveTo] = useState(null); 239 | const [showPromotionDialog, setShowPromotionDialog] = useState(false); 240 | const [rightClickedSquares, setRightClickedSquares] = useState({}); 241 | const [moveSquares, setMoveSquares] = useState({}); 242 | const [optionSquares, setOptionSquares] = useState({}); 243 | 244 | function safeGameMutate(modify) { 245 | setGame((g) => { 246 | const update = { ...g }; 247 | modify(update); 248 | return update; 249 | }); 250 | } 251 | 252 | function getMoveOptions(square) { 253 | const moves = game.moves({ 254 | square, 255 | verbose: true, 256 | }); 257 | if (moves.length === 0) { 258 | setOptionSquares({}); 259 | return false; 260 | } 261 | 262 | const newSquares = {}; 263 | moves.map((move) => { 264 | newSquares[move.to] = { 265 | background: 266 | game.get(move.to) && 267 | game.get(move.to).color !== game.get(square).color 268 | ? "radial-gradient(circle, rgba(0,0,0,.1) 85%, transparent 85%)" 269 | : "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)", 270 | borderRadius: "50%", 271 | }; 272 | return move; 273 | }); 274 | newSquares[square] = { 275 | background: "rgba(255, 255, 0, 0.4)", 276 | }; 277 | setOptionSquares(newSquares); 278 | return true; 279 | } 280 | 281 | function makeRandomMove() { 282 | const possibleMoves = game.moves(); 283 | 284 | // exit if the game is over 285 | if (game.game_over() || game.in_draw() || possibleMoves.length === 0) 286 | return; 287 | 288 | const randomIndex = Math.floor(Math.random() * possibleMoves.length); 289 | safeGameMutate((game) => { 290 | game.move(possibleMoves[randomIndex]); 291 | }); 292 | } 293 | 294 | function onSquareClick(square) { 295 | setRightClickedSquares({}); 296 | 297 | // from square 298 | if (!moveFrom) { 299 | const hasMoveOptions = getMoveOptions(square); 300 | if (hasMoveOptions) setMoveFrom(square); 301 | return; 302 | } 303 | 304 | // to square 305 | if (!moveTo) { 306 | // check if valid move before showing dialog 307 | const moves = game.moves({ 308 | moveFrom, 309 | verbose: true, 310 | }); 311 | const foundMove = moves.find( 312 | (m) => m.from === moveFrom && m.to === square 313 | ); 314 | // not a valid move 315 | if (!foundMove) { 316 | // check if clicked on new piece 317 | const hasMoveOptions = getMoveOptions(square); 318 | // if new piece, setMoveFrom, otherwise clear moveFrom 319 | setMoveFrom(hasMoveOptions ? square : ""); 320 | return; 321 | } 322 | 323 | // valid move 324 | setMoveTo(square); 325 | 326 | // if promotion move 327 | if ( 328 | (foundMove.color === "w" && 329 | foundMove.piece === "p" && 330 | square[1] === "8") || 331 | (foundMove.color === "b" && 332 | foundMove.piece === "p" && 333 | square[1] === "1") 334 | ) { 335 | setShowPromotionDialog(true); 336 | return; 337 | } 338 | 339 | // is normal move 340 | const gameCopy = { ...game }; 341 | const move = gameCopy.move({ 342 | from: moveFrom, 343 | to: square, 344 | promotion: "q", 345 | }); 346 | 347 | // if invalid, setMoveFrom and getMoveOptions 348 | if (move === null) { 349 | const hasMoveOptions = getMoveOptions(square); 350 | if (hasMoveOptions) setMoveFrom(square); 351 | return; 352 | } 353 | 354 | setGame(gameCopy); 355 | 356 | setTimeout(makeRandomMove, 300); 357 | setMoveFrom(""); 358 | setMoveTo(null); 359 | setOptionSquares({}); 360 | return; 361 | } 362 | } 363 | 364 | function onPromotionPieceSelect(piece) { 365 | // if no piece passed then user has cancelled dialog, don't make move and reset 366 | if (piece) { 367 | const gameCopy = { ...game }; 368 | gameCopy.move({ 369 | from: moveFrom, 370 | to: moveTo, 371 | promotion: piece[1].toLowerCase() ?? "q", 372 | }); 373 | setGame(gameCopy); 374 | setTimeout(makeRandomMove, 300); 375 | } 376 | 377 | setMoveFrom(""); 378 | setMoveTo(null); 379 | setShowPromotionDialog(false); 380 | setOptionSquares({}); 381 | return true; 382 | } 383 | 384 | function onSquareRightClick(square) { 385 | const colour = "rgba(0, 0, 255, 0.4)"; 386 | setRightClickedSquares({ 387 | ...rightClickedSquares, 388 | [square]: 389 | rightClickedSquares[square] && 390 | rightClickedSquares[square].backgroundColor === colour 391 | ? undefined 392 | : { backgroundColor: colour }, 393 | }); 394 | } 395 | 396 | return ( 397 |
398 | 418 | 431 | 444 |
445 | ); 446 | }; 447 | 448 | export const PremovesEnabled = () => { 449 | const [game, setGame] = useState(new Chess()); 450 | const [currentTimeout, setCurrentTimeout] = useState(); 451 | const chessboardRef = useRef(null); 452 | 453 | function safeGameMutate(modify) { 454 | setGame((g) => { 455 | const update = { ...g }; 456 | modify(update); 457 | return update; 458 | }); 459 | } 460 | 461 | function makeRandomMove() { 462 | const possibleMoves = game.moves(); 463 | 464 | // exit if the game is over 465 | if (game.game_over() || game.in_draw() || possibleMoves.length === 0) 466 | return; 467 | 468 | const randomIndex = Math.floor(Math.random() * possibleMoves.length); 469 | safeGameMutate((game) => { 470 | game.move(possibleMoves[randomIndex]); 471 | }); 472 | } 473 | 474 | function onDrop(sourceSquare, targetSquare, piece) { 475 | const gameCopy = { ...game }; 476 | const move = gameCopy.move({ 477 | from: sourceSquare, 478 | to: targetSquare, 479 | promotion: piece[1].toLowerCase() ?? "q", 480 | }); 481 | setGame(gameCopy); 482 | 483 | // illegal move 484 | if (move === null) return false; 485 | 486 | // store timeout so it can be cleared on undo/reset so computer doesn't execute move 487 | const newTimeout = setTimeout(makeRandomMove, 2000); 488 | setCurrentTimeout(newTimeout); 489 | return true; 490 | } 491 | 492 | return ( 493 |
494 | piece[0] === "w"} 499 | onPieceDrop={onDrop} 500 | customBoardStyle={{ 501 | borderRadius: "4px", 502 | boxShadow: "0 2px 10px rgba(0, 0, 0, 0.5)", 503 | }} 504 | ref={chessboardRef} 505 | allowDragOutsideBoard={false} 506 | /> 507 | 521 | 537 |
538 | ); 539 | }; 540 | 541 | export const StyledBoard = () => { 542 | const [game, setGame] = useState(new Chess()); 543 | 544 | function safeGameMutate(modify) { 545 | setGame((g) => { 546 | const update = { ...g }; 547 | modify(update); 548 | return update; 549 | }); 550 | } 551 | 552 | function onDrop(sourceSquare, targetSquare, piece) { 553 | const gameCopy = { ...game }; 554 | const move = gameCopy.move({ 555 | from: sourceSquare, 556 | to: targetSquare, 557 | promotion: piece[1].toLowerCase() ?? "q", 558 | }); 559 | setGame(gameCopy); 560 | return move; 561 | } 562 | 563 | const pieces = [ 564 | "wP", 565 | "wN", 566 | "wB", 567 | "wR", 568 | "wQ", 569 | "wK", 570 | "bP", 571 | "bN", 572 | "bB", 573 | "bR", 574 | "bQ", 575 | "bK", 576 | ]; 577 | 578 | const customPieces = useMemo(() => { 579 | const pieceComponents = {}; 580 | pieces.forEach((piece) => { 581 | pieceComponents[piece] = ({ squareWidth }) => ( 582 |
590 | ); 591 | }); 592 | return pieceComponents; 593 | }, []); 594 | 595 | return ( 596 |
597 | 610 | 620 | 630 |
631 | ); 632 | }; 633 | 634 | export const StyledNotations = () => { 635 | const [game, setGame] = useState(new Chess()); 636 | 637 | return ( 638 |
639 | 648 |
649 | ); 650 | }; 651 | 652 | export const Styled3DBoard = () => { 653 | const engine = useMemo(() => new Engine(), []); 654 | const game = useMemo(() => new Chess(), []); 655 | 656 | const [gamePosition, setGamePosition] = useState(game.fen()); 657 | 658 | function findBestMove() { 659 | engine.evaluatePosition(game.fen()); 660 | 661 | engine.onMessage(({ bestMove }) => { 662 | if (bestMove) { 663 | game.move({ 664 | from: bestMove.substring(0, 2), 665 | to: bestMove.substring(2, 4), 666 | promotion: bestMove.substring(4, 5), 667 | }); 668 | 669 | setGamePosition(game.fen()); 670 | } 671 | }); 672 | } 673 | 674 | function onDrop(sourceSquare, targetSquare, piece) { 675 | const move = game.move({ 676 | from: sourceSquare, 677 | to: targetSquare, 678 | promotion: piece[1].toLowerCase() ?? "q", 679 | }); 680 | setGamePosition(game.fen()); 681 | 682 | // illegal move 683 | if (move === null) return false; 684 | 685 | // exit if the game is over 686 | if (game.game_over() || game.in_draw()) return false; 687 | 688 | findBestMove(); 689 | 690 | return true; 691 | } 692 | 693 | const [activeSquare, setActiveSquare] = useState(""); 694 | 695 | const threeDPieces = useMemo(() => { 696 | const pieces = [ 697 | { piece: "wP", pieceHeight: 1 }, 698 | { piece: "wN", pieceHeight: 1.2 }, 699 | { piece: "wB", pieceHeight: 1.2 }, 700 | { piece: "wR", pieceHeight: 1.2 }, 701 | { piece: "wQ", pieceHeight: 1.5 }, 702 | { piece: "wK", pieceHeight: 1.6 }, 703 | { piece: "bP", pieceHeight: 1 }, 704 | { piece: "bN", pieceHeight: 1.2 }, 705 | { piece: "bB", pieceHeight: 1.2 }, 706 | { piece: "bR", pieceHeight: 1.2 }, 707 | { piece: "bQ", pieceHeight: 1.5 }, 708 | { piece: "bK", pieceHeight: 1.6 }, 709 | ]; 710 | 711 | const pieceComponents = {}; 712 | pieces.forEach(({ piece, pieceHeight }) => { 713 | pieceComponents[piece] = ({ squareWidth, square }) => ( 714 |
722 | 732 |
733 | ); 734 | }); 735 | return pieceComponents; 736 | }, []); 737 | 738 | return ( 739 |
740 |
741 | 750 | 760 |
761 | setActiveSquare(sq)} 802 | onMouseOutSquare={(sq) => setActiveSquare("")} 803 | /> 804 |
805 | ); 806 | }; 807 | 808 | export const CustomSquare = () => { 809 | const CustomSquareRenderer = forwardRef( 810 | (props, ref) => { 811 | const { children, square, squareColor, style } = props; 812 | 813 | return ( 814 |
815 | {children} 816 |
832 | {square} 833 |
834 |
835 | ); 836 | } 837 | ); 838 | 839 | return ( 840 |
841 | 846 |
847 | ); 848 | }; 849 | 850 | export const AnalysisBoard = () => { 851 | const engine = useMemo(() => new Engine(), []); 852 | const game = useMemo(() => new Chess(), []); 853 | const inputRef = useRef(null); 854 | const [chessBoardPosition, setChessBoardPosition] = useState(game.fen()); 855 | const [positionEvaluation, setPositionEvaluation] = useState(0); 856 | const [depth, setDepth] = useState(10); 857 | const [bestLine, setBestline] = useState(""); 858 | const [possibleMate, setPossibleMate] = useState(""); 859 | 860 | function findBestMove() { 861 | engine.evaluatePosition(chessBoardPosition, 18); 862 | 863 | engine.onMessage(({ positionEvaluation, possibleMate, pv, depth }) => { 864 | if (depth && depth < 10) return; 865 | 866 | positionEvaluation && 867 | setPositionEvaluation( 868 | ((game.turn() === "w" ? 1 : -1) * Number(positionEvaluation)) / 100 869 | ); 870 | possibleMate && setPossibleMate(possibleMate); 871 | depth && setDepth(depth); 872 | pv && setBestline(pv); 873 | }); 874 | } 875 | 876 | function onDrop(sourceSquare, targetSquare, piece) { 877 | const move = game.move({ 878 | from: sourceSquare, 879 | to: targetSquare, 880 | promotion: piece[1].toLowerCase() ?? "q", 881 | }); 882 | setPossibleMate(""); 883 | setChessBoardPosition(game.fen()); 884 | 885 | // illegal move 886 | if (move === null) return false; 887 | 888 | engine.stop(); 889 | setBestline(""); 890 | 891 | if (game.game_over() || game.in_draw()) return false; 892 | 893 | return true; 894 | } 895 | 896 | useEffect(() => { 897 | if (!game.game_over() || game.in_draw()) { 898 | findBestMove(); 899 | } 900 | }, [chessBoardPosition]); 901 | 902 | const bestMove = bestLine?.split(" ")?.[0]; 903 | const handleFenInputChange = (e) => { 904 | const { valid } = game.validate_fen(e.target.value); 905 | 906 | if (valid && inputRef.current) { 907 | inputRef.current.value = e.target.value; 908 | game.load(e.target.value); 909 | setChessBoardPosition(game.fen()); 910 | } 911 | }; 912 | return ( 913 |
914 |

915 | Position Evaluation:{" "} 916 | {possibleMate ? `#${possibleMate}` : positionEvaluation} 917 | {"; "} 918 | Depth: {depth} 919 |

920 |
921 | Best line: {bestLine.slice(0, 40)} ... 922 |
923 | 929 | 949 | 960 | 971 |
972 | ); 973 | }; 974 | export const BoardWithCustomArrows = () => { 975 | const colorVariants = [ 976 | "darkred", 977 | "#48AD7E", 978 | "rgb(245, 192, 0)", 979 | "#093A3E", 980 | "#F75590", 981 | "#F3752B", 982 | "#058ED9", 983 | ]; 984 | const [activeColor, setActiveColor] = useState(colorVariants[0]); 985 | const [customArrows, setCustomArrows] = useState([ 986 | ["a2", "a3", colorVariants[0]], 987 | ["b2", "b4", colorVariants[1]], 988 | ["c2", "c5", colorVariants[2]], 989 | ]); 990 | return ( 991 |
992 |
999 |
Choose arrow color
1000 |
1007 | {colorVariants.map((color) => ( 1008 |
setActiveColor(color)} 1020 | /> 1021 | ))} 1022 |
1023 |
1024 | 1030 |
1031 | ); 1032 | }; 1033 | 1034 | /////////////////////////////////// 1035 | ////////// ManualBoardEditor ////// 1036 | /////////////////////////////////// 1037 | export const ManualBoardEditor = () => { 1038 | const game = useMemo(() => new Chess("8/8/8/8/8/8/8/8 w - - 0 1"), []); // empty board 1039 | const [boardOrientation, setBoardOrientation] = 1040 | useState<"white" | "black">("white"); 1041 | const [boardWidth, setBoardWidth] = useState(360); 1042 | const [fenPosition, setFenPosition] = useState(game.fen()); 1043 | 1044 | const handleSparePieceDrop = (piece, targetSquare) => { 1045 | const color = piece[0]; 1046 | const type = piece[1].toLowerCase(); 1047 | 1048 | const success = game.put({ type, color }, targetSquare); 1049 | 1050 | if (success) { 1051 | setFenPosition(game.fen()); 1052 | } else { 1053 | alert( 1054 | `The board already contains ${color === "w" ? "WHITE" : "BLACK"} KING` 1055 | ); 1056 | } 1057 | 1058 | return success; 1059 | }; 1060 | 1061 | const handlePieceDrop = (sourceSquare, targetSquare, piece) => { 1062 | const color = piece[0]; 1063 | const type = piece[1].toLowerCase(); 1064 | 1065 | // this is hack to avoid chess.js bug, which I've fixed in the latest version https://github.com/jhlywa/chess.js/pull/426 1066 | game.remove(sourceSquare); 1067 | game.remove(targetSquare); 1068 | const success = game.put({ type, color }, targetSquare); 1069 | 1070 | if (success) setFenPosition(game.fen()); 1071 | 1072 | return success; 1073 | }; 1074 | 1075 | const handlePieceDropOffBoard = (sourceSquare) => { 1076 | game.remove(sourceSquare); 1077 | setFenPosition(game.fen()); 1078 | }; 1079 | 1080 | const handleFenInputChange = (e) => { 1081 | const fen = e.target.value; 1082 | const { valid } = game.validate_fen(fen); 1083 | 1084 | setFenPosition(fen); 1085 | if (valid) { 1086 | game.load(fen); 1087 | setFenPosition(game.fen()); 1088 | } 1089 | }; 1090 | const pieces = [ 1091 | "wP", 1092 | "wN", 1093 | "wB", 1094 | "wR", 1095 | "wQ", 1096 | "wK", 1097 | "bP", 1098 | "bN", 1099 | "bB", 1100 | "bR", 1101 | "bQ", 1102 | "bK", 1103 | ]; 1104 | 1105 | return ( 1106 |
1113 | 1114 |
1115 |
1121 | {pieces.slice(6, 12).map((piece) => ( 1122 | 1128 | ))} 1129 |
1130 | 1144 |
1150 | {pieces.slice(0, 6).map((piece) => ( 1151 | 1157 | ))} 1158 |
1159 |
1160 |
1161 | 1170 | 1179 | 1189 |
1190 | 1196 |
1197 |
1198 | ); 1199 | }; 1200 | -------------------------------------------------------------------------------- /stories/StockfishIntegration.mdx: -------------------------------------------------------------------------------- 1 | import { Canvas, Meta } from "@storybook/blocks"; 2 | 3 | import * as StockfishIntegrationStories from './StockfishIntegration.stories'; 4 | 5 | 6 | 7 | # `Stockfish` Integration with `react-chessboard` 8 | 9 | > ℹ️ *You can use "Stockfish" with "react-chessboard" to craft chess games against bots, aid players in learning, analyze their matches, or assist in identifying optimal moves. 10 | > Stockfish is a powerful chess engine, considered one of the best in the world. Using a search algorithm and evaluation functions, it plays chess at a high level, and has 3000+ FIDE ELO rating. 11 | > Currently there are two stockfish engine implementations which you can use directly on your browser. [stockfish.wasm](https://github.com/lichess-org/stockfish.wasm) and [stockfish.js](http://github.com/nmrugg/stockfish.js). 12 | > They both are open source and free to use in any project. Although their strength of playing chess are the same, the "stockfish.wasm" is lighter and offers better calculating performance, but it is not compatible with old browsers.* 13 | 14 |
15 | 16 | #### For playing against stockfish using "react-chessboard" you have to do these steps below: 17 | 18 | 1. ###### Download "stockfish.js" file: 19 | 20 | You can download the "stockfish.js" file directly from [react-chessboard GitHub repository](https://github.com/Clariity/react-chessboard/blob/main/stories/stockfish/stockfish.js). For the latest updates and more info visit https://github.com/nmrugg/stockfish.js 21 | 22 | *** 23 | 24 | 2. ###### Place downloaded "stockfish.js" file into your React app's "public" folder: 25 | 26 | Since "stockfish.js" requires substantial calculation resources, it should be run as a [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers). Placing it in the "public" folder allows it to be used from any point in your React app. 27 | 28 | *** 29 | 30 | 3. ###### Run "stockfish.js" as a web worker and it is ready to handle your UCI commands 31 | 32 | Below is a simple usage example of 'stockfish.js': 33 | 34 | ```js 35 | useEffect(() => { 36 | const stockfish = new Worker("./stockfish.js"); 37 | const DEPTH = 8; // number of halfmoves the engine looks ahead 38 | const FEN_POSITION = 39 | "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; 40 | 41 | stockfish.postMessage("uci"); 42 | stockfish.postMessage(`position fen ${FEN_POSITION}`); 43 | stockfish.postMessage(`go depth ${DEPTH}`); 44 | 45 | stockfish.onmessage = (e) => { 46 | console.log(e.data); // in the console output you will see `bestmove e2e4` message 47 | }; 48 | }, []); 49 | ``` 50 | 51 | *** 52 | 53 | 4. ##### Create `Engine` class with stockfish commands you need for easy use 54 | 55 | The more advanced example of `Engine` class you can find [in our GitHub](https://github.com/Clariity/react-chessboard/blob/main/stories/stockfish/engine.ts) 56 | 57 | ```js 58 | class Engine { 59 | constructor() { 60 | this.stockfish = new Worker("./stockfish.js"); 61 | this.onMessage = (callback) => { 62 | this.stockfish.addEventListener("message", (e) => { 63 | const bestMove = e.data?.match(/bestmove\s+(\S+)/)?.[1]; 64 | 65 | callback({ bestMove }); 66 | }); 67 | }; 68 | // Init engine 69 | this.sendMessage("uci"); 70 | this.sendMessage("isready"); 71 | } 72 | 73 | evaluatePosition(fen, depth) { 74 | this.stockfish.postMessage(`position fen ${fen}`); 75 | this.stockfish.postMessage(`go depth ${depth}`); 76 | } 77 | stop() { 78 | this.sendMessage("stop"); // Run when changing positions 79 | } 80 | quit() { 81 | this.sendMessage("quit"); // Good to run this before unmounting. 82 | } 83 | } 84 | ``` 85 | 86 | *** 87 | 88 | 5. ##### Ready! You now can use [Engine](https://github.com/Clariity/react-chessboard/blob/main/stories/stockfish/engine.ts) for detecting the best moves on your `react-chessboard` 89 | 90 | The example below will create game where stockfish plays agianst itself 91 | 92 | ```jsx 93 | export const StockfishVsStockfish = () => { 94 | const engine = useMemo(() => new Engine(), []); 95 | const game = useMemo(() => new Chess(), []); 96 | const [chessBoardPosition, setChessBoardPosition] = useState(game.fen()); 97 | 98 | function findBestMove() { 99 | engine.evaluatePosition(game.fen(), 10); 100 | engine.onMessage(({ bestMove }) => { 101 | if (bestMove) { 102 | game.move({ 103 | from: bestMove.substring(0, 2), 104 | to: bestMove.substring(2, 4), 105 | promotion: bestMove.substring(4, 5), 106 | }); 107 | 108 | setChessBoardPosition(game.fen()); 109 | } 110 | }); 111 | } 112 | 113 | useEffect(() => { 114 | if (!game.game_over() || game.in_draw()) { 115 | setTimeout(findBestMove, 300); 116 | } 117 | }, [chessBoardPosition]); 118 | 119 | return ; 120 | }; 121 | ``` 122 | -------------------------------------------------------------------------------- /stories/StockfishIntegration.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { Meta, StoryObj } from '@storybook/react'; 3 | 4 | import { Chessboard } from "../src"; 5 | 6 | const boardWrapper = { 7 | width: `70vw`, 8 | maxWidth: "70vh", 9 | margin: "3rem auto", 10 | }; 11 | 12 | const meta: Meta = { 13 | component: Chessboard, 14 | decorators: [ 15 | (Story) => ( 16 |
17 | 18 |
19 | ), 20 | ], 21 | }; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | export const Default: Story = { 27 | args: { 28 | id: 'Default', 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /stories/media/3d-pieces/bB.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/3d-pieces/bB.webp -------------------------------------------------------------------------------- /stories/media/3d-pieces/bK.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/3d-pieces/bK.webp -------------------------------------------------------------------------------- /stories/media/3d-pieces/bN.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/3d-pieces/bN.webp -------------------------------------------------------------------------------- /stories/media/3d-pieces/bP.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/3d-pieces/bP.webp -------------------------------------------------------------------------------- /stories/media/3d-pieces/bQ.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/3d-pieces/bQ.webp -------------------------------------------------------------------------------- /stories/media/3d-pieces/bR.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/3d-pieces/bR.webp -------------------------------------------------------------------------------- /stories/media/3d-pieces/wB.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/3d-pieces/wB.webp -------------------------------------------------------------------------------- /stories/media/3d-pieces/wK.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/3d-pieces/wK.webp -------------------------------------------------------------------------------- /stories/media/3d-pieces/wN.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/3d-pieces/wN.webp -------------------------------------------------------------------------------- /stories/media/3d-pieces/wP.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/3d-pieces/wP.webp -------------------------------------------------------------------------------- /stories/media/3d-pieces/wQ.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/3d-pieces/wQ.webp -------------------------------------------------------------------------------- /stories/media/3d-pieces/wR.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/3d-pieces/wR.webp -------------------------------------------------------------------------------- /stories/media/bB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/bB.png -------------------------------------------------------------------------------- /stories/media/bK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/bK.png -------------------------------------------------------------------------------- /stories/media/bN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/bN.png -------------------------------------------------------------------------------- /stories/media/bP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/bP.png -------------------------------------------------------------------------------- /stories/media/bQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/bQ.png -------------------------------------------------------------------------------- /stories/media/bR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/bR.png -------------------------------------------------------------------------------- /stories/media/wB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/wB.png -------------------------------------------------------------------------------- /stories/media/wK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/wK.png -------------------------------------------------------------------------------- /stories/media/wN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/wN.png -------------------------------------------------------------------------------- /stories/media/wP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/wP.png -------------------------------------------------------------------------------- /stories/media/wQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/wQ.png -------------------------------------------------------------------------------- /stories/media/wR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/wR.png -------------------------------------------------------------------------------- /stories/media/wood-pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/media/wood-pattern.png -------------------------------------------------------------------------------- /stories/stockfish/engine.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Stockfish.js (http://github.com/nmrugg/stockfish.js) 3 | * License: GPL 4 | */ 5 | 6 | /* 7 | * Description of the universal chess interface (UCI) https://gist.github.com/aliostad/f4470274f39d29b788c1b09519e67372/ 8 | */ 9 | 10 | const stockfish = new Worker("./stockfish.wasm.js"); 11 | 12 | type EngineMessage = { 13 | /** stockfish engine message in UCI format*/ 14 | uciMessage: string; 15 | /** found best move for current position in format `e2e4`*/ 16 | bestMove?: string; 17 | /** found best move for opponent in format `e7e5` */ 18 | ponder?: string; 19 | /** material balance's difference in centipawns(IMPORTANT! stockfish gives the cp score in terms of whose turn it is)*/ 20 | positionEvaluation?: string; 21 | /** count of moves until mate */ 22 | possibleMate?: string; 23 | /** the best line found */ 24 | pv?: string; 25 | /** number of halfmoves the engine looks ahead */ 26 | depth?: number; 27 | }; 28 | 29 | export default class Engine { 30 | stockfish: Worker; 31 | onMessage: (callback: (messageData: EngineMessage) => void) => void; 32 | isReady: boolean; 33 | 34 | constructor() { 35 | this.stockfish = stockfish; 36 | this.isReady = false; 37 | this.onMessage = (callback) => { 38 | this.stockfish.addEventListener("message", (e) => { 39 | callback(this.transformSFMessageData(e)); 40 | }); 41 | }; 42 | this.init(); 43 | } 44 | 45 | private transformSFMessageData(e) { 46 | const uciMessage = e?.data ?? e; 47 | 48 | return { 49 | uciMessage, 50 | bestMove: uciMessage.match(/bestmove\s+(\S+)/)?.[1], 51 | ponder: uciMessage.match(/ponder\s+(\S+)/)?.[1], 52 | positionEvaluation: uciMessage.match(/cp\s+(\S+)/)?.[1], 53 | possibleMate: uciMessage.match(/mate\s+(\S+)/)?.[1], 54 | pv: uciMessage.match(/ pv\s+(.*)/)?.[1], 55 | depth: Number(uciMessage.match(/ depth\s+(\S+)/)?.[1]) ?? 0, 56 | }; 57 | } 58 | 59 | init() { 60 | this.stockfish.postMessage("uci"); 61 | this.stockfish.postMessage("isready"); 62 | this.onMessage(({ uciMessage }) => { 63 | if (uciMessage === "readyok") { 64 | this.isReady = true; 65 | } 66 | }); 67 | } 68 | 69 | onReady(callback) { 70 | this.onMessage(({ uciMessage }) => { 71 | if (uciMessage === "readyok") { 72 | callback(); 73 | } 74 | }); 75 | } 76 | 77 | evaluatePosition(fen, depth = 12) { 78 | if (depth > 24) depth = 24; 79 | 80 | this.stockfish.postMessage(`position fen ${fen}`); 81 | this.stockfish.postMessage(`go depth ${depth}`); 82 | } 83 | 84 | stop() { 85 | this.stockfish.postMessage("stop"); // Run when searching takes too long time and stockfish will return you the bestmove of the deep it has reached 86 | } 87 | 88 | terminate() { 89 | this.isReady = false; 90 | this.stockfish.postMessage("quit"); // Run this before chessboard unmounting. 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /stories/stockfish/stockfish.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/0f72cff7f35147c8f221127a761d8829736549d9/stories/stockfish/stockfish.wasm -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "lib": [ 15 | "ES5", 16 | "ES2015", 17 | "ES2016", 18 | "DOM", 19 | "ESNext" 20 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 21 | "jsx": "react-jsx" /* Specify what JSX code is generated. */, 22 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 23 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 24 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 25 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 26 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 27 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 28 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 29 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 30 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 31 | 32 | /* Modules */ 33 | "module": "ES2015" /* Specify what module code is generated. */, 34 | "target": "ES2015", 35 | // "rootDir": "./", /* Specify the root folder within your source files. */ 36 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 37 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 38 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 39 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 40 | "types": ["node"] /* Specify type package names to be included without being referenced in a source file. */, 41 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 42 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 43 | // "resolveJsonModule": true, /* Enable importing .json files. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | "moduleResolution": "node", 46 | 47 | /* JavaScript Support */ 48 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 49 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 50 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 51 | 52 | /* Emit */ 53 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 54 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 55 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 56 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | "outDir": "dist" /* Specify an output folder for all emitted files. */, 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 67 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 68 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 69 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 70 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 71 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 72 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 73 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 74 | "declarationDir": "dist", /* Specify the output directory for generated declaration files. */ 75 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 76 | 77 | /* Interop Constraints */ 78 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 79 | "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, 80 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 83 | 84 | /* Type Checking */ 85 | "strict": true /* Enable all strict type-checking options. */, 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | "noUnusedLocals": true /* Enable error reporting when local variables aren't read. */, 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | }, 109 | "include": ["src/**/*.ts", "src/**/*.tsx"] 110 | } 111 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://openapi.vercel.sh/vercel.json", 3 | "buildCommand": "npm run build-storybook", 4 | "devCommand": "npm run storybook", 5 | "installCommand": "npm install", 6 | "framework": null, 7 | "outputDirectory": "./storybook-static" 8 | } 9 | --------------------------------------------------------------------------------