├── .coveralls.yml ├── tsconfig.json ├── src ├── square.js ├── main.js ├── piece.js ├── gameValidation.js ├── simpleGameClient.js ├── game.js ├── uciGameClient.js ├── pieceValidation.js ├── boardValidation.js ├── board.js └── algebraicGameClient.js ├── vitest.config.mjs ├── examples └── usage.js ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── test └── src │ ├── main.js │ ├── uciGameClient.js │ ├── simpleGameClient.js │ ├── game.js │ ├── gameValidation.js │ ├── pieceValidation.js │ ├── boardValidation.js │ ├── board.js │ └── algebraicGameClient.js ├── AGENTS.md ├── package.json ├── eslint.config.mjs ├── @types └── chess │ └── chess.d.ts └── README.md /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Node 12", 4 | "compilerOptions": { 5 | "lib": ["es2019", "es2020.promise", "es2020.bigint", "es2020.string"], 6 | "module": "commonjs", 7 | "target": "es2019", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | } 13 | } -------------------------------------------------------------------------------- /src/square.js: -------------------------------------------------------------------------------- 1 | /** 2 | The simple definition of a rank & file within a board. 3 | 4 | Additionally, if a Piece occupies a square on a board, the 5 | square contains the reference to the piece. 6 | */ 7 | 8 | export class Square { 9 | constructor (file, rank) { 10 | this.file = file; 11 | this.piece = null; 12 | this.rank = rank; 13 | } 14 | 15 | static create (file, rank) { 16 | return new Square(file, rank); 17 | } 18 | } 19 | 20 | export default { Square }; 21 | -------------------------------------------------------------------------------- /vitest.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'node', 6 | include: ['test/src/**/*.js'], 7 | threads: false, 8 | coverage: { 9 | provider: 'v8', 10 | reporter: ['text', 'lcov'], 11 | reportsDirectory: 'coverage', 12 | include: ['src/**/*.js'], 13 | exclude: ['test/**', 'node_modules/**', 'coverage/**', 'cjs/**', '@types/**'] 14 | } 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /examples/usage.js: -------------------------------------------------------------------------------- 1 | import chess from 'chess'; 2 | 3 | // create a game client 4 | const gameClient = chess.create({ PGN : true }); 5 | let move, status; 6 | 7 | // look at the status and valid moves 8 | status = gameClient.getStatus(); 9 | 10 | // make a move 11 | move = gameClient.move('a4'); 12 | 13 | // look at the status again after the move to see 14 | // the opposing side's available moves 15 | status = gameClient.getStatus(); 16 | 17 | // output 18 | console.log(JSON.stringify(status, 0, 2)); -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { AlgebraicGameClient } from './algebraicGameClient.js'; 2 | import { SimpleGameClient } from './simpleGameClient.js'; 3 | import { UCIGameClient } from './uciGameClient.js'; 4 | 5 | export const create = (opts) => AlgebraicGameClient.create(opts); 6 | export const createSimple = () => SimpleGameClient.create(); 7 | export const fromFEN = (fen, opts) => AlgebraicGameClient.fromFEN(fen, opts); 8 | export const createUCI = () => UCIGameClient.create(); 9 | 10 | // exports 11 | export default { 12 | create, 13 | createSimple, 14 | createUCI, 15 | fromFEN 16 | }; 17 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: brozeph # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | dist 10 | .nyc_output 11 | cjs 12 | 13 | # Packages # 14 | ############ 15 | # it's better to unpack these files and commit the raw source 16 | # git has its own built in compression methods 17 | *.7z 18 | *.dmg 19 | *.gz 20 | *.iso 21 | *.jar 22 | *.rar 23 | *.tar 24 | *.zip 25 | 26 | # Logs and databases # 27 | ###################### 28 | *.log 29 | *.sql 30 | *.sqlite 31 | 32 | # OS generated files # 33 | ###################### 34 | .DS_Store? 35 | ehthumbs.db 36 | Icon? 37 | Thumbs.db 38 | 39 | # Everything else # 40 | ###################### 41 | .vscode 42 | node-chess.tmproj #textmate project file 43 | node_modules 44 | lib-cov 45 | reports 46 | coverage -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | 3 | # Compiled source # 4 | ################### 5 | *.com 6 | *.class 7 | *.dll 8 | *.exe 9 | *.o 10 | *.so 11 | 12 | # Packages # 13 | ############ 14 | # it's better to unpack these files and commit the raw source 15 | # git has its own built in compression methods 16 | *.7z 17 | *.dmg 18 | *.gz 19 | *.iso 20 | *.jar 21 | *.rar 22 | *.tar 23 | *.zip 24 | 25 | # Logs and databases # 26 | ###################### 27 | *.log 28 | *.sql 29 | *.sqlite 30 | 31 | # OS generated files # 32 | ###################### 33 | .DS_Store? 34 | ehthumbs.db 35 | Icon? 36 | Thumbs.db 37 | 38 | # Everything else # 39 | ###################### 40 | .nyc_output 41 | .babelrc 42 | .coveralls.yml 43 | .gitignore 44 | .travis.yml 45 | .vscode 46 | gulpfile.babel.js 47 | lib-cov 48 | node-chess.tmproj #textmate project file 49 | node_modules 50 | reports 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 2 | 3 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [pull_request] 3 | env: 4 | CI: true 5 | 6 | jobs: 7 | test: 8 | name: Node ${{ matrix.node }} on ${{ matrix.os }} 9 | runs-on: ${{ matrix.os }} 10 | 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | node: [20, 22, 24] 15 | os: [ubuntu-latest, windows-latest] 16 | 17 | steps: 18 | - name: Clone repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Set Node.js version 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ matrix.node }} 25 | cache: 'npm' 26 | 27 | - name: Install npm dependencies 28 | run: npm ci 29 | 30 | - name: Run lint 31 | run: npm run lint 32 | 33 | - name: Run tests (Vitest + coverage) 34 | run: npm test 35 | 36 | - name: Coveralls 37 | uses: coverallsapp/github-action@master 38 | with: 39 | github-token: ${{ secrets.GITHUB_TOKEN }} 40 | path-to-lcov: ./coverage/lcov.info 41 | flag-name: ${{matrix.os}}-node-${{ matrix.node }} 42 | parallel: true 43 | 44 | finish: 45 | needs: test 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Coveralls Finished 49 | uses: coverallsapp/github-action@master 50 | with: 51 | github-token: ${{ secrets.GITHUB_TOKEN }} 52 | parallel-finished: true 53 | -------------------------------------------------------------------------------- /test/src/main.js: -------------------------------------------------------------------------------- 1 | /* eslint no-magic-numbers:0 */ 2 | import { assert, describe, it } from 'vitest'; 3 | import chess, { create, createSimple, createUCI } from '../../src/main.js'; 4 | import { AlgebraicGameClient } from '../../src/algebraicGameClient.js'; 5 | import { SimpleGameClient } from '../../src/simpleGameClient.js'; 6 | 7 | describe('main entry', () => { 8 | it('create() should return an AlgebraicGameClient', () => { 9 | const gc = create(); 10 | assert.ok(gc instanceof AlgebraicGameClient, 'should be AlgebraicGameClient'); 11 | const s = gc.getStatus(); 12 | assert.strictEqual(typeof s.isCheck, 'boolean'); 13 | assert.ok(typeof gc.move === 'function'); 14 | }); 15 | 16 | it('createSimple() should return a SimpleGameClient', () => { 17 | const sgc = createSimple(); 18 | assert.ok(sgc instanceof SimpleGameClient, 'should be SimpleGameClient'); 19 | const s = sgc.getStatus(); 20 | assert.strictEqual(typeof s.isStalemate, 'boolean'); 21 | assert.ok(typeof sgc.move === 'function'); 22 | }); 23 | 24 | it('default export should expose create and createSimple', () => { 25 | assert.strictEqual(typeof chess.create, 'function'); 26 | assert.strictEqual(typeof chess.createSimple, 'function'); 27 | assert.ok(chess.create() instanceof AlgebraicGameClient); 28 | assert.ok(chess.createSimple() instanceof SimpleGameClient); 29 | }); 30 | 31 | it('named and default export should expose createUCI', () => { 32 | assert.strictEqual(typeof createUCI, 'function'); 33 | assert.strictEqual(typeof chess.createUCI, 'function'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # AGENTS.md for node-chess 2 | 3 | ## Project Overview 4 | 5 | This is a Node.js library for calculating chess moves. 6 | 7 | ## Build and Test Commands 8 | 9 | **Installation:** 10 | 11 | - Use `npm install` at the project root to install all dependencies. 12 | 13 | **Development:** 14 | 15 | - Start the development server for both frontend and backend: `pnpm dev` 16 | - Start the frontend server only: `pnpm --filter web dev` 17 | 18 | **Testing:** 19 | 20 | - Run the full test suite for all packages: `npm run test` 21 | - All code changes must pass the entire test suite before merging. 22 | - Run the linter: `npm run lint` 23 | - All linting issues must be resolved before merging. 24 | 25 | ## Coding Conventions and Style 26 | 27 | - **Linting:** We use ESLint with the Airbnb style guide for TypeScript. Run `npm run lint` to check for style and coding errors. 28 | - **Naming:** 29 | - Components and files use PascalCase (e.g., `Button.tsx`). 30 | - Functions and variables use camelCase (e.g., `getUsers`). 31 | - Constants use screaming snake case (e.g., `API_ENDPOINT`). 32 | - **TypeScript:** Use strict mode and explicitly type all function arguments and returns. Avoid using `any`. 33 | - **Comments:** 34 | - Use JSDoc style comments for all functions and classes. Include parameter and return types. 35 | - Use inline comments sparingly to explain complex logic. Start inline comments with a lower case letter and do not end with a period. 36 | 37 | ## Testing Guidelines 38 | 39 | ## PR Instructions 40 | 41 | - **Commit Messages:** Follow the Conventional Commits specification (e.g., `feat:`, `fix:`, `docs:`) to ensure changelogs are generated correctly. 42 | - **Pre-Commit Checks:** Always run `npm run build`, `npm run lint` and `npm run test` before committing your code. 43 | 44 | ## Security Considerations 45 | 46 | - **Secrets:** Never hard-code API keys, credentials, or other sensitive information. 47 | - **Dependencies:** All dependencies must be vetted and installed via `npm`. No manual installations. 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chess", 3 | "description": "An algebraic notation driven chess engine that can validate board position and produce a list of viable moves (notated).", 4 | "version": "1.5.1", 5 | "contributors": [ 6 | { 7 | "name": "Joshua Thomas", 8 | "email": "joshua.thomas@gmail.com", 9 | "url": "https://github.com/brozeph" 10 | }, 11 | { 12 | "name": "Denis Efremov", 13 | "url": "https://github.com/Piterden" 14 | }, 15 | { 16 | "name": "Fun Planet", 17 | "url": "https://github.com/dipamsen" 18 | }, 19 | { 20 | "name": "Lee Danilek", 21 | "url": "https://githb.com/ldanilek" 22 | }, 23 | { 24 | "name": "Ayush Thakur", 25 | "url": "https://githb.com/ayshthkr" 26 | }, 27 | { 28 | "name": "Lewis", 29 | "url": "https://github.com/ctjlewis" 30 | } 31 | ], 32 | "exports": { 33 | ".": { 34 | "types": "./@types/chess/chess.d.ts", 35 | "import": "./src/main.js", 36 | "require": "./cjs/main.cjs" 37 | } 38 | }, 39 | "type": "module", 40 | "engine": "node >= 16.0", 41 | "repository": { 42 | "type": "git", 43 | "url": "https://github.com/brozeph/node-chess.git" 44 | }, 45 | "license": "MIT", 46 | "homepage": "https://brozeph.github.io/node-chess", 47 | "bugs": "https://github.com/brozeph/node-chess/issues", 48 | "keywords": [ 49 | "chess", 50 | "algebraic notation" 51 | ], 52 | "main": "./cjs/main.cjs", 53 | "types": "@types/chess/chess.d.ts", 54 | "scripts": { 55 | "lint": "npx eslint ./src/** ./test/**", 56 | "pretest": "npx eslint ./src/** ./test/**", 57 | "test": "vitest run --coverage", 58 | "test:unit": "vitest", 59 | "build": "esbuild src/main.js --bundle --platform=node --format=cjs --outfile=cjs/main.cjs", 60 | "prepack": "npm run build" 61 | }, 62 | "dependencies": { 63 | "crypto-js": "^4.2.0" 64 | }, 65 | "devDependencies": { 66 | "@vitest/coverage-v8": "^4.0.4", 67 | "coveralls": "^3.1.1", 68 | "esbuild": "^0.25.11", 69 | "eslint": "^9.38.0", 70 | "vitest": "^4.0.4" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/piece.js: -------------------------------------------------------------------------------- 1 | /** 2 | The Piece is a definition of a piece that can be played on the board. 3 | 4 | The uid property of the Piece is not intended to be durable across 5 | sessions, but only to uniquely identify the piece on the board. Right 6 | now the property is used by board.getSquareByPiece as pieces are not 7 | otherwise uniquely identifiable (i.e. getSquareByPiece(Pawn) would 8 | return the first square found with a Pawn on it rather than the exact 9 | square intended). Additionally, the uid of the Piece is used in 10 | BoardValidation to ensure there is correllation between the piece and 11 | valid squares to which the piece can move. 12 | */ 13 | 14 | // types 15 | export var PieceType = { 16 | Bishop : 'bishop', 17 | King : 'king', 18 | Knight : 'knight', 19 | Pawn : 'pawn', 20 | Queen : 'queen', 21 | Rook : 'rook' 22 | }; 23 | 24 | export var SideType = { 25 | Black : { name : 'black' }, 26 | White : { name : 'white' } 27 | }; 28 | 29 | export class Piece { 30 | constructor (side, notation) { 31 | this.moveCount = 0; 32 | this.notation = notation; 33 | this.side = side; 34 | this.type = null; 35 | } 36 | 37 | static createBishop (side) { 38 | return new Bishop(side); 39 | } 40 | 41 | static createKing (side) { 42 | return new King(side); 43 | } 44 | 45 | static createKnight (side) { 46 | return new Knight(side); 47 | } 48 | 49 | static createPawn (side) { 50 | return new Pawn(side); 51 | } 52 | 53 | static createQueen (side) { 54 | return new Queen(side); 55 | } 56 | 57 | static createRook (side) { 58 | return new Rook(side); 59 | } 60 | } 61 | 62 | export class Bishop extends Piece { 63 | constructor (side) { 64 | super(side, 'B'); 65 | 66 | this.type = PieceType.Bishop; 67 | } 68 | } 69 | 70 | export class King extends Piece { 71 | constructor (side) { 72 | super(side, 'K'); 73 | 74 | this.type = PieceType.King; 75 | } 76 | } 77 | 78 | export class Knight extends Piece { 79 | constructor (side) { 80 | super(side, 'N'); 81 | 82 | this.type = PieceType.Knight; 83 | } 84 | } 85 | 86 | export class Pawn extends Piece { 87 | constructor (side) { 88 | super(side, ''); 89 | 90 | this.type = PieceType.Pawn; 91 | } 92 | } 93 | 94 | export class Queen extends Piece { 95 | constructor (side) { 96 | super(side, 'Q'); 97 | 98 | this.type = PieceType.Queen; 99 | } 100 | } 101 | 102 | export class Rook extends Piece { 103 | constructor (side) { 104 | super(side, 'R'); 105 | 106 | this.type = PieceType.Rook; 107 | } 108 | } 109 | 110 | export default { 111 | Piece, 112 | PieceType, 113 | SideType 114 | }; 115 | -------------------------------------------------------------------------------- /src/gameValidation.js: -------------------------------------------------------------------------------- 1 | /** 2 | GameValidation is the 3rd phase of validation for the game 3 | and is intended to support Game level events. Examples of Game 4 | scope validation include Check, Checkmate, 3-fold position 5 | repetition and pawn promotion. 6 | */ 7 | import { BoardValidation } from './boardValidation.js'; 8 | import { PieceType } from './piece.js'; 9 | 10 | export class GameValidation { 11 | constructor (game) { 12 | this.game = game; 13 | } 14 | 15 | static create (game) { 16 | return new GameValidation(game); 17 | } 18 | 19 | findKingSquare (side) { 20 | let 21 | i = 0, 22 | squares = this.game.board.getSquares(side); 23 | 24 | for (i = 0; i < squares.length; i++) { 25 | if (squares[i].piece.type === PieceType.King) { 26 | return squares[i]; 27 | } 28 | } 29 | } 30 | 31 | isRepetition () { 32 | let 33 | hash = '', 34 | hashCount = [], 35 | i = 0; 36 | 37 | // analyze 3-fold repetition (draw) 38 | for (i = 0; i < this.game.moveHistory.length; i++) { 39 | hash = this.game.moveHistory[i].hashCode; 40 | hashCount[hash] = hashCount[hash] ? hashCount[hash] + 1 : 1; 41 | 42 | /* eslint no-magic-numbers: 0 */ 43 | if (hashCount[hash] === 3) { 44 | return true; 45 | } 46 | } 47 | 48 | return false; 49 | } 50 | 51 | start (callback) { 52 | // ensure callback is set 53 | callback = callback || ((err, result) => new Promise((resolve, reject) => { 54 | if (err) { 55 | return reject(err); 56 | } 57 | 58 | return resolve(result); 59 | })); 60 | 61 | let 62 | kingSquare = null, 63 | result = { 64 | isCheck : false, 65 | isCheckmate : false, 66 | isFiftyMoveDraw : false, 67 | isRepetition : false, 68 | isStalemate : false, 69 | validMoves : [] 70 | }, 71 | setResult = (v, result, isKingAttacked) => { 72 | return (err, validMoves) => { 73 | if (err) { 74 | return callback(err); 75 | } 76 | 77 | result.isCheck = isKingAttacked && validMoves.length > 0; 78 | result.isCheckmate = isKingAttacked && validMoves.length === 0; 79 | result.isStalemate = !isKingAttacked && validMoves.length === 0; 80 | result.isRepetition = v.isRepetition(); 81 | result.validMoves = validMoves; 82 | 83 | return callback(null, result); 84 | }; 85 | }, 86 | v = BoardValidation.create(this.game); 87 | 88 | if (this.game) { 89 | // find current side king square 90 | kingSquare = this.findKingSquare(this.game.getCurrentSide()); 91 | 92 | // find valid moves 93 | return v.start(setResult(this, result, v.isSquareAttacked(kingSquare))); 94 | } 95 | 96 | return callback(new Error('game is invalid')); 97 | } 98 | } 99 | 100 | export default { GameValidation }; 101 | -------------------------------------------------------------------------------- /test/src/uciGameClient.js: -------------------------------------------------------------------------------- 1 | /* eslint no-magic-numbers:0 */ 2 | import { assert, describe, it } from 'vitest'; 3 | import { PieceType, SideType, Piece } from '../../src/piece.js'; 4 | import { UCIGameClient } from '../../src/uciGameClient.js'; 5 | 6 | describe('UCIGameClient', () => { 7 | it('should have proper status once board is created', () => { 8 | const gc = UCIGameClient.create(); 9 | const s = gc.getStatus(); 10 | 11 | assert.strictEqual(s.isCheck, false); 12 | assert.strictEqual(s.isCheckmate, false); 13 | assert.strictEqual(s.isRepetition, false); 14 | assert.strictEqual(s.isStalemate, false); 15 | assert.strictEqual(Object.keys(s.uciMoves).length, 20); 16 | }); 17 | 18 | it('should trigger move events on UCI moves', () => { 19 | const gc = UCIGameClient.create(); 20 | const moveEvent = []; 21 | gc.on('move', (ev) => moveEvent.push(ev)); 22 | 23 | gc.move('b2b4'); 24 | gc.move('e7e6'); 25 | 26 | assert.ok(moveEvent); 27 | assert.strictEqual(moveEvent.length, 2); 28 | }); 29 | 30 | it('should perform a pawn capture using UCI', () => { 31 | const gc = UCIGameClient.create(); 32 | 33 | gc.move('e2e4'); 34 | gc.move('d7d5'); 35 | const r = gc.move('e4d5'); 36 | 37 | assert.strictEqual(r.move.capturedPiece.type, PieceType.Pawn); 38 | }); 39 | 40 | it('should castle using UCI coordinates', () => { 41 | const gc = UCIGameClient.create(); 42 | const castleEvent = []; 43 | gc.on('castle', (ev) => castleEvent.push(ev)); 44 | 45 | // clear path for white long castle (e1c1) 46 | gc.game.board.getSquare('b1').piece = null; 47 | gc.game.board.getSquare('c1').piece = null; 48 | gc.game.board.getSquare('d1').piece = null; 49 | 50 | gc.getStatus(true); 51 | gc.move('e1c1'); 52 | 53 | assert.ok(castleEvent); 54 | assert.strictEqual(castleEvent.length, 1); 55 | }); 56 | 57 | it('should handle pawn promotion via UCI and enumerate all promotion options', () => { 58 | const gc = UCIGameClient.create(); 59 | 60 | // setup white pawn a7->a8=Q checkmate scenario 61 | gc.game.board.getSquare('a7').piece = null; 62 | gc.game.board.getSquare('a8').piece = null; 63 | gc.game.board.getSquare('b8').piece = null; 64 | gc.game.board.getSquare('c8').piece = null; 65 | gc.game.board.getSquare('d8').piece = null; 66 | gc.game.board.getSquare('a2').piece = null; 67 | gc.game.board.getSquare('a7').piece = Piece.createPawn(SideType.White); 68 | gc.game.board.getSquare('a7').piece.moveCount = 1; 69 | 70 | const pre = gc.getStatus(true); 71 | assert.isUndefined(pre.uciMoves['a7a8']); 72 | assert.isDefined(pre.uciMoves['a7a8q']); 73 | assert.isDefined(pre.uciMoves['a7a8r']); 74 | assert.isDefined(pre.uciMoves['a7a8b']); 75 | assert.isDefined(pre.uciMoves['a7a8n']); 76 | const m = gc.move('a7a8q'); 77 | const r = gc.getStatus(); 78 | 79 | assert.strictEqual(m.move.postSquare.piece.type, PieceType.Queen); 80 | assert.strictEqual(r.isCheckmate, true); 81 | }); 82 | 83 | it('should throw on invalid UCI', () => { 84 | const gc = UCIGameClient.create(); 85 | assert.throws(() => gc.move('e9e4')); 86 | assert.throws(() => gc.move('abcd')); 87 | }); 88 | 89 | it('should expose capture history via getCaptureHistory()', () => { 90 | const gc = UCIGameClient.create(); 91 | 92 | gc.move('e2e4'); 93 | gc.move('d7d5'); 94 | const cap = gc.move('e4d5'); 95 | 96 | const h1 = gc.getCaptureHistory(); 97 | assert.strictEqual(h1.length, 1); 98 | assert.strictEqual(h1[0].type, PieceType.Pawn); 99 | 100 | cap.undo(); 101 | const h2 = gc.getCaptureHistory(); 102 | assert.strictEqual(h2.length, 0); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /src/simpleGameClient.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import { Game } from './game.js'; 3 | import { GameValidation } from './gameValidation.js'; 4 | import { Piece } from './piece.js'; 5 | 6 | // private methods 7 | function isMoveValid (src, dest, validMoves) { 8 | let 9 | i = 0, 10 | isFound = (expr, sq) => { 11 | return ((typeof expr === 'string' && sq.file + sq.rank === expr) || 12 | (expr && expr.rank && expr.file && 13 | sq.file === expr.file && sq.rank === expr.rank)); 14 | }, 15 | squares = []; 16 | 17 | for (i = 0; i < validMoves.length; i++) { 18 | if (isFound(src, validMoves[i].src)) { 19 | squares = validMoves[i].squares; 20 | } 21 | } 22 | 23 | if (squares && squares.length > 0) { 24 | for (i = 0; i < squares.length; i++) { 25 | if (isFound(dest, squares[i])) { 26 | return true; 27 | } 28 | } 29 | } 30 | 31 | return false; 32 | } 33 | 34 | function updateGameClient (gameClient) { 35 | return gameClient.validation.start((err, result) => { 36 | if (err) { 37 | throw new Error(err); 38 | } 39 | 40 | gameClient.isCheck = result.isCheck; 41 | gameClient.isCheckmate = result.isCheckmate; 42 | gameClient.isRepetition = result.isRepetition; 43 | gameClient.isStalemate = result.isStalemate; 44 | gameClient.validMoves = result.validMoves; 45 | }); 46 | } 47 | 48 | // ctor 49 | export class SimpleGameClient extends EventEmitter { 50 | constructor (game) { 51 | super(); 52 | 53 | this.isCheck = false; 54 | this.isCheckmate = false; 55 | this.isRepetition = false; 56 | this.isStalemate = false; 57 | this.game = game; 58 | this.validMoves = []; 59 | this.validation = GameValidation.create(this.game); 60 | 61 | // bubble the game and board events 62 | ['check', 'checkmate'].forEach((ev) => { 63 | this.game.on(ev, (data) => this.emit(ev, data)); 64 | }); 65 | 66 | ['capture', 'castle', 'enPassant', 'move', 'promote'].forEach((ev) => { 67 | this.game.board.on(ev, (data) => this.emit(ev, data)); 68 | }); 69 | } 70 | 71 | static create () { 72 | let 73 | game = Game.create(), 74 | gameClient = new SimpleGameClient(game); 75 | 76 | updateGameClient(gameClient); 77 | 78 | return gameClient; 79 | } 80 | 81 | getStatus (forceUpdate) { 82 | if (forceUpdate) { 83 | updateGameClient(this); 84 | } 85 | 86 | return { 87 | board : this.game.board, 88 | isCheck : this.isCheck, 89 | isCheckmate : this.isCheckmate, 90 | isRepetition : this.isRepetition, 91 | isStalemate : this.isStalemate, 92 | validMoves : this.validMoves 93 | }; 94 | } 95 | 96 | move (src, dest, promo) { 97 | let 98 | move = null, 99 | side = this.game.getCurrentSide(); 100 | 101 | if (src && dest && isMoveValid(src, dest, this.validMoves)) { 102 | move = this.game.board.move(src, dest); 103 | 104 | if (move) { 105 | // apply pawn promotion if applicable 106 | if (promo) { 107 | let piece; 108 | 109 | switch (promo) { 110 | case 'B': 111 | piece = Piece.createBishop(side); 112 | break; 113 | case 'N': 114 | piece = Piece.createKnight(side); 115 | break; 116 | case 'Q': 117 | piece = Piece.createQueen(side); 118 | break; 119 | case 'R': 120 | piece = Piece.createRook(side); 121 | break; 122 | default: 123 | piece = null; 124 | break; 125 | } 126 | 127 | if (piece) { 128 | this.game.board.promote(move.move.postSquare, piece); 129 | /* 130 | p.moveCount = move.move.postSquare.piece.moveCount; 131 | move.move.postSquare.piece = p; 132 | //*/ 133 | } 134 | } 135 | 136 | updateGameClient(this); 137 | return move; 138 | } 139 | } 140 | 141 | throw new Error(`Move is invalid (${ src } to ${ dest })`); 142 | } 143 | 144 | getCaptureHistory () { 145 | return this.game.captureHistory; 146 | } 147 | } 148 | 149 | export default { SimpleGameClient }; 150 | -------------------------------------------------------------------------------- /src/game.js: -------------------------------------------------------------------------------- 1 | /** 2 | Games contain the history of a board and the board itself. 3 | 4 | At time of writing this, the game is also intended to store some 5 | degree of information regarding the opponents and keys that 6 | could be used for storage, etc. 7 | */ 8 | import base64 from 'crypto-js/enc-base64.js' 9 | import { Board } from './board.js'; 10 | import { EventEmitter } from 'events'; 11 | import md5 from 'crypto-js/md5.js'; 12 | import { SideType } from './piece.js'; 13 | 14 | function addToHistory (game) { 15 | return (ev) => { 16 | let 17 | hashCode = game.getHashCode(), 18 | move = new Move( 19 | ev.prevSquare, 20 | ev.postSquare, 21 | ev.capturedPiece, 22 | ev.algebraic, 23 | ev.castle, 24 | ev.enPassant, 25 | hashCode); 26 | 27 | game.moveHistory.push(move); 28 | }; 29 | } 30 | 31 | function denotePromotionInHistory (game) { 32 | return () => { 33 | let 34 | latest = game.moveHistory[ 35 | game.moveHistory.length - 1]; 36 | 37 | if (latest) { 38 | latest.promotion = true; 39 | } 40 | }; 41 | } 42 | 43 | function removeFromHistory (game) { 44 | return () => { 45 | game.moveHistory.pop(); 46 | 47 | // find the previous move piece 48 | let m = game.moveHistory[game.moveHistory.length - 1]; 49 | 50 | // update last moved piece 51 | if (m) { 52 | game.board.lastMovedPiece = m.piece; 53 | } 54 | }; 55 | } 56 | 57 | export class Game extends EventEmitter { 58 | constructor (board) { 59 | super(); 60 | 61 | this.board = board; 62 | this.captureHistory = []; 63 | this.moveHistory = []; 64 | } 65 | 66 | static create () { 67 | let 68 | board = Board.create(), 69 | game = new Game(board); 70 | 71 | // handle move and promotion events correctly 72 | board.on('move', (ev) => { 73 | addToHistory(game)(ev); 74 | if (ev && ev.capturedPiece) { 75 | game.captureHistory.push(ev.capturedPiece); 76 | } 77 | }); 78 | 79 | board.on('promote', denotePromotionInHistory(game)); 80 | 81 | board.on('undo', (ev) => { 82 | removeFromHistory(game)(ev); 83 | if (ev && ev.capturedPiece && game.captureHistory.length > 0) { 84 | // last move was a capture, remove it from capture history 85 | game.captureHistory.pop(); 86 | } 87 | }); 88 | 89 | return game; 90 | } 91 | 92 | getCurrentSide () { 93 | return this.moveHistory.length % 2 === 0 ? 94 | SideType.White : 95 | SideType.Black; 96 | } 97 | 98 | getHashCode () { 99 | let 100 | i = 0, 101 | sum = ''; 102 | 103 | for (i = 0; i < this.board.squares.length; i++) { 104 | if (this.board.squares[i].piece !== null) { 105 | sum += [ 106 | this.board.squares[i].file, 107 | this.board.squares[i].rank, 108 | (this.board.squares[i].piece.side === SideType.White ? 'w' : 'b'), 109 | this.board.squares[i].piece.notation, 110 | (i < (this.board.squares.length - 1) ? '-' : '')].join(''); 111 | } 112 | } 113 | 114 | // generate hash code for board 115 | let digest = md5(sum); 116 | return base64.stringify(digest); 117 | } 118 | 119 | static load (moveHistory) { 120 | let 121 | board = Board.create(), 122 | game = new Game(board), 123 | i = 0; 124 | 125 | // handle move and promotion events correctly 126 | board.on('move', (ev) => { 127 | addToHistory(game)(ev); 128 | if (ev && ev.capturedPiece) { 129 | game.captureHistory.push(ev.capturedPiece); 130 | } 131 | }); 132 | 133 | board.on('promote', denotePromotionInHistory(game)); 134 | 135 | // apply move history 136 | for (i = 0; i < moveHistory.length; i++) { 137 | board.move( 138 | board.getSquare( 139 | moveHistory[i].prevFile, 140 | moveHistory[i].prevRank), 141 | board.getSquare( 142 | moveHistory[i].postFile, 143 | moveHistory[i].postRank)); 144 | } 145 | 146 | return game; 147 | } 148 | } 149 | 150 | export class Move { 151 | constructor (originSquare, targetSquare, capturedPiece, notation, castle, enPassant, hash) { 152 | this.algebraic = notation; 153 | this.capturedPiece = capturedPiece; 154 | this.castle = castle; 155 | this.enPassant = enPassant; 156 | this.hashCode = hash; 157 | this.piece = targetSquare.piece; 158 | this.promotion = false; 159 | this.postFile = targetSquare.file; 160 | this.postRank = targetSquare.rank; 161 | this.prevFile = originSquare.file; 162 | this.prevRank = originSquare.rank; 163 | } 164 | } 165 | 166 | export default { Game, Move }; 167 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | 3 | export default [{ 4 | languageOptions: { 5 | globals: { 6 | ...Object.fromEntries(Object.entries(globals.browser).map(([key]) => [key, "off"])), 7 | ...globals.node, 8 | assert: true, 9 | }, 10 | 11 | ecmaVersion: 8, 12 | sourceType: "module", 13 | }, 14 | 15 | rules: { 16 | "array-bracket-spacing": [2, "never"], 17 | "array-callback-return": 2, 18 | "arrow-parens": 2, 19 | 20 | "arrow-spacing": [2, { 21 | before: true, 22 | after: true, 23 | }], 24 | 25 | "new-cap": 2, 26 | "object-curly-spacing": [2, "always"], 27 | "block-spacing": [2, "always"], 28 | "brace-style": [2, "1tbs"], 29 | "callback-return": 2, 30 | camelcase: 2, 31 | "comma-dangle": [2, "never"], 32 | 33 | "comma-spacing": [2, { 34 | before: false, 35 | after: true, 36 | }], 37 | 38 | "comma-style": [1, "last"], 39 | curly: 2, 40 | "default-case": 2, 41 | eqeqeq: 2, 42 | 43 | "generator-star-spacing": [2, { 44 | before: true, 45 | after: false, 46 | }], 47 | 48 | "guard-for-in": 2, 49 | "handle-callback-err": 2, 50 | "no-await-in-loop": 2, 51 | "no-caller": 2, 52 | "no-case-declarations": 2, 53 | "no-cond-assign": 2, 54 | "no-confusing-arrow": 2, 55 | "no-const-assign": 2, 56 | "no-constant-condition": 2, 57 | "no-control-regex": 2, 58 | "no-debugger": 2, 59 | "no-dupe-args": 2, 60 | "no-dupe-keys": 2, 61 | "no-duplicate-case": 2, 62 | "no-empty": 2, 63 | "no-empty-character-class": 2, 64 | "no-empty-pattern": 2, 65 | "no-eq-null": 2, 66 | "no-eval": 2, 67 | "no-ex-assign": 2, 68 | "no-extra-bind": 2, 69 | "no-extra-boolean-cast": 2, 70 | "no-extra-label": 2, 71 | "no-extra-parens": [2, "functions"], 72 | "no-extra-semi": 2, 73 | "no-fallthrough": 2, 74 | "no-func-assign": 2, 75 | 76 | "no-implicit-coercion": [2, { 77 | boolean: true, 78 | number: true, 79 | string: true, 80 | }], 81 | 82 | "no-implied-eval": 2, 83 | "no-inner-declarations": 2, 84 | "no-invalid-regexp": 2, 85 | "no-invalid-this": 0, 86 | "no-irregular-whitespace": 2, 87 | "no-iterator": 2, 88 | "no-labels": 2, 89 | "no-lone-blocks": 2, 90 | "no-loop-func": 2, 91 | 92 | "no-magic-numbers": [2, { 93 | ignore: [-1, 0, 1, 2], 94 | ignoreArrayIndexes: true, 95 | }], 96 | 97 | "no-mixed-requires": [0, { 98 | grouping: true, 99 | allowCall: true, 100 | }], 101 | 102 | "no-mixed-spaces-and-tabs": 2, 103 | "no-multi-spaces": 2, 104 | "no-multi-str": 2, 105 | "no-native-reassign": 2, 106 | "no-negated-in-lhs": 2, 107 | "no-new": 2, 108 | "no-new-func": 2, 109 | "no-new-require": 2, 110 | "no-new-wrappers": 2, 111 | "no-obj-calls": 2, 112 | "no-octal": 2, 113 | "no-octal-escape": 2, 114 | "no-param-reassign": 0, 115 | "no-path-concat": 2, 116 | "no-proto": 2, 117 | "no-redeclare": 2, 118 | "no-regex-spaces": 2, 119 | "no-return-assign": 2, 120 | "no-self-assign": 2, 121 | "no-self-compare": 2, 122 | "no-sequences": 2, 123 | "no-sparse-arrays": 2, 124 | "no-sync": 2, 125 | "no-throw-literal": 2, 126 | "no-undef": 2, 127 | "no-undefined": 2, 128 | "no-unexpected-multiline": 2, 129 | "no-unmodified-loop-condition": 2, 130 | "no-unreachable": 2, 131 | "no-unused-expressions": 2, 132 | "no-unused-labels": 2, 133 | 134 | "no-unused-vars": [2, { 135 | vars: "all", 136 | }], 137 | 138 | "no-useless-call": 2, 139 | "no-useless-concat": 2, 140 | "no-void": 2, 141 | "no-with": 2, 142 | "object-shorthand": 2, 143 | quotes: [2, "single"], 144 | radix: 2, 145 | "use-isnan": 2, 146 | 147 | "sort-imports": [2, { 148 | ignoreCase: true, 149 | ignoreMemberSort: true, 150 | }], 151 | 152 | "sort-keys": [2, "asc", { 153 | caseSensitive: true, 154 | natural: true, 155 | }], 156 | 157 | "sort-vars": [2, { 158 | ignoreCase: true, 159 | }], 160 | 161 | "valid-typeof": 2, 162 | "vars-on-top": 2, 163 | "wrap-iife": 2, 164 | yoda: 2, 165 | }, 166 | }]; 167 | -------------------------------------------------------------------------------- /@types/chess/chess.d.ts: -------------------------------------------------------------------------------- 1 | // Typescript Declaration File for Node Chess module 2 | // https://www.npmjs.com/package/chess 3 | 4 | export function create(opts?: { PGN: boolean }): AlgebraicGameClient 5 | export function createSimple(): SimpleGameClient 6 | export function fromFEN(fen: string, opts?: { PGN: boolean }): AlgebraicGameClient 7 | export function createUCI(): UCIGameClient 8 | 9 | export interface GameStatus { 10 | /** Whether either of the side is under check */ 11 | isCheck: boolean 12 | /** Whether either of the side is checkmated */ 13 | isCheckMate: boolean 14 | /** Whether game has ended by threefold repetition */ 15 | isRepetition: boolean 16 | /** Whether game has ended by stalemate */ 17 | isStalemate: boolean 18 | } 19 | 20 | export interface SimpleGameStatus extends GameStatus { 21 | /** Current board configuration */ 22 | board: ChessBoard 23 | } 24 | 25 | export interface AlgebraicGameStatus extends SimpleGameStatus { 26 | /** Hash of next possible moves with key as notation and value as src-dest mapping */ 27 | notatedMoves: Record 28 | } 29 | 30 | export interface UCIGameStatus extends SimpleGameStatus { 31 | /** Hash of next possible moves with key as UCI string and value as src-dest mapping */ 32 | uciMoves: Record 33 | } 34 | 35 | export interface GameClient extends GameStatus { 36 | /** The Game object, which includes the board and move history. */ 37 | game: Game 38 | /** An array of pieces (src) which can move to the different squares. */ 39 | validMoves: ValidMove[] 40 | /** Game Validation Object */ 41 | validation: GameValidation 42 | /** Add event listeners */ 43 | on(event: ChessEvent, cbk: () => void): void 44 | /** 45 | * Make a move on the board 46 | * @param notation Notation of move in PGN format 47 | */ 48 | move(notation: string): PlayedMove 49 | getStatus(): AlgebraicGameStatus | SimpleGameStatus 50 | /** Returns the list of captured pieces in order */ 51 | getCaptureHistory(): Piece[] 52 | } 53 | 54 | export interface SimpleGameClient extends GameClient { 55 | getStatus(): SimpleGameStatus 56 | } 57 | 58 | export interface AlgebraicGameClient extends GameClient { 59 | /** Hash of next possible moves with key as notation and value as src-dest mapping */ 60 | notatedMoves: Record 61 | /** Whether notation is safe for PGN or not */ 62 | PGN: boolean 63 | getStatus(): AlgebraicGameStatus 64 | getFen(): string 65 | } 66 | 67 | export interface UCIGameClient extends GameStatus { 68 | game: Game 69 | validMoves: ValidMove[] 70 | validation: GameValidation 71 | on(event: ChessEvent, cbk: () => void): void 72 | /** Make a move on the board using UCI notation */ 73 | move(uci: string): PlayedMove 74 | getStatus(): UCIGameStatus 75 | /** Returns the list of captured pieces in order */ 76 | getCaptureHistory(): Piece[] 77 | } 78 | 79 | export type File = string 80 | export type Rank = number 81 | export type ChessEvent = 'check' | 'checkmate' 82 | 83 | export interface PlayedMove { 84 | move: { 85 | algebraic: string 86 | capturedPiece: Piece 87 | castle: boolean 88 | enPassant: boolean 89 | postSquare: Square 90 | prevSquare: Square 91 | } 92 | undo(): void 93 | } 94 | 95 | export interface Game { 96 | board: ChessBoard 97 | captureHistory: Piece[] 98 | moveHistory: Move[] 99 | } 100 | 101 | export interface GameValidation { 102 | game: Game 103 | } 104 | 105 | export interface ChessBoard { 106 | squares: Square[] 107 | lastMovedPiece: Piece 108 | } 109 | 110 | export interface Move { 111 | algebraic: string 112 | capturedPiece: Piece 113 | hashCode: string 114 | piece: Piece 115 | promotion: boolean 116 | postFile: File 117 | postRank: Rank 118 | prevFile: File 119 | prevRank: Rank 120 | } 121 | 122 | export interface NotatedMove { 123 | dest: Square 124 | src: Square 125 | } 126 | 127 | export interface ValidMove { 128 | squares: Square[] 129 | src: Square 130 | } 131 | 132 | export interface Square { 133 | file: File 134 | piece: Piece 135 | rank: Rank 136 | } 137 | 138 | export interface Side { 139 | name: 'white' | 'black' 140 | } 141 | 142 | export interface IPiece { 143 | type: string 144 | notation: string 145 | moveCount: number 146 | side: Side 147 | } 148 | 149 | export class AbstractPiece implements IPiece { 150 | type: string 151 | notation: string 152 | moveCount: number 153 | side: Side 154 | } 155 | 156 | export type Piece = Pawn | Knight | Bishop | Rook | Queen | King 157 | 158 | export class Pawn extends AbstractPiece { 159 | notation: '' 160 | type: 'pawn' 161 | } 162 | 163 | export class Knight extends AbstractPiece { 164 | notation: 'N' 165 | type: 'knight' 166 | } 167 | 168 | export class Bishop extends AbstractPiece { 169 | notation: 'B' 170 | type: 'bishop' 171 | } 172 | 173 | export class Rook extends AbstractPiece { 174 | notation: 'R' 175 | type: 'rook' 176 | } 177 | 178 | export class Queen extends AbstractPiece { 179 | notation: 'Q' 180 | type: 'queen' 181 | } 182 | 183 | export class King extends AbstractPiece { 184 | notation: 'K' 185 | type: 'king' 186 | } 187 | 188 | export declare const Chess: { 189 | create: typeof create; 190 | createSimple: typeof createSimple; 191 | fromFEN: typeof fromFEN; 192 | createUCI: typeof createUCI; 193 | }; 194 | 195 | export default Chess; 196 | -------------------------------------------------------------------------------- /test/src/simpleGameClient.js: -------------------------------------------------------------------------------- 1 | /* eslint no-magic-numbers:0 */ 2 | import { assert, describe, it } from 'vitest'; 3 | import { Piece, PieceType, SideType } from '../../src/piece.js'; 4 | import { SimpleGameClient } from '../../src/simpleGameClient.js'; 5 | 6 | describe('SimpleGameClient', () => { 7 | // test create and getStatus 8 | it('should properly create simple game client', () => { 9 | let 10 | gc = SimpleGameClient.create(), 11 | s = gc.getStatus(); 12 | 13 | assert.strictEqual(s.isCheck, false); 14 | assert.strictEqual(s.isCheckmate, false); 15 | assert.strictEqual(s.isRepetition, false); 16 | assert.strictEqual(s.isStalemate, false); 17 | assert.strictEqual(Object.keys(s.validMoves).length, 10); 18 | }); 19 | 20 | // test pawn move 21 | it('should properly represent status after first pawn moves', () => { 22 | let 23 | gc = SimpleGameClient.create(), 24 | s = null; 25 | 26 | gc.move('b2', 'b4'); 27 | gc.move('e7', 'e6'); 28 | 29 | s = gc.getStatus(); 30 | 31 | assert.strictEqual(s.isCheck, false); 32 | assert.strictEqual(s.isCheckmate, false); 33 | assert.strictEqual(s.isRepetition, false); 34 | assert.strictEqual(s.isStalemate, false); 35 | assert.strictEqual(Object.keys(s.validMoves).length, 11); 36 | }); 37 | 38 | // test invalid notation 39 | it('should properly throw exception for invalid moves', () => { 40 | let gc = SimpleGameClient.create(); 41 | 42 | assert.throws(() => { 43 | gc.move('h6'); 44 | }); 45 | assert.throws(() => { 46 | gc.move('e2', 'z9'); 47 | }); 48 | assert.throws(() => { 49 | gc.move('e2', 'e5'); 50 | }); 51 | }); 52 | 53 | // Issue #1 - Ensure no phantom pawns appear after sequence of moves in SimpleGameClient 54 | it('should not have a random Pawn appear on the board after a specific sequence of moves (bug fix test)', () => { 55 | let 56 | b, 57 | gc = SimpleGameClient.create(); 58 | 59 | b = gc.game.board; 60 | 61 | gc.move('e2', 'e4'); 62 | gc.move('e7', 'e5'); 63 | 64 | gc.move('g1', 'f3'); 65 | gc.move('b8', 'c6'); 66 | 67 | gc.move('f1', 'b5'); 68 | gc.move('g8', 'f6'); 69 | 70 | gc.move('e1', 'g1'); 71 | gc.move('f6', 'e4'); 72 | 73 | gc.move('d2', 'd4'); 74 | gc.move('e4', 'd6'); 75 | 76 | gc.move('b5', 'c6'); 77 | 78 | assert.ok(b.getSquare('c5').piece === null, 'Phantom piece appears after move from c5 to c6'); 79 | }); 80 | 81 | // Issue #23 - Show who is attacking the King 82 | it ('should properly emit check and indicate attackers of the King', () => { 83 | let 84 | checkResult = null, 85 | gc = SimpleGameClient.create(); 86 | 87 | gc.on('check', (result) => (checkResult = result)); 88 | 89 | // position the board for a promotion next move 90 | gc.game.board.getSquare('b1').piece = null; 91 | gc.game.board.getSquare('f6').piece = Piece.createKnight(SideType.White); 92 | gc.game.board.getSquare('f6').piece.moveCount = 1; 93 | 94 | // move to trigger evaluation that King is check 95 | gc.move('a2', 'a3'); 96 | 97 | // force recalculation of board position 98 | gc.getStatus(true); 99 | 100 | // make sure Pawn promotions are present 101 | assert.isDefined(checkResult); 102 | assert.strictEqual(checkResult.attackingSquare.piece.type, PieceType.Knight); 103 | }); 104 | 105 | // getCaptureHistory: track captures and undo 106 | it('should expose capture history via getCaptureHistory()', () => { 107 | const gc = SimpleGameClient.create(); 108 | gc.move('e2', 'e4'); 109 | gc.move('d7', 'd5'); 110 | const cap = gc.move('e4', 'd5'); 111 | 112 | const h1 = gc.getCaptureHistory(); 113 | assert.strictEqual(h1.length, 1); 114 | assert.strictEqual(h1[0].type, PieceType.Pawn); 115 | 116 | cap.undo(); 117 | const h2 = gc.getCaptureHistory(); 118 | assert.strictEqual(h2.length, 0); 119 | }); 120 | 121 | it('should emit castle event when castling by coordinates', () => { 122 | const gc = SimpleGameClient.create(); 123 | const castleEvents = []; 124 | gc.on('castle', (ev) => castleEvents.push(ev)); 125 | 126 | // clear path for white castle short (e1 -> g1) 127 | gc.game.board.getSquare('f1').piece = null; 128 | gc.game.board.getSquare('g1').piece = null; 129 | 130 | // force update to compute valid moves with cleared path 131 | gc.getStatus(true); 132 | gc.move('e1', 'g1'); 133 | 134 | assert.strictEqual(castleEvents.length, 1); 135 | }); 136 | 137 | it('should handle en passant and emit event', () => { 138 | const gc = SimpleGameClient.create(); 139 | const enPassantEvents = []; 140 | gc.on('enPassant', (ev) => enPassantEvents.push(ev)); 141 | 142 | // Setup: e2e4, a7a6, e4e5, d7d5, e5d6 e.p. 143 | gc.move('e2', 'e4'); 144 | gc.move('a7', 'a6'); 145 | gc.move('e4', 'e5'); 146 | gc.move('d7', 'd5'); 147 | 148 | const m = gc.move('e5', 'd6'); 149 | assert.ok(m.move.enPassant); 150 | assert.strictEqual(enPassantEvents.length, 1); 151 | }); 152 | 153 | it('should handle pawn promotion and emit event', () => { 154 | const gc = SimpleGameClient.create(); 155 | const promoteEvents = []; 156 | gc.on('promote', (ev) => promoteEvents.push(ev)); 157 | 158 | // Setup white pawn on a7 ready to promote, clear a8 and block pieces 159 | gc.game.board.getSquare('a7').piece = null; 160 | gc.game.board.getSquare('a8').piece = null; 161 | gc.game.board.getSquare('b8').piece = null; 162 | gc.game.board.getSquare('c8').piece = null; 163 | gc.game.board.getSquare('d8').piece = null; 164 | gc.game.board.getSquare('a2').piece = null; 165 | gc.game.board.getSquare('a7').piece = Piece.createPawn(SideType.White); 166 | gc.game.board.getSquare('a7').piece.moveCount = 1; 167 | 168 | gc.getStatus(true); 169 | const m = gc.move('a7', 'a8', 'Q'); 170 | 171 | assert.strictEqual(m.move.postSquare.piece.type, PieceType.Queen); 172 | assert.strictEqual(promoteEvents.length, 1); 173 | assert.strictEqual(gc.game.moveHistory[0].promotion, true); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /src/uciGameClient.js: -------------------------------------------------------------------------------- 1 | /* eslint sort-imports: 0 */ 2 | import { EventEmitter } from 'events'; 3 | import { Game } from './game.js'; 4 | import { GameValidation } from './gameValidation.js'; 5 | import { Piece } from './piece.js'; 6 | import { PieceType } from './piece.js'; 7 | 8 | // private helpers 9 | 10 | function parseUCI(uci) { 11 | if (typeof uci !== 'string') { 12 | return null; 13 | } 14 | 15 | // UCI format: e2e4, e7e8q (promotion), case-insensitive for promo 16 | let 17 | formatRegex = /^([a-h][1-8])([a-h][1-8])([qrbnQRBN])?$/, 18 | uciMove = uci.trim().match(formatRegex); 19 | 20 | if (!uciMove) { 21 | return null; 22 | } 23 | 24 | let 25 | dest = { file: uciMove[2][0], rank: Number(uciMove[2][1]) }, 26 | promo = uciMove[3] ? uciMove[3].toUpperCase() : '', 27 | src = { file: uciMove[1][0], rank: Number(uciMove[1][1]) }; 28 | 29 | return { dest, promo, src }; 30 | } 31 | 32 | function updateGameClient(gameClient) { 33 | return gameClient.validation.start((err, result) => { 34 | if (err) { 35 | throw new Error(err); 36 | } 37 | 38 | gameClient.isCheck = result.isCheck; 39 | gameClient.isCheckmate = result.isCheckmate; 40 | gameClient.isRepetition = result.isRepetition; 41 | gameClient.isStalemate = result.isStalemate; 42 | gameClient.validMoves = result.validMoves; 43 | gameClient.uciMoves = notateUCI(result.validMoves); 44 | }); 45 | } 46 | 47 | function notateUCI(validMoves) { 48 | let 49 | i = 0, 50 | isPromotion = false, 51 | notation = {}; 52 | 53 | // iterate through all valid moves and create UCI notation 54 | for (; i < validMoves.length; i++) { 55 | let 56 | p = validMoves[i].src.piece, 57 | src = validMoves[i].src; 58 | 59 | // reset inner index for each piece's move list 60 | for (let n = 0; n < validMoves[i].squares.length; n++) { 61 | // get the destination square for this move 62 | let sq = validMoves[i].squares[n]; 63 | 64 | // base notation 65 | let base = `${src.file}${src.rank}${sq.file}${sq.rank}`; 66 | 67 | // check for potential promotion 68 | /* eslint no-magic-numbers: 0 */ 69 | isPromotion = 70 | (sq.rank === 8 || sq.rank === 1) && 71 | p.type === PieceType.Pawn; 72 | 73 | if (isPromotion) { 74 | // add all promotion options 75 | ['q', 'r', 'b', 'n'].forEach((promo) => { 76 | notation[`${base}${promo}`] = { 77 | dest: sq, 78 | src 79 | }; 80 | }); 81 | 82 | continue 83 | } 84 | 85 | // regular move 86 | notation[base] = { 87 | dest: sq, 88 | src 89 | }; 90 | } 91 | } 92 | 93 | return notation; 94 | } 95 | 96 | export class UCIGameClient extends EventEmitter { 97 | constructor(game) { 98 | super(); 99 | 100 | this.game = game; 101 | this.isCheck = false; 102 | this.isCheckmate = false; 103 | this.isRepetition = false; 104 | this.isStalemate = false; 105 | this.uciMoves = {}; 106 | this.validMoves = []; 107 | this.validation = GameValidation.create(this.game); 108 | 109 | // bubble the game and board events 110 | ['check', 'checkmate'].forEach((ev) => { 111 | this.game.on(ev, (data) => this.emit(ev, data)); 112 | }); 113 | 114 | ['capture', 'castle', 'enPassant', 'move', 'promote', 'undo'].forEach((ev) => { 115 | this.game.board.on(ev, (data) => this.emit(ev, data)); 116 | }); 117 | 118 | const self = this; 119 | this.on('undo', () => { 120 | // force an update 121 | self.getStatus(true); 122 | }); 123 | } 124 | 125 | static create() { 126 | let 127 | game = Game.create(), 128 | gameClient = new UCIGameClient(game); 129 | 130 | updateGameClient(gameClient); 131 | 132 | return gameClient; 133 | } 134 | 135 | getStatus(forceUpdate) { 136 | if (forceUpdate) { 137 | updateGameClient(this); 138 | } 139 | 140 | return { 141 | board: this.game.board, 142 | isCheck: this.isCheck, 143 | isCheckmate: this.isCheckmate, 144 | isRepetition: this.isRepetition, 145 | isStalemate: this.isStalemate, 146 | uciMoves: this.uciMoves 147 | }; 148 | } 149 | 150 | getCaptureHistory() { 151 | return this.game.captureHistory; 152 | } 153 | 154 | move(uci) { 155 | let 156 | canonical = null, 157 | dest = null, 158 | move = null, 159 | parsed = parseUCI(uci), 160 | promo = null, 161 | requiresPromotion = false, 162 | side = null, 163 | src = null, 164 | srcSquare = null; 165 | 166 | if (!parsed) { 167 | throw new Error(`UCI is invalid (${uci})`); 168 | } 169 | 170 | // destructure the parsed UCI move 171 | ({ src, dest, promo } = parsed); 172 | 173 | // normalize UCI key to compare with generated map 174 | canonical = promo 175 | ? `${src.file}${src.rank}${dest.file}${dest.rank}${promo.toLowerCase()}` 176 | : `${src.file}${src.rank}${dest.file}${dest.rank}`; 177 | 178 | // ensure move exactly matches a generated UCI move 179 | if (!this.uciMoves || !this.uciMoves[canonical]) { 180 | throw new Error(`Move is invalid (${uci})`); 181 | } 182 | 183 | // determine the current side 184 | side = this.game.getCurrentSide(); 185 | 186 | // additional safety: enforce promotion semantics 187 | srcSquare = this.game.board.getSquare(src.file, src.rank); 188 | requiresPromotion = 189 | srcSquare && srcSquare.piece && srcSquare.piece.type === PieceType.Pawn && 190 | (dest.rank === 8 || dest.rank === 1); 191 | 192 | if (requiresPromotion && !promo) { 193 | throw new Error(`Promotion required for move (${uci})`); 194 | } 195 | 196 | if (promo && !requiresPromotion) { 197 | throw new Error(`Promotion flag not allowed for move (${uci})`); 198 | } 199 | 200 | // make the move 201 | move = this.game.board.move(`${src.file}${src.rank}`, `${dest.file}${dest.rank}`); 202 | if (move) { 203 | // apply pawn promotion if applicable (already validated above) 204 | if (promo) { 205 | let piece; 206 | switch (promo) { 207 | case 'B': 208 | piece = Piece.createBishop(side); 209 | break; 210 | case 'N': 211 | piece = Piece.createKnight(side); 212 | break; 213 | case 'Q': 214 | piece = Piece.createQueen(side); 215 | break; 216 | case 'R': 217 | piece = Piece.createRook(side); 218 | break; 219 | default: 220 | piece = null; 221 | break; 222 | } 223 | 224 | if (piece) { 225 | this.game.board.promote(move.move.postSquare, piece); 226 | } 227 | } 228 | 229 | updateGameClient(this); 230 | 231 | return move; 232 | } 233 | 234 | throw new Error(`Move is invalid (${uci})`); 235 | } 236 | } 237 | 238 | export default { UCIGameClient }; 239 | -------------------------------------------------------------------------------- /test/src/game.js: -------------------------------------------------------------------------------- 1 | /* eslint no-magic-numbers:0 */ 2 | import { assert, describe, it } from 'vitest'; 3 | import { PieceType, SideType } from '../../src/piece.js'; 4 | import { Game } from '../../src/game.js'; 5 | 6 | describe('Game', () => { 7 | // make sure there is no move history when game is created 8 | it('should have no move history when game is created', () => { 9 | let g = Game.create(); 10 | 11 | assert.strictEqual(g.moveHistory.length, 0); 12 | }); 13 | 14 | // verify move history is tracked on game when a move is made on board 15 | it('should have move history length of 2 after 2 moves are made', () => { 16 | let 17 | b, 18 | g = Game.create(); 19 | 20 | b = g.board; 21 | 22 | b.move(b.getSquare('d', 2), b.getSquare('d', 4)); 23 | b.move(b.getSquare('d', 7), b.getSquare('d', 5)); 24 | 25 | assert.strictEqual(g.moveHistory.length, 2); 26 | }); 27 | 28 | // verify move history is updated on game when an undo occurs on board 29 | it('should have move history length of 1 after 2 moves and undo', () => { 30 | let 31 | b, 32 | g = Game.create(), 33 | m; 34 | 35 | b = g.board; 36 | 37 | b.move(b.getSquare('d', 2), b.getSquare('d', 4)); 38 | m = b.move(b.getSquare('d', 7), b.getSquare('d', 5)); 39 | m.undo(); 40 | 41 | assert.strictEqual(g.moveHistory.length, 1); 42 | }); 43 | 44 | // ensure that when simulated moves are made on board, game history is not incremented 45 | it('should have no move history when only simulated moves are made', () => { 46 | let 47 | b, 48 | g = Game.create(), 49 | r; 50 | 51 | b = g.board; 52 | r = b.move(b.getSquare('e', 2), b.getSquare('e', 4), true); 53 | 54 | assert.strictEqual(g.moveHistory.length, 0); 55 | 56 | r.undo(); 57 | 58 | assert.strictEqual(g.moveHistory.length, 0); 59 | }); 60 | 61 | // ensure board position hash is accurate across moves 62 | it('should have accurate hash code after each move stored in move history', () => { 63 | let 64 | b, 65 | g = Game.create(); 66 | 67 | b = g.board; 68 | 69 | b.move(b.getSquare('d', 2), b.getSquare('d', 4)); 70 | b.move(b.getSquare('d', 7), b.getSquare('d', 5)); 71 | 72 | b.move(b.getSquare('c', 1), b.getSquare('f', 4)); 73 | b.move(b.getSquare('c', 8), b.getSquare('f', 5)); 74 | b.move(b.getSquare('f', 4), b.getSquare('c', 1)); 75 | b.move(b.getSquare('f', 5), b.getSquare('c', 8)); 76 | 77 | b.move(b.getSquare('c', 1), b.getSquare('f', 4)); 78 | b.move(b.getSquare('c', 8), b.getSquare('f', 5)); 79 | b.move(b.getSquare('f', 4), b.getSquare('c', 1)); 80 | b.move(b.getSquare('f', 5), b.getSquare('c', 8)); 81 | 82 | assert.strictEqual(g.moveHistory.length, 10); 83 | assert.ok(g.moveHistory[0].hashCode !== g.moveHistory[1].hashCode); 84 | assert.strictEqual(g.moveHistory[1].hashCode, g.moveHistory[5].hashCode); 85 | assert.strictEqual(g.moveHistory[3].hashCode, g.moveHistory[7].hashCode); 86 | assert.strictEqual(g.moveHistory[5].hashCode, g.moveHistory[9].hashCode); 87 | }); 88 | 89 | it('should properly have notation in move history after move when supplied', () => { 90 | let 91 | b, 92 | g = Game.create(); 93 | 94 | b = g.board; 95 | 96 | b.move('d2', 'd4', 'd4'); 97 | assert.strictEqual(g.moveHistory.length, 1); 98 | assert.ok(g.moveHistory[0].algebraic === 'd4'); 99 | }); 100 | 101 | it('should not have notation in move history after move when omitted', () => { 102 | let 103 | b, 104 | g = Game.create(); 105 | 106 | b = g.board; 107 | 108 | b.move('d2', 'd4'); 109 | assert.strictEqual(g.moveHistory.length, 1); 110 | assert.ok(typeof g.moveHistory[0].algebraic === 'undefined'); 111 | }); 112 | 113 | // ensure board position hash is accurate across games 114 | it('should have consistent board hash across different game objects with same move histories', () => { 115 | let 116 | b1, 117 | b2, 118 | g1 = Game.create(), 119 | g2 = Game.create(); 120 | 121 | b1 = g1.board; 122 | b2 = g2.board; 123 | 124 | b1.move(b1.getSquare('d', 2), b1.getSquare('d', 4)); 125 | b1.move(b1.getSquare('d', 7), b1.getSquare('d', 5)); 126 | 127 | b2.move(b2.getSquare('d', 2), b2.getSquare('d', 4)); 128 | b2.move(b2.getSquare('d', 7), b2.getSquare('d', 5)); 129 | 130 | assert.strictEqual(g1.moveHistory[0].hashCode, g2.moveHistory[0].hashCode); 131 | assert.strictEqual(g1.moveHistory[1].hashCode, g2.moveHistory[1].hashCode); 132 | }); 133 | 134 | // Issue #1 - Ensure no phantom pawns appear after sequence of moves 135 | it('should not have phantom pawn appear after specific sequence of moves - Issue #1', () => { 136 | let 137 | b, 138 | g = Game.create(); 139 | 140 | b = g.board; 141 | 142 | b.move(b.getSquare('e', 2), b.getSquare('e', 4)); 143 | b.move(b.getSquare('e', 7), b.getSquare('e', 5)); 144 | 145 | b.move(b.getSquare('g', 1), b.getSquare('f', 3)); 146 | b.move(b.getSquare('b', 8), b.getSquare('c', 6)); 147 | 148 | b.move(b.getSquare('f', 1), b.getSquare('b', 5)); 149 | b.move(b.getSquare('g', 8), b.getSquare('f', 6)); 150 | 151 | b.move(b.getSquare('e', 1), b.getSquare('g', 1)); 152 | b.move(b.getSquare('f', 6), b.getSquare('e', 4)); 153 | 154 | b.move(b.getSquare('d', 2), b.getSquare('d', 4)); 155 | b.move(b.getSquare('e', 4), b.getSquare('d', 6)); 156 | 157 | b.move(b.getSquare('b', 5), b.getSquare('c', 6)); 158 | 159 | assert.ok(b.getSquare('c5').piece === null); 160 | }); 161 | 162 | // ensure load from moveHistory results in board in appropriate state 163 | }); 164 | 165 | describe('Game capture history', () => { 166 | it('should track captures and undo correctly', () => { 167 | const g = Game.create(); 168 | const b = g.board; 169 | 170 | // e2e4, d7d5, e4xd5 171 | b.move(b.getSquare('e', 2), b.getSquare('e', 4)); 172 | b.move(b.getSquare('d', 7), b.getSquare('d', 5)); 173 | const cap = b.move(b.getSquare('e', 4), b.getSquare('d', 5)); 174 | 175 | // verify capture tracked 176 | if (g.captureHistory.length !== 1) { 177 | throw new Error('captureHistory should contain one capture'); 178 | } 179 | 180 | const c = g.captureHistory[0]; 181 | 182 | if (!c || c.type !== PieceType.Pawn || c.side !== SideType.Black) { 183 | throw new Error('captureHistory should contain captured black pawn'); 184 | } 185 | 186 | // undo and verify capture removed 187 | cap.undo(); 188 | if (g.captureHistory.length !== 0) { 189 | throw new Error('captureHistory should be empty after undo'); 190 | } 191 | }); 192 | 193 | it('should track multiple captures in order', () => { 194 | const g = Game.create(); 195 | const b = g.board; 196 | 197 | // e2e4, d7d5, e4xd5 (capture 1) 198 | b.move(b.getSquare('e', 2), b.getSquare('e', 4)); 199 | b.move(b.getSquare('d', 7), b.getSquare('d', 5)); 200 | b.move(b.getSquare('e', 4), b.getSquare('d', 5)); 201 | 202 | // c7c5, d5xc5 (capture 2) 203 | b.move(b.getSquare('c', 7), b.getSquare('c', 5)); 204 | const cap2 = b.move(b.getSquare('d', 5), b.getSquare('c', 5)); 205 | 206 | if (g.captureHistory.length !== 2) { 207 | throw new Error('captureHistory should contain two captures'); 208 | } 209 | 210 | const [c1, c2] = g.captureHistory; 211 | 212 | if (!c1 || c1.type !== PieceType.Pawn || c1.side !== SideType.Black) { 213 | throw new Error('first capture should be black pawn from d5'); 214 | } 215 | 216 | if (!c2 || c2.type !== PieceType.Pawn || c2.side !== SideType.Black) { 217 | throw new Error('second capture should be black pawn from c5'); 218 | } 219 | 220 | // undo last capture reduces capture history by 1 221 | cap2.undo(); 222 | if (g.captureHistory.length !== 1) { 223 | throw new Error('captureHistory should have one capture after undoing the last capture'); 224 | } 225 | }); 226 | }); 227 | -------------------------------------------------------------------------------- /test/src/gameValidation.js: -------------------------------------------------------------------------------- 1 | /* eslint no-magic-numbers:0 */ 2 | import { assert, describe, it } from 'vitest'; 3 | import { Game } from '../../src/game.js'; 4 | import { GameValidation } from '../../src/gameValidation.js'; 5 | 6 | describe('GameValidation', () => { 7 | // validate check 8 | it('should properly indicate check', () => { 9 | let 10 | b, 11 | g = Game.create(), 12 | v = GameValidation.create(g); 13 | 14 | b = g.board; 15 | 16 | // put king into check 17 | b.move(b.getSquare('d', 2), b.getSquare('d', 4)); 18 | b.move(b.getSquare('e', 7), b.getSquare('e', 5)); 19 | b.move(b.getSquare('d', 4), b.getSquare('e', 5)); 20 | b.move(b.getSquare('f', 8), b.getSquare('b', 4)); 21 | 22 | v.start((err, result) => { 23 | if (err) { 24 | throw err; 25 | } 26 | 27 | assert.strictEqual(result.isCheck, true); 28 | assert.strictEqual(result.isCheckmate, false); 29 | assert.strictEqual(result.isRepetition, false); 30 | assert.strictEqual(result.isStalemate, false); 31 | }); 32 | }); 33 | 34 | // validate Knight check (part 1) 35 | it('should properly indicate check due to Knight (part 1)', () => { 36 | let 37 | b, 38 | g = Game.create(), 39 | v = GameValidation.create(g); 40 | 41 | b = g.board; 42 | 43 | b.move(b.getSquare('b1'), b.getSquare('c7')); 44 | 45 | v.start((err, result) => { 46 | if (err) { 47 | throw err; 48 | } 49 | 50 | assert.strictEqual(result.isCheck, true); 51 | }); 52 | }); 53 | 54 | // validate Knight check (part 2) 55 | it('should properly indicate check due to Knight (part 2)', () => { 56 | let 57 | b, 58 | g = Game.create(), 59 | v = GameValidation.create(g); 60 | 61 | b = g.board; 62 | 63 | b.move(b.getSquare('b1'), b.getSquare('c3')); 64 | b.move(b.getSquare('g8'), b.getSquare('f6')); 65 | b.move(b.getSquare('c3'), b.getSquare('b5')); 66 | b.move(b.getSquare('f6'), b.getSquare('g8')); 67 | b.move(b.getSquare('b5'), b.getSquare('d6')); 68 | 69 | v.start((err, result) => { 70 | if (err) { 71 | throw err; 72 | } 73 | 74 | assert.strictEqual(result.isCheck, true); 75 | }); 76 | }); 77 | 78 | // validate checkmate 79 | it('should properly indicate checkmate', () => { 80 | let 81 | b, 82 | g = Game.create(), 83 | v = GameValidation.create(g); 84 | 85 | b = g.board; 86 | 87 | // put king into checkmate 88 | b.move(b.getSquare('e', 2), b.getSquare('e', 4)); 89 | b.move(b.getSquare('f', 7), b.getSquare('f', 6)); 90 | b.move(b.getSquare('d', 2), b.getSquare('d', 4)); 91 | b.move(b.getSquare('g', 7), b.getSquare('g', 5)); 92 | b.move(b.getSquare('d', 1), b.getSquare('h', 5)); 93 | 94 | v.start((err, result) => { 95 | if (err) { 96 | throw err; 97 | } 98 | 99 | assert.strictEqual(result.validMoves.length, 0); 100 | assert.strictEqual(result.isCheck, false); 101 | assert.strictEqual(result.isCheckmate, true); 102 | assert.strictEqual(result.isRepetition, false); 103 | assert.strictEqual(result.isStalemate, false); 104 | }); 105 | }); 106 | 107 | // validate stalemate 108 | it('should properly indicate stalemate', () => { 109 | let 110 | b, 111 | g = Game.create(), 112 | v = GameValidation.create(g); 113 | 114 | b = g.board; 115 | 116 | // remove several pieces from the board 117 | b.getSquare('a1').piece = null; 118 | b.getSquare('b1').piece = null; 119 | b.getSquare('c1').piece = null; 120 | b.getSquare('d1').piece = null; 121 | b.getSquare('f1').piece = null; 122 | b.getSquare('g1').piece = null; 123 | b.getSquare('h1').piece = null; 124 | b.getSquare('a2').piece = null; 125 | b.getSquare('b2').piece = null; 126 | b.getSquare('c2').piece = null; 127 | b.getSquare('d2').piece = null; 128 | b.getSquare('f2').piece = null; 129 | b.getSquare('g2').piece = null; 130 | b.getSquare('h2').piece = null; 131 | b.getSquare('a8').piece = null; 132 | b.getSquare('b8').piece = null; 133 | b.getSquare('c8').piece = null; 134 | b.getSquare('d8').piece = null; 135 | b.getSquare('f8').piece = null; 136 | b.getSquare('g8').piece = null; 137 | b.getSquare('h8').piece = null; 138 | b.getSquare('a7').piece = null; 139 | b.getSquare('b7').piece = null; 140 | b.getSquare('c7').piece = null; 141 | b.getSquare('d7').piece = null; 142 | b.getSquare('e7').piece = null; 143 | b.getSquare('f7').piece = null; 144 | b.getSquare('g7').piece = null; 145 | b.getSquare('h7').piece = null; 146 | 147 | // put black king in stalemate 148 | b.move('e1', 'e6', true); 149 | b.move('e2', 'e7'); 150 | 151 | v.start((err, result) => { 152 | if (err) { 153 | throw err; 154 | } 155 | 156 | assert.strictEqual(result.validMoves.length, 0); 157 | assert.strictEqual(result.isCheck, false); 158 | assert.strictEqual(result.isCheckmate, false); 159 | assert.strictEqual(result.isRepetition, false); 160 | assert.strictEqual(result.isStalemate, true); 161 | }); 162 | }); 163 | 164 | // validate threefold repetition rule 165 | // Fischer vs Petrosian, Buenos Aires, 1971, round 3 166 | it('should properly indicate 3-fold repetition', () => { 167 | let 168 | b, 169 | g = Game.create(), 170 | v = GameValidation.create(g); 171 | 172 | b = g.board; 173 | 174 | // 1. e4 e6 175 | b.move('e2', 'e4'); 176 | b.move('e7', 'e6'); 177 | // 2. d4 d5 178 | b.move('d2', 'd4'); 179 | b.move('d7', 'd5'); 180 | // 3. Nc3 Nf6 181 | b.move('b1', 'c3'); 182 | b.move('g8', 'f6'); 183 | // 4. Bg5 dxe4 184 | b.move('c1', 'g5'); 185 | b.move('d5', 'e4'); 186 | // 5. Nxe4 Be7 187 | b.move('c3', 'e4'); 188 | b.move('f8', 'e7'); 189 | // 6. Bxf6 gxf6 190 | b.move('g5', 'f6'); 191 | b.move('g7', 'f6'); 192 | // 7. g3 f5 193 | b.move('g2', 'g3'); 194 | b.move('f6', 'f5'); 195 | // 8. Nc3 Bf6 196 | b.move('e4', 'c3'); 197 | b.move('e7', 'f6'); 198 | // 9. Nge2 Nc6 199 | b.move('g1', 'e2'); 200 | b.move('b8', 'c6'); 201 | // 10. d5 exd5 202 | b.move('d4', 'd5'); 203 | b.move('e6', 'd5'); 204 | // 11. Nxd5 Bxb2 205 | b.move('c3', 'd5'); 206 | b.move('f6', 'b2'); 207 | // 12. Bg2 O-O 208 | b.move('f1', 'g2'); 209 | b.move('e8', 'g8'); 210 | // 13. O-O Bh8 211 | b.move('e1', 'g1'); 212 | b.move('b2', 'h8'); 213 | // 14. Nef4 Ne5 214 | b.move('e2', 'f4'); 215 | b.move('c6', 'e5'); 216 | // 15. Qh5 Ng6 217 | b.move('d1', 'h5'); 218 | b.move('e5', 'g6'); 219 | // 16. Rad1 c6 220 | b.move('a1', 'd1'); 221 | b.move('c7', 'c6'); 222 | // 17. Ne3 Qf6 223 | b.move('d5', 'e3'); 224 | b.move('d8', 'f6'); 225 | // 18. Kh1 Bg7 226 | b.move('g1', 'h1'); 227 | b.move('h8', 'g7'); 228 | // 19. Bh3 Ne7 229 | b.move('g2', 'h3'); 230 | b.move('g6', 'e7'); 231 | // 20. Rd3 Be6 232 | b.move('d1', 'd3'); 233 | b.move('c8', 'e6'); 234 | // 21. Rfd1 Bh6 235 | b.move('f1', 'd1'); 236 | b.move('g7', 'h6'); 237 | // 22. Rd4 Bxf4 238 | b.move('d3', 'd4'); 239 | b.move('h6', 'f4'); 240 | // 23. Rxf4 Rad8 241 | b.move('d4', 'f4'); 242 | b.move('a8', 'd8'); 243 | // 24. Rxd8 Rxd8 244 | b.move('d1', 'd8'); 245 | b.move('f8', 'd8'); 246 | // 25. Bxf5 Nxf5 247 | b.move('h3', 'f5'); 248 | b.move('e7', 'f5'); 249 | // 26. Nxf5 Rd5 250 | b.move('e3', 'f5'); 251 | b.move('d8', 'd5'); 252 | // 27. g4 Bxf5 253 | b.move('g3', 'g4'); 254 | b.move('e6', 'f5'); 255 | // 28. gxf5 h6 256 | b.move('g4', 'f5'); 257 | b.move('h7', 'h6'); 258 | // 29. h3 Kh7 259 | b.move('h2', 'h3'); 260 | b.move('g8', 'h7'); 261 | // 30. Qe2 Qe5 262 | b.move('h5', 'e2'); 263 | b.move('f6', 'e5'); 264 | // 31. Qh5 Qf6 265 | b.move('e2', 'h5'); 266 | b.move('e5', 'f6'); 267 | // 32. Qe2 Re5 268 | b.move('h5', 'e2'); 269 | b.move('d5', 'e5'); 270 | // 33. Qd3 Rd5 271 | b.move('e2', 'd3'); 272 | b.move('e5', 'd5'); 273 | // 34. Qe2 274 | b.move('d3', 'e2'); 275 | 276 | v.start((err, result) => { 277 | if (err) { 278 | throw err; 279 | } 280 | 281 | assert.ok(result.validMoves.length > 0); 282 | assert.strictEqual(result.isCheck, false); 283 | assert.strictEqual(result.isCheckmate, false); 284 | assert.strictEqual(result.isRepetition, true); 285 | assert.strictEqual(result.isStalemate, false); 286 | }); 287 | }); 288 | }); 289 | -------------------------------------------------------------------------------- /src/pieceValidation.js: -------------------------------------------------------------------------------- 1 | /** 2 | The general idea behind PieceValidation is to examine an individual piece 3 | and determine (with the information available from about that single piece) 4 | what move options are available for that piece. 5 | 6 | The PieceValidation doesn't alter any properties of the piece, the board 7 | or any squares. Additionally, the PieceValidation is suitable for 1 phase of 8 | the evaluation of viable move options for a piece... the BoardValidation 9 | component handles the overall evaluation of what moves are possible for the 10 | board in its current state. 11 | */ 12 | import { PieceType, SideType } from './piece.js'; 13 | import { NeighborType } from './board.js'; 14 | 15 | export class PieceValidation { 16 | constructor (board) { 17 | this.allowBackward = false; 18 | this.allowDiagonal = false; 19 | this.allowForward = false; 20 | this.allowHorizontal = false; 21 | this.board = board; 22 | this.type = null; 23 | this.repeat = 0; 24 | } 25 | 26 | applySpecialValidation () { 27 | // do nothing... 28 | // overridden in the concrete validation classes 29 | // where special logic is required 30 | } 31 | 32 | static create (piece, board) { 33 | switch (piece) { 34 | case PieceType.Bishop: 35 | return new BishopValidation(board); 36 | case PieceType.King: 37 | return new KingValidation(board); 38 | case PieceType.Knight: 39 | return new KnightValidation(board); 40 | case PieceType.Pawn: 41 | return new PawnValidation(board); 42 | case PieceType.Queen: 43 | return new QueenValidation(board); 44 | case PieceType.Rook: 45 | return new RookValidation(board); 46 | default: 47 | return null; 48 | } 49 | } 50 | 51 | start (src, callback) { 52 | // ensure callback is set 53 | callback = callback || ((err, destinationSquares) => new Promise((resolve, reject) => { 54 | if (err) { 55 | return reject(err); 56 | } 57 | 58 | return resolve(destinationSquares); 59 | })); 60 | 61 | let opt = { 62 | destSquares : [], 63 | origin : src, 64 | piece : src ? src.piece : null 65 | }; 66 | 67 | const findMoveOptions = function (b, r, n) { 68 | let 69 | block = false, 70 | capture = false, 71 | currentSquare = b.getNeighborSquare(opt.origin, n), 72 | i = 0; 73 | 74 | while (currentSquare && i < r) { 75 | block = currentSquare.piece !== null && 76 | (opt.piece.type === PieceType.Pawn || 77 | currentSquare.piece.side === opt.piece.side); 78 | capture = (currentSquare.piece && !block); 79 | 80 | if (!block) { 81 | opt.destSquares.push(currentSquare); 82 | } 83 | 84 | if (capture || block) { 85 | currentSquare = null; 86 | } else { 87 | currentSquare = b.getNeighborSquare(currentSquare, n); 88 | i++; 89 | } 90 | } 91 | }; 92 | 93 | if (!opt.piece || opt.piece.type !== this.type) { 94 | return callback(new Error('piece is invalid')); 95 | } 96 | 97 | if (this.board && opt.origin) { 98 | // forward squares 99 | if (this.allowForward) { 100 | findMoveOptions(this.board, this.repeat, 101 | opt.piece.side === SideType.White ? 102 | NeighborType.Above : 103 | NeighborType.Below); 104 | } 105 | 106 | // backward squares 107 | if (this.allowBackward) { 108 | findMoveOptions(this.board, this.repeat, 109 | opt.piece.side === SideType.White ? 110 | NeighborType.Below : 111 | NeighborType.Above); 112 | } 113 | 114 | // horizontal squares 115 | if (this.allowHorizontal) { 116 | findMoveOptions(this.board, this.repeat, NeighborType.Left); 117 | findMoveOptions(this.board, this.repeat, NeighborType.Right); 118 | } 119 | 120 | // diagonal squares 121 | if (this.allowDiagonal) { 122 | findMoveOptions(this.board, this.repeat, NeighborType.AboveLeft); 123 | findMoveOptions(this.board, this.repeat, NeighborType.BelowRight); 124 | findMoveOptions(this.board, this.repeat, NeighborType.BelowLeft); 125 | findMoveOptions(this.board, this.repeat, NeighborType.AboveRight); 126 | } 127 | 128 | // apply additional validation logic 129 | this.applySpecialValidation(opt); 130 | 131 | // callback 132 | return callback(null, opt.destSquares); 133 | } 134 | 135 | return callback(new Error('board is invalid')); 136 | } 137 | } 138 | 139 | export class BishopValidation extends PieceValidation { 140 | constructor (board) { 141 | super(board); 142 | 143 | // base validation properties 144 | this.allowDiagonal = true; 145 | this.type = PieceType.Bishop; 146 | this.repeat = 8; 147 | } 148 | } 149 | 150 | export class KingValidation extends PieceValidation { 151 | constructor (board) { 152 | super(board); 153 | 154 | // base validation properties 155 | this.allowBackward = true; 156 | this.allowDiagonal = true; 157 | this.allowForward = true; 158 | this.allowHorizontal = true; 159 | this.type = PieceType.King; 160 | this.repeat = 1; 161 | } 162 | 163 | applySpecialValidation () { 164 | // check for castle? 165 | } 166 | } 167 | 168 | export class KnightValidation extends PieceValidation { 169 | constructor (board) { 170 | super(board); 171 | 172 | // base validation properties 173 | this.type = PieceType.Knight; 174 | this.repeat = 1; 175 | } 176 | 177 | applySpecialValidation (opt) { 178 | // add knight move options 179 | let 180 | aboveLeft = this.board.getNeighborSquare( 181 | opt.origin, 182 | NeighborType.AboveLeft), 183 | aboveRight = this.board.getNeighborSquare( 184 | opt.origin, 185 | NeighborType.AboveRight), 186 | belowLeft = this.board.getNeighborSquare( 187 | opt.origin, 188 | NeighborType.BelowLeft), 189 | belowRight = this.board.getNeighborSquare( 190 | opt.origin, 191 | NeighborType.BelowRight), 192 | i = 0, 193 | p = null, 194 | squares = []; 195 | 196 | if (aboveLeft) { 197 | squares.push(this.board.getNeighborSquare( 198 | aboveLeft, 199 | NeighborType.Above)); 200 | 201 | squares.push(this.board.getNeighborSquare( 202 | aboveLeft, 203 | NeighborType.Left)); 204 | } 205 | 206 | if (aboveRight) { 207 | squares.push(this.board.getNeighborSquare( 208 | aboveRight, 209 | NeighborType.Above)); 210 | 211 | squares.push(this.board.getNeighborSquare( 212 | aboveRight, 213 | NeighborType.Right)); 214 | } 215 | 216 | if (belowLeft) { 217 | squares.push(this.board.getNeighborSquare( 218 | belowLeft, 219 | NeighborType.Below)); 220 | 221 | squares.push(this.board.getNeighborSquare( 222 | belowLeft, 223 | NeighborType.Left)); 224 | } 225 | 226 | if (belowRight) { 227 | squares.push(this.board.getNeighborSquare( 228 | belowRight, 229 | NeighborType.Below)); 230 | 231 | squares.push(this.board.getNeighborSquare( 232 | belowRight, 233 | NeighborType.Right)); 234 | } 235 | 236 | for (i = 0; i < squares.length; i++) { 237 | if (squares[i]) { 238 | // check for enemy piece on square 239 | p = squares[i] ? squares[i].piece : null; 240 | if (!p || p.side !== opt.piece.side) { 241 | opt.destSquares.push(squares[i]); 242 | } 243 | } 244 | } 245 | } 246 | } 247 | 248 | export class PawnValidation extends PieceValidation { 249 | constructor (board) { 250 | super(board); 251 | 252 | // base validation properties 253 | this.allowForward = true; 254 | this.type = PieceType.Pawn; 255 | this.repeat = 1; 256 | } 257 | 258 | /* eslint no-magic-numbers:0 */ 259 | applySpecialValidation (opt) { 260 | // check for capture 261 | let 262 | i = 0, 263 | p = null, 264 | sq = null, 265 | squares = [ 266 | this.board.getNeighborSquare(opt.origin, 267 | opt.piece.side === SideType.White ? 268 | NeighborType.AboveLeft : 269 | NeighborType.BelowLeft), 270 | this.board.getNeighborSquare(opt.origin, 271 | opt.piece.side === SideType.White ? 272 | NeighborType.AboveRight : 273 | NeighborType.BelowRight)]; 274 | 275 | // check for capture 276 | for (i = 0; i < squares.length; i++) { 277 | // check for enemy piece on square 278 | p = squares[i] ? squares[i].piece : null; 279 | if (p && p.side !== opt.piece.side) { 280 | opt.destSquares.push(squares[i]); 281 | } 282 | } 283 | 284 | // check for double square first move 285 | if (opt.piece.moveCount === 0 && 286 | opt.destSquares.length && // Fix for issue #15 (originally looked for length of 1) 287 | opt.destSquares[0].piece === null) { // Fix for issue #1 288 | sq = this.board.getNeighborSquare( 289 | opt.destSquares[0], 290 | opt.piece.side === SideType.White ? 291 | NeighborType.Above : 292 | NeighborType.Below); 293 | 294 | if (!sq.piece) { 295 | opt.destSquares.push(sq); 296 | } 297 | 298 | // check for en passant 299 | } else if (opt.origin.rank === 300 | (opt.piece.side === SideType.White ? 5 : 4)) { 301 | // get squares left & right of pawn 302 | squares = [ 303 | this.board.getNeighborSquare(opt.origin, NeighborType.Left), 304 | this.board.getNeighborSquare(opt.origin, NeighborType.Right)]; 305 | i = 0; 306 | 307 | for (i = 0; i < squares.length; i++) { 308 | // check for pawn on square 309 | p = squares[i] ? squares[i].piece : null; 310 | if (p && 311 | p.type === PieceType.Pawn && 312 | p.side !== opt.piece.side && 313 | p.moveCount === 1 && 314 | this.board.lastMovedPiece === p) { 315 | 316 | opt.destSquares.push( 317 | this.board.getNeighborSquare( 318 | squares[i], 319 | p.side === SideType.Black ? 320 | NeighborType.Above : 321 | NeighborType.Below)); 322 | } 323 | } 324 | } 325 | } 326 | } 327 | 328 | export class QueenValidation extends PieceValidation { 329 | constructor (board) { 330 | super(board); 331 | 332 | // base validation properties 333 | this.allowBackward = true; 334 | this.allowDiagonal = true; 335 | this.allowForward = true; 336 | this.allowHorizontal = true; 337 | this.repeat = 8; 338 | this.type = PieceType.Queen; 339 | } 340 | } 341 | 342 | export class RookValidation extends PieceValidation { 343 | constructor (board) { 344 | super(board); 345 | 346 | // base validation properties 347 | this.allowBackward = true; 348 | this.allowForward = true; 349 | this.allowHorizontal = true; 350 | this.repeat = 8; 351 | this.type = PieceType.Rook; 352 | } 353 | } 354 | 355 | export default { PieceValidation }; 356 | -------------------------------------------------------------------------------- /src/boardValidation.js: -------------------------------------------------------------------------------- 1 | /** 2 | BoardValidation determines viable move options for all pieces 3 | given the current state of the board. If it's the White sides turn 4 | to attack, only White piece move options are evaluated (and visa versa). 5 | 6 | BoardValidation is intended to be the 2nd phase of move 7 | validation that is capable of taking into account factors across pieces 8 | on the board (and not just the pieces themselves). For example, King 9 | castle eligibility is determined based on whether or not both the candidate 10 | King and Rook pieces have not moved and whether or not the path of travel 11 | for the King would result in the King being placed in check at any 12 | point during the travel. Individual Piece validation wouldn't be sufficient 13 | to determine whether or not this move is possible. 14 | 15 | Additionally, isSquareAttacked exists on the BoardValidation object. While 16 | this method could have easily existed within the PieceValidation object 17 | I've kept it in BoardValidation so that PieceValidation remains truly 18 | agnostic of the other pieces on the same board. 19 | 20 | Due to how BoardValidation actually functions, the client only needs to 21 | instantiate a BoardValidation for the Game and call the start method 22 | in order to evaluate every Piece's valid move options. There is no need 23 | to call PieceValidation (and doing so wouldn't give an accurate picture 24 | of what is possible anyway). 25 | */ 26 | import { PieceType, SideType } from './piece.js'; 27 | import { NeighborType } from './board.js'; 28 | import { PieceValidation } from './pieceValidation.js'; 29 | 30 | export class BoardValidation { 31 | constructor (game) { 32 | this.board = game ? game.board : null; 33 | this.game = game; 34 | } 35 | 36 | static create (game) { 37 | return new BoardValidation(game); 38 | } 39 | 40 | evaluateCastle (validMoves) { 41 | let 42 | getValidSquares = (sq) => { 43 | let i = 0; 44 | 45 | for (i = 0; i < validMoves.length; i++) { 46 | if (validMoves[i].src === sq) { 47 | return validMoves[i].squares; 48 | } 49 | } 50 | }, 51 | interimMove = null, 52 | // eslint-disable-next-line no-magic-numbers 53 | rank = this.game.getCurrentSide() === SideType.White ? 1 : 8, 54 | squares = { 55 | 'a' : this.board.getSquare('a', rank), 56 | 'b' : this.board.getSquare('b', rank), 57 | 'c' : this.board.getSquare('c', rank), 58 | 'd' : this.board.getSquare('d', rank), 59 | 'e' : this.board.getSquare('e', rank), 60 | 'f' : this.board.getSquare('f', rank), 61 | 'g' : this.board.getSquare('g', rank), 62 | 'h' : this.board.getSquare('h', rank) 63 | }; 64 | 65 | // is king eligible 66 | if (squares.e.piece && 67 | squares.e.piece.type === PieceType.King && 68 | squares.e.piece.moveCount === 0 && 69 | !this.isSquareAttacked(squares.e)) { 70 | 71 | // is left rook eligible 72 | if (squares.a.piece && 73 | squares.a.piece.type === PieceType.Rook && 74 | squares.a.piece.moveCount === 0) { 75 | 76 | // are the squares between king and rook clear 77 | if (!squares.b.piece && 78 | !squares.c.piece && 79 | !squares.d.piece) { 80 | 81 | // when king moves through squares between, is it in check 82 | interimMove = this.board.move(squares.e, squares.d, true); 83 | if (!this.isSquareAttacked(squares.d)) { 84 | interimMove.undo(); 85 | interimMove = this.board.move(squares.e, squares.c, true); 86 | 87 | if (!this.isSquareAttacked(squares.c)) { 88 | getValidSquares(squares.e).push(squares.c); 89 | } 90 | } 91 | interimMove.undo(); 92 | } 93 | } 94 | 95 | // is right rook eligible 96 | if (squares.h.piece && 97 | squares.h.piece.type === PieceType.Rook && 98 | squares.h.piece.moveCount === 0) { 99 | 100 | // are the squares between king and rook clear 101 | if (!squares.g.piece && !squares.f.piece) { 102 | // when king moves through squares between, is it in check 103 | interimMove = this.board.move(squares.e, squares.f, true); 104 | if (!this.isSquareAttacked(squares.f)) { 105 | interimMove.undo(); 106 | interimMove = this.board.move(squares.e, squares.g, true); 107 | 108 | if (!this.isSquareAttacked(squares.g)) { 109 | getValidSquares(squares.e).push(squares.g); 110 | } 111 | } 112 | interimMove.undo(); 113 | } 114 | } 115 | } 116 | } 117 | 118 | filterKingAttack (kingSquare, moves) { 119 | let 120 | filteredMoves = [], 121 | i = 0, 122 | isCheck = false, 123 | n = 0, 124 | r = null, 125 | squares = []; 126 | 127 | for (i = 0; i < moves.length; i++) { 128 | squares = []; 129 | 130 | for (n = 0; n < moves[i].squares.length; n++) { 131 | // simulate move on the board 132 | r = this.board.move(moves[i].src, moves[i].squares[n], true); 133 | 134 | // check if king is attacked 135 | if (moves[i].squares[n].piece.type !== PieceType.King) { 136 | isCheck = this.isSquareAttacked(kingSquare); 137 | } else { 138 | isCheck = this.isSquareAttacked(moves[i].squares[n]); 139 | } 140 | 141 | // reverse the move 142 | r.undo(); 143 | 144 | if (!isCheck) { 145 | squares.push(moves[i].squares[n]); 146 | } 147 | } 148 | 149 | if (squares && squares.length > 0) { 150 | filteredMoves.push({ 151 | squares, 152 | src : moves[i].src 153 | }); 154 | } 155 | } 156 | 157 | return filteredMoves; 158 | } 159 | 160 | findAttackers (sq) { 161 | if (!sq || !sq.piece) { 162 | return { 163 | attacked : false, 164 | blocked : false 165 | }; 166 | } 167 | 168 | let 169 | /* eslint no-invalid-this: 0 */ 170 | isAttacked = (b, n) => { 171 | let 172 | context = {}, 173 | currentSquare = b.getNeighborSquare(sq, n); 174 | 175 | while (currentSquare) { 176 | context = { 177 | attacked : currentSquare.piece && currentSquare.piece.side !== sq.piece.side, 178 | blocked : currentSquare.piece && currentSquare.piece.side === sq.piece.side, 179 | piece : currentSquare.piece, 180 | square : currentSquare 181 | }; 182 | 183 | if (context.attacked) { 184 | // verify that the square is actually attacked 185 | PieceValidation 186 | .create(context.piece.type, b) 187 | .start(currentSquare, setAttacked(context)); 188 | currentSquare = null; 189 | } else if (context.blocked) { 190 | currentSquare = null; 191 | } else { 192 | currentSquare = b.getNeighborSquare(currentSquare, n); 193 | } 194 | } 195 | 196 | return context; 197 | }, 198 | isAttackedByKnight = (b, n) => { 199 | let 200 | context, 201 | currentSquare = b.getNeighborSquare(sq, n); 202 | 203 | context = { 204 | attacked : false, 205 | blocked : false, 206 | piece : currentSquare ? currentSquare.piece : currentSquare, 207 | square : currentSquare 208 | }; 209 | 210 | if (currentSquare && 211 | currentSquare.piece && 212 | currentSquare.piece.type === PieceType.Knight) { 213 | PieceValidation 214 | .create(PieceType.Knight, b) 215 | .start(currentSquare, setAttacked(context)); 216 | } 217 | 218 | return context; 219 | }, 220 | self = this, 221 | setAttacked = (c) => { 222 | return (err, squares) => { 223 | if (!err) { 224 | let i = 0; 225 | for (i = 0; i < squares.length; i++) { 226 | if (squares[i] === sq) { 227 | c.attacked = true; 228 | return; 229 | } 230 | } 231 | } 232 | 233 | c.attacked = false; 234 | }; 235 | }; 236 | 237 | return [ 238 | isAttacked(self.board, NeighborType.Above), 239 | isAttacked(self.board, NeighborType.AboveRight), 240 | isAttacked(self.board, NeighborType.Right), 241 | isAttacked(self.board, NeighborType.BelowRight), 242 | isAttacked(self.board, NeighborType.Below), 243 | isAttacked(self.board, NeighborType.BelowLeft), 244 | isAttacked(self.board, NeighborType.Left), 245 | isAttacked(self.board, NeighborType.AboveLeft), 246 | // fix for issue #4 247 | isAttackedByKnight(self.board, NeighborType.KnightAboveRight), 248 | isAttackedByKnight(self.board, NeighborType.KnightRightAbove), 249 | isAttackedByKnight(self.board, NeighborType.KnightBelowRight), 250 | isAttackedByKnight(self.board, NeighborType.KnightRightBelow), 251 | isAttackedByKnight(self.board, NeighborType.KnightBelowLeft), 252 | isAttackedByKnight(self.board, NeighborType.KnightLeftBelow), 253 | isAttackedByKnight(self.board, NeighborType.KnightAboveLeft), 254 | isAttackedByKnight(self.board, NeighborType.KnightLeftAbove) 255 | ].filter((result) => result.attacked); 256 | } 257 | 258 | isSquareAttacked (sq) { 259 | return this.findAttackers(sq).length !== 0; 260 | } 261 | 262 | start (callback) { 263 | // ensure callback is set 264 | callback = callback || ((err, validMoves) => new Promise((resolve, reject) => { 265 | if (err) { 266 | return reject(err); 267 | } 268 | 269 | return resolve(validMoves); 270 | })); 271 | 272 | let 273 | i = 0, 274 | kingSquare = null, 275 | setValidMoves = (v, sq) => { 276 | return (err, squares) => { 277 | if (err) { 278 | return callback(err); 279 | } 280 | 281 | if (squares && squares.length > 0) { 282 | v.push({ 283 | squares, 284 | src : sq 285 | }); 286 | } 287 | }; 288 | }, 289 | squares = [], 290 | validMoves = []; 291 | 292 | if (this.board) { 293 | // get squares with pieces for which to evaluate move options 294 | squares = this.board.getSquares(this.game.getCurrentSide()); 295 | 296 | for (i = 0; i < squares.length; i++) { 297 | // set king to refer to later 298 | if (squares[i].piece.type === PieceType.King) { 299 | kingSquare = squares[i]; 300 | } 301 | 302 | if (squares[i] && squares[i].piece) { 303 | PieceValidation 304 | .create(squares[i].piece.type, this.board) 305 | .start(squares[i], setValidMoves(validMoves, squares[i])); 306 | } 307 | } 308 | 309 | // perform king castle validation 310 | this.evaluateCastle(validMoves); 311 | 312 | // make sure moves only contain escape & non-check options 313 | validMoves = this.filterKingAttack(kingSquare, validMoves); 314 | 315 | // find any pieces attacking the king 316 | this.findAttackers(kingSquare).forEach((attacker) => { 317 | if (!validMoves.length) { 318 | this.game.emit( 319 | 'checkmate', { 320 | attackingSquare : attacker.square, 321 | kingSquare 322 | }); 323 | 324 | return; 325 | } 326 | 327 | this.game.emit( 328 | 'check', { 329 | attackingSquare : attacker.square, 330 | kingSquare 331 | }); 332 | }); 333 | } else { 334 | return callback(new Error('board is invalid')); 335 | } 336 | 337 | return callback(null, validMoves); 338 | } 339 | } 340 | 341 | export default { BoardValidation }; -------------------------------------------------------------------------------- /src/board.js: -------------------------------------------------------------------------------- 1 | /** 2 | The Board is the representation of the current position of the pieces on 3 | the squares it contains. 4 | */ 5 | import { Piece, PieceType, SideType } from './piece.js'; 6 | import { EventEmitter } from 'events'; 7 | import { Square } from './square.js'; 8 | 9 | // types 10 | export var NeighborType = { 11 | Above : { offset : 8 }, 12 | AboveLeft : { offset : 7 }, 13 | AboveRight : { offset : 9 }, 14 | Below : { offset : -8 }, 15 | BelowLeft : { offset : -9 }, 16 | BelowRight : { offset : -7 }, 17 | KnightAboveLeft : { offset : 15 }, 18 | KnightAboveRight : { offset : 17 }, 19 | KnightBelowLeft : { offset : -17 }, 20 | KnightBelowRight : { offset : -15 }, 21 | KnightLeftAbove : { offset : 6 }, 22 | KnightLeftBelow : { offset : -10 }, 23 | KnightRightAbove : { offset : 10 }, 24 | KnightRightBelow : { offset : -6 }, 25 | Left : { offset : -1 }, 26 | Right : { offset : 1 } 27 | }; 28 | 29 | // ctor 30 | export class Board extends EventEmitter { 31 | constructor (squares) { 32 | super(); 33 | 34 | this.squares = squares; 35 | } 36 | 37 | static create () { 38 | let 39 | b = new Board([]), 40 | f = 0, 41 | i = 0, 42 | r = 0, 43 | sq = null; 44 | 45 | /* eslint no-magic-numbers:0 */ 46 | for (i = 0; i < 64; i++) { 47 | f = Math.floor(i % 8); 48 | r = Math.floor(i / 8) + 1; 49 | sq = Square.create('abcdefgh'[f], r); 50 | 51 | b.squares.push(sq); 52 | 53 | if (r === 1 || r === 8) { // Named pieces 54 | if (f === 0 || f === 7) { // Rookage 55 | sq.piece = Piece.createRook( 56 | r === 1 ? SideType.White : SideType.Black 57 | ); 58 | } else if (f === 1 || f === 6) { // Knights 59 | sq.piece = Piece.createKnight( 60 | r === 1 ? SideType.White : SideType.Black 61 | ); 62 | } else if (f === 2 || f === 5) { // Bish's 63 | sq.piece = Piece.createBishop( 64 | r === 1 ? SideType.White : SideType.Black 65 | ); 66 | } else if (f === 3) { 67 | sq.piece = Piece.createQueen( 68 | r === 1 ? SideType.White : SideType.Black 69 | ); 70 | } else { 71 | sq.piece = Piece.createKing( 72 | r === 1 ? SideType.White : SideType.Black 73 | ); 74 | } 75 | } else if (r === 2 || r === 7) { // Pawns 76 | sq.piece = Piece.createPawn( 77 | r === 2 ? SideType.White : SideType.Black 78 | ); 79 | } 80 | } 81 | 82 | return b; 83 | } 84 | 85 | static load (fen) { 86 | /* eslint sort-keys: 0 */ 87 | const pieces = { 88 | b: { arg: SideType.Black, method: 'createBishop' }, 89 | B: { arg: SideType.White, method: 'createBishop' }, 90 | k: { arg: SideType.Black, method: 'createKing' }, 91 | K: { arg: SideType.White, method: 'createKing' }, 92 | n: { arg: SideType.Black, method: 'createKnight' }, 93 | N: { arg: SideType.White, method: 'createKnight' }, 94 | p: { arg: SideType.Black, method: 'createPawn' }, 95 | P: { arg: SideType.White, method: 'createPawn' }, 96 | q: { arg: SideType.Black, method: 'createQueen' }, 97 | Q: { arg: SideType.White, method: 'createQueen' }, 98 | r: { arg: SideType.Black, method: 'createRook' }, 99 | R: { arg: SideType.White, method: 'createRook' } 100 | }; 101 | 102 | const [board/* , turn, castling, enPassant, halfs, moves */] = fen.split(' '); 103 | const lines = board.split('/') 104 | .map((line, rank) => { 105 | const arr = line.split(''); 106 | let file = 0; 107 | 108 | return arr.reduce((acc, cur) => { 109 | if (!isNaN(Number(cur))) { 110 | for (let i = 0; i < Number(cur); i += 1) { 111 | acc.push(Square.create('abcdefgh'[file], 8 - rank)); 112 | file = file < 7 ? file + 1 : 0; 113 | } 114 | } else { 115 | const square = Square.create('abcdefgh'[file], 8 - rank); 116 | square.piece = Piece[pieces[cur].method](pieces[cur].arg); 117 | acc.push(square); 118 | file = file < 7 ? file + 1 : 0; 119 | } 120 | return acc; 121 | }, []); 122 | }); 123 | 124 | return new Board(lines.reduce((acc, cur) => { 125 | acc.push(...cur); 126 | return acc; 127 | }, [])); 128 | } 129 | 130 | getFen () { 131 | const fen = []; 132 | const squares = this.squares 133 | .reduce((acc, cur, idx) => { 134 | const outerIdx = parseInt(idx / 8, 10); 135 | acc[outerIdx] = acc[outerIdx] || []; 136 | acc[outerIdx].push(cur); 137 | return acc; 138 | }, []) 139 | .flatMap((row) => row.reverse()) 140 | .reverse(); 141 | 142 | for (let i = 0; i < squares.length; i += 1) { 143 | const square = squares[i]; 144 | 145 | if (square.file === 'a' && square.rank < 8) { 146 | fen.push('/'); 147 | } 148 | 149 | if (square.piece) { 150 | const transform = `to${square.piece.side.name === 'white' ? 'Upp' : 'Low'}erCase`; 151 | fen.push((square.piece.notation || 'p')[transform]()); 152 | } else { 153 | if (isNaN(Number(fen[fen.length - 1]))) { 154 | fen.push(1); 155 | } else { 156 | fen[fen.length - 1] += 1; 157 | } 158 | } 159 | } 160 | 161 | return fen.join(''); 162 | } 163 | 164 | getNeighborSquare (sq, n) { 165 | if (sq && n) { 166 | // validate boundaries of board 167 | if (sq.file === 'a' && (n === NeighborType.AboveLeft || 168 | n === NeighborType.BelowLeft || 169 | n === NeighborType.Left)) { 170 | return null; 171 | } 172 | 173 | if (sq.file === 'h' && (n === NeighborType.AboveRight || 174 | n === NeighborType.BelowRight || 175 | n === NeighborType.Right)) { 176 | return null; 177 | } 178 | 179 | if (sq.rank === 1 && (n === NeighborType.Below || 180 | n === NeighborType.BelowLeft || 181 | n === NeighborType.BelowRight)) { 182 | return null; 183 | } 184 | 185 | if (sq.rank === 8 && (n === NeighborType.Above || 186 | n === NeighborType.AboveLeft || 187 | n === NeighborType.AboveRight)) { 188 | return null; 189 | } 190 | 191 | // validate file 192 | let 193 | fIndex = 'abcdefgh'.indexOf(sq.file), 194 | i = 0; 195 | 196 | if (fIndex !== -1 && sq.rank > 0 && sq.rank < 9) { 197 | // find the index 198 | i = 8 * (sq.rank - 1) + fIndex + n.offset; 199 | if (this.squares && this.squares.length > i && i > -1) { 200 | return this.squares[i]; 201 | } 202 | } 203 | } 204 | 205 | return null; 206 | } 207 | 208 | getSquare (f, r) { 209 | // check for shorthand 210 | if (typeof f === 'string' && f.length === 2 && !r) { 211 | r = parseInt(f.charAt(1), 10); 212 | f = f.charAt(0); 213 | } 214 | 215 | // validate file 216 | let 217 | fIndex = 'abcdefgh'.indexOf(f), 218 | i = 0; 219 | 220 | if (fIndex !== -1 && r > 0 && r < 9) { 221 | // Find the index 222 | i = 8 * (r - 1) + fIndex; 223 | if (this.squares && this.squares.length > i) { 224 | return this.squares[i]; 225 | } 226 | } 227 | 228 | return null; 229 | } 230 | 231 | getSquares (side) { 232 | const list = []; 233 | 234 | for (let i = 0; i < this.squares.length; i++) { 235 | if (this.squares[i].piece && this.squares[i].piece.side === side) { 236 | list.push(this.squares[i]); 237 | } 238 | } 239 | 240 | return list; 241 | } 242 | 243 | move (src, dest, n) { 244 | if (typeof src === 'string' && src.length === 2) { 245 | src = this.getSquare(src); 246 | } 247 | 248 | if (typeof dest === 'string' && dest.length === 2) { 249 | dest = this.getSquare(dest); 250 | } 251 | 252 | let simulate; 253 | 254 | if (typeof n === 'boolean') { 255 | simulate = n; 256 | n = null; 257 | } 258 | 259 | if (src && src.file && src.rank && dest && dest.file && dest.rank) { 260 | let 261 | move = { 262 | algebraic : n, 263 | capturedPiece : dest.piece, 264 | castle : false, 265 | enPassant : false, 266 | postSquare : dest, 267 | prevSquare : src 268 | }, 269 | p = src.piece, 270 | sq = null, 271 | undo = (b, m) => { 272 | return () => { 273 | if (!simulate) { 274 | // ensure no harm can be done if called multiple times 275 | if (m.undone) { 276 | throw new Error('cannot undo a move multiple times'); 277 | } 278 | } 279 | 280 | // backout move by returning the squares to their state prior to the move 281 | m.prevSquare.piece = m.postSquare.piece; 282 | m.postSquare.piece = m.capturedPiece; 283 | 284 | // handle standard scenario 285 | if (!m.enPassant) { 286 | m.postSquare.piece = m.capturedPiece; 287 | } 288 | 289 | // handle en-passant scenario 290 | if (m.enPassant) { 291 | b.getSquare( 292 | m.postSquare.file, 293 | m.prevSquare.rank 294 | ).piece = m.capturedPiece; 295 | 296 | // there is no piece on the post square in the event of 297 | // an en-passant, clear anything that me be present as 298 | // a result of the move (fix for issue #8) 299 | m.postSquare.piece = null; 300 | } 301 | 302 | // handle castle scenario 303 | if (m.castle) { 304 | sq = b.getSquare( 305 | move.postSquare.file === 'g' ? 'f' : 'd', 306 | move.postSquare.rank); 307 | 308 | b.getSquare( 309 | move.postSquare.file === 'g' ? 'h' : 'a', 310 | move.postSquare.rank 311 | ).piece = sq.piece; 312 | sq.piece = null; 313 | } 314 | 315 | // if not a simulation, reset the move count 316 | if (!simulate) { 317 | // correct the moveCount for the piece 318 | m.prevSquare.piece.moveCount = m.prevSquare.piece.moveCount - 1; 319 | 320 | // indicate move has been undone 321 | m.undone = true; 322 | 323 | // emit an undo event 324 | b.emit('undo', m); 325 | } 326 | }; 327 | }; 328 | 329 | dest.piece = p; 330 | move.castle = p.type === PieceType.King && 331 | p.moveCount === 0 && 332 | (move.postSquare.file === 'g' || move.postSquare.file === 'c'); 333 | move.enPassant = p.type === PieceType.Pawn && 334 | move.capturedPiece === null && 335 | move.postSquare.file !== move.prevSquare.file; 336 | move.prevSquare.piece = null; 337 | 338 | // check for en-passant 339 | if (move.enPassant) { 340 | sq = this.getSquare(move.postSquare.file, move.prevSquare.rank); 341 | move.capturedPiece = sq.piece; 342 | sq.piece = null; 343 | } 344 | 345 | // check for castle 346 | if (move.castle) { 347 | sq = this.getSquare( 348 | move.postSquare.file === 'g' ? 'h' : 'a', 349 | move.postSquare.rank 350 | ); 351 | 352 | if (sq.piece === null) { 353 | move.castle = false; 354 | } else { 355 | this.getSquare( 356 | move.postSquare.file === 'g' ? 'f' : 'd', 357 | move.postSquare.rank 358 | ).piece = sq.piece; 359 | sq.piece = null; 360 | } 361 | } 362 | 363 | if (!simulate) { 364 | p.moveCount++; 365 | this.lastMovedPiece = p; 366 | 367 | if (move.capturedPiece) { 368 | this.emit('capture', move); 369 | } 370 | 371 | if (move.castle) { 372 | this.emit('castle', move); 373 | } 374 | 375 | if (move.enPassant) { 376 | this.emit('enPassant', move); 377 | } 378 | 379 | this.emit('move', move); 380 | } 381 | 382 | return { 383 | move, 384 | undo : undo(this, move) 385 | }; 386 | } 387 | } 388 | 389 | promote (sq, p) { 390 | // update move count and last piece 391 | p.moveCount = sq.piece.moveCount; 392 | this.lastMovedPiece = p; 393 | 394 | // set to square 395 | sq.piece = p; 396 | 397 | this.emit('promote', sq); 398 | 399 | return sq; 400 | } 401 | } 402 | 403 | // exports 404 | export default { Board, NeighborType }; 405 | -------------------------------------------------------------------------------- /src/algebraicGameClient.js: -------------------------------------------------------------------------------- 1 | /* eslint sort-imports: 0 */ 2 | import { EventEmitter } from 'events'; 3 | import { Board } from './board.js'; 4 | import { Game } from './game.js'; 5 | import { GameValidation } from './gameValidation.js'; 6 | import { Piece } from './piece.js'; 7 | import { PieceType } from './piece.js'; 8 | import { SideType } from './piece.js'; 9 | 10 | // private methods 11 | function getNotationPrefix (src, dest, movesForPiece) { 12 | let 13 | containsDest = (squares) => { 14 | let n = 0; 15 | 16 | for (; n < squares.length; n++) { 17 | if (squares[n] === dest) { 18 | return true; 19 | } 20 | } 21 | 22 | return false; 23 | }, 24 | file = '', 25 | fileHash = {}, 26 | i = 0, 27 | prefix = src.piece.notation, 28 | rank = 0, 29 | rankHash = {}; 30 | 31 | for (; i < movesForPiece.length; i++) { 32 | if (containsDest(movesForPiece[i].squares)) { 33 | file = movesForPiece[i].src.file; 34 | rank = movesForPiece[i].src.rank; 35 | 36 | fileHash[file] = (typeof fileHash[file] !== 'undefined' ? fileHash[file] + 1 : 1); 37 | rankHash[rank] = (typeof rankHash[rank] !== 'undefined' ? rankHash[rank] + 1 : 1); 38 | } 39 | } 40 | 41 | if (Object.keys(fileHash).length > 1) { 42 | prefix += src.file; 43 | } 44 | 45 | if (Object.keys(rankHash).length > Object.keys(fileHash).length) { 46 | prefix += src.rank; 47 | } 48 | 49 | return prefix; 50 | } 51 | 52 | function getValidMovesByPieceType (pieceType, validMoves) { 53 | let 54 | byPiece = [], 55 | i = 0; 56 | 57 | for (; i < validMoves.length; i++) { 58 | if (validMoves[i].src.piece.type === pieceType) { 59 | byPiece.push(validMoves[i]); 60 | } 61 | } 62 | 63 | return byPiece; 64 | } 65 | 66 | function notate (validMoves, gameClient) { 67 | let 68 | algebraicNotation = {}, 69 | i = 0, 70 | isPromotion = false, 71 | movesForPiece = [], 72 | n = 0, 73 | p = null, 74 | prefix = '', 75 | sq = null, 76 | src = null, 77 | suffix = ''; 78 | 79 | // iterate through each starting squares valid moves 80 | for (; i < validMoves.length; i++) { 81 | src = validMoves[i].src; 82 | p = src.piece; 83 | 84 | // iterate each potential move and build prefix and suffix for notation 85 | for (n = 0; n < validMoves[i].squares.length; n++) { 86 | prefix = ''; 87 | sq = validMoves[i].squares[n]; 88 | 89 | // set suffix for notation 90 | suffix = (sq.piece ? 'x' : '') + sq.file + sq.rank; 91 | 92 | // check for potential promotion 93 | /* eslint no-magic-numbers: 0 */ 94 | isPromotion = 95 | (sq.rank === 8 || sq.rank === 1) && 96 | p.type === PieceType.Pawn; 97 | 98 | // squares with pawns 99 | if (sq.piece && p.type === PieceType.Pawn) { 100 | prefix = src.file; 101 | } 102 | 103 | // en passant 104 | // fix for #53 105 | if (p.type === PieceType.Pawn && 106 | src.file !== sq.file && 107 | !sq.piece) { 108 | prefix = [src.file, 'x'].join(''); 109 | } 110 | 111 | // squares with Bishop, Knight, Queen or Rook pieces 112 | if (p.type === PieceType.Bishop || 113 | p.type === PieceType.Knight || 114 | p.type === PieceType.Queen || 115 | p.type === PieceType.Rook) { 116 | // if there is more than 1 of the specified piece on the board, 117 | // can more than 1 land on the specified square? 118 | movesForPiece = getValidMovesByPieceType(p.type, validMoves); 119 | if (movesForPiece.length > 1) { 120 | prefix = getNotationPrefix(src, sq, movesForPiece); 121 | } else { 122 | prefix = src.piece.notation; 123 | } 124 | } 125 | 126 | // squares with a King piece 127 | if (p.type === PieceType.King) { 128 | // look for castle left and castle right 129 | if (src.file === 'e' && sq.file === 'g') { 130 | // fix for issue #13 - if PGN is specified should be letters, not numbers 131 | prefix = gameClient.PGN ? 'O-O' : '0-0'; 132 | suffix = ''; 133 | } else if (src.file === 'e' && sq.file === 'c') { 134 | // fix for issue #13 - if PGN is specified should be letters, not numbers 135 | prefix = gameClient.PGN ? 'O-O-O' : '0-0-0'; 136 | suffix = ''; 137 | } else { 138 | prefix = src.piece.notation; 139 | } 140 | } 141 | 142 | // set the notation 143 | if (isPromotion) { 144 | // Rook promotion 145 | algebraicNotation[prefix + suffix + 'R'] = { 146 | dest : sq, 147 | src 148 | }; 149 | 150 | // Knight promotion 151 | algebraicNotation[prefix + suffix + 'N'] = { 152 | dest : sq, 153 | src 154 | }; 155 | 156 | // Bishop promotion 157 | algebraicNotation[prefix + suffix + 'B'] = { 158 | dest : sq, 159 | src 160 | }; 161 | 162 | // Queen promotion 163 | algebraicNotation[prefix + suffix + 'Q'] = { 164 | dest : sq, 165 | src 166 | }; 167 | } else { 168 | algebraicNotation[prefix + suffix] = { 169 | dest : sq, 170 | src 171 | }; 172 | } 173 | } 174 | } 175 | 176 | return algebraicNotation; 177 | } 178 | 179 | function parseNotation (notation) { 180 | let 181 | captureRegex = /^[a-h]x[a-h][1-8]$/, 182 | parseDest = ''; 183 | 184 | // try and parse the notation 185 | parseDest = notation.substring(notation.length - 2); 186 | 187 | if (notation.length > 2) { 188 | // check for preceding pawn capture style notation (i.e. a-h x) 189 | if (captureRegex.test(notation)) { 190 | return parseDest; 191 | } 192 | 193 | return notation.charAt(0) + parseDest; 194 | } 195 | 196 | return ''; 197 | } 198 | 199 | function updateGameClient (gameClient) { 200 | gameClient.validation.start((err, result) => { 201 | if (err) { 202 | throw new Error(err); 203 | } 204 | 205 | gameClient.isCheck = result.isCheck; 206 | gameClient.isCheckmate = result.isCheckmate; 207 | gameClient.isRepetition = result.isRepetition; 208 | gameClient.isStalemate = result.isStalemate; 209 | gameClient.notatedMoves = notate(result.validMoves, gameClient); 210 | gameClient.validMoves = result.validMoves; 211 | }); 212 | } 213 | 214 | export class AlgebraicGameClient extends EventEmitter { 215 | constructor (game, opts) { 216 | super(); 217 | 218 | this.game = game; 219 | this.isCheck = false; 220 | this.isCheckmate = false; 221 | this.isRepetition = false; 222 | this.isStalemate = false; 223 | this.notatedMoves = {}; 224 | // for issue #13, adding options allowing consumers to specify 225 | // PGN (Portable Game Notation)... essentially, this makes castle moves 226 | // appear as capital letter O rather than the number 0 227 | this.PGN = (opts && typeof opts.PGN === 'boolean') ? opts.PGN : false; 228 | this.validMoves = []; 229 | this.validation = GameValidation.create(this.game); 230 | 231 | // bubble the game and board events 232 | ['check', 'checkmate'].forEach((ev) => { 233 | this.game.on(ev, (data) => this.emit(ev, data)); 234 | }); 235 | 236 | ['capture', 'castle', 'enPassant', 'move', 'promote', 'undo'].forEach((ev) => { 237 | this.game.board.on(ev, (data) => this.emit(ev, data)); 238 | }); 239 | 240 | let self = this; 241 | this.on('undo', () => { 242 | // force an update 243 | self.getStatus(true); 244 | }); 245 | } 246 | 247 | static create (opts) { 248 | let 249 | game = Game.create(), 250 | gameClient = new AlgebraicGameClient(game, opts); 251 | 252 | updateGameClient(gameClient); 253 | 254 | return gameClient; 255 | } 256 | 257 | static fromFEN (fen, opts) { 258 | if (!fen || typeof fen !== 'string') { 259 | throw new Error('FEN must be a non-empty string'); 260 | } 261 | 262 | // create a standard game so listeners/history are wired 263 | let 264 | game = Game.create(), 265 | loadedBoard = Board.load(fen); 266 | 267 | // copy piece placement from loaded board to preserve board indexing and listeners 268 | for (let i = 0; i < game.board.squares.length; i++) { 269 | game.board.squares[i].piece = null; 270 | } 271 | 272 | for (let i = 0; i < loadedBoard.squares.length; i++) { 273 | let sq = loadedBoard.squares[i]; 274 | if (sq.piece) { 275 | let target = game.board.getSquare(sq.file, sq.rank); 276 | target.piece = sq.piece; 277 | } 278 | } 279 | 280 | game.board.lastMovedPiece = null; 281 | 282 | // derive side to move from FEN (default to White if missing) 283 | let parts = fen.split(' '); 284 | let active = parts[1] || 'w'; 285 | let baseSide = active === 'b' ? SideType.Black : SideType.White; 286 | 287 | // override getCurrentSide to honor FEN and alternate thereafter 288 | let whiteFirst = baseSide === SideType.White; 289 | 290 | /* eslint no-param-reassign: 0 */ 291 | game.getCurrentSide = function getCurrentSideAfterFENLoad () { 292 | return (this.moveHistory.length % 2 === 0) ? 293 | (whiteFirst ? SideType.White : SideType.Black) : 294 | (whiteFirst ? SideType.Black : SideType.White); 295 | }; 296 | 297 | const gameClient = new AlgebraicGameClient(game, opts); 298 | updateGameClient(gameClient); 299 | 300 | return gameClient; 301 | } 302 | 303 | getStatus (forceUpdate) { 304 | if (forceUpdate) { 305 | updateGameClient(this); 306 | } 307 | 308 | return { 309 | board : this.game.board, 310 | isCheck : this.isCheck, 311 | isCheckmate : this.isCheckmate, 312 | isRepetition : this.isRepetition, 313 | isStalemate : this.isStalemate, 314 | notatedMoves : this.notatedMoves 315 | }; 316 | } 317 | 318 | getFen () { 319 | return this.game.board.getFen(); 320 | } 321 | 322 | getCaptureHistory () { 323 | return this.game.captureHistory; 324 | } 325 | 326 | move (notation, isFuzzy) { 327 | let 328 | move = null, 329 | notationRegex = /^[BKQNR]?[a-h]?[1-8]?[x-]?[a-h][1-8][+#]?$/, 330 | p = null, 331 | promo = '', 332 | side = this.game.getCurrentSide(); 333 | 334 | if (notation && typeof notation === 'string') { 335 | // clean notation of extra or alternate chars 336 | notation = notation 337 | .replace(/\!/g, '') 338 | .replace(/\+/g, '') 339 | .replace(/\#/g, '') 340 | .replace(/\=/g, '') 341 | .replace(/\\/g, ''); 342 | 343 | // fix for issue #13 - if PGN is specified, should be letters not numbers 344 | if (this.PGN) { 345 | notation = notation.replace(/0/g, 'O'); 346 | } else { 347 | notation = notation.replace(/O/g, '0'); 348 | } 349 | 350 | // check for pawn promotion 351 | if (notation.charAt(notation.length - 1).match(/[BNQR]/)) { 352 | promo = notation.charAt(notation.length - 1); 353 | } 354 | 355 | // use it directly or attempt to parse it if not found 356 | if (this.notatedMoves[notation]) { 357 | move = this.game.board.move( 358 | this.notatedMoves[notation].src, 359 | this.notatedMoves[notation].dest, 360 | notation); 361 | } else if (notation.match(notationRegex) && notation.length > 1 && !isFuzzy) { 362 | return this.move(parseNotation(notation), true); 363 | } else if (isFuzzy) { 364 | throw new Error(`Invalid move (${notation})`); 365 | } 366 | 367 | if (move) { 368 | // apply pawn promotion 369 | if (promo) { 370 | switch (promo) { 371 | case 'B': 372 | p = Piece.createBishop(side); 373 | break; 374 | case 'N': 375 | p = Piece.createKnight(side); 376 | break; 377 | case 'Q': 378 | p = Piece.createQueen(side); 379 | break; 380 | case 'R': 381 | p = Piece.createRook(side); 382 | break; 383 | default: 384 | p = Piece.createPawn(side); 385 | } 386 | 387 | if (p) { 388 | this.game.board.promote(move.move.postSquare, p); 389 | } 390 | } 391 | 392 | updateGameClient(this); 393 | 394 | return move; 395 | } 396 | } 397 | 398 | throw new Error(`Notation is invalid (${notation})`); 399 | } 400 | } 401 | 402 | export default { AlgebraicGameClient }; 403 | -------------------------------------------------------------------------------- /test/src/pieceValidation.js: -------------------------------------------------------------------------------- 1 | /* eslint no-magic-numbers:0 */ 2 | import { assert, describe, it } from 'vitest'; 3 | import { Board } from '../../src/board.js'; 4 | import { PieceType } from '../../src/piece.js'; 5 | import { PieceValidation } from '../../src/pieceValidation.js'; 6 | 7 | describe('PieceValidation', () => { 8 | function checkForSquare (f, r, s) { 9 | let i = 0; 10 | 11 | for (; i < s.length; i++) { 12 | if (s[i].file === f && s[i].rank === r) { 13 | return true; 14 | } 15 | } 16 | 17 | return false; 18 | } 19 | 20 | // ensure invalid piece error is returned 21 | it('should throw exception if validation is created for wrong piece', () => { 22 | let 23 | b = Board.create(), 24 | pv = PieceValidation.create(PieceType.Bishop, b); 25 | 26 | pv.start(b.getSquare('a', 2), (err) => { 27 | assert.ok(err); 28 | assert.strictEqual(err.message, 'piece is invalid'); 29 | }); 30 | 31 | pv.start(null, (err) => { 32 | assert.ok(err); 33 | assert.strictEqual(err.message, 'piece is invalid'); 34 | }); 35 | }); 36 | 37 | // validate bishop validator 38 | it('should create bishop validation correctly', () => { 39 | let 40 | b = Board.create(), 41 | pv = PieceValidation.create(PieceType.Bishop, b); 42 | 43 | assert.strictEqual(pv.allowDiagonal, true); 44 | assert.strictEqual(pv.type, PieceType.Bishop); 45 | assert.strictEqual(pv.repeat, 8); 46 | }); 47 | 48 | // check bishop validator moves 49 | it('should properly represent bishop moves when blocked', () => { 50 | let 51 | b = Board.create(), 52 | pv = PieceValidation.create(PieceType.Bishop, b); 53 | 54 | pv.start(b.getSquare('c', 1), (err, squares) => { 55 | assert.strictEqual(err, null); 56 | assert.strictEqual(squares.length, 0); 57 | }); 58 | }); 59 | 60 | // check bishop validator moves 61 | it('should properly represent white bishop moves when not blocked', () => { 62 | let 63 | b = Board.create(), 64 | pv = PieceValidation.create(PieceType.Bishop, b); 65 | 66 | b.move(b.getSquare('d', 2), b.getSquare('d', 3)); 67 | b.move(b.getSquare('c', 1), b.getSquare('e', 3)); 68 | 69 | pv.start(b.getSquare('e', 3), (err, squares) => { 70 | assert.strictEqual(err, null); 71 | assert.strictEqual(squares.length, 9); 72 | assert.strictEqual(checkForSquare('a', 7, squares), true, 'Ba7'); 73 | assert.strictEqual(checkForSquare('h', 6, squares), true, 'Bh6'); 74 | assert.strictEqual(checkForSquare('c', 1, squares), true, 'Bc1'); 75 | }); 76 | }); 77 | 78 | // check bishop validator moves 79 | it('should properly represent black bishop moves when not blocked', () => { 80 | let 81 | b = Board.create(), 82 | pv = PieceValidation.create(PieceType.Bishop, b); 83 | 84 | b.move(b.getSquare('d', 7), b.getSquare('d', 6)); 85 | b.move(b.getSquare('c', 8), b.getSquare('e', 6)); 86 | 87 | pv.start(b.getSquare('e', 6), (err, squares) => { 88 | assert.strictEqual(err, null); 89 | assert.strictEqual(squares.length, 9); 90 | assert.strictEqual(checkForSquare('a', 2, squares), true, 'Ba2'); 91 | assert.strictEqual(checkForSquare('h', 3, squares), true, 'Bh3'); 92 | assert.strictEqual(checkForSquare('c', 8, squares), true, 'Bc8'); 93 | }); 94 | }); 95 | 96 | // test king validation create 97 | it('should properly create king validation', () => { 98 | let 99 | b = Board.create(), 100 | pv = PieceValidation.create(PieceType.King, b); 101 | 102 | assert.strictEqual(pv.allowForward, true); 103 | assert.strictEqual(pv.allowBackward, true); 104 | assert.strictEqual(pv.allowHorizontal, true); 105 | assert.strictEqual(pv.allowDiagonal, true); 106 | assert.strictEqual(pv.type, PieceType.King); 107 | assert.strictEqual(pv.repeat, 1); 108 | }); 109 | 110 | // test knight validation create 111 | it('should properly create knight validation', () => { 112 | let 113 | b = Board.create(), 114 | pv = PieceValidation.create(PieceType.Knight, b); 115 | 116 | assert.strictEqual(pv.type, PieceType.Knight); 117 | assert.strictEqual(pv.repeat, 1); 118 | }); 119 | 120 | // test knight validation moves 121 | it('should properly represent white knight first moves', () => { 122 | let 123 | b = Board.create(), 124 | pv = PieceValidation.create(PieceType.Knight, b); 125 | 126 | pv.start(b.getSquare('b', 1), (err, squares) => { 127 | assert.strictEqual(err, null); 128 | assert.strictEqual(squares.length, 2); 129 | assert.strictEqual(checkForSquare('a', 3, squares), true, 'Na3'); 130 | assert.strictEqual(checkForSquare('c', 3, squares), true, 'Nc3'); 131 | }); 132 | }); 133 | 134 | // test knight validation moves 135 | it('should properly represent black knight first moves', () => { 136 | let 137 | b = Board.create(), 138 | pv = PieceValidation.create(PieceType.Knight, b); 139 | 140 | pv.start(b.getSquare('b', 8), (err, squares) => { 141 | assert.strictEqual(err, null); 142 | assert.strictEqual(squares.length, 2); 143 | assert.strictEqual(checkForSquare('a', 6, squares), true, 'Na3'); 144 | assert.strictEqual(checkForSquare('c', 6, squares), true, 'Nc3'); 145 | }); 146 | }); 147 | 148 | // test knight validation moves 149 | it('should properly represent white knight second moves', () => { 150 | let 151 | b = Board.create(), 152 | pv = PieceValidation.create(PieceType.Knight, b); 153 | 154 | b.move(b.getSquare('b', 1), b.getSquare('c', 3)); 155 | 156 | pv.start(b.getSquare('c', 3), (err, squares) => { 157 | assert.strictEqual(err, null); 158 | assert.strictEqual(squares.length, 5); 159 | assert.strictEqual(checkForSquare('b', 1, squares), true, 'Nb1'); 160 | assert.strictEqual(checkForSquare('a', 4, squares), true, 'Na4'); 161 | assert.strictEqual(checkForSquare('b', 5, squares), true, 'Nb5'); 162 | assert.strictEqual(checkForSquare('d', 5, squares), true, 'Nd5'); 163 | assert.strictEqual(checkForSquare('e', 4, squares), true, 'Ne4'); 164 | }); 165 | }); 166 | 167 | // test pawn validation create 168 | it('should properly create pawn validation', () => { 169 | let 170 | b = Board.create(), 171 | pv = PieceValidation.create(PieceType.Pawn, b); 172 | 173 | assert.strictEqual(pv.allowForward, true); 174 | assert.strictEqual(pv.type, PieceType.Pawn); 175 | assert.strictEqual(pv.repeat, 1); 176 | }); 177 | 178 | // test pawn validation moves 179 | it('should properly represent white pawn first moves', () => { 180 | let 181 | b = Board.create(), 182 | pv = PieceValidation.create(PieceType.Pawn, b); 183 | 184 | pv.start(b.getSquare('a', 2), (err, squares) => { 185 | assert.strictEqual(err, null); 186 | assert.strictEqual(squares.length, 2); 187 | assert.strictEqual(squares[1].rank, 4); 188 | }); 189 | }); 190 | 191 | // test pawn validation moves 192 | it('should properly represent white pawn second move', () => { 193 | let 194 | b = Board.create(), 195 | pv = PieceValidation.create(PieceType.Pawn, b); 196 | 197 | b.move(b.getSquare('a', 2), b.getSquare('a', 3)); 198 | 199 | pv.start(b.getSquare('a', 3), (err, squares) => { 200 | if (err) { 201 | throw err; 202 | } 203 | 204 | assert.strictEqual(squares.length, 1); 205 | assert.strictEqual(squares[0].rank, 4); 206 | }); 207 | }); 208 | 209 | // test pawn validation moves 210 | it('should properly represent black pawn first moves', () => { 211 | let 212 | b = Board.create(), 213 | pv = PieceValidation.create(PieceType.Pawn, b); 214 | 215 | pv.start(b.getSquare('a', 7), (err, squares) => { 216 | if (err) { 217 | throw err; 218 | } 219 | 220 | assert.strictEqual(squares.length, 2); 221 | assert.strictEqual(squares[1].rank, 5); 222 | }); 223 | }); 224 | 225 | // test pawn validation moves 226 | it('should properly represent black pawn second move', () => { 227 | let 228 | b = Board.create(), 229 | pv = PieceValidation.create(PieceType.Pawn, b); 230 | 231 | b.move(b.getSquare('a', 7), b.getSquare('a', 6)); 232 | 233 | pv.start(b.getSquare('a', 6), (err, squares) => { 234 | if (err) { 235 | throw err; 236 | } 237 | 238 | assert.strictEqual(squares.length, 1); 239 | assert.strictEqual(squares[0].rank, 5); 240 | }); 241 | }); 242 | 243 | // test pawn validation moves while pawn is blocked 244 | it('should properly represent pawn moves when blocked', () => { 245 | let 246 | b = Board.create(), 247 | pv = PieceValidation.create(PieceType.Pawn, b); 248 | 249 | b.move(b.getSquare('e', 2), b.getSquare('e', 4)); 250 | b.move(b.getSquare('e', 7), b.getSquare('e', 6)); 251 | b.move(b.getSquare('e', 4), b.getSquare('e', 5)); 252 | 253 | pv.start(b.getSquare('e', 5), (err, squares) => { 254 | if (err) { 255 | throw err; 256 | } 257 | 258 | assert.strictEqual(squares.length, 0); 259 | }); 260 | }); 261 | 262 | // verify en-passant 263 | it('should properly represent en-passant as available move', () => { 264 | let 265 | b = Board.create(), 266 | pv = PieceValidation.create(PieceType.Pawn, b); 267 | 268 | // turn 1 269 | b.move(b.getSquare('e', 2), b.getSquare('e', 4)); 270 | b.move(b.getSquare('e', 7), b.getSquare('e', 6)); 271 | 272 | // turn 2 273 | b.move(b.getSquare('e', 4), b.getSquare('e', 5)); 274 | b.move(b.getSquare('f', 7), b.getSquare('f', 5)); 275 | 276 | pv.start(b.getSquare('e', 5), (err, squares) => { 277 | if (err) { 278 | throw err; 279 | } 280 | 281 | assert.strictEqual(squares.length, 1); 282 | assert.strictEqual(squares[0].rank, 6); 283 | assert.strictEqual(squares[0].file, 'f'); 284 | }); 285 | }); 286 | 287 | // verify en-passant (issue #3) 288 | it('should properly not allow en-passant as available move', () => { 289 | let 290 | b = Board.create(), 291 | pv = PieceValidation.create(PieceType.Pawn, b); 292 | 293 | // turn 1 294 | b.move(b.getSquare('e', 2), b.getSquare('e', 4)); 295 | b.move(b.getSquare('e', 7), b.getSquare('e', 6)); 296 | 297 | // turn 2 298 | b.move(b.getSquare('e', 4), b.getSquare('e', 5)); 299 | b.move(b.getSquare('f', 7), b.getSquare('f', 5)); 300 | 301 | // turn 3 302 | b.move(b.getSquare('a', 2), b.getSquare('a', 3)); 303 | b.move(b.getSquare('a', 7), b.getSquare('a', 6)); 304 | 305 | pv.start(b.getSquare('e', 5), (err, squares) => { 306 | if (err) { 307 | throw err; 308 | } 309 | 310 | assert.strictEqual(squares.length, 0); 311 | }); 312 | }); 313 | 314 | // verify pawn capture moves 315 | it('should properly allow pawn capture to the right moves', () => { 316 | let 317 | b = Board.create(), 318 | pv = PieceValidation.create(PieceType.Pawn, b); 319 | 320 | b.move(b.getSquare('e', 2), b.getSquare('e', 4)); 321 | b.move(b.getSquare('f', 7), b.getSquare('f', 6)); 322 | b.move(b.getSquare('e', 4), b.getSquare('e', 5)); 323 | 324 | pv.start(b.getSquare('e', 5), (err, squares) => { 325 | if (err) { 326 | throw err; 327 | } 328 | 329 | assert.strictEqual(squares.length, 2); 330 | assert.strictEqual(squares[1].rank, 6); 331 | assert.strictEqual(squares[1].file, 'f'); 332 | }); 333 | }); 334 | 335 | // verify pawn capture moves 336 | it('should properly allow pawn capture to the left moves', () => { 337 | let 338 | b = Board.create(), 339 | pv = PieceValidation.create(PieceType.Pawn, b); 340 | 341 | b.move(b.getSquare('e', 2), b.getSquare('e', 4)); 342 | b.move(b.getSquare('d', 7), b.getSquare('d', 6)); 343 | b.move(b.getSquare('e', 4), b.getSquare('e', 5)); 344 | 345 | pv.start(b.getSquare('e', 5), (err, squares) => { 346 | if (err) { 347 | throw err; 348 | } 349 | 350 | assert.strictEqual(squares.length, 2); 351 | assert.strictEqual(squares[1].rank, 6); 352 | assert.strictEqual(squares[1].file, 'd'); 353 | }); 354 | }); 355 | 356 | // test queen validation create 357 | it('should properly create queen validation', () => { 358 | let 359 | b = Board.create(), 360 | pv = PieceValidation.create(PieceType.Queen, b); 361 | 362 | assert.strictEqual(pv.allowForward, true); 363 | assert.strictEqual(pv.allowBackward, true); 364 | assert.strictEqual(pv.allowHorizontal, true); 365 | assert.strictEqual(pv.allowDiagonal, true); 366 | assert.strictEqual(pv.type, PieceType.Queen); 367 | assert.strictEqual(pv.repeat, 8); 368 | }); 369 | 370 | // test queen validation moves 371 | it('should properly allow queen moves across ranks and files', () => { 372 | let 373 | b = Board.create(), 374 | pv = PieceValidation.create(PieceType.Queen, b); 375 | 376 | b.move(b.getSquare('e', 2), b.getSquare('e', 5)); 377 | b.move(b.getSquare('d', 1), b.getSquare('f', 3)); 378 | 379 | pv.start(b.getSquare('f', 3), (err, squares) => { 380 | if (err) { 381 | throw err; 382 | } 383 | 384 | assert.strictEqual(err, null); 385 | assert.strictEqual(squares.length, 19); 386 | assert.strictEqual(checkForSquare('f', 7, squares), true, 'Qf7'); 387 | assert.strictEqual(checkForSquare('b', 7, squares), true, 'Qb7'); 388 | assert.strictEqual(checkForSquare('h', 3, squares), true, 'Qh3'); 389 | }); 390 | }); 391 | 392 | // test rook validation create 393 | it('should properly create rook validation', () => { 394 | let 395 | b = Board.create(), 396 | pv = PieceValidation.create(PieceType.Rook, b); 397 | 398 | assert.strictEqual(pv.allowForward, true); 399 | assert.strictEqual(pv.allowBackward, true); 400 | assert.strictEqual(pv.allowHorizontal, true); 401 | assert.strictEqual(pv.type, PieceType.Rook); 402 | assert.strictEqual(pv.repeat, 8); 403 | }); 404 | 405 | // test rook validation moves 406 | it('should properly represent blocked white rook moves', () => { 407 | let 408 | b = Board.create(), 409 | pv = PieceValidation.create(PieceType.Rook, b); 410 | 411 | pv.start(b.getSquare('a', 1), (err, squares) => { 412 | if (err) { 413 | throw err; 414 | } 415 | 416 | assert.strictEqual(err, null); 417 | assert.strictEqual(squares.length, 0); 418 | }); 419 | }); 420 | 421 | // test rook validation moves 422 | it('should properly represent white rook moves when not blocked', () => { 423 | let 424 | b = Board.create(), 425 | pv = PieceValidation.create(PieceType.Rook, b); 426 | 427 | b.move(b.getSquare('a', 2), b.getSquare('a', 4)); 428 | b.move(b.getSquare('a', 1), b.getSquare('a', 3)); 429 | 430 | pv.start(b.getSquare('a', 3), (err, squares) => { 431 | if (err) { 432 | throw err; 433 | } 434 | 435 | assert.strictEqual(err, null); 436 | assert.strictEqual(squares.length, 9); 437 | 438 | let 439 | files = '', 440 | i = 0; 441 | 442 | for (; i < squares.length; i++) { 443 | files += squares[i].file; 444 | } 445 | 446 | assert.strictEqual(files, 'aabcdefgh'); 447 | }); 448 | }); 449 | 450 | // test rook validation moves 451 | it('should properly represent black rook moves when not blocked', () => { 452 | let 453 | b = Board.create(), 454 | pv = PieceValidation.create(PieceType.Rook, b); 455 | 456 | b.move(b.getSquare('a', 7), b.getSquare('a', 5)); 457 | b.move(b.getSquare('a', 8), b.getSquare('a', 6)); 458 | 459 | pv.start(b.getSquare('a', 6), (err, squares) => { 460 | if (err) { 461 | throw err; 462 | } 463 | 464 | assert.strictEqual(err, null); 465 | assert.strictEqual(squares.length, 9); 466 | 467 | let 468 | files = '', 469 | i = 0; 470 | 471 | for (; i < squares.length; i++) { 472 | files += squares[i].file; 473 | } 474 | 475 | assert.strictEqual(files, 'aabcdefgh'); 476 | }); 477 | }); 478 | 479 | // test rook validation moves including capture 480 | it('should properly represent rook moves including captures', () => { 481 | let 482 | b = Board.create(), 483 | pv = PieceValidation.create(PieceType.Rook, b); 484 | 485 | // kill the white pawn in-front of the rook 486 | b.getSquare('a', 2).piece = null; 487 | 488 | pv.start(b.getSquare('a', 1), (err, squares) => { 489 | if (err) { 490 | throw err; 491 | } 492 | 493 | assert.strictEqual(err, null); 494 | assert.strictEqual(squares.length, 6); 495 | 496 | let 497 | i = 0, 498 | sumRanks = 0; 499 | 500 | for (; i < squares.length; i++) { 501 | sumRanks += squares[i].rank; 502 | } 503 | 504 | assert.strictEqual(sumRanks, 2+3+4+5+6+7); 505 | }); 506 | }); 507 | }); 508 | -------------------------------------------------------------------------------- /test/src/boardValidation.js: -------------------------------------------------------------------------------- 1 | /* eslint no-magic-numbers:0 */ 2 | import { assert, describe, it } from 'vitest'; 3 | import { BoardValidation } from '../../src/boardValidation.js'; 4 | import { Game } from '../../src/game.js'; 5 | 6 | describe('BoardValidation', () => { 7 | let getValidSquares = (sq, validMoves) => { 8 | let i = 0; 9 | 10 | for (; i < validMoves.length; i++) { 11 | if (validMoves[i].src === sq) { 12 | return validMoves[i].squares; 13 | } 14 | } 15 | }; 16 | 17 | // validate error creating BoardValidation when board is null 18 | it('should fail if validation object is created without a valid board', () => { 19 | let bv = BoardValidation.create(null); 20 | 21 | bv.start((err) => { 22 | assert.ok(err); 23 | assert.strictEqual(err.message, 'board is invalid'); 24 | }); 25 | }); 26 | 27 | // ensure board and game set properly when BoardValidation is created 28 | it('should properly reflect board and game when validation object is created', () => { 29 | let 30 | b, 31 | bv, 32 | g = Game.create(); 33 | 34 | b = g.board; 35 | bv = BoardValidation.create(g); 36 | 37 | assert.strictEqual(bv.board, b); 38 | }); 39 | 40 | // ensure validation returns appropriate piece move options based on turn 41 | it('should properly indicate that a white Pawn has 2 valid moves', () => { 42 | let 43 | b, 44 | bv, 45 | g = Game.create(), 46 | squares = []; 47 | 48 | b = g.board; 49 | bv = BoardValidation.create(g); 50 | 51 | bv.start((err, validMoves) => { 52 | if (err) { 53 | throw err; 54 | } 55 | 56 | squares = getValidSquares( 57 | b.getSquare('e', 2), 58 | validMoves); 59 | 60 | assert.strictEqual(squares.length, 2); 61 | }); 62 | }); 63 | 64 | // ensure validation returns appropriate piece move options based on turn 65 | it('testBoardValidation_BlackPawn_NoValidMoves', () => { 66 | let 67 | b, 68 | bv, 69 | g = Game.create(), 70 | squares = []; 71 | 72 | b = g.board; 73 | bv = BoardValidation.create(g); 74 | 75 | bv.start((err, validMoves) => { 76 | if (err) { 77 | throw err; 78 | } 79 | 80 | squares = getValidSquares( 81 | b.getSquare('e', 7), 82 | validMoves); 83 | 84 | assert.ok(typeof squares === 'undefined'); 85 | }); 86 | }); 87 | 88 | // validate is square attacked on piece not being attacked 89 | it('testBoardValidation_WhiteKing_IsNotAttacked', () => { 90 | let 91 | b, 92 | bv, 93 | g = Game.create(), 94 | kingSquare; 95 | 96 | b = g.board; 97 | bv = BoardValidation.create(g); 98 | kingSquare = b.getSquare('e', 1); 99 | 100 | assert.strictEqual(bv.isSquareAttacked(kingSquare), false); 101 | }); 102 | 103 | // validate is square attacked on piece being attacked 104 | it('testBoardValidation_BlackPawn_IsAttacked', () => { 105 | let 106 | b, 107 | bv, 108 | g = Game.create(); 109 | 110 | b = g.board; 111 | bv = BoardValidation.create(g); 112 | 113 | b.move(b.getSquare('d', 2), b.getSquare('d', 4)); 114 | b.move(b.getSquare('d', 7), b.getSquare('d', 5)); 115 | b.move(b.getSquare('c', 1), b.getSquare('g', 5)); 116 | 117 | assert.strictEqual(bv.isSquareAttacked(b.getSquare('e', 7)), true); 118 | }); 119 | 120 | // ensure is square attacked accurately tracks king being attacked 121 | it('testBoardValidation_BlackKing_IsAttacked', () => { 122 | let 123 | b, 124 | bv, 125 | g = Game.create(); 126 | 127 | b = g.board; 128 | bv = BoardValidation.create(g); 129 | 130 | b.move(b.getSquare('e', 2), b.getSquare('e', 4)); 131 | b.move(b.getSquare('f', 7), b.getSquare('f', 5)); 132 | b.move(b.getSquare('d', 1), b.getSquare('h', 5)); 133 | 134 | assert.strictEqual(bv.isSquareAttacked(b.getSquare('e', 8)), true); 135 | }); 136 | 137 | // ensure is square attacked accurately tracks that's it not being attacked 138 | // based on bug found where board logic thought square was attacked by pawn more 139 | // than 1 square away diagonally 140 | it('testBoardValidation_BlackKing_IsNotAttacked', () => { 141 | let 142 | b, 143 | bv, 144 | g = Game.create(); 145 | 146 | b = g.board; 147 | bv = BoardValidation.create(g); 148 | 149 | b.move('g2', 'g4'); 150 | b.move('f7', 'f6'); 151 | b.move('g4', 'g5'); 152 | b.move('f6', 'f5'); 153 | b.move('g5', 'g6'); 154 | 155 | assert.strictEqual(bv.isSquareAttacked(b.getSquare('e', 8)), false); 156 | }); 157 | 158 | // validate castle rule to left 159 | it('testBoardValidation_WhiteKingCastle_Left', () => { 160 | let 161 | b, 162 | bv, 163 | g = Game.create(), 164 | squares = []; 165 | 166 | b = g.board; 167 | bv = BoardValidation.create(g); 168 | 169 | b.getSquare('b', 1).piece = null; 170 | b.getSquare('c', 1).piece = null; 171 | b.getSquare('d', 1).piece = null; 172 | 173 | bv.start((err, validMoves) => { 174 | if (err) { 175 | throw err; 176 | } 177 | 178 | squares = getValidSquares( 179 | b.getSquare('e', 1), 180 | validMoves); 181 | 182 | assert.strictEqual(squares.length, 2); 183 | assert.strictEqual(squares[1].file, 'c'); 184 | }); 185 | }); 186 | 187 | // validate castle rule to right 188 | it('testBoardValidation_BlackKingCastle_Right', () => { 189 | let 190 | b, 191 | bv, 192 | g = Game.create(), 193 | squares = []; 194 | 195 | b = g.board; 196 | bv = BoardValidation.create(g); 197 | 198 | b.getSquare('f', 8).piece = null; 199 | b.getSquare('g', 8).piece = null; 200 | 201 | b.move(b.getSquare('a', 2), b.getSquare('a', 4)); 202 | 203 | bv.start((err, validMoves) => { 204 | if (err) { 205 | throw err; 206 | } 207 | 208 | squares = getValidSquares( 209 | b.getSquare('e', 8), 210 | validMoves); 211 | 212 | assert.strictEqual(squares.length, 2); 213 | assert.strictEqual(squares[1].file, 'g'); 214 | }); 215 | }); 216 | 217 | // validate castle rule no longer applies when king has moved 218 | it('testBoardValidation_WhiteKingCastle_KingMoved', () => { 219 | let 220 | b, 221 | bv, 222 | g = Game.create(), 223 | squares = []; 224 | 225 | b = g.board; 226 | bv = BoardValidation.create(g); 227 | 228 | // clear squares between king and rook 229 | b.getSquare('f', 1).piece = null; 230 | b.getSquare('g', 1).piece = null; 231 | 232 | // move king 233 | b.move(b.getSquare('e', 1), b.getSquare('f', 1)); 234 | b.move(b.getSquare('f', 1), b.getSquare('e', 1)); 235 | 236 | bv.start((err, validMoves) => { 237 | if (err) { 238 | throw err; 239 | } 240 | 241 | squares = getValidSquares( 242 | b.getSquare('e', 1), 243 | validMoves); 244 | 245 | assert.strictEqual(squares.length, 1); 246 | }); 247 | }); 248 | 249 | // validate castle rule no longer applies when rook has moved 250 | it('testBoardValidation_WhiteKingCastle_RookMoved', () => { 251 | let 252 | b, 253 | bv, 254 | g = Game.create(), 255 | squares = []; 256 | 257 | b = g.board; 258 | bv = BoardValidation.create(g); 259 | 260 | // clear squares between king and rook 261 | b.getSquare('f', 1).piece = null; 262 | b.getSquare('g', 1).piece = null; 263 | 264 | // move rook 265 | b.move(b.getSquare('h', 1), b.getSquare('f', 1)); 266 | b.move(b.getSquare('f', 1), b.getSquare('h', 1)); 267 | 268 | bv.start((err, validMoves) => { 269 | if (err) { 270 | throw err; 271 | } 272 | 273 | squares = getValidSquares( 274 | b.getSquare('e', 1), 275 | validMoves); 276 | 277 | assert.strictEqual(squares.length, 1); 278 | }); 279 | }); 280 | 281 | // validate only move options are to block check 282 | it('testBoardValidation_WhiteKingInCheck_BlockMovesOnly', () => { 283 | let 284 | b, 285 | bishopSquare, 286 | bv, 287 | g = Game.create(), 288 | knightSquare, 289 | queenSquare, 290 | squares = []; 291 | 292 | b = g.board; 293 | bishopSquare = b.getSquare('f', 1); 294 | bv = BoardValidation.create(g); 295 | knightSquare = b.getSquare('g', 1); 296 | queenSquare = b.getSquare('d', 1); 297 | 298 | // put king in check 299 | b.getSquare('e', 2).piece = null; 300 | b.getSquare('e', 7).piece = null; 301 | b.move(b.getSquare('d', 8), b.getSquare('e', 7), true); 302 | 303 | bv.start((err, validMoves) => { 304 | if (err) { 305 | throw err; 306 | } 307 | 308 | squares = getValidSquares(queenSquare, validMoves); 309 | assert.strictEqual(squares.length, 1); 310 | 311 | squares = getValidSquares(bishopSquare, validMoves); 312 | assert.strictEqual(squares.length, 1); 313 | 314 | squares = getValidSquares(knightSquare, validMoves); 315 | assert.strictEqual(squares.length, 1); 316 | 317 | assert.strictEqual(validMoves.length, 3); 318 | }); 319 | }); 320 | 321 | it('should properly trigger game to emit check when King is placed in check', () => { 322 | let 323 | b, 324 | bv, 325 | checkResult = [], 326 | g = Game.create(); 327 | 328 | b = g.board; 329 | bv = BoardValidation.create(g); 330 | 331 | // capture check event 332 | g.on('check', (result) => (checkResult.push(result))); 333 | 334 | // prepare board by eliminating a few pieces 335 | b.getSquare('e', 2).piece = null; 336 | b.getSquare('e', 7).piece = null; 337 | b.getSquare('f', 8).piece = null; 338 | 339 | // arrange double check scenario via reveal 340 | b.move(b.getSquare('b', 1), b.getSquare('c', 3)); 341 | b.move(b.getSquare('a', 7), b.getSquare('a', 6)); 342 | 343 | b.move(b.getSquare('c', 3), b.getSquare('e', 4)); 344 | b.move(b.getSquare('a', 6), b.getSquare('a', 5)); 345 | 346 | // Queen preparing to put in check 347 | b.move(b.getSquare('d', 1), b.getSquare('e', 2)); 348 | b.move(b.getSquare('a', 5), b.getSquare('a', 4)); 349 | 350 | // double-check (a checkmate... Queen and Knight both attacking) 351 | b.move(b.getSquare('e', 4), b.getSquare('f', 6)); 352 | 353 | bv.start((err) => { 354 | if (err) { 355 | throw err; 356 | } 357 | 358 | assert.ok(checkResult); 359 | // Should emit two events, not a single event 360 | assert.strictEqual(checkResult.length, 2); 361 | }); 362 | }); 363 | 364 | it('should properly trigger game to emit multiple checkmate events when King is placed in checkmate by multiple pieces', () => { 365 | let 366 | b, 367 | bv, 368 | checkmateResult = [], 369 | checkResult = [], 370 | g = Game.create(); 371 | 372 | b = g.board; 373 | bv = BoardValidation.create(g); 374 | 375 | // capture check event 376 | g.on('check', (result) => checkResult.push(result)); 377 | g.on('checkmate', (result) => checkmateResult.push(result)); 378 | 379 | // prepare board by eliminating a couple pieces 380 | b.getSquare('e', 2).piece = null; 381 | b.getSquare('e', 7).piece = null; 382 | 383 | // arrange double check scenario via reveal 384 | b.move(b.getSquare('b', 1), b.getSquare('c', 3)); 385 | b.move(b.getSquare('a', 7), b.getSquare('a', 6)); 386 | 387 | b.move(b.getSquare('c', 3), b.getSquare('e', 4)); 388 | b.move(b.getSquare('a', 6), b.getSquare('a', 5)); 389 | 390 | // Queen preparing to put in check 391 | b.move(b.getSquare('d', 1), b.getSquare('e', 2)); 392 | b.move(b.getSquare('a', 5), b.getSquare('a', 4)); 393 | 394 | // double-check (a checkmate... Queen and Knight both attacking) 395 | b.move(b.getSquare('e', 4), b.getSquare('f', 6)); 396 | 397 | bv.start((err) => { 398 | if (err) { 399 | throw err; 400 | } 401 | 402 | assert.ok(checkResult); 403 | assert.ok(checkmateResult); 404 | 405 | assert.strictEqual(checkResult.length, 0); 406 | assert.strictEqual(checkmateResult.length, 2); 407 | }); 408 | }); 409 | 410 | it('should properly trigger game to emit single checkmate event when King is placed in checkmate', () => { 411 | let 412 | b, 413 | bv, 414 | checkmateResult = [], 415 | checkResult = [], 416 | g = Game.create(); 417 | 418 | b = g.board; 419 | bv = BoardValidation.create(g); 420 | 421 | // capture check event 422 | g.on('check', (result) => checkResult.push(result)); 423 | g.on('checkmate', (result) => checkmateResult.push(result)); 424 | 425 | // prepare board by eliminating a couple pieces 426 | b.getSquare('e', 2).piece = null; 427 | b.getSquare('f', 7).piece = null; 428 | b.getSquare('g', 7).piece = null; 429 | 430 | // Queen preparing to put in checkmate 431 | b.move(b.getSquare('d', 1), b.getSquare('h', 5)); 432 | 433 | bv.start((err) => { 434 | if (err) { 435 | throw err; 436 | } 437 | 438 | assert.ok(checkResult); 439 | assert.ok(checkmateResult); 440 | 441 | assert.strictEqual(checkResult.length, 0); 442 | assert.strictEqual(checkmateResult.length, 1); 443 | }); 444 | }); 445 | 446 | // validate inability to castle while king is in check 447 | it('testBoardValidation_WhiteKingCastle_KingInCheck', () => { 448 | let 449 | b, 450 | bv, 451 | g = Game.create(), 452 | squares = []; 453 | 454 | b = g.board; 455 | bv = BoardValidation.create(g); 456 | 457 | // clear squares between king and rook 458 | b.getSquare('f', 1).piece = null; 459 | b.getSquare('g', 1).piece = null; 460 | 461 | // put king in check 462 | b.getSquare('e', 2).piece = null; 463 | b.getSquare('e', 7).piece = null; 464 | b.move(b.getSquare('d', 8), b.getSquare('e', 7), true); 465 | 466 | bv.start((err, validMoves) => { 467 | if (err) { 468 | throw err; 469 | } 470 | 471 | squares = getValidSquares( 472 | b.getSquare('e', 1), 473 | validMoves); 474 | 475 | assert.strictEqual(squares.length, 1); 476 | }); 477 | }); 478 | 479 | // validate inability to castle through or into check 480 | it('testBoardValidation_WhiteKingCastle_MoveThroughCheck', () => { 481 | let 482 | b, 483 | bv, 484 | g = Game.create(), 485 | squares = []; 486 | 487 | b = g.board; 488 | bv = BoardValidation.create(g); 489 | 490 | // clear squares between king and rook 491 | b.getSquare('f', 1).piece = null; 492 | b.getSquare('g', 1).piece = null; 493 | 494 | // put attacker in castle path 495 | b.getSquare('f', 2).piece = null; 496 | b.getSquare('e', 7).piece = null; 497 | b.move(b.getSquare('d', 8), b.getSquare('f', 6), true); 498 | 499 | bv.start((err, validMoves) => { 500 | if (err) { 501 | throw err; 502 | } 503 | 504 | squares = getValidSquares( 505 | b.getSquare('e', 1), 506 | validMoves); 507 | 508 | assert.ok(typeof squares === 'undefined'); 509 | }); 510 | }); 511 | 512 | // validate inability to move into check 513 | it('testBoardValidation_WhiteKing_UnableToExposeCheck', () => { 514 | let 515 | b, 516 | bv, 517 | g = Game.create(), 518 | squares = []; 519 | 520 | b = g.board; 521 | bv = BoardValidation.create(g); 522 | 523 | // block king with knight and attack with queen 524 | b.getSquare('e', 2).piece = null; 525 | b.getSquare('e', 7).piece = null; 526 | b.move(b.getSquare('d', 8), b.getSquare('e', 7), true); 527 | b.move(b.getSquare('g', 1), b.getSquare('e', 2), true); 528 | 529 | bv.start((err, validMoves) => { 530 | if (err) { 531 | throw err; 532 | } 533 | 534 | squares = getValidSquares( 535 | b.getSquare('e', 2), 536 | validMoves); 537 | 538 | assert.ok(typeof squares === 'undefined'); 539 | }); 540 | }); 541 | 542 | // validate checkmate (no available moves) 543 | it('testBoardValidation_BlackKing_Checkmate', () => { 544 | let 545 | b, 546 | bv, 547 | g = Game.create(); 548 | 549 | b = g.board; 550 | bv = BoardValidation.create(g); 551 | 552 | // put king into checkmate 553 | b.move(b.getSquare('e', 2), b.getSquare('e', 4)); 554 | b.move(b.getSquare('f', 7), b.getSquare('f', 6)); 555 | b.move(b.getSquare('d', 2), b.getSquare('d', 4)); 556 | b.move(b.getSquare('g', 7), b.getSquare('g', 5)); 557 | b.move(b.getSquare('d', 1), b.getSquare('h', 5)); 558 | 559 | bv.start((err, validMoves) => { 560 | if (err) { 561 | throw err; 562 | } 563 | 564 | assert.strictEqual(validMoves.length, 0); 565 | }); 566 | }); 567 | 568 | // validate pieces don't disappear after validation 569 | it('testBoardValidation_Pawn_Disappears', () => { 570 | let 571 | b, 572 | bv, 573 | g = Game.create(); 574 | 575 | b = g.board; 576 | bv = BoardValidation.create(g); 577 | 578 | b.move('e2', 'e4'); 579 | b.move('e7', 'e6'); 580 | b.move('d2', 'd4'); 581 | b.move('d7', 'd5'); 582 | 583 | bv.start(() => { 584 | assert.ok(b.getSquare('d4').piece !== null); 585 | }); 586 | 587 | b.move('b1', 'c3'); 588 | 589 | bv.start(() => { 590 | assert.ok(b.getSquare('d4').piece !== null, 'pawn has disappeared during validation'); 591 | }); 592 | }); 593 | }); 594 | -------------------------------------------------------------------------------- /test/src/board.js: -------------------------------------------------------------------------------- 1 | /* eslint no-magic-numbers:0 */ 2 | import { assert, describe, it } from 'vitest'; 3 | import { Board, NeighborType } from '../../src/board.js'; 4 | import { PieceType, SideType } from '../../src/piece.js'; 5 | 6 | describe('Board', () => { 7 | describe('#create()', () => { 8 | // ensure 64 squares 9 | it('should return 64 squares', () => { 10 | let b = Board.create(); 11 | assert.strictEqual(b.squares.length, 64); 12 | }); 13 | }); 14 | 15 | describe('#load(FEN)', () => { 16 | // ensure 64 squares 17 | it('should return 64 squares', () => { 18 | let b = Board.load('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR'); 19 | assert.strictEqual(b.squares.length, 64); 20 | }); 21 | }); 22 | 23 | describe('#getSquare()', () => { 24 | // ensure squares retrieved via getSquare are correct 25 | it('should be square a1', () => { 26 | let 27 | b = Board.create(), 28 | s = b.getSquare('a', 1); 29 | 30 | assert.strictEqual(s.rank, 1); 31 | assert.strictEqual(s.file, 'a'); 32 | }); 33 | 34 | // ensure shorthand for getSquare works as expected 35 | it('should be square a1 via shorthand', () => { 36 | let 37 | b = Board.create(), 38 | s = b.getSquare('a1'); 39 | 40 | assert.strictEqual(s.rank, 1); 41 | assert.strictEqual(s.file, 'a'); 42 | }); 43 | 44 | // ensure squares retrieved via getSquare are correct 45 | it('should be square a8', () => { 46 | let 47 | b = Board.create(), 48 | s = b.getSquare('a', 8); 49 | 50 | assert.strictEqual(s.rank, 8); 51 | assert.strictEqual(s.file, 'a'); 52 | }); 53 | 54 | // ensure squares retrieved via getSquare are correct 55 | it('should be square h1', () => { 56 | let 57 | b = Board.create(), 58 | s = b.getSquare('h', 1); 59 | 60 | assert.strictEqual(s.rank, 1); 61 | assert.strictEqual(s.file, 'h'); 62 | }); 63 | 64 | // ensure squares retrieved via getSquare are correct 65 | it('should be square h8', () => { 66 | let 67 | b = Board.create(), 68 | s = b.getSquare('h', 8); 69 | 70 | assert.strictEqual(s.rank, 8); 71 | assert.strictEqual(s.file, 'h'); 72 | }); 73 | 74 | // ensure squares retrieved via getSquare are correct 75 | it('should be square 5e', () => { 76 | let 77 | b = Board.create(), 78 | s = b.getSquare('e', 5); 79 | 80 | assert.strictEqual(s.rank, 5); 81 | assert.strictEqual(s.file, 'e'); 82 | }); 83 | 84 | // ensure squares requested with invalid data are null 85 | it('should be null square (invalid rank)', () => { 86 | let b = Board.create(); 87 | 88 | assert.equal(b.getSquare('a', 0), null); 89 | }); 90 | 91 | // ensure squares requested with invalid data are null 92 | it('should be null square (invalid file)', () => { 93 | let b = Board.create(); 94 | 95 | assert.equal(b.getSquare('i', 1), null); 96 | }); 97 | 98 | // ensure corrupted board returns null square 99 | it('should be null square (corrupted board)', () => { 100 | let b = Board.create(); 101 | b.squares = []; 102 | 103 | assert.equal(b.getSquare('a', 1), null); 104 | }); 105 | }); 106 | 107 | describe('#getSquare().piece', () => { 108 | // ensure pieces are placed properly on squares 109 | it('should be White King on e1', () => { 110 | let 111 | b = Board.create(), 112 | p = b.getSquare('e', 1).piece; 113 | 114 | assert.strictEqual(p.type, PieceType.King); 115 | assert.strictEqual(p.side, SideType.White); 116 | }); 117 | 118 | // ensure pieces are placed properly on squares 119 | it('should be Black Queen on d8', () => { 120 | let 121 | b = Board.create(), 122 | p = b.getSquare('d', 8).piece; 123 | 124 | assert.strictEqual(p.type, PieceType.Queen); 125 | assert.strictEqual(p.side, SideType.Black); 126 | }); 127 | 128 | // ensure moveCount of piece isn't incremented during Board.create() 129 | it('should be White Pawn with move count of 0', () => { 130 | let 131 | b = Board.create(), 132 | p = b.getSquare('d', 2).piece; 133 | 134 | assert.strictEqual(p.type, PieceType.Pawn); 135 | assert.strictEqual(p.side, SideType.White); 136 | assert.strictEqual(p.moveCount, 0); 137 | }); 138 | }); 139 | 140 | describe('#getSquares(SideType)', () => { 141 | // validate getSquares(SideType) works correctly 142 | it('should return all White squares', () => { 143 | let 144 | b = Board.create(), 145 | i = 0, 146 | kingCount = 0, 147 | pawnCount = 0, 148 | squares = b.getSquares(SideType.White); 149 | 150 | assert.strictEqual(squares.length, 16); 151 | assert.strictEqual(squares[i].piece.side, SideType.White); 152 | 153 | for (; i < squares.length; i++) { 154 | if (squares[i].piece.type === PieceType.Pawn) { 155 | pawnCount++; 156 | } else if (squares[i].piece.type === PieceType.King) { 157 | kingCount++; 158 | } 159 | } 160 | 161 | assert.strictEqual(pawnCount, 8); 162 | assert.strictEqual(kingCount, 1); 163 | }); 164 | 165 | // validate getSquares(SideType) works correctly 166 | it('should return all Black squares', () => { 167 | let 168 | b = Board.create(), 169 | i = 0, 170 | kingCount = 0, 171 | pawnCount = 0, 172 | squares = b.getSquares(SideType.Black); 173 | 174 | assert.strictEqual(squares.length, 16); 175 | assert.strictEqual(squares[i].piece.side, SideType.Black); 176 | 177 | for (; i < squares.length; i++) { 178 | if (squares[i].piece.type === PieceType.Pawn) { 179 | pawnCount++; 180 | } else if (squares[i].piece.type === PieceType.King) { 181 | kingCount++; 182 | } 183 | } 184 | 185 | assert.strictEqual(pawnCount, 8); 186 | assert.strictEqual(kingCount, 1); 187 | }); 188 | }); 189 | 190 | describe('#getNeighborSquare(NeighborType)', () => { 191 | // validate getSquares(SideType) works correctly 192 | it('should return square e3 when going above e2', () => { 193 | let 194 | b = Board.create(), 195 | sq1 = b.getSquare('e', 2), 196 | sq2 = b.getNeighborSquare(sq1, NeighborType.Above); 197 | 198 | assert.strictEqual(sq2.rank, 3); 199 | assert.strictEqual(sq2.file, 'e'); 200 | }); 201 | 202 | // verify getNeighborSquare(NeighborType) returns null for invalid boundaries 203 | it('should return null square when going above a8', () => { 204 | let 205 | b = Board.create(), 206 | sq1 = b.getSquare('a', 8), 207 | sq2 = b.getNeighborSquare(sq1, NeighborType.Above); 208 | 209 | assert.strictEqual(sq2, null); 210 | }); 211 | 212 | // verify getNeighborSquare(NeighborType) 213 | it('should return square d3 when going above left of e2 ', () => { 214 | let 215 | b = Board.create(), 216 | sq1 = b.getSquare('e', 2), 217 | sq2 = b.getNeighborSquare(sq1, NeighborType.AboveLeft); 218 | 219 | assert.strictEqual(sq2.rank, 3); 220 | assert.strictEqual(sq2.file, 'd'); 221 | }); 222 | 223 | // verify getNeighborSquare(NeighborType) 224 | it('should return f3 square when going above left of e2', () => { 225 | let 226 | b = Board.create(), 227 | sq1 = b.getSquare('e', 2), 228 | sq2 = b.getNeighborSquare(sq1, NeighborType.AboveRight); 229 | 230 | assert.strictEqual(sq2.rank, 3); 231 | assert.strictEqual(sq2.file, 'f'); 232 | }); 233 | 234 | // verify getNeighborSquare(NeighborType) 235 | it('should return e1 square when going below of e2', () => { 236 | let 237 | b = Board.create(), 238 | sq1 = b.getSquare('e', 2), 239 | sq2 = b.getNeighborSquare(sq1, NeighborType.Below); 240 | 241 | assert.strictEqual(sq2.rank, 1); 242 | assert.strictEqual(sq2.file, 'e'); 243 | }); 244 | 245 | // verify getNeighborSquare(NeighborType) returns null for invalid boundaries 246 | it('should return null square below a1', () => { 247 | let 248 | b = Board.create(), 249 | sq1 = b.getSquare('a', 1), 250 | sq2 = b.getNeighborSquare(sq1, NeighborType.Below); 251 | 252 | assert.strictEqual(sq2, null); 253 | }); 254 | 255 | // verify getNeighborSquare(NeighborType) 256 | it('should return d1 below left of e2', () => { 257 | let 258 | b = Board.create(), 259 | sq1 = b.getSquare('e', 2), 260 | sq2 = b.getNeighborSquare(sq1, NeighborType.BelowLeft); 261 | 262 | assert.strictEqual(sq2.rank, 1); 263 | assert.strictEqual(sq2.file, 'd'); 264 | }); 265 | 266 | // verify getNeighborSquare(NeighborType) 267 | it('should return f1 below right of e2', () => { 268 | let 269 | b = Board.create(), 270 | sq1 = b.getSquare('e', 2), 271 | sq2 = b.getNeighborSquare(sq1, NeighborType.BelowRight); 272 | 273 | assert.strictEqual(sq2.rank, 1); 274 | assert.strictEqual(sq2.file, 'f'); 275 | }); 276 | 277 | // verify getNeighborSquare(NeighborType) 278 | it('should return d2 left of e2', () => { 279 | let 280 | b = Board.create(), 281 | sq1 = b.getSquare('e', 2), 282 | sq2 = b.getNeighborSquare(sq1, NeighborType.Left); 283 | 284 | assert.strictEqual(sq2.rank, 2); 285 | assert.strictEqual(sq2.file, 'd'); 286 | }); 287 | 288 | // verify getNeighborSquare(NeighborType) returns null for invalid boundaries 289 | it('should return null square left of a2', () => { 290 | let 291 | b = Board.create(), 292 | sq1 = b.getSquare('a', 2), 293 | sq2 = b.getNeighborSquare(sq1, NeighborType.Left); 294 | 295 | assert.strictEqual(sq2, null); 296 | }); 297 | 298 | // verify getNeighborSquare(NeighborType) 299 | it('should return f2 right of e2', () => { 300 | let 301 | b = Board.create(), 302 | sq1 = b.getSquare('e', 2), 303 | sq2 = b.getNeighborSquare(sq1, NeighborType.Right); 304 | 305 | assert.strictEqual(sq2.rank, 2); 306 | assert.strictEqual(sq2.file, 'f'); 307 | }); 308 | 309 | // verify getNeighborSquare(NeighborType) returns null for invalid boundaries 310 | it('should return null square right of h2', () => { 311 | let 312 | b = Board.create(), 313 | sq1 = b.getSquare('h', 2), 314 | sq2 = b.getNeighborSquare(sq1, NeighborType.Right); 315 | 316 | assert.strictEqual(sq2, null); 317 | }); 318 | }); 319 | 320 | describe('#move()', () => { 321 | // verify that moving a piece actually results in the piece being moved 322 | it('should have pieces on the correct squares after moving', () => { 323 | let b = Board.create(); 324 | 325 | b.move(b.getSquare('e', 2), b.getSquare('e', 4)); 326 | b.move(b.getSquare('f', 7), b.getSquare('f', 5)); 327 | b.move(b.getSquare('d', 1), b.getSquare('h', 5)); 328 | 329 | assert.strictEqual(b.getSquare('e', 4).piece.type, PieceType.Pawn); 330 | assert.strictEqual(b.getSquare('f', 5).piece.type, PieceType.Pawn); 331 | assert.strictEqual(b.getSquare('h', 5).piece.type, PieceType.Queen); 332 | }); 333 | 334 | // ensure shorthand for move works as expected 335 | it('should support shorthand move', () => { 336 | let b = Board.create(); 337 | 338 | b.move('e2', 'e4'); 339 | 340 | assert.strictEqual(b.getSquare('e', 4).piece.type, PieceType.Pawn); 341 | }); 342 | 343 | // verify simulation of move provides backout method that doesn't corrupt board 344 | it('should have non-corrupt board when backing out a simple move', () => { 345 | let 346 | b = Board.create(), 347 | r = b.move(b.getSquare('e', 2), b.getSquare('e', 4), true); 348 | 349 | assert.strictEqual(b.getSquare('e', 2).piece, null); 350 | assert.strictEqual(b.getSquare('e', 4).piece.type, PieceType.Pawn); 351 | 352 | r.undo(); 353 | 354 | assert.strictEqual(b.getSquare('e', 4).piece, null); 355 | assert.strictEqual(b.getSquare('e', 2).piece.type, PieceType.Pawn); 356 | }); 357 | 358 | it('should not emit an event when calling undo and move is simulated', () => { 359 | let 360 | b = Board.create(), 361 | mv, 362 | r = b.move(b.getSquare('e', 2), b.getSquare('e', 4), true); 363 | 364 | b.on('undo', (m) => { 365 | mv = m; 366 | }); 367 | 368 | r.undo(); 369 | 370 | assert.ok(typeof mv === 'undefined'); 371 | }); 372 | 373 | it('should emit an event when calling undo', () => { 374 | let 375 | b = Board.create(), 376 | mv, 377 | r = b.move(b.getSquare('e', 2), b.getSquare('e', 4)); 378 | 379 | b.on('undo', (m) => { 380 | mv = m; 381 | }); 382 | 383 | r.undo(); 384 | 385 | assert.ok(mv); 386 | }); 387 | 388 | it('should only allow undo to be called once', () => { 389 | let 390 | b = Board.create(), 391 | mv, 392 | r = b.move(b.getSquare('e', 2), b.getSquare('e', 4)); 393 | 394 | b.on('undo', (m) => { 395 | mv = m; 396 | }); 397 | 398 | r.undo(); 399 | 400 | assert.throws(() => { 401 | mv.undo(); 402 | }); 403 | 404 | assert.throws(() => { 405 | r.undo(); 406 | }); 407 | }); 408 | 409 | // validate board.move for en-passant and proper capture of opposing pawn 410 | it('should support en-passant', () => { 411 | let b = Board.create(); 412 | 413 | b.move(b.getSquare('e', 2), b.getSquare('e', 4)); 414 | b.move(b.getSquare('e', 7), b.getSquare('e', 6)); 415 | b.move(b.getSquare('e', 4), b.getSquare('e', 5)); 416 | b.move(b.getSquare('f', 7), b.getSquare('f', 5)); 417 | b.move(b.getSquare('e', 5), b.getSquare('f', 6)); 418 | 419 | assert.strictEqual(b.getSquare('f', 5).piece, null); 420 | }); 421 | 422 | // validate simulated board.move undo for en-passant 423 | it('should be able to backout an en-passant', () => { 424 | let 425 | b = Board.create(), 426 | r = null; 427 | 428 | b.move(b.getSquare('e', 2), b.getSquare('e', 4)); 429 | b.move(b.getSquare('e', 7), b.getSquare('e', 6)); 430 | b.move(b.getSquare('e', 4), b.getSquare('e', 5)); 431 | b.move(b.getSquare('f', 7), b.getSquare('f', 5)); 432 | 433 | r = b.move(b.getSquare('e', 5), b.getSquare('f', 6), true); 434 | 435 | assert.strictEqual(b.getSquare('f', 5).piece, null); 436 | 437 | r.undo(); 438 | 439 | assert.ok(b.getSquare('f', 5).piece !== null); 440 | assert.strictEqual(b.getSquare('f', 5).piece.type, PieceType.Pawn); 441 | }); 442 | 443 | // validate board.move for castle and proper swap with rook 444 | it('should support a proper castle to the right from rank 8 ', () => { 445 | let b = Board.create(); 446 | 447 | b.getSquare('f', 8).piece = null; 448 | b.getSquare('g', 8).piece = null; 449 | 450 | b.move(b.getSquare('e', 8), b.getSquare('g', 8)); 451 | 452 | assert.ok(b.getSquare('f', 8).piece !== null); 453 | assert.ok(b.getSquare('h', 8).piece === null); 454 | assert.strictEqual(b.getSquare('f', 8).piece.type, PieceType.Rook); 455 | }); 456 | 457 | // validate board.move for castle and proper swap with rook 458 | it('should support a proper castle to the left from rank 1', () => { 459 | let b = Board.create(); 460 | 461 | b.getSquare('b', 1).piece = null; 462 | b.getSquare('c', 1).piece = null; 463 | b.getSquare('d', 1).piece = null; 464 | 465 | b.move(b.getSquare('e', 1), b.getSquare('c', 1)); 466 | 467 | assert.ok(b.getSquare('d', 1).piece !== null); 468 | assert.ok(b.getSquare('a', 1).piece === null); 469 | assert.strictEqual(b.getSquare('d', 1).piece.type, PieceType.Rook); 470 | }); 471 | 472 | // validate simulated board.move undo for castle 473 | it('should properly undo a castle to the right during simulation', () => { 474 | let 475 | b = Board.create(), 476 | r = null; 477 | 478 | b.getSquare('f', 8).piece = null; 479 | b.getSquare('g', 8).piece = null; 480 | 481 | r = b.move(b.getSquare('e', 8), b.getSquare('g', 8), true); 482 | 483 | assert.ok(b.getSquare('f', 8).piece !== null); 484 | assert.ok(b.getSquare('h', 8).piece === null); 485 | assert.strictEqual(b.getSquare('f', 8).piece.type, PieceType.Rook); 486 | 487 | r.undo(); 488 | 489 | assert.ok(b.getSquare('f', 8).piece === null); 490 | assert.ok(b.getSquare('h', 8).piece !== null); 491 | assert.strictEqual(b.getSquare('h', 8).piece.type, PieceType.Rook); 492 | }); 493 | 494 | // validate simulated board.move undo for castle 495 | it('should properly undo a castle to the left during simulation', () => { 496 | let 497 | b = Board.create(), 498 | r = null; 499 | 500 | b.getSquare('b', 1).piece = null; 501 | b.getSquare('c', 1).piece = null; 502 | b.getSquare('d', 1).piece = null; 503 | 504 | r = b.move(b.getSquare('e', 1), b.getSquare('c', 1), true); 505 | 506 | assert.ok(b.getSquare('d', 1).piece !== null); 507 | assert.ok(b.getSquare('a', 1).piece === null); 508 | assert.strictEqual(b.getSquare('d', 1).piece.type, PieceType.Rook); 509 | 510 | r.undo(); 511 | 512 | assert.ok(b.getSquare('d', 1).piece === null); 513 | assert.ok(b.getSquare('a', 1).piece !== null); 514 | assert.strictEqual(b.getSquare('a', 1).piece.type, PieceType.Rook); 515 | }); 516 | 517 | // validate pawn capture works as expected 518 | it('should properly recognize a pawn capture', () => { 519 | let 520 | b = Board.create(), 521 | r = null; 522 | 523 | b.move('e2', 'e4'); 524 | b.move('d7', 'd5'); 525 | b.move('d2', 'd4'); 526 | r = b.move('d5', 'e4'); 527 | 528 | assert.strictEqual(b.getSquare('e4').piece.type, PieceType.Pawn); 529 | assert.strictEqual(b.getSquare('d4').piece.type, PieceType.Pawn); 530 | assert.ok(b.getSquare('d5').piece === null); 531 | assert.strictEqual(r.move.capturedPiece.type, PieceType.Pawn); 532 | }); 533 | 534 | // test pawn simulate move piece and no pieces disappear 535 | it('should have a pawn disappear after undo', () => { 536 | let 537 | b = Board.create(), 538 | r = null; 539 | 540 | b.move('e2', 'e4'); 541 | b.move('d7', 'd5'); 542 | b.move('d2', 'd4'); 543 | 544 | assert.strictEqual(b.getSquare('d4').piece.type, PieceType.Pawn); 545 | 546 | r = b.move('d5', 'e4', true); 547 | 548 | assert.ok(b.getSquare('d4').piece !== null, 'pawn disappears during the move'); 549 | 550 | r.undo(); 551 | 552 | assert.ok(b.getSquare('d4').piece !== null, 'pawn disappears during the undo'); 553 | }); 554 | 555 | it('should properly return notation when supplied', () => { 556 | let 557 | b = Board.create(), 558 | r = null; 559 | 560 | r = b.move('e2', 'e4', 'e4'); 561 | 562 | assert.ok(r.move.algebraic === 'e4', 'notation properly noted'); 563 | }); 564 | 565 | it('should not return notation when omitted', () => { 566 | let 567 | b = Board.create(), 568 | r = null; 569 | 570 | r = b.move('e2', 'e4'); 571 | 572 | assert.ok(typeof r.move.algebraic === 'undefined', 'notation improperly present'); 573 | }); 574 | }); 575 | }); 576 | -------------------------------------------------------------------------------- /test/src/algebraicGameClient.js: -------------------------------------------------------------------------------- 1 | /* eslint no-magic-numbers:0 */ 2 | import { assert, describe, it } from 'vitest'; 3 | import { Piece, PieceType, SideType } from '../../src/piece.js'; 4 | import { AlgebraicGameClient } from '../../src/algebraicGameClient.js'; 5 | 6 | describe('AlgebraicGameClient', () => { 7 | // test create and getStatus 8 | it('should have proper status once board is created', () => { 9 | let 10 | gc = AlgebraicGameClient.create(), 11 | s = gc.getStatus(); 12 | 13 | assert.strictEqual(s.isCheck, false); 14 | assert.strictEqual(s.isCheckmate, false); 15 | assert.strictEqual(s.isRepetition, false); 16 | assert.strictEqual(s.isStalemate, false); 17 | assert.strictEqual(Object.keys(s.notatedMoves).length, 20); 18 | }); 19 | 20 | // test move event 21 | it('should trigger event when moving a piece', () => { 22 | let 23 | gc = AlgebraicGameClient.create(), 24 | moveEvent = []; 25 | 26 | gc.on('move', (ev) => moveEvent.push(ev)); 27 | 28 | gc.move('b4'); 29 | gc.move('e6'); 30 | 31 | assert.ok(moveEvent); 32 | assert.strictEqual(moveEvent.length, 2); 33 | }); 34 | 35 | // test pawn move 36 | it('should have proper board status after moving a piece', () => { 37 | let 38 | gc = AlgebraicGameClient.create(), 39 | s = null; 40 | 41 | gc.move('b4'); 42 | gc.move('e6'); 43 | 44 | s = gc.getStatus(); 45 | 46 | assert.strictEqual(s.isCheck, false); 47 | assert.strictEqual(s.isCheckmate, false); 48 | assert.strictEqual(s.isRepetition, false); 49 | assert.strictEqual(s.isStalemate, false); 50 | assert.strictEqual(Object.keys(s.notatedMoves).length, 21); 51 | }); 52 | 53 | // test pawn capture enemy 54 | it('should recognize piece capture', () => { 55 | let 56 | gc = AlgebraicGameClient.create(), 57 | r = null; 58 | 59 | gc.move('e4'); 60 | gc.move('d5'); 61 | r = gc.move('exd5'); 62 | 63 | assert.strictEqual(r.move.capturedPiece.type, PieceType.Pawn); 64 | }); 65 | 66 | // test capture event 67 | it('should properly emit a capture event', () => { 68 | let 69 | captureEvent = [], 70 | gc = AlgebraicGameClient.create(); 71 | 72 | gc.on('capture', (ev) => captureEvent.push(ev)); 73 | 74 | gc.move('e4'); 75 | gc.move('d5'); 76 | gc.move('exd5'); 77 | 78 | assert.ok(captureEvent); 79 | assert.strictEqual(captureEvent.length, 1); 80 | }); 81 | 82 | // test notation in history 83 | it('should properly record notation in history', () => { 84 | let gc = AlgebraicGameClient.create(); 85 | 86 | gc.move('e4'); 87 | gc.move('d5'); 88 | gc.move('exd5'); 89 | 90 | assert.strictEqual(gc.game.moveHistory[2].algebraic, 'exd5'); 91 | }); 92 | 93 | // getCaptureHistory: track captures and undo 94 | it('should expose capture history via getCaptureHistory()', () => { 95 | let gc = AlgebraicGameClient.create(); 96 | 97 | gc.move('e4'); 98 | gc.move('d5'); 99 | const cap = gc.move('exd5'); 100 | 101 | const h1 = gc.getCaptureHistory(); 102 | assert.strictEqual(h1.length, 1); 103 | assert.strictEqual(h1[0].type, PieceType.Pawn); 104 | 105 | cap.undo(); 106 | const h2 = gc.getCaptureHistory(); 107 | assert.strictEqual(h2.length, 0); 108 | }); 109 | 110 | // test 2 face pieces with same square destination on different rank and file 111 | it('should properly notate two Knights that can occupy same square for their respective moves', () => { 112 | let 113 | gc = AlgebraicGameClient.create(), 114 | s = null; 115 | 116 | gc.move('Nc3'); 117 | gc.move('Nf6'); // black N 118 | gc.move('Nd5'); 119 | gc.move('Ng8'); // black N 120 | gc.move('Nf4'); 121 | gc.move('Nf6'); // black N 122 | 123 | s = gc.getStatus(); 124 | 125 | assert.ok(typeof s.notatedMoves.Nfh3 !== 'undefined', 'Nfh3'); 126 | assert.ok(typeof s.notatedMoves.Ngh3 !== 'undefined', 'Ngh3'); 127 | }); 128 | 129 | // test 2 face pieces with same square destination on different ranks 130 | it('should properly notate two Rooks that can occupy same square from different ranks', () => { 131 | let 132 | gc = AlgebraicGameClient.create(), 133 | s = null; 134 | 135 | gc.move('a4'); 136 | gc.move('a5'); 137 | gc.move('h4'); 138 | gc.move('h5'); 139 | gc.move('Ra3'); 140 | gc.move('Ra6'); 141 | gc.move('Rhh3'); 142 | gc.move('Rhh6'); 143 | 144 | s = gc.getStatus(); 145 | 146 | assert.ok(typeof s.notatedMoves.Rae3 !== 'undefined', 'Rae3'); 147 | assert.ok(typeof s.notatedMoves.Rhe3 !== 'undefined', 'Rhe3'); 148 | }); 149 | 150 | // test 2 face pieces with same square destination on different files 151 | it('should properly notate two Rooks that can occupy same square from different files', () => { 152 | let 153 | gc = AlgebraicGameClient.create(), 154 | s = null; 155 | 156 | gc.move('a4'); 157 | gc.move('a5'); 158 | gc.move('h4'); 159 | gc.move('h5'); 160 | gc.move('Ra3'); 161 | gc.move('Ra6'); 162 | gc.move('Rhh3'); 163 | gc.move('Rhh6'); 164 | gc.move('Rae3'); 165 | gc.move('Rh8'); 166 | gc.move('Re6'); 167 | gc.move('Ra8'); 168 | gc.move('Rhe3'); 169 | gc.move('Ra6'); 170 | 171 | s = gc.getStatus(); 172 | 173 | assert.ok(typeof s.notatedMoves.R6e5 !== 'undefined', 'R6e5'); 174 | assert.ok(typeof s.notatedMoves.R3e5 !== 'undefined', 'R3e5'); 175 | }); 176 | 177 | // test castle left 178 | it('should properly notate white King castle left and trigger event', () => { 179 | let 180 | castleEvent = [], 181 | gc = AlgebraicGameClient.create(), 182 | s = null; 183 | 184 | gc.on('castle', (ev) => castleEvent.push(ev)); 185 | 186 | gc.game.board.getSquare('b1').piece = null; 187 | gc.game.board.getSquare('c1').piece = null; 188 | gc.game.board.getSquare('d1').piece = null; 189 | 190 | s = gc.getStatus(true); 191 | 192 | assert.ok(typeof s.notatedMoves['0-0-0'] !== 'undefined', '0-0-0'); 193 | 194 | // perform castle move 195 | gc.move('0-0-0'); 196 | 197 | // validate event 198 | assert.ok(castleEvent); 199 | assert.strictEqual(castleEvent.length, 1); 200 | }); 201 | 202 | // test castle left 203 | it('should properly notate white King castle left as letters when PGN is true', () => { 204 | let 205 | castleEvent = [], 206 | gc = AlgebraicGameClient.create({ PGN : true }), 207 | s = null; 208 | 209 | gc.on('castle', (ev) => castleEvent.push(ev)); 210 | 211 | gc.game.board.getSquare('b1').piece = null; 212 | gc.game.board.getSquare('c1').piece = null; 213 | gc.game.board.getSquare('d1').piece = null; 214 | 215 | s = gc.getStatus(true); 216 | 217 | assert.ok(typeof s.notatedMoves['O-O-O'] !== 'undefined', 'O-O-O'); 218 | 219 | // perform castle move 220 | gc.move('O-O-O'); 221 | 222 | // validate event 223 | assert.ok(castleEvent); 224 | assert.strictEqual(castleEvent.length, 1); 225 | }); 226 | 227 | // test castle right 228 | it('should properly notate black King castle right and trigger event', () => { 229 | let 230 | castleEvent = [], 231 | gc = AlgebraicGameClient.create(), 232 | s = null; 233 | 234 | gc.on('castle', (ev) => castleEvent.push(ev)); 235 | 236 | gc.game.board.getSquare('f8').piece = null; 237 | gc.game.board.getSquare('g8').piece = null; 238 | gc.getStatus(true); 239 | gc.move('a4'); 240 | s = gc.getStatus(); 241 | 242 | assert.ok(typeof s.notatedMoves['0-0'] !== 'undefined', '0-0'); 243 | 244 | // perform castle move 245 | gc.move('0-0'); 246 | 247 | // validate event 248 | assert.ok(castleEvent); 249 | assert.strictEqual(castleEvent.length, 1); 250 | }); 251 | 252 | // test castle right 253 | it('should properly notate black King castle right as letters when PGN is true', () => { 254 | let 255 | castleEvent = [], 256 | gc = AlgebraicGameClient.create({ PGN : true }), 257 | s = null; 258 | 259 | gc.on('castle', (ev) => castleEvent.push(ev)); 260 | 261 | gc.game.board.getSquare('f8').piece = null; 262 | gc.game.board.getSquare('g8').piece = null; 263 | gc.getStatus(true); 264 | gc.move('a4'); 265 | s = gc.getStatus(); 266 | 267 | assert.ok(typeof s.notatedMoves['O-O'] !== 'undefined', 'O-O'); 268 | 269 | // perform castle move 270 | gc.move('O-O'); 271 | 272 | // validate event 273 | assert.ok(castleEvent); 274 | assert.strictEqual(castleEvent.length, 1); 275 | }); 276 | 277 | // validate parse notation with O-O-O 278 | it('should properly recognize white King castle left notation', () => { 279 | let 280 | gc = AlgebraicGameClient.create(), 281 | m = null; 282 | 283 | gc.game.board.getSquare('b1').piece = null; 284 | gc.game.board.getSquare('c1').piece = null; 285 | gc.game.board.getSquare('d1').piece = null; 286 | gc.getStatus(true); 287 | m = gc.move('O-O-O'); 288 | 289 | assert.ok(m !== null, 'parse O-O-O'); 290 | assert.ok(m.move.castle); 291 | }); 292 | 293 | // validate parse notation with O-O-O 294 | it('should properly recognize white King castle left notation when PGN is true', () => { 295 | let 296 | gc = AlgebraicGameClient.create({ PGN : true }), 297 | m = null; 298 | 299 | gc.game.board.getSquare('b1').piece = null; 300 | gc.game.board.getSquare('c1').piece = null; 301 | gc.game.board.getSquare('d1').piece = null; 302 | gc.getStatus(true); 303 | m = gc.move('0-0-0'); 304 | 305 | assert.ok(m !== null, 'parse 0-0-0'); 306 | assert.ok(m.move.castle); 307 | }); 308 | 309 | // validate parse notation with O-O 310 | it('should properly recognize black King castle right notation', () => { 311 | let 312 | gc = AlgebraicGameClient.create(), 313 | m = null; 314 | 315 | gc.game.board.getSquare('f8').piece = null; 316 | gc.game.board.getSquare('g8').piece = null; 317 | gc.getStatus(true); 318 | gc.move('a4'); 319 | m = gc.move('O-O'); 320 | 321 | assert.ok(m !== null, 'parse O-O'); 322 | assert.ok(m.move.castle); 323 | }); 324 | 325 | // validate parse notation with O-O 326 | it('should properly recognize black King castle right notation when PGN is true', () => { 327 | let 328 | gc = AlgebraicGameClient.create(), 329 | m = null; 330 | 331 | gc.game.board.getSquare('f8').piece = null; 332 | gc.game.board.getSquare('g8').piece = null; 333 | gc.getStatus(true); 334 | gc.move('a4'); 335 | m = gc.move('0-0'); 336 | 337 | assert.ok(m !== null, 'parse 0-0'); 338 | assert.ok(m.move.castle); 339 | }); 340 | 341 | // test pawn promotion 342 | // adding for issue #6 343 | it('should properly show valid White Pawn promotions', () => { 344 | let 345 | gc = AlgebraicGameClient.create(), 346 | r = null; 347 | 348 | gc.game.board.getSquare('a7').piece = null; 349 | gc.game.board.getSquare('a8').piece = null; 350 | gc.game.board.getSquare('a2').piece = null; 351 | gc.game.board.getSquare('a7').piece = Piece.createPawn(SideType.White); 352 | gc.game.board.getSquare('a7').piece.moveCount = 1; 353 | 354 | r = gc.getStatus(true); 355 | 356 | assert.isUndefined(r.notatedMoves['a8'], 'pawn should promote'); 357 | assert.isDefined(r.notatedMoves['a8R'], 'pawn promotion to rook'); 358 | assert.isDefined(r.notatedMoves['a8N'], 'pawn promotion to Knight'); 359 | assert.isDefined(r.notatedMoves['a8B'], 'pawn promotion to Bishop'); 360 | assert.isDefined(r.notatedMoves['a8Q'], 'pawn promotion to Queen'); 361 | }); 362 | 363 | it('should properly show valid Black Pawn promotions', () => { 364 | let 365 | gc = AlgebraicGameClient.create(), 366 | r = null; 367 | 368 | gc.game.board.getSquare('a2').piece = null; 369 | gc.game.board.getSquare('a1').piece = null; 370 | gc.game.board.getSquare('a7').piece = null; 371 | gc.game.board.getSquare('a2').piece = Piece.createPawn(SideType.Black); 372 | gc.game.board.getSquare('a2').piece.moveCount = 1; 373 | 374 | gc.getStatus(true); 375 | gc.move('h4'); 376 | r = gc.getStatus(true); 377 | 378 | assert.isUndefined(r.notatedMoves['a1'], 'pawn should promote'); 379 | assert.isDefined(r.notatedMoves['a1R'], 'pawn promotion to rook'); 380 | assert.isDefined(r.notatedMoves['a1N'], 'pawn promotion to Knight'); 381 | assert.isDefined(r.notatedMoves['a1B'], 'pawn promotion to Bishop'); 382 | assert.isDefined(r.notatedMoves['a1Q'], 'pawn promotion to Queen'); 383 | }); 384 | 385 | // test pawn promotion 386 | it('should properly recognize White Pawn promotion to Rook', () => { 387 | let 388 | gc = AlgebraicGameClient.create(), 389 | m = null, 390 | r = null; 391 | 392 | gc.game.board.getSquare('a7').piece = null; 393 | gc.game.board.getSquare('a8').piece = null; 394 | gc.game.board.getSquare('b8').piece = null; 395 | gc.game.board.getSquare('c8').piece = null; 396 | gc.game.board.getSquare('d8').piece = null; 397 | gc.game.board.getSquare('a2').piece = null; 398 | gc.game.board.getSquare('a7').piece = Piece.createPawn(SideType.White); 399 | gc.game.board.getSquare('a7').piece.moveCount = 1; 400 | 401 | gc.getStatus(true); 402 | m = gc.move('a8R'); 403 | r = gc.getStatus(); 404 | 405 | assert.strictEqual(m.move.postSquare.piece.type, PieceType.Rook); 406 | assert.strictEqual(r.isCheckmate, true); 407 | assert.strictEqual(gc.game.moveHistory[0].promotion, true); 408 | }); 409 | 410 | // test pawn promotion 411 | it('should properly recognize Black Pawn promotion to Rook', () => { 412 | let 413 | gc = AlgebraicGameClient.create(), 414 | m = null, 415 | r = null; 416 | 417 | gc.game.board.getSquare('a2').piece = null; 418 | gc.game.board.getSquare('a1').piece = null; 419 | gc.game.board.getSquare('b1').piece = null; 420 | gc.game.board.getSquare('c1').piece = null; 421 | gc.game.board.getSquare('d1').piece = null; 422 | gc.game.board.getSquare('a7').piece = null; 423 | gc.game.board.getSquare('a2').piece = Piece.createPawn(SideType.Black); 424 | gc.game.board.getSquare('a2').piece.moveCount = 1; 425 | 426 | gc.getStatus(true); 427 | gc.move('h3'); 428 | m = gc.move('a1R'); 429 | r = gc.getStatus(); 430 | 431 | assert.strictEqual(m.move.postSquare.piece.type, PieceType.Rook); 432 | assert.strictEqual(r.isCheckmate, true); 433 | assert.strictEqual(gc.game.moveHistory[0].promotion, false); 434 | assert.strictEqual(gc.game.moveHistory[1].promotion, true); 435 | }); 436 | 437 | // test pawn promotion event 438 | it('should properly fire Pawn promotion event', () => { 439 | let 440 | gc = AlgebraicGameClient.create(), 441 | promoteEvent = []; 442 | 443 | gc.on('promote', (ev) => promoteEvent.push(ev)); 444 | 445 | gc.game.board.getSquare('a7').piece = null; 446 | gc.game.board.getSquare('a8').piece = null; 447 | gc.game.board.getSquare('b8').piece = null; 448 | gc.game.board.getSquare('c8').piece = null; 449 | gc.game.board.getSquare('d8').piece = null; 450 | gc.game.board.getSquare('a2').piece = null; 451 | gc.game.board.getSquare('a7').piece = Piece.createPawn(SideType.White); 452 | gc.game.board.getSquare('a7').piece.moveCount = 1; 453 | 454 | gc.getStatus(true); 455 | gc.move('a8R'); 456 | 457 | assert.strictEqual(gc.game.moveHistory[0].promotion, true); 458 | assert.strictEqual(promoteEvent.length, 1); 459 | }); 460 | 461 | // test ambiguous notation 462 | it('should throw exception when notation is too ambiguous to determine which piece to move', () => { 463 | let gc = AlgebraicGameClient.create(); 464 | 465 | gc.move('a4'); 466 | gc.move('a5'); 467 | gc.move('h4'); 468 | gc.move('h5'); 469 | gc.move('Ra3'); 470 | gc.move('Ra6'); 471 | 472 | assert.throws(() => { 473 | gc.move('Rh3'); 474 | }); // could be Rhh3 or Rah3 475 | }); 476 | 477 | // test invalid notation 478 | it('should throw an exception when the notation provided is fail', () => { 479 | let gc = AlgebraicGameClient.create(); 480 | 481 | assert.throws(() => { 482 | gc.move('h6'); 483 | }); 484 | assert.throws(() => { 485 | gc.move('z9'); 486 | }); 487 | }); 488 | 489 | // test overly specified notation 490 | it('should properly parse overly verbose notation', () => { 491 | let 492 | gc = AlgebraicGameClient.create(), 493 | m = null; 494 | 495 | m = gc.move('Nb1c3'); 496 | 497 | assert.ok(m !== null); 498 | assert.strictEqual(m.move.postSquare.file, 'c'); 499 | assert.strictEqual(m.move.postSquare.rank, 3); 500 | assert.strictEqual(m.move.postSquare.piece.type, PieceType.Knight); 501 | }); 502 | 503 | // Issue #1 - Ensure no phantom pawns appear after sequence of moves in AlgebraicGameClient 504 | it('should not have a random Pawn appear on the board after a specific sequence of moves (bug fix test)', () => { 505 | let 506 | gc = AlgebraicGameClient.create(), 507 | s = gc.game.board.getSquare('c5'); 508 | 509 | // turn 1 510 | gc.move('e4'); 511 | gc.move('e5'); 512 | 513 | // turn 2 514 | gc.move('Nf3'); 515 | gc.move('Nc6'); 516 | 517 | // turn 3 518 | gc.move('Bb5'); 519 | gc.move('Nf6'); 520 | 521 | // turn 4 522 | gc.move('O-O'); 523 | gc.move('Nxe4'); 524 | 525 | // turn 5 526 | gc.move('d4'); 527 | gc.move('Nd6'); 528 | 529 | assert.ok(s.piece === null, 'Phantom piece appears prior to Bxc6'); 530 | 531 | // turn 6 532 | gc.move('Bxc6'); 533 | 534 | assert.ok(s.piece === null, 'Phantom piece appears after Bxc6'); 535 | }); 536 | 537 | // Issue #3 - Ensure no phantom pawns appear after sequence of moves 538 | it('should not have a random Black Pawn appear on the board (bug fix test)', () => { 539 | let 540 | gc = AlgebraicGameClient.create(), 541 | s = gc.game.board.getSquare('a6'); 542 | 543 | // turn 1 544 | gc.move('e4'); 545 | gc.move('e5'); 546 | 547 | // turn 2 548 | gc.move('d3'); 549 | gc.move('Nc6'); 550 | 551 | // turn 3 552 | gc.move('Nf3'); 553 | gc.move('Bb4'); 554 | 555 | // turn 4 556 | gc.move('Nfd2'); 557 | gc.move('d6'); 558 | 559 | // turn 5 560 | gc.move('a3'); 561 | gc.move('Bc5'); 562 | 563 | // turn 6 564 | gc.move('Be2'); 565 | gc.move('Qf6'); 566 | 567 | // turn 7 568 | gc.move('0-0'); 569 | gc.move('Bxf2'); 570 | 571 | // turn 8 572 | gc.move('Rxf2'); 573 | gc.move('Qe6'); 574 | 575 | // turn 9 576 | gc.move('Nc4'); 577 | gc.move('Nd4'); 578 | 579 | // turn 10 580 | gc.move('Bf1'); 581 | gc.move('Bd7'); 582 | 583 | // turn 11 584 | gc.move('c3'); 585 | gc.move('Nb3'); 586 | 587 | // turn 12 588 | gc.move('Ra2'); 589 | gc.move('Ba4'); 590 | 591 | // turn 13 592 | gc.move('Qc2'); 593 | gc.move('Nh6'); 594 | 595 | // turn 14 596 | gc.move('d4'); 597 | gc.move('Ng4'); 598 | 599 | // turn 15 600 | gc.move('Rf3'); 601 | gc.move('b5'); 602 | 603 | // turn 16 604 | gc.move('Nxe5'); 605 | gc.move('Nxc1'); 606 | 607 | // turn 17 608 | gc.move('Qxc1'); 609 | gc.move('dxe5'); 610 | 611 | // turn 18 612 | gc.move('Ra1'); 613 | gc.move('Rb8'); 614 | 615 | // turn 19 616 | gc.move('h3'); 617 | gc.move('Rb6'); 618 | 619 | // turn 20 620 | gc.move('hxg4'); 621 | gc.move('Qxg4'); 622 | 623 | // turn 21 624 | gc.move('Nd2'); 625 | gc.move('a5'); 626 | 627 | // turn 22 628 | gc.move('dxe5'); 629 | gc.move('Rc6'); 630 | 631 | // turn 23 632 | gc.move('c4'); 633 | gc.move('h5'); 634 | 635 | // turn 24 636 | gc.move('Rb1'); 637 | gc.move('Rhh6'); 638 | 639 | // turn 25 640 | gc.move('Ra1'); 641 | gc.move('Rce6'); 642 | 643 | // turn 26 644 | gc.move('Bd3'); 645 | gc.move('Rxe5'); 646 | 647 | // turn 27 648 | gc.move('cxb5'); 649 | 650 | assert.ok(s.piece === null, 'Phantom piece appears prior to Rg6'); 651 | 652 | gc.move('Rg6'); 653 | 654 | assert.ok(s.piece === null, 'Phantom piece appears after Rg6'); 655 | }); 656 | 657 | // Issue #4 - Ensure proper checkmate detection with Knight 658 | it('should properly detect checkmate', () => { 659 | let 660 | gc = AlgebraicGameClient.create(), 661 | status = null; 662 | 663 | gc.move('e4'); 664 | gc.move('e5'); 665 | 666 | 667 | gc.move('Nc3'); 668 | gc.move('d6'); 669 | 670 | 671 | gc.move('Bc4'); 672 | gc.move('Be6'); 673 | 674 | 675 | gc.move('Bb3'); 676 | gc.move('Nf6'); 677 | 678 | 679 | gc.move('Nge2'); 680 | gc.move('Nh5'); 681 | 682 | 683 | gc.move('Bxe6'); 684 | gc.move('fxe6'); 685 | 686 | 687 | gc.move('d4'); 688 | gc.move('Be7'); 689 | 690 | 691 | gc.move('dxe5'); 692 | gc.move('dxe5'); 693 | 694 | 695 | gc.move('Qxd8'); 696 | gc.move('Bxd8'); 697 | 698 | 699 | gc.move('Be3'); 700 | gc.move('0-0'); 701 | 702 | 703 | gc.move('0-0-0'); 704 | gc.move('Nc6'); 705 | 706 | 707 | gc.move('Rhf1'); 708 | gc.move('Bh4'); 709 | 710 | 711 | gc.move('Nb5'); 712 | gc.move('Rac8'); 713 | 714 | 715 | gc.move('f3'); 716 | gc.move('a6'); 717 | 718 | 719 | gc.move('Nbc3'); 720 | gc.move('Nb4'); 721 | 722 | 723 | gc.move('Bc5'); 724 | gc.move('Nxa2'); 725 | 726 | 727 | gc.move('Nxa2'); 728 | gc.move('b6'); 729 | 730 | 731 | gc.move('Bxf8'); 732 | gc.move('Rxf8'); 733 | 734 | 735 | gc.move('Nb4'); 736 | gc.move('a5'); 737 | 738 | 739 | gc.move('Nc6'); 740 | gc.move('Ra8'); 741 | 742 | 743 | gc.move('Nxe5'); 744 | gc.move('c5'); 745 | 746 | 747 | gc.move('Rd6'); 748 | gc.move('Rc8'); 749 | 750 | 751 | gc.move('Rxb6'); 752 | gc.move('c4'); 753 | 754 | 755 | gc.move('f4'); 756 | gc.move('c3'); 757 | 758 | 759 | gc.move('Nxc3'); 760 | gc.move('Rxc3'); 761 | 762 | 763 | gc.move('Rb8'); 764 | 765 | status = gc.getStatus(); 766 | assert.ok(typeof status.notatedMoves['Kf7'] === 'undefined'); 767 | }); 768 | 769 | // Issue #8 - Ensure no extraneous Black Pawn 770 | it('should not have a random Black Pawn appear on the board (bug fix test)', () => { 771 | let 772 | gc = AlgebraicGameClient.create(), 773 | s = gc.game.board.getSquare('e6'); 774 | 775 | gc.move('d4'); 776 | gc.move('a6'); 777 | 778 | 779 | gc.move('d5'); 780 | 781 | assert.ok(s.piece === null, 'phantom piece appears before e5'); 782 | 783 | gc.move('e5'); 784 | 785 | assert.ok(s.piece === null, 'phantom piece appears after e5'); 786 | }); 787 | 788 | // Issue #15 - Ensure Pawn can move two spaces correctly on the first move 789 | it('should not block first move of two squares by Pawns incorrectly (bug fix test)', () => { 790 | let 791 | gc = AlgebraicGameClient.create(), 792 | status; 793 | 794 | gc.move('e4'); 795 | gc.move('a5'); 796 | 797 | gc.move('Ba6'); 798 | 799 | status = gc.getStatus(); 800 | 801 | assert.isDefined(status.notatedMoves['b5'], 'Pawn able to advance two squares'); 802 | }); 803 | 804 | // Issue #17 - Move pawn to promotion, other pieces of same color should not have promotion 805 | it('should properly notate future promotions after the first promotion (bug fix test)', () => { 806 | let 807 | gc = AlgebraicGameClient.create(), 808 | r = null; 809 | 810 | gc.game.board.getSquare('c7').piece = null; 811 | gc.game.board.getSquare('c8').piece = null; 812 | gc.game.board.getSquare('c2').piece = null; 813 | gc.game.board.getSquare('c7').piece = Piece.createPawn(SideType.White); 814 | gc.game.board.getSquare('c7').piece.moveCount = 1; 815 | gc.game.board.getSquare('h7').piece = null; 816 | gc.game.board.getSquare('h7').piece = Piece.createBishop(SideType.White); 817 | gc.game.board.getSquare('h7').piece.moveCount = 1; 818 | 819 | // force recalculation of board position 820 | r = gc.getStatus(true); 821 | 822 | // make sure only Pawns can be promoted 823 | assert.isDefined(r.notatedMoves['cxb8R'], 'pawn promotion to rook'); 824 | assert.isDefined(r.notatedMoves['cxb8N'], 'pawn promotion to Knight'); 825 | assert.isDefined(r.notatedMoves['cxb8B'], 'pawn promotion to Bishop'); 826 | assert.isDefined(r.notatedMoves['cxb8Q'], 'pawn promotion to Queen'); 827 | assert.isDefined(r.notatedMoves['cxd8R'], 'pawn promotion to rook'); 828 | assert.isDefined(r.notatedMoves['cxd8N'], 'pawn promotion to Knight'); 829 | assert.isDefined(r.notatedMoves['cxd8B'], 'pawn promotion to Bishop'); 830 | assert.isDefined(r.notatedMoves['cxd8Q'], 'pawn promotion to Queen'); 831 | assert.isUndefined(r.notatedMoves['Bxg8R'], 'Bishop should not promote'); 832 | }); 833 | 834 | // Issue #18 - Missing Pawn promotion moves 835 | it('should properly notate future promotions after the first promotion (bug fix test)', () => { 836 | let 837 | gc = AlgebraicGameClient.create(), 838 | r = null; 839 | 840 | // position the board for a promotion next move 841 | gc.game.board.getSquare('c7').piece = null; 842 | gc.game.board.getSquare('c2').piece = null; 843 | gc.game.board.getSquare('c7').piece = Piece.createPawn(SideType.White); 844 | gc.game.board.getSquare('c7').piece.moveCount = 1; 845 | 846 | // force recalculation of board position 847 | r = gc.getStatus(true); 848 | 849 | // make sure Pawn promotions are present 850 | assert.isUndefined(r.notatedMoves['cxb8'], 'pawn should promote'); 851 | assert.isDefined(r.notatedMoves['cxb8Q'], 'should allow promote to queen'); 852 | assert.isDefined(r.notatedMoves['cxb8R'], 'should allow promote to rook'); 853 | assert.isDefined(r.notatedMoves['cxb8B'], 'should allow promote to bishop'); 854 | assert.isDefined(r.notatedMoves['cxb8N'], 'should allow promote to knight'); 855 | }); 856 | 857 | // Issue #23 - Show who is attacking the King 858 | it('should properly emit check and indicate attackers of the King', () => { 859 | let 860 | checkResult = null, 861 | gc = AlgebraicGameClient.create(), 862 | r = null; 863 | 864 | gc.on('check', (result) => (checkResult = result)); 865 | 866 | // position the board for a promotion next move 867 | gc.game.board.getSquare('b1').piece = null; 868 | gc.game.board.getSquare('f6').piece = Piece.createKnight(SideType.White); 869 | gc.game.board.getSquare('f6').piece.moveCount = 1; 870 | 871 | // move to trigger evaluation that King is check 872 | gc.move('a3'); 873 | 874 | // force recalculation of board position 875 | r = gc.getStatus(true); 876 | 877 | // make sure Pawn promotions are present 878 | assert.isDefined(checkResult); 879 | assert.strictEqual(checkResult.attackingSquare.piece.type, PieceType.Knight); 880 | assert.isDefined(r.notatedMoves['exf6'], 'should allow capture of attacking Knight'); 881 | assert.isDefined(r.notatedMoves['gxf6'], 'should allow capture of attacking Knight'); 882 | assert.isDefined(r.notatedMoves['Nxf6'], 'should allow capture of attacking Knight'); 883 | }); 884 | 885 | // Issue #43 - Parser can't handle PGN notation for gxf3+ 886 | /* 887 | [Event "Gibraltar Masters 2019"] 888 | [Site "Caleta ENG"] 889 | [Date "2019.01.22"] 890 | [Round "1.110"] 891 | [White "Bianco,V"] 892 | [Black "Larino Nieto,D"] 893 | [Result "0-1"] 894 | [WhiteElo "2018"] 895 | [BlackElo "2432"] 896 | [ECO "C41"] 897 | 898 | 1.d4 d6 2.e4 Nf6 3.Nc3 e5 4.Nf3 Nbd7 5.Bc4 Nb6 6.dxe5 Nxc4 7.exf6 Qxf6 8.Bg5 Nxb2 899 | 9.Qd2 Qe6 10.Nd5 Qxe4+ 11.Kf1 Qc4+ 12.Kg1 Be6 13.Ne3 Qc5 14.Rb1 Na4 15.c4 Nb6 900 | 16.Qb2 h6 17.Bh4 Rg8 18.Nd4 g5 19.Nxe6 fxe6 20.Qf6 Qe5 21.Qxe5 dxe5 22.Bg3 O-O-O 901 | 23.Bxe5 Bc5 24.h4 g4 25.Kh2 Rd2 26.Kg3 Nd7 27.Bb2 Bd6+ 28.f4 gxf3+ 29.Kxf3 Rg3+ 902 | 30.Ke4 Nc5+ 0-1 903 | //*/ 904 | it('should properly handle notation that is similar to gxf3+', () => { 905 | let 906 | gc = AlgebraicGameClient.create(), 907 | status; 908 | 909 | gc.move('d4'); 910 | gc.move('d6'); 911 | 912 | gc.move('e4'); 913 | gc.move('Nf6'); 914 | 915 | gc.move('Nc3'); 916 | gc.move('e5'); 917 | 918 | gc.move('Nf3'); 919 | gc.move('Nbd7'); 920 | 921 | gc.move('Bc4'); 922 | gc.move('Nb6'); 923 | 924 | gc.move('dxe5'); 925 | gc.move('Nxc4'); 926 | 927 | gc.move('exf6'); 928 | gc.move('Qxf6'); 929 | 930 | gc.move('Bg5'); 931 | gc.move('Nxb2'); 932 | 933 | gc.move('Qd2'); 934 | gc.move('Qe6'); 935 | 936 | gc.move('Nd5'); 937 | gc.move('Qxe4+'); 938 | 939 | gc.move('Kf1'); 940 | gc.move('Qc4+'); 941 | 942 | gc.move('Kg1'); 943 | gc.move('Be6'); 944 | 945 | gc.move('Ne3'); 946 | gc.move('Qc5'); 947 | 948 | gc.move('Rb1'); 949 | gc.move('Na4'); 950 | 951 | gc.move('c4'); 952 | gc.move('Nb6'); 953 | 954 | gc.move('Qb2'); 955 | gc.move('h6'); 956 | 957 | gc.move('Bh4'); 958 | gc.move('Rg8'); 959 | 960 | gc.move('Nd4'); 961 | gc.move('g5'); 962 | 963 | gc.move('Nxe6'); 964 | gc.move('fxe6'); 965 | 966 | gc.move('Qf6'); 967 | gc.move('Qe5'); 968 | 969 | gc.move('Qxe5'); 970 | gc.move('dxe5'); 971 | 972 | gc.move('Bg3'); 973 | gc.move('O-O-O'); 974 | 975 | gc.move('Bxe5'); 976 | gc.move('Bc5'); 977 | 978 | gc.move('h4'); 979 | gc.move('g4'); 980 | 981 | gc.move('Kh2'); 982 | gc.move('Rd2'); 983 | 984 | gc.move('Kg3'); 985 | gc.move('Nd7'); 986 | 987 | gc.move('Bb2'); 988 | gc.move('Bd6+'); 989 | 990 | gc.move('f4'); 991 | // #43 - test previoulsy failed here: unable to parse gxf3+ and reduced notation to gf3 to retry parse 992 | gc.move('gxf3+'); 993 | 994 | gc.move('Kxf3'); 995 | gc.move('Rg3+'); 996 | 997 | gc.move('Ke4'); 998 | gc.move('Nc5+'); 999 | 1000 | status = gc.getStatus(); 1001 | assert.ok(status.isCheckmate, 'should properly parse gxf3+'); 1002 | }); 1003 | 1004 | // Issue #53 1005 | // Algebraic and PGN formatting of en Passant is not correct 1006 | it('should properly notate en Passant and trigger event', () => { 1007 | let 1008 | enPassantEvent = [], 1009 | gc = AlgebraicGameClient.create(), 1010 | status; 1011 | 1012 | gc.on('enPassant', (ev) => enPassantEvent.push(ev)); 1013 | 1014 | gc.move('e4'); 1015 | gc.move('d5'); 1016 | gc.move('e5'); 1017 | gc.move('f5'); 1018 | 1019 | status = gc.getStatus(); 1020 | 1021 | assert.isUndefined(status.notatedMoves['f6'], 'should properly notate en Passant'); 1022 | assert.isDefined(status.notatedMoves['exf6'], 'should properly notate en Passant'); 1023 | 1024 | // make the en Passant move 1025 | gc.move('exf6'); 1026 | 1027 | assert.ok(enPassantEvent); 1028 | assert.strictEqual(enPassantEvent.length, 1); 1029 | }); 1030 | 1031 | // getFen test 1032 | it('should properly generate FEN of start position', () => { 1033 | let 1034 | fen = null, 1035 | gc = AlgebraicGameClient.create(); 1036 | 1037 | fen = gc.getFen(); 1038 | 1039 | assert.strictEqual(fen, 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR'); 1040 | }); 1041 | 1042 | // fromFEN test 1043 | it('should create from FEN and respect side to move', () => { 1044 | let 1045 | fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1', 1046 | gc = AlgebraicGameClient.fromFEN(fen); 1047 | 1048 | // board layout should match 1049 | assert.strictEqual(gc.getFen(), 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR'); 1050 | 1051 | // since it's black to move, a white move should be invalid 1052 | assert.throws(() => gc.move('e4')); 1053 | 1054 | // a black move should be allowed 1055 | let m = gc.move('e5'); 1056 | assert.strictEqual(m.move.postSquare.file, 'e'); 1057 | assert.strictEqual(m.move.postSquare.rank, 5); 1058 | assert.strictEqual(m.move.postSquare.piece.side, SideType.Black); 1059 | }); 1060 | 1061 | // Issue #71 - move.undo() does not properly update game statusß 1062 | it('should properly return game client to the correct state when calling undo', () => { 1063 | let client = AlgebraicGameClient.create(); 1064 | 1065 | client.move('e4'); 1066 | client.move('c5').undo(); 1067 | 1068 | let sts = client.getStatus(); 1069 | assert.ok((sts.board.lastMovedPiece.side.name === 'white'), 'previously moved piece should reflect the piece before the last move occurred'); 1070 | assert.ok(sts.notatedMoves['c5'], 'available moves should include move that was undone'); 1071 | }); 1072 | 1073 | // Issue #77 - move.undo() fails on first move 1074 | it('should properly undo the first move without error', () => { 1075 | let client = AlgebraicGameClient.create(); 1076 | 1077 | client.move('e4').undo(); 1078 | 1079 | let sts = client.getStatus(); 1080 | 1081 | assert.ok(sts.notatedMoves['e4'], 'available moves should include move that was undone'); 1082 | }); 1083 | }); 1084 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `node-chess` - the algebraic chess engine 2 | 3 | `node-chess` is an algebraic notation driven chess engine that can validate board position and produce a list of viable moves (notated). 4 | 5 | [![Coverage Status](https://coveralls.io/repos/brozeph/node-chess/badge.png?branch=main)](https://coveralls.io/r/brozeph/node-chess?branch=main) 6 | 7 | ## Features 8 | 9 | * Accepts moves in algebraic notation 10 | * Loads board position from FEN (Forsyth-Edwards Notation) 11 | * Supports UCI (Universal Chess Interface) coordinate format 12 | * Lists valid moves in algebraic notation 13 | * Fuzzy algebraic notation parsing 14 | * En Passant validation 15 | * 3-fold repetition detection 16 | * Stalemate detection 17 | * Check detection 18 | * Checkmate detection 19 | * Undo moves easily 20 | * Easily readable object structure 21 | * High unit test coverage 22 | 23 | ## Installation 24 | 25 | ```bash 26 | npm install chess 27 | ``` 28 | 29 | ## Public API 30 | 31 | ### Create a new game 32 | 33 | ```javascript 34 | import chess from 'chess'; 35 | // or, with CommonJS 36 | // const chess = require('chess'); 37 | 38 | // create a game client 39 | const gameClient = chess.create(); 40 | let move, status; 41 | 42 | // capture events 43 | gameClient.on('check', (attack) => { 44 | // get more details about the attack on the King 45 | console.log(attack); 46 | }); 47 | 48 | // look at the status and valid moves 49 | status = gameClient.getStatus(); 50 | 51 | // make a move 52 | move = gameClient.move('a4'); 53 | 54 | // look at the status again after the move to see 55 | // the opposing side's available moves 56 | status = gameClient.getStatus(); 57 | ``` 58 | 59 | ### Load a game from FEN 60 | 61 | ```javascript 62 | import chess from 'chess'; 63 | // or, with CommonJS 64 | // const chess = require('chess'); 65 | 66 | // load a game client from FEN 67 | const fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1'; 68 | const gameClient = chess.fromFEN(fen); 69 | let move, status; 70 | 71 | // look at the status and valid moves 72 | status = gameClient.getStatus(); 73 | 74 | // make a move 75 | move = gameClient.move('a4'); 76 | 77 | // look at the status again after the move to see 78 | // the opposing side's available moves 79 | status = gameClient.getStatus(); 80 | ``` 81 | 82 | ### PGN (Portable Game Format) Algebraic Game Client 83 | 84 | To ensure the notation returned is safe for PGN, you must supply PGN as an option in the call to `create`: 85 | 86 | ```javascript 87 | import chess from 'chess'; 88 | 89 | // const chess = require('chess'); 90 | 91 | // create a game client 92 | const gameClient = chess.create({ PGN : true }); 93 | let move, status; 94 | 95 | // look at the status and valid moves 96 | status = gameClient.getStatus(); 97 | 98 | // make a move 99 | move = gameClient.move('a4'); 100 | 101 | // look at the status again after the move to see 102 | // the opposing side's available moves 103 | status = gameClient.getStatus(); 104 | ``` 105 | 106 | ### Universal Chess Interface (UCI) Game Client 107 | 108 | The library also supports the Universal Chess Interface (UCI) coordinate format for moves and for listing valid moves. 109 | 110 | ```javascript 111 | import chess from 'chess'; 112 | 113 | // Create a UCI-based game client 114 | const uci = chess.createUCI(); 115 | 116 | // Inspect current status and valid UCI moves 117 | let status = uci.getStatus(); 118 | // status.uciMoves is a map of all legal UCI moves from the position 119 | console.log(Object.keys(status.uciMoves)); // e.g., [ 'e2e4', 'g1f3', ... ] 120 | 121 | // Make UCI moves 122 | uci.move('e2e4'); // white 123 | uci.move('e7e5'); // black 124 | 125 | // Promotions are encoded with a trailing piece letter: q, r, b, n 126 | // For example, promote a pawn to a queen 127 | // uci.move('a7a8q'); 128 | ``` 129 | 130 | ### Capture History 131 | 132 | Each game client exposes a simple way to retrieve captured pieces in order of capture. 133 | 134 | ```javascript 135 | import chess from 'chess'; 136 | 137 | // Works with any client: create(), createSimple(), or createUCI() 138 | const gc = chess.create(); 139 | 140 | gc.move('e4'); 141 | gc.move('d5'); 142 | const capture = gc.move('exd5'); 143 | 144 | // Retrieve captured pieces (latest at the end) 145 | const captured = gc.getCaptureHistory(); 146 | console.log(captured.length); // 1 147 | console.log(captured[0].type); // 'pawn' 148 | 149 | // Undo also rolls back capture history 150 | capture.undo(); 151 | console.log(gc.getCaptureHistory().length); // 0 152 | ``` 153 | 154 | ### Game Events 155 | 156 | The game client (both algebraic, simple) emit a number of events when scenarios occur on the board over the course of a match. 157 | 158 | ```javascript 159 | import chess from 'chess'; 160 | 161 | // const chess = require('chess'); 162 | 163 | // create a game client 164 | const gameClient = chess.create({ PGN : true }); 165 | 166 | // when a capture occurs 167 | gameClient.on('capture', (move) => { 168 | console.log('A piece has been captured!'); 169 | console.log(move); 170 | }); 171 | 172 | // when a castle occurs 173 | gameClient.on('castle', (move) => { 174 | console.log('A castle has occured!'); 175 | console.log(move); 176 | }); 177 | 178 | // when a King is placed in check 179 | gameClient.on('check', (attack) => { 180 | console.log('The King is under attack!'); 181 | console.log(attack); 182 | }); 183 | 184 | // when King is placed in checkmate 185 | gameClient.on('checkmate', (attack) => { 186 | console.log('The game has ended due to checkmate!'); 187 | console.log(attack); 188 | }); 189 | 190 | // when en Passant occurs 191 | gameClient.on('enPassant', (move) => { 192 | console.log('An en Passant has occured!'); 193 | console.log(move); 194 | }); 195 | 196 | // when a move occurs on the board 197 | gameClient.on('move', (move) => { 198 | console.log('A piece was moved!'); 199 | console.log(move); 200 | }); 201 | 202 | // when a Pawn promotion occurs 203 | gameClient.on('promote', (square) => { 204 | console.log('A Pawn has been promoted!'); 205 | console.log(square); 206 | }); 207 | 208 | // when an undo function is called on a move 209 | gameClient.on('undo', (move) => { 210 | console.log('A previous move was undone!'); 211 | console.log(move); 212 | }); 213 | ``` 214 | 215 | #### The `capture` Event 216 | 217 | The `capture` event is emitted when a piece has been captured during game play. The `capture` event data is the same as the [move](#the-gameclientmove-function) object that is provided as a response to [gameClient.move()](#the-gameclientmove-function). 218 | 219 | #### The `check` Event 220 | 221 | The `check` event is emitted for each attack on a King that occurs on the board. In the event a single move results in multiple pieces putting a King in check, multiple `check` events will be emitted, one for each attack. 222 | 223 | ##### The `attack` Object 224 | 225 | The attack object contains the attacking square and the King square. The properties of the attack object are: 226 | 227 | * attackingSquare - The square object from which the attacker originates which includes the piece conducting the attack 228 | * kingSquare - The square object representing the King that is under attack 229 | 230 | ```javascript 231 | { 232 | attackingSquare : { 233 | file: 'f', 234 | rank: 6, 235 | piece: { 236 | moveCount: 3, 237 | side: { 238 | name: 'white' 239 | }, 240 | type: 'knight', 241 | notation: 'N' 242 | } 243 | }, 244 | kingSquare : { 245 | file: 'e', 246 | rank: 8, 247 | piece: { 248 | moveCount: 0, 249 | side: { 250 | name: 'black' 251 | }, 252 | type: 'king', 253 | notation: 'K' 254 | } 255 | } 256 | } 257 | ``` 258 | 259 | #### The `checkmate` Event 260 | 261 | The `checkmate` event is emitted when checkmate has been detected on the board. The `checkmate` event data is the same as the [attack](#the-attack-object) object that is provided for the `check` event. 262 | 263 | #### The `castle` Event 264 | 265 | The `castle` event is emitted when a castle move occurs on the board. The `castle` event data is the [move](#the-gameclientmove-function) object that is also returned when performing a [gameClient.move()](#the-gameclientmove-function). 266 | 267 | #### The `enPassant` Event 268 | 269 | When en Passant occurs, the `enPassant` event is emitted. The `enPassant` event data is the [move](#the-gameclientmove-function) object that is also returned when performing a [gameClient.move()](#the-gameclientmove-function). 270 | 271 | #### The `move` Event 272 | 273 | Any time a move occurs on the board, the `move` event is emitted. The `enPassant` event data is the [move](#the-gameclientmove-function) object that is also returned when performing a [gameClient.move()](#the-gameclientmove-function). 274 | 275 | #### The `promote` Event 276 | 277 | When a Pawn promotion occurs, the `promote` event is emitted. The `promote` event data is the Square object upon which the newly promoted piece resides, which looks as follows: 278 | 279 | ```javascript 280 | { 281 | file: 'a', 282 | piece: { 283 | moveCount: 2, 284 | notation: 'R', 285 | side: { 286 | name: 'white' 287 | }, 288 | type: 'rook' }, 289 | rank: 8 290 | } 291 | ``` 292 | 293 | #### The `undo` Event 294 | 295 | The `undo` event is emitted when a previous move that occured on the board is reversed using the `undo` method. The `undo` event data is the same [move](#the-gameclientmove-function) object that is also returned when performing a [gameClient.move()](#the-gameclientmove-function). 296 | 297 | ### The `gameClient.move()` Function 298 | 299 | From the above example, the response object that is returned when calling chess.move() looks like the following: 300 | 301 | ```javascript 302 | { 303 | move: { 304 | // the captured piece (if capture occurred) 305 | capturedPiece: null, 306 | // was the move a castle? 307 | castle: false, 308 | // was the move en Passant? 309 | enPassant: false, 310 | // tje square a piece was moved to 311 | postSquare: { 312 | file: 'a', 313 | rank: 4, 314 | piece: { 315 | moveCount: 1, 316 | side: { 317 | name: 'white' 318 | }, 319 | type: 'pawn', 320 | notation: 'R' 321 | } 322 | }, 323 | // the square that the piece came from 324 | prevSquare: { 325 | file: 'a', 326 | rank: 2, 327 | piece: null 328 | } 329 | }, 330 | // undo() can be used to back out the previous move 331 | undo: __function__ 332 | } 333 | ``` 334 | 335 | #### The `move` Object 336 | 337 | The move object contains a collection of properties and an undo function pointer. The five properties of the move object are: 338 | 339 | * capturedPiece - If a piece was captured during the move, it will be represented here. 340 | * castle - If the move was a castle, this will be set to true, otherwise false. 341 | * enPassant - If the move was en passant, this will be set to true, otherwise false. 342 | * postSquare - The destination square object for the move. 343 | * prevSquare - The square object from which the move was originated. 344 | 345 | ##### The `undo()` Function 346 | 347 | To back out the move: 348 | 349 | ```javascript 350 | move.undo(); 351 | ``` 352 | 353 | ### The `gameClient.getStatus()` Function 354 | 355 | The status object is as follows (abbreviated in parts to improve readability): 356 | 357 | ```javascript 358 | { 359 | // this is the top level board 360 | board: { 361 | // an array of all squares on the board 362 | squares: [{ 363 | file: 'a', 364 | rank: 1, 365 | piece: { 366 | moveCount: 0, 367 | side: { 368 | name: 'white' 369 | }, 370 | type: 'rook', 371 | notation: 'R' 372 | } 373 | }, 374 | /* the rest of the squares... */ 375 | ] 376 | }, 377 | isCheck: false, // is the King currently in check? 378 | isCheckmate: false, // is the King currently in checkmate? 379 | isRepetition: false, // has 3-fold repetition occurred? 380 | isStalemate: false, // is the board in stalemate? 381 | // all possible moves (notated) with details for each move 382 | notatedMoves: { 383 | a3: { 384 | src: { 385 | file: 'a' 386 | rank: 2, 387 | piece: { 388 | moveCount: 0, 389 | side: { 390 | name: 'white' 391 | }, 392 | type: 'pawn', 393 | notation: 'R' 394 | } 395 | }, 396 | dest: { 397 | file: 'a', 398 | rank: 3, 399 | piece: null 400 | } 401 | }, 402 | /* the rest of the available moves... */ 403 | } 404 | } 405 | ``` 406 | 407 | #### The `status` Object 408 | 409 | The status object returned via the getStatus() function call contains several Object properties: 410 | 411 | * board - The underlying board Object which contains the collection of squares. 412 | * isCheck - If the status of the board is check, this will be true. 413 | * isCheckmate - If the status of the board is checkmate, this will be true. Additionally, the notatedMoves property will be empty. 414 | * isRepetition - If 3-fold repetition has occurred, this will be true. The notatedMoves property will not be empty as the game can technically continue. 415 | * isStalemate - If the board is in stalemate, this will be set to true. 416 | * notatedMoves - A hash containing all available moves on the board. 417 | 418 | ##### The `status.notatedMoves` Object 419 | 420 | Each object within the notatedMoves hash represents a possible move. The key to the hash is the algebraic notation of the move. The value for each key in the hash has two properties: 421 | 422 | * src - The starting square (which contains a piece) of the move 423 | * dest - The destination square of the move 424 | 425 | The following code is an example of how to iterate the available notated moves for the game. 426 | 427 | ```javascript 428 | import chess from 'chess'; 429 | // const chess = require('chess'); 430 | const gameClient = chess.create(); 431 | 432 | let 433 | i = 0, 434 | key = '', 435 | status = gameClient.getStatus(); 436 | 437 | Object.keys(status.notatedMoves).map((key, index) => { 438 | console.log(status.notatedMoves[key]); 439 | return { ...status.notatedMoves[key], key }; 440 | }); 441 | ``` 442 | 443 | #### Example usage 444 | 445 | The following usage of the code is playing out the 3rd game in the series between Fischer and Petrosian in Buenos Aires, 1971. The game ended a draw due to 3 fold repetition. 446 | 447 | ```javascript 448 | import chess from 'chess'; 449 | const util = require('util'); 450 | 451 | // const chess = require('chess'); 452 | 453 | const gameClient = chess.create(); 454 | 455 | // 1. e4 e6 456 | gameClient.move('e4'); 457 | gameClient.move('e6'); 458 | // 2. d4 d5 459 | gameClient.move('d4'); 460 | gameClient.move('d5'); 461 | // 3. Nc3 Nf6 462 | gameClient.move('Nc3'); 463 | gameClient.move('Nf6'); 464 | // 4. Bg5 dxe4 465 | gameClient.move('Bg5'); 466 | gameClient.move('dxe4'); 467 | // 5. Nxe4 Be7 468 | gameClient.move('Nxe4'); 469 | gameClient.move('Be7'); 470 | // 6. Bxf6 gxf6 471 | gameClient.move('Bxf6'); 472 | gameClient.move('gxf6'); 473 | // 7. g3 f5 474 | gameClient.move('g3'); 475 | gameClient.move('f5'); 476 | // 8. Nc3 Bf6 477 | gameClient.move('Nc3'); 478 | gameClient.move('Bf6'); 479 | // 9. Nge2 Nc6 480 | gameClient.move('Nge2'); 481 | gameClient.move('Nc6'); 482 | // 10. d5 exd5 483 | gameClient.move('d5'); 484 | gameClient.move('exd5'); 485 | // 11. Nxd5 Bxb2 486 | gameClient.move('Nxd5'); 487 | gameClient.move('Bxb2'); 488 | // 12. Bg2 O-O 489 | gameClient.move('Bg2'); 490 | gameClient.move('0-0'); 491 | // 13. O-O Bh8 492 | gameClient.move('0-0'); 493 | gameClient.move('Bh8'); 494 | // 14. Nef4 Ne5 495 | gameClient.move('Nef4'); 496 | gameClient.move('Ne5'); 497 | // 15. Qh5 Ng6 498 | gameClient.move('Qh5'); 499 | gameClient.move('Ng6'); 500 | // 16. Rad1 c6 501 | gameClient.move('Rad1'); 502 | gameClient.move('c6'); 503 | // 17. Ne3 Qf6 504 | gameClient.move('Ne3'); 505 | gameClient.move('Qf6'); 506 | // 18. Kh1 Bg7 507 | gameClient.move('Kh1'); 508 | gameClient.move('Bg7'); 509 | // 19. Bh3 Ne7 510 | gameClient.move('Bh3'); 511 | gameClient.move('Ne7'); 512 | // 20. Rd3 Be6 513 | gameClient.move('Rd3'); 514 | gameClient.move('Be6'); 515 | // 21. Rfd1 Bh6 516 | gameClient.move('Rfd1'); 517 | gameClient.move('Bh6'); 518 | // 22. Rd4 Bxf4 519 | gameClient.move('Rd4'); 520 | gameClient.move('Bxf4'); 521 | // 23. Rxf4 Rad8 522 | gameClient.move('Rxf4'); 523 | gameClient.move('Rad8'); 524 | // 24. Rxd8 Rxd8 525 | gameClient.move('Rxd8'); 526 | gameClient.move('Rxd8'); 527 | // 25. Bxf5 Nxf5 528 | gameClient.move('Bxf5'); 529 | gameClient.move('Nxf5'); 530 | // 26. Nxf5 Rd5 531 | gameClient.move('Nxf5'); 532 | gameClient.move('Rd5'); 533 | // 27. g4 Bxf5 534 | gameClient.move('g4'); 535 | gameClient.move('Bxf5'); 536 | // 28. gxf5 h6 537 | gameClient.move('gxf5'); 538 | gameClient.move('h6'); 539 | // 29. h3 Kh7 540 | gameClient.move('h3'); 541 | gameClient.move('Kh7'); 542 | // 30. Qe2 Qe5 543 | gameClient.move('Qe2'); 544 | gameClient.move('Qe5'); 545 | // 31. Qh5 Qf6 546 | gameClient.move('Qh5'); 547 | gameClient.move('Qf6'); 548 | // 32. Qe2 Re5 549 | gameClient.move('Qe2'); 550 | gameClient.move('Re5'); 551 | // 33. Qd3 Rd5 552 | gameClient.move('Qd3'); 553 | gameClient.move('Rd5'); 554 | // 34. Qe2 555 | gameClient.move('Qe2'); 556 | 557 | console.log(util.inspect(gameClient.getStatus(), false, 7)); 558 | ``` 559 | 560 | ##### Output 561 | 562 | The above code produces the following output: 563 | 564 | ```javascript 565 | { 566 | board: { 567 | squares: [ 568 | { file: 'a', rank: 1, piece: null }, 569 | { file: 'b', rank: 1, piece: null }, 570 | { file: 'c', rank: 1, piece: null }, 571 | { file: 'd', rank: 1, piece: null }, 572 | { file: 'e', rank: 1, piece: null }, 573 | { file: 'f', rank: 1, piece: null }, 574 | { file: 'g', rank: 1, piece: null }, 575 | { file: 'h', 576 | rank: 1, 577 | piece: 578 | { moveCount: 2, 579 | side: { name: 'white' }, 580 | type: 'king', 581 | notation: 'K' } }, 582 | { file: 'a', 583 | rank: 2, 584 | piece: 585 | { moveCount: 0, 586 | side: { name: 'white' }, 587 | type: 'pawn', 588 | notation: '' } }, 589 | { file: 'b', rank: 2, piece: null }, 590 | { file: 'c', 591 | rank: 2, 592 | piece: 593 | { moveCount: 0, 594 | side: { name: 'white' }, 595 | type: 'pawn', 596 | notation: '' } }, 597 | { file: 'd', rank: 2, piece: null }, 598 | { file: 'e', 599 | rank: 2, 600 | piece: 601 | { moveCount: 6, 602 | side: { name: 'white' }, 603 | type: 'queen', 604 | notation: 'Q' } }, 605 | { file: 'f', 606 | rank: 2, 607 | piece: 608 | { moveCount: 0, 609 | side: { name: 'white' }, 610 | type: 'pawn', 611 | notation: '' } }, 612 | { file: 'g', rank: 2, piece: null }, 613 | { file: 'h', rank: 2, piece: null }, 614 | { file: 'a', rank: 3, piece: null }, 615 | { file: 'b', rank: 3, piece: null }, 616 | { file: 'c', rank: 3, piece: null }, 617 | { file: 'd', rank: 3, piece: null }, 618 | { file: 'e', rank: 3, piece: null }, 619 | { file: 'f', rank: 3, piece: null }, 620 | { file: 'g', rank: 3, piece: null }, 621 | { file: 'h', 622 | rank: 3, 623 | piece: 624 | { moveCount: 1, 625 | side: { name: 'white' }, 626 | type: 'pawn', 627 | notation: '' } }, 628 | { file: 'a', rank: 4, piece: null }, 629 | { file: 'b', rank: 4, piece: null }, 630 | { file: 'c', rank: 4, piece: null }, 631 | { file: 'd', rank: 4, piece: null }, 632 | { file: 'e', rank: 4, piece: null }, 633 | { file: 'f', 634 | rank: 4, 635 | piece: 636 | { moveCount: 4, 637 | side: { name: 'white' }, 638 | type: 'rook', 639 | notation: 'R' } }, 640 | { file: 'g', rank: 4, piece: null }, 641 | { file: 'h', rank: 4, piece: null }, 642 | { file: 'a', rank: 5, piece: null }, 643 | { file: 'b', rank: 5, piece: null }, 644 | { file: 'c', rank: 5, piece: null }, 645 | { file: 'd', 646 | rank: 5, 647 | piece: 648 | { moveCount: 4, 649 | side: { name: 'black' }, 650 | type: 'rook', 651 | notation: 'R' } }, 652 | { file: 'e', rank: 5, piece: null }, 653 | { file: 'f', 654 | rank: 5, 655 | piece: 656 | { moveCount: 3, 657 | side: { name: 'white' }, 658 | type: 'pawn', 659 | notation: '' } }, 660 | { file: 'g', rank: 5, piece: null }, 661 | { file: 'h', rank: 5, piece: null }, 662 | { file: 'a', rank: 6, piece: null }, 663 | { file: 'b', rank: 6, piece: null }, 664 | { file: 'c', 665 | rank: 6, 666 | piece: 667 | { moveCount: 1, 668 | side: { name: 'black' }, 669 | type: 'pawn', 670 | notation: '' } }, 671 | { file: 'd', rank: 6, piece: null }, 672 | { file: 'e', rank: 6, piece: null }, 673 | { file: 'f', 674 | rank: 6, 675 | piece: 676 | { moveCount: 3, 677 | side: { name: 'black' }, 678 | type: 'queen', 679 | notation: 'Q' } }, 680 | { file: 'g', rank: 6, piece: null }, 681 | { file: 'h', 682 | rank: 6, 683 | piece: 684 | { moveCount: 1, 685 | side: { name: 'black' }, 686 | type: 'pawn', 687 | notation: '' } }, 688 | { file: 'a', 689 | rank: 7, 690 | piece: 691 | { moveCount: 0, 692 | side: { name: 'black' }, 693 | type: 'pawn', 694 | notation: '' } }, 695 | { file: 'b', 696 | rank: 7, 697 | piece: 698 | { moveCount: 0, 699 | side: { name: 'black' }, 700 | type: 'pawn', 701 | notation: '' } }, 702 | { file: 'c', rank: 7, piece: null }, 703 | { file: 'd', rank: 7, piece: null }, 704 | { file: 'e', rank: 7, piece: null }, 705 | { file: 'f', 706 | rank: 7, 707 | piece: 708 | { moveCount: 0, 709 | side: { name: 'black' }, 710 | type: 'pawn', 711 | notation: '' } }, 712 | { file: 'g', rank: 7, piece: null }, 713 | { file: 'h', 714 | rank: 7, 715 | piece: 716 | { moveCount: 2, 717 | side: { name: 'black' }, 718 | type: 'king', 719 | notation: 'K' } }, 720 | { file: 'a', rank: 8, piece: null }, 721 | { file: 'b', rank: 8, piece: null }, 722 | { file: 'c', rank: 8, piece: null }, 723 | { file: 'd', rank: 8, piece: null }, 724 | { file: 'e', rank: 8, piece: null }, 725 | { file: 'f', rank: 8, piece: null }, 726 | { file: 'g', rank: 8, piece: null }, 727 | { file: 'h', rank: 8, piece: null } 728 | ], 729 | }, 730 | isCheck: false, 731 | isCheckmate: false, 732 | isRepetition: true, 733 | isStalemate: false, 734 | notatedMoves: 735 | { Rd4: 736 | { src: 737 | { file: 'd', 738 | rank: 5, 739 | piece: 740 | { moveCount: 4, 741 | side: { name: 'black' }, 742 | type: 'rook', 743 | notation: 'R' } }, 744 | dest: { file: 'd', rank: 4, piece: null } }, 745 | Rd3: 746 | { src: 747 | { file: 'd', 748 | rank: 5, 749 | piece: 750 | { moveCount: 4, 751 | side: { name: 'black' }, 752 | type: 'rook', 753 | notation: 'R' } }, 754 | dest: { file: 'd', rank: 3, piece: null } }, 755 | Rd2: 756 | { src: 757 | { file: 'd', 758 | rank: 5, 759 | piece: 760 | { moveCount: 4, 761 | side: { name: 'black' }, 762 | type: 'rook', 763 | notation: 'R' } }, 764 | dest: { file: 'd', rank: 2, piece: null } }, 765 | Rd1: 766 | { src: 767 | { file: 'd', 768 | rank: 5, 769 | piece: 770 | { moveCount: 4, 771 | side: { name: 'black' }, 772 | type: 'rook', 773 | notation: 'R' } }, 774 | dest: { file: 'd', rank: 1, piece: null } }, 775 | Rd6: 776 | { src: 777 | { file: 'd', 778 | rank: 5, 779 | piece: 780 | { moveCount: 4, 781 | side: { name: 'black' }, 782 | type: 'rook', 783 | notation: 'R' } }, 784 | dest: { file: 'd', rank: 6, piece: null } }, 785 | Rd7: 786 | { src: 787 | { file: 'd', 788 | rank: 5, 789 | piece: 790 | { moveCount: 4, 791 | side: { name: 'black' }, 792 | type: 'rook', 793 | notation: 'R' } }, 794 | dest: { file: 'd', rank: 7, piece: null } }, 795 | Rd8: 796 | { src: 797 | { file: 'd', 798 | rank: 5, 799 | piece: 800 | { moveCount: 4, 801 | side: { name: 'black' }, 802 | type: 'rook', 803 | notation: 'R' } }, 804 | dest: { file: 'd', rank: 8, piece: null } }, 805 | Rc5: 806 | { src: 807 | { file: 'd', 808 | rank: 5, 809 | piece: 810 | { moveCount: 4, 811 | side: { name: 'black' }, 812 | type: 'rook', 813 | notation: 'R' } }, 814 | dest: { file: 'c', rank: 5, piece: null } }, 815 | Rb5: 816 | { src: 817 | { file: 'd', 818 | rank: 5, 819 | piece: 820 | { moveCount: 4, 821 | side: { name: 'black' }, 822 | type: 'rook', 823 | notation: 'R' } }, 824 | dest: { file: 'b', rank: 5, piece: null } }, 825 | Ra5: 826 | { src: 827 | { file: 'd', 828 | rank: 5, 829 | piece: 830 | { moveCount: 4, 831 | side: { name: 'black' }, 832 | type: 'rook', 833 | notation: 'R' } }, 834 | dest: { file: 'a', rank: 5, piece: null } }, 835 | Re5: 836 | { src: 837 | { file: 'd', 838 | rank: 5, 839 | piece: 840 | { moveCount: 4, 841 | side: { name: 'black' }, 842 | type: 'rook', 843 | notation: 'R' } }, 844 | dest: { file: 'e', rank: 5, piece: null } }, 845 | Rxf5: 846 | { src: 847 | { file: 'd', 848 | rank: 5, 849 | piece: 850 | { moveCount: 4, 851 | side: { name: 'black' }, 852 | type: 'rook', 853 | notation: 'R' } }, 854 | dest: 855 | { file: 'f', 856 | rank: 5, 857 | piece: 858 | { moveCount: 3, 859 | side: { name: 'white' }, 860 | type: 'pawn', 861 | notation: '' } } }, 862 | c5: 863 | { src: 864 | { file: 'c', 865 | rank: 6, 866 | piece: 867 | { moveCount: 1, 868 | side: { name: 'black' }, 869 | type: 'pawn', 870 | notation: '' } }, 871 | dest: { file: 'c', rank: 5, piece: null } }, 872 | Qxf5: 873 | { src: 874 | { file: 'f', 875 | rank: 6, 876 | piece: 877 | { moveCount: 3, 878 | side: { name: 'black' }, 879 | type: 'queen', 880 | notation: 'Q' } }, 881 | dest: 882 | { file: 'f', 883 | rank: 5, 884 | piece: 885 | { moveCount: 3, 886 | side: { name: 'white' }, 887 | type: 'pawn', 888 | notation: '' } } }, 889 | Qe6: 890 | { src: 891 | { file: 'f', 892 | rank: 6, 893 | piece: 894 | { moveCount: 3, 895 | side: { name: 'black' }, 896 | type: 'queen', 897 | notation: 'Q' } }, 898 | dest: { file: 'e', rank: 6, piece: null } }, 899 | Qd6: 900 | { src: 901 | { file: 'f', 902 | rank: 6, 903 | piece: 904 | { moveCount: 3, 905 | side: { name: 'black' }, 906 | type: 'queen', 907 | notation: 'Q' } }, 908 | dest: { file: 'd', rank: 6, piece: null } }, 909 | Qg6: 910 | { src: 911 | { file: 'f', 912 | rank: 6, 913 | piece: 914 | { moveCount: 3, 915 | side: { name: 'black' }, 916 | type: 'queen', 917 | notation: 'Q' } }, 918 | dest: { file: 'g', rank: 6, piece: null } }, 919 | Qe7: 920 | { src: 921 | { file: 'f', 922 | rank: 6, 923 | piece: 924 | { moveCount: 3, 925 | side: { name: 'black' }, 926 | type: 'queen', 927 | notation: 'Q' } }, 928 | dest: { file: 'e', rank: 7, piece: null } }, 929 | Qd8: 930 | { src: 931 | { file: 'f', 932 | rank: 6, 933 | piece: 934 | { moveCount: 3, 935 | side: { name: 'black' }, 936 | type: 'queen', 937 | notation: 'Q' } }, 938 | dest: { file: 'd', rank: 8, piece: null } }, 939 | Qg5: 940 | { src: 941 | { file: 'f', 942 | rank: 6, 943 | piece: 944 | { moveCount: 3, 945 | side: { name: 'black' }, 946 | type: 'queen', 947 | notation: 'Q' } }, 948 | dest: { file: 'g', rank: 5, piece: null } }, 949 | Qh4: 950 | { src: 951 | { file: 'f', 952 | rank: 6, 953 | piece: 954 | { moveCount: 3, 955 | side: { name: 'black' }, 956 | type: 'queen', 957 | notation: 'Q' } }, 958 | dest: { file: 'h', rank: 4, piece: null } }, 959 | Qe5: 960 | { src: 961 | { file: 'f', 962 | rank: 6, 963 | piece: 964 | { moveCount: 3, 965 | side: { name: 'black' }, 966 | type: 'queen', 967 | notation: 'Q' } }, 968 | dest: { file: 'e', rank: 5, piece: null } }, 969 | Qd4: 970 | { src: 971 | { file: 'f', 972 | rank: 6, 973 | piece: 974 | { moveCount: 3, 975 | side: { name: 'black' }, 976 | type: 'queen', 977 | notation: 'Q' } }, 978 | dest: { file: 'd', rank: 4, piece: null } }, 979 | Qc3: 980 | { src: 981 | { file: 'f', 982 | rank: 6, 983 | piece: 984 | { moveCount: 3, 985 | side: { name: 'black' }, 986 | type: 'queen', 987 | notation: 'Q' } }, 988 | dest: { file: 'c', rank: 3, piece: null } }, 989 | Qb2: 990 | { src: 991 | { file: 'f', 992 | rank: 6, 993 | piece: 994 | { moveCount: 3, 995 | side: { name: 'black' }, 996 | type: 'queen', 997 | notation: 'Q' } }, 998 | dest: { file: 'b', rank: 2, piece: null } }, 999 | Qa1: 1000 | { src: 1001 | { file: 'f', 1002 | rank: 6, 1003 | piece: 1004 | { moveCount: 3, 1005 | side: { name: 'black' }, 1006 | type: 'queen', 1007 | notation: 'Q' } }, 1008 | dest: { file: 'a', rank: 1, piece: null } }, 1009 | Qg7: 1010 | { src: 1011 | { file: 'f', 1012 | rank: 6, 1013 | piece: 1014 | { moveCount: 3, 1015 | side: { name: 'black' }, 1016 | type: 'queen', 1017 | notation: 'Q' } }, 1018 | dest: { file: 'g', rank: 7, piece: null } }, 1019 | Qh8: 1020 | { src: 1021 | { file: 'f', 1022 | rank: 6, 1023 | piece: 1024 | { moveCount: 3, 1025 | side: { name: 'black' }, 1026 | type: 'queen', 1027 | notation: 'Q' } }, 1028 | dest: { file: 'h', rank: 8, piece: null } }, 1029 | h5: 1030 | { src: 1031 | { file: 'h', 1032 | rank: 6, 1033 | piece: 1034 | { moveCount: 1, 1035 | side: { name: 'black' }, 1036 | type: 'pawn', 1037 | notation: '' } }, 1038 | dest: { file: 'h', rank: 5, piece: null } }, 1039 | a6: 1040 | { src: 1041 | { file: 'a', 1042 | rank: 7, 1043 | piece: 1044 | { moveCount: 0, 1045 | side: { name: 'black' }, 1046 | type: 'pawn', 1047 | notation: '' } }, 1048 | dest: { file: 'a', rank: 6, piece: null } }, 1049 | a5: 1050 | { src: 1051 | { file: 'a', 1052 | rank: 7, 1053 | piece: 1054 | { moveCount: 0, 1055 | side: { name: 'black' }, 1056 | type: 'pawn', 1057 | notation: '' } }, 1058 | dest: { file: 'a', rank: 5, piece: null } }, 1059 | b6: 1060 | { src: 1061 | { file: 'b', 1062 | rank: 7, 1063 | piece: 1064 | { moveCount: 0, 1065 | side: { name: 'black' }, 1066 | type: 'pawn', 1067 | notation: '' } }, 1068 | dest: { file: 'b', rank: 6, piece: null } }, 1069 | b5: 1070 | { src: 1071 | { file: 'b', 1072 | rank: 7, 1073 | piece: 1074 | { moveCount: 0, 1075 | side: { name: 'black' }, 1076 | type: 'pawn', 1077 | notation: '' } }, 1078 | dest: { file: 'b', rank: 5, piece: null } }, 1079 | Kh8: 1080 | { src: 1081 | { file: 'h', 1082 | rank: 7, 1083 | piece: 1084 | { moveCount: 2, 1085 | side: { name: 'black' }, 1086 | type: 'king', 1087 | notation: 'K' } }, 1088 | dest: { file: 'h', rank: 8, piece: null } }, 1089 | Kg7: 1090 | { src: 1091 | { file: 'h', 1092 | rank: 7, 1093 | piece: 1094 | { moveCount: 2, 1095 | side: { name: 'black' }, 1096 | type: 'king', 1097 | notation: 'K' } }, 1098 | dest: { file: 'g', rank: 7, piece: null } }, 1099 | Kg8: 1100 | { src: 1101 | { file: 'h', 1102 | rank: 7, 1103 | piece: 1104 | { moveCount: 2, 1105 | side: { name: 'black' }, 1106 | type: 'king', 1107 | notation: 'K' } }, 1108 | dest: { file: 'g', rank: 8, piece: null } } } } 1109 | ``` 1110 | --------------------------------------------------------------------------------