├── artifacts ├── engine_selftest.txt ├── windows │ └── wasabi.cmd ├── linux │ └── wasabi ├── wavm_checksums.txt └── release_notes.md ├── tools ├── tuning │ ├── .gitignore │ └── config_example.yml ├── bookgen │ ├── Pipfile │ ├── util.py │ ├── zobrist.py │ ├── Pipfile.lock │ └── bookgen.py └── gen_version_ts.js ├── public ├── robots.txt ├── favicon.png ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── screenshots ├── chess_mobile.png └── chess_screenshot.png ├── version.ts ├── assembly ├── tsconfig.json ├── io │ ├── browser │ │ ├── clock.ts │ │ └── stdio.ts │ ├── wasi │ │ ├── clock.ts │ │ ├── abort.ts │ │ └── stdio.ts │ └── index.ts ├── __tests__ │ ├── opening-book.spec.ts │ ├── perft.performance.ts │ ├── random.spec.ts │ ├── history.spec.ts │ ├── move-notation.spec.ts │ ├── history-heuristics.spec.ts │ ├── transposition-table.spec.ts │ ├── engine.performance.ts │ ├── move-ordering.ts │ ├── bitboard.spec.ts │ ├── perft.spec.ts │ ├── util.spec.ts │ └── engine.spec.ts ├── random.ts ├── history.ts ├── opening-book.ts ├── perft.ts ├── move-ordering.ts ├── pieces.ts ├── util.ts ├── transposition-table.ts ├── history-heuristics.ts ├── uci-move-notation.ts ├── index.ts ├── fen.ts └── bitboard.ts ├── as-pect-perft.config.js ├── as-pect.config.js ├── .github └── workflows │ ├── test.yml │ └── release.yml ├── src ├── index.css ├── index.js ├── ui │ ├── img │ │ ├── white_rook.svg │ │ ├── black_rook.svg │ │ ├── black_pawn.svg │ │ ├── white_pawn.svg │ │ ├── white_knight.svg │ │ ├── black_bishop.svg │ │ ├── white_bishop.svg │ │ ├── black_knight.svg │ │ ├── black_king.svg │ │ ├── white_king.svg │ │ ├── white_queen.svg │ │ └── black_queen.svg │ ├── App.js │ ├── Board.js │ ├── PromotionPieceSelection.js │ ├── AnimatedSpinner.js │ ├── Piece.js │ ├── Field.js │ ├── GameMenu.js │ └── Game.js └── engine │ ├── constants.js │ ├── move.js │ └── engine.worker.js ├── .gitignore ├── package.json └── README.md /artifacts/engine_selftest.txt: -------------------------------------------------------------------------------- 1 | test 2 | quit 3 | -------------------------------------------------------------------------------- /tools/tuning/.gitignore: -------------------------------------------------------------------------------- 1 | config.yml 2 | tuning_result.yml 3 | -------------------------------------------------------------------------------- /artifacts/windows/wasabi.cmd: -------------------------------------------------------------------------------- 1 | .\wavm\bin\wavm.exe run .\engine.wasm 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhonert/chess/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhonert/chess/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhonert/chess/HEAD/public/logo512.png -------------------------------------------------------------------------------- /screenshots/chess_mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhonert/chess/HEAD/screenshots/chess_mobile.png -------------------------------------------------------------------------------- /screenshots/chess_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhonert/chess/HEAD/screenshots/chess_screenshot.png -------------------------------------------------------------------------------- /version.ts: -------------------------------------------------------------------------------- 1 | // Auto-generated file: to update the version use 'npm version ...' 2 | export const VERSION="1.5.1"; -------------------------------------------------------------------------------- /artifacts/linux/wasabi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | SCRIPT_DIR=$(dirname $(readlink -f $0)) 3 | $SCRIPT_DIR/wavm/bin/wavm run $SCRIPT_DIR/engine.wasm 4 | -------------------------------------------------------------------------------- /assembly/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../node_modules/assemblyscript/std/assembly.json", 3 | "include": [ 4 | "./**/*.ts", 5 | "../node_modules/@as-pect/assembly/types/as-pect.d.ts" 6 | ] 7 | } -------------------------------------------------------------------------------- /artifacts/wavm_checksums.txt: -------------------------------------------------------------------------------- 1 | a0542e95d378a86ae73609acfe0208ef2ef70bd3d5645946b73b8b18a9be6e84 ./artifacts/wavm-windows.zip 2 | 10cec94139b2ebf18792ae8b0e8e8780be63f938f188332ef96367c20f9a74e6 ./artifacts/wavm-linux.tar.gz 3 | -------------------------------------------------------------------------------- /tools/bookgen/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | numpy = "*" 8 | chess = "*" 9 | 10 | [dev-packages] 11 | 12 | [requires] 13 | python_version = "3.9" 14 | -------------------------------------------------------------------------------- /tools/gen_version_ts.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const packageJson = require('../package.json'); 3 | fs.writeFileSync('version.ts', 4 | `// Auto-generated file: to update the version use 'npm version ...' 5 | export const VERSION="${packageJson.version}";`); 6 | -------------------------------------------------------------------------------- /as-pect-perft.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | include: ["assembly/__tests__/**/*.performance.ts"], 3 | flags: { 4 | "--runtime": ["incremental"], 5 | "--use": ["IS_WASI=0","ASC_RTRACE=1"], 6 | "--optimizeLevel": "2", 7 | "--shrinkLevel": "0", 8 | "--converge": [], 9 | }, 10 | disclude: [/node_modules/], 11 | outputBinary: false, 12 | }; 13 | -------------------------------------------------------------------------------- /as-pect.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | include: ["assembly/__tests__/**/*.spec.ts"], 3 | add: ["assembly/__tests__/**/*.include.ts"], 4 | flags: { 5 | "--runtime": ["incremental"], // Acceptable values are: incremental, minimal and stub 6 | "--use": ["IS_WASI=0","ASC_RTRACE=1"] 7 | }, 8 | disclude: [/node_modules/], 9 | imports: {}, 10 | outputBinary: false, 11 | }; 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | 14 | - name: Setup Node 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: '14.x' 18 | 19 | - name: Run tests 20 | env: 21 | CI: true 22 | run: | 23 | npm ci 24 | npm test 25 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | background-color: white; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | 16 | * { 17 | box-sizing: border-box; 18 | } 19 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Chess", 3 | "name": "Chess", 4 | "icons": [ 5 | { 6 | "src": "favicon.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "sizes": "512x512", 18 | "type": "image/png" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .idea 26 | 27 | # caches 28 | **/__pycache__ 29 | 30 | # Generated as-pect type definition file 31 | as-pect.d.ts 32 | 33 | # Generated wasm binary and source map 34 | public/as-api.wasm 35 | public/as-api.wasm.map 36 | 37 | # PGN files 38 | **/pgn/* 39 | 40 | # FEN files 41 | **/fen/* 42 | -------------------------------------------------------------------------------- /tools/tuning/config_example.yml: -------------------------------------------------------------------------------- 1 | engine: 2 | cmd: /absolutePath/to/chess-engine 3 | 4 | options: 5 | debug_log: false 6 | test_positions_file: fen/quiet.fen 7 | concurrency: 8 8 | 9 | tuning: 10 | - name: QueenValue 11 | value: 950 12 | 13 | - name: RookValue 14 | value: 500 15 | 16 | - name: BishopValue 17 | value: 350 18 | 19 | - name: KnightValue 20 | value: 350 21 | 22 | - name: PawnValue 23 | value: 100 24 | 25 | # Options with list values will be send to the engine as: 26 | # set option KnightMobBonus0 value 0 27 | # set option KnightMobBonus1 value 0 28 | # set option KnightMobBonus2 value 0 29 | # ... 30 | - name: KnightMobBonus 31 | value: [ 0, 0, 0, 0, 0, 0, 0, 0, 0 ] 32 | -------------------------------------------------------------------------------- /assembly/io/browser/clock.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2019 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | @inline 20 | export function currentMillis(): i64 { 21 | return Date.now(); 22 | } 23 | -------------------------------------------------------------------------------- /artifacts/release_notes.md: -------------------------------------------------------------------------------- 1 | 2 | This release focussed on tuning the evaluation function :balance_scale: 3 | > The Web App version of **Wasabi Chess** can be played [**here**](https://mhonert.github.io/chess). 4 | 5 | ## Changes 6 | - New tuning tool 7 | - Tuned all evaluation parameters 8 | - Replaced simple mobility score calculation with mobility score table 9 | - Replaced simple king safety calculation with king threat score table 10 | - Improved game phase calculation for tapered eval 11 | - Added endgame piece square tables 12 | 13 | ## Installation 14 | - Download and unpack the archive for your platform (Linux or Windows 8/10) 15 | - Configure your UCI client (e.g. PyChess, Arena, cutechess) 16 | - … on Windows ⇒ to run *wasabi.cmd* 17 | - … on Linux ⇒ to run *wasabi* 18 | 19 | > :warning: the bundled **WAVM** runtime is not compatible with Windows 7 - a workaround is described in the [Wiki](https://github.com/mhonert/chess/wiki/Windows-7) 20 | 21 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import React from 'react'; 20 | import ReactDOM from 'react-dom'; 21 | import './index.css'; 22 | import App from './ui/App'; 23 | 24 | ReactDOM.render(, document.getElementById('root')); 25 | -------------------------------------------------------------------------------- /src/ui/img/white_rook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/engine/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | export const BLACK = -1; 20 | export const WHITE = 1; 21 | 22 | export const P = 1; // pawn 23 | export const N = 2; // knight 24 | export const B = 3; // bishop 25 | export const R = 4; // rook 26 | export const Q = 5; // queen 27 | export const K = 6; // king 28 | -------------------------------------------------------------------------------- /assembly/io/browser/stdio.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2019 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | export function writeLine(message: string): void { 20 | trace(message); 21 | } 22 | 23 | export function writeError(message: string): void { 24 | trace(message); 25 | } 26 | 27 | export function readLine(maxLength: i32): string { 28 | throw new Error("readLine not supported in browser"); 29 | } -------------------------------------------------------------------------------- /assembly/io/wasi/clock.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2019 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { clock_time_get, clockid } from 'bindings/wasi'; 20 | 21 | @inline 22 | export function currentMillis(): i64 { 23 | let time_ptr = changetype(new ArrayBuffer(8)); 24 | clock_time_get(clockid.MONOTONIC, 1_000_000, time_ptr); 25 | return load(time_ptr) / 1_000_000; 26 | } 27 | -------------------------------------------------------------------------------- /assembly/io/wasi/abort.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2019 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { writeError } from './stdio'; 20 | import { proc_exit } from 'bindings/wasi'; 21 | 22 | // Custom abort function 23 | @global 24 | export function _abort(message: string = "", file: string = "", line: u32 = 0, column: u32 = 0): void { 25 | writeError("Fatal error occured!"); 26 | writeError(file + ": line " + line.toString() + "@" + column.toString() + " - error: " + message); 27 | proc_exit(128); 28 | } 29 | -------------------------------------------------------------------------------- /src/engine/move.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2019 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | export class Move { 20 | constructor(piece, start, end) { 21 | this.start = start; 22 | this.end = end; 23 | this.encodedMove = Math.abs(piece) | (start << 3) | (end << 10); 24 | } 25 | 26 | static fromEncodedMove(encodedMove) { 27 | const piece = encodedMove & 0x7; // Bits 0-2 28 | const start = (encodedMove >> 3) & 0x7F; // Bits 3-10 29 | const end = (encodedMove >> 10) & 0x7F; // Bit 10-17 30 | 31 | return new Move(piece, start, end); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /assembly/io/wasi/stdio.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2019 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { Console } from "as-wasi"; 20 | 21 | export function writeLine(str: string): void { 22 | Console.log(str); 23 | } 24 | 25 | export function writeError(str: string): void { 26 | Console.error(str); 27 | } 28 | 29 | // Reads characters one by one until a line feed character occurs or the stream ended 30 | export function readLine(): string { 31 | const line = Console.readLine(); 32 | if (line === null) { 33 | return ""; 34 | } 35 | 36 | return line as string; 37 | } 38 | -------------------------------------------------------------------------------- /assembly/__tests__/opening-book.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { fromFEN, STARTPOS } from '../fen'; 20 | import { isValidMove } from '../move-generation'; 21 | import { findOpeningMove } from '../opening-book'; 22 | 23 | describe("Find moves from opening book", () => { 24 | it("Finds moves for multiple plies", () => { 25 | const board = fromFEN(STARTPOS); 26 | 27 | for (let ply = 1; ply <= 4; ply++) { 28 | const openingMove = findOpeningMove(board); 29 | expect(isValidMove(board, board.getActivePlayer(), openingMove)).toBeTruthy("No move found for ply #" + ply.toString()) 30 | board.performEncodedMove(openingMove); 31 | } 32 | }) 33 | }); 34 | -------------------------------------------------------------------------------- /assembly/random.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | const multiplier: u64 = 6364136223846793005; 20 | const increment: u64 = 1442695040888963407; 21 | 22 | // Create pseudo random numbers using a "Permuted Congruential Generator" (see https://en.wikipedia.org/wiki/Permuted_congruential_generator) 23 | export class Random { 24 | state: u64 = 0x4d595df4d0f33173; 25 | 26 | rand32(): u32 { 27 | let x = this.state; 28 | const count: u32 = u32(x >> u64(59)); 29 | this.state = x * multiplier + increment; 30 | x ^= x >> 18; 31 | 32 | return rotr(u32(x >> 27), count); 33 | } 34 | 35 | rand64(): u64 { 36 | return (u64(this.rand32()) << 32) | u64(this.rand32()); 37 | } 38 | 39 | updateSeed(seed: u64): void { 40 | this.state = seed * multiplier + increment; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /assembly/io/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2019 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import {currentMillis as wasiCurrentMillis} from './wasi/clock'; 20 | import {currentMillis as browserCurrentMillis} from './browser/clock'; 21 | 22 | import {readLine as wasiReadLine, writeLine as wasiWriteLine, writeError as wasiWriteError} from './wasi/stdio'; 23 | import {readLine as browserReadLine, writeLine as browserWriteLine, writeError as browserWriteError} from './browser/stdio'; 24 | 25 | export namespace clock { 26 | export const currentMillis = (IS_WASI == 1 ? wasiCurrentMillis : browserCurrentMillis); 27 | } 28 | 29 | export namespace stdio { 30 | export const readLine = (IS_WASI == 1 ? wasiReadLine : browserReadLine); 31 | export const writeLine = (IS_WASI == 1 ? wasiWriteLine : browserWriteLine); 32 | export const writeError = (IS_WASI == 1 ? wasiWriteError : browserWriteError); 33 | } -------------------------------------------------------------------------------- /src/ui/img/black_rook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /assembly/__tests__/perft.performance.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { fromFEN } from '../fen'; 20 | import { perft } from '../perft'; 21 | 22 | 23 | describe('Perft - Performance Test', () => { 24 | it('calculates correct moves for performance test', () => { 25 | expect(measurePerft("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 6)).toBe(119_060_324); 26 | expect(measurePerft("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1", 5)).toBe(193_690_690); 27 | }); 28 | }); 29 | 30 | function measurePerft(fen: string, depth: i32): u64 { 31 | const board = fromFEN(fen); 32 | 33 | const start = Date.now(); 34 | const result = perft(board, depth); 35 | 36 | const duration = Date.now() - start; 37 | const nodesPerSecond = result * 1000 / duration; 38 | 39 | trace(fen + ": Duration (ms) : " + duration.toString()); 40 | trace(fen + ": Nodes per second: " + nodesPerSecond.toString()); 41 | 42 | return result; 43 | } 44 | 45 | 46 | -------------------------------------------------------------------------------- /assembly/__tests__/random.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2019 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 20 | import { Random } from '../random'; 21 | 22 | describe("Random number generated", () => { 23 | 24 | it("quickly calculates evenly distributed random numbers", () => { 25 | const rnd = new Random(); 26 | 27 | const numberCounts = new Array(6); 28 | numberCounts.fill(0, 0, numberCounts.length); 29 | 30 | const iterations = 1_000_000; 31 | 32 | for (let i = 0; i < iterations; i++) { 33 | const number = u32(rnd.rand64() % 6); 34 | numberCounts[number]++; 35 | } 36 | 37 | const deviationTolerance = i32(iterations * 0.001); // accept a low deviation from the "ideal" distribution 38 | 39 | const idealDistribution = iterations / 6; 40 | for (let i = 0; i < numberCounts.length; i++) { 41 | const deviationFromIdeal = abs(idealDistribution - numberCounts[i]); 42 | expect(deviationFromIdeal).toBeLessThan(deviationTolerance); 43 | } 44 | }); 45 | 46 | }); 47 | 48 | -------------------------------------------------------------------------------- /assembly/__tests__/history.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 20 | import { PositionHistory } from '../history'; 21 | 22 | describe("Position History", () => { 23 | 24 | it("Detects threefold repetition", () => { 25 | const history = new PositionHistory(); 26 | history.push(1); 27 | history.push(1); 28 | expect(history.isThreefoldRepetion()).toBeFalsy("Position only occured twice"); 29 | 30 | history.push(1); 31 | expect(history.isThreefoldRepetion()).toBeTruthy("Threefold repetion not detected"); 32 | }); 33 | 34 | it("Detects single repetition", () => { 35 | const history = new PositionHistory(); 36 | history.push(1); 37 | expect(history.isSingleRepetition()).toBeFalsy("Position only occured once"); 38 | history.push(2); 39 | expect(history.isSingleRepetition()).toBeFalsy("Position only occured once"); 40 | history.push(1); 41 | expect(history.isSingleRepetition()).toBeTruthy("Single repetition not detected"); 42 | }); 43 | 44 | }); 45 | -------------------------------------------------------------------------------- /assembly/__tests__/move-notation.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2019 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { QUEEN } from '../pieces'; 20 | import { UCIMove } from '../uci-move-notation'; 21 | 22 | describe("UCIMove.fromUCINotation", () => { 23 | it("reads standard move", () => { 24 | const move = UCIMove.fromUCINotation("e2e4"); 25 | expect(move.start).toBe(52, "start") 26 | expect(move.end).toBe(36, "end"); 27 | expect(move.promotionPiece).toBe(0, "promotion piece"); 28 | }); 29 | 30 | it("reads promotion move", () => { 31 | const move = UCIMove.fromUCINotation("a7a8q"); 32 | expect(move.start).toBe(8, "start") 33 | expect(move.end).toBe(0, "end"); 34 | expect(move.promotionPiece).toBe(QUEEN, "promotion piece"); 35 | }); 36 | }); 37 | 38 | describe("UCIMove.toUCINotation", () => { 39 | it("writes standard move", () => { 40 | const move = new UCIMove(52, 36); 41 | expect(move.toUCINotation()).toBe("e2e4"); 42 | }); 43 | 44 | it("writes promotion move", () => { 45 | const move = new UCIMove(8, 0, QUEEN); 46 | expect(move.toUCINotation()).toBe("a7a8q"); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/ui/App.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import React from 'react'; 20 | import Game from './Game'; 21 | import { DndProvider } from 'react-dnd'; 22 | import HTML5Backend from 'react-dnd-html5-backend'; 23 | import TouchBackend from 'react-dnd-touch-backend'; 24 | 25 | const multiBackends = (...backendFactories) => 26 | function(manager) { 27 | const backends = backendFactories.map(b => b(manager)); 28 | return { 29 | setup: (...args) => 30 | backends.forEach(b => b.setup.apply(b, args)), 31 | teardown: (...args) => 32 | backends.forEach(b => b.teardown.apply(b, args)), 33 | connectDropTarget: (...args) => 34 | backends.forEach(b => b.connectDropTarget.apply(b, args)), 35 | connectDragPreview: (...args) => 36 | backends.forEach(b => b.connectDragPreview.apply(b, args)), 37 | connectDragSource: (...args) => 38 | backends.forEach(b => b.connectDragSource.apply(b, args)), 39 | }; 40 | }; 41 | 42 | function App() { 43 | return ( 44 |
45 | 46 | 47 | 48 |
49 | ); 50 | } 51 | 52 | export default App; 53 | -------------------------------------------------------------------------------- /assembly/history.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | export class PositionHistory { 20 | private positions: StaticArray = new StaticArray(1024); 21 | private index: i32 = 0; 22 | 23 | push(hash: u64): void { 24 | unchecked(this.positions[this.index++] = hash); 25 | } 26 | 27 | pop(): void { 28 | this.index--; 29 | } 30 | 31 | isThreefoldRepetion(): bool { 32 | if (this.index <= 2) { 33 | return false; 34 | } 35 | 36 | const hash = unchecked(this.positions[this.index - 1]); 37 | 38 | let count = 0; 39 | for (let i = 0; i < this.index - 1; i++) { 40 | if (unchecked(this.positions[i]) == hash) { 41 | count++; 42 | if (count == 2) { 43 | return true; 44 | } 45 | } 46 | } 47 | 48 | return false; 49 | } 50 | 51 | isSingleRepetition(): bool { 52 | if (this.index <= 1) { 53 | return false; 54 | } 55 | 56 | const hash = unchecked(this.positions[this.index - 1]); 57 | for (let i = 0; i < this.index - 1; i++) { 58 | if (unchecked(this.positions[i]) == hash) { 59 | return true; 60 | } 61 | } 62 | 63 | return false; 64 | } 65 | 66 | clear(): void { 67 | this.index = 0; 68 | } 69 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasabi-chess", 3 | "version": "1.5.1", 4 | "private": true, 5 | "homepage": "https://mhonert.github.io/chess", 6 | "license": "GPL-3.0-or-later", 7 | "dependencies": { 8 | "@fortawesome/fontawesome-svg-core": "^1.2.30", 9 | "@fortawesome/free-solid-svg-icons": "^5.14.0", 10 | "@fortawesome/react-fontawesome": "^0.1.11", 11 | "as-wasi": "^0.4.4", 12 | "react": "^16.13.1", 13 | "react-dnd": "^10.0.2", 14 | "react-dnd-html5-backend": "^10.0.2", 15 | "react-dnd-touch-backend": "^10.0.2", 16 | "react-dom": "^16.13.1", 17 | "react-scripts": "4.0.0", 18 | "styled-components": "^5.1.1" 19 | }, 20 | "scripts": { 21 | "build:web": "npm run build:web-engine && react-scripts build", 22 | "build:web-engine": "asc assembly/index.ts -O2 --exportRuntime --use IS_WASI=0 -b public/as-api.wasm", 23 | "build:uci": "asc assembly/uci.ts -O2 --use IS_WASI=1 --use abort=_abort -b build/engine.wasm -t build/engine.wast", 24 | "build": "npm run build:web", 25 | "start": "npm run build:web-engine && react-scripts start", 26 | "test": "asp --verbose", 27 | "perft": "asp --verbose -c as-pect-perft.config.js --file=perft.performance.ts", 28 | "perft:engine": "asp --verbose -c as-pect-perft.config.js --file=engine.performance.ts", 29 | "predeploy": "npm run build:web", 30 | "deploy": "gh-pages -d build", 31 | "version": "node tools/gen_version_ts.js && git add version.ts" 32 | }, 33 | "eslintConfig": { 34 | "extends": "react-app" 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@as-pect/cli": "^6.0.0", 50 | "assemblyscript": "0.18.15", 51 | "babel-plugin-macros": "^2.8.0", 52 | "gh-pages": "^2.2.0", 53 | "workerize-loader": "^1.3.0" 54 | }, 55 | "babelMacros": { 56 | "styledComponents": { 57 | "pure": true 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tools/bookgen/util.py: -------------------------------------------------------------------------------- 1 | # A free and open source chess game using AssemblyScript and React 2 | # Copyright (C) 2020 mhonert (https://github.com/mhonert) 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | # Convert from python-chess board location to Wasabi chess engine location system 18 | import numpy as np 19 | 20 | 21 | # Convert from python-chess board position to Wasabi engine board position 22 | def square_to_bb(sq): 23 | return (7 - int(sq / 8)) * 8 + (sq & 7) 24 | 25 | 26 | # Encode piece ID, from and to board position of a move as a single 16 bit unsigned integer 27 | def encode_move(piece_id, move_from, move_to): 28 | bb_from = square_to_bb(move_from) 29 | bb_to = square_to_bb(move_to) 30 | return piece_id | (bb_from << 3) | (bb_to << 10) 31 | 32 | 33 | # Right-rotate the bits of a 32 Bit integer 34 | def rotr32(n, rotations): 35 | return np.uint32(n >> rotations) | (n << (32 - rotations)) 36 | 37 | 38 | # Calculate pseudo-random numbers 39 | class Random: 40 | 41 | state = np.uint64(0x4d595df4d0f33173) 42 | multiplier = np.uint64(6364136223846793005) 43 | increment = np.uint64(1442695040888963407) 44 | 45 | def rand32(self): 46 | x = self.state 47 | count = np.uint32(x >> np.uint64(59)) 48 | self.state = x * self.multiplier + self.increment 49 | x ^= x >> np.uint64(18) 50 | 51 | return np.uint32(rotr32(np.uint32(x >> np.uint64(27)), count)) 52 | 53 | def rand64(self): 54 | return (np.uint64(self.rand32()) << np.uint64(32)) | np.uint64(self.rand32()) 55 | -------------------------------------------------------------------------------- /src/ui/img/black_pawn.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 29 | 49 | 54 | 55 | -------------------------------------------------------------------------------- /src/ui/img/white_pawn.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 29 | 49 | 54 | 55 | -------------------------------------------------------------------------------- /assembly/opening-book.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { Board } from './board'; 20 | import { Random } from './random'; 21 | import { clock } from './io'; 22 | import { getOpeningBookI32, getOpeningBookU32 } from './opening-book-data'; 23 | 24 | const rnd = new Random(); 25 | 26 | // Returns an encoded opening move the current position or 0 if none exists 27 | export function findOpeningMove(board: Board): i32 { 28 | const ply = board.getHalfMoveCount(); 29 | const maxPly = getOpeningBookPlyLimit(); 30 | if (ply >= maxPly) { 31 | return 0; 32 | } 33 | 34 | const startIndex = getOpeningBookU32(ply + 1); 35 | let entriesLeft = getOpeningBookI32(startIndex); 36 | 37 | const boardLowHash = board.getHash() & 0xFFFFFFFF; 38 | const boardHighHash = board.getHash() >> 32; 39 | 40 | let index = startIndex + 1; 41 | do { 42 | const moveCount = getOpeningBookU32(index + 2); 43 | 44 | if (boardLowHash == getOpeningBookU32(index) && boardHighHash == getOpeningBookU32(index + 1)) { 45 | const selectedMoveNum = rnd.rand32() % moveCount; 46 | return getOpeningBookI32(index + 3 + selectedMoveNum); 47 | } 48 | 49 | index += moveCount + 3; 50 | entriesLeft--; 51 | } while (entriesLeft > 0); 52 | 53 | // No move found 54 | return 0; 55 | } 56 | 57 | export function getOpeningBookPlyLimit(): i32 { 58 | return getOpeningBookI32(0); 59 | } 60 | 61 | export function randomizeOpeningBookMoves(): void { 62 | rnd.updateSeed(u64(clock.currentMillis())); 63 | } -------------------------------------------------------------------------------- /assembly/__tests__/history-heuristics.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2019 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { HistoryHeuristics } from '../history-heuristics'; 20 | import { encodeMove } from '../move-generation'; 21 | import { QUEEN, ROOK } from '../pieces'; 22 | import { WHITE } from '../board'; 23 | 24 | describe("Killer move table", () => { 25 | 26 | it("Sets primary and secondary killer move entries correctly", () => { 27 | const table = new HistoryHeuristics(); 28 | const moveA = encodeMove(QUEEN, 1, 2); 29 | const moveB = encodeMove(ROOK, 4, 5); 30 | table.update(1, WHITE, 1, 2, moveA); 31 | table.update(1, WHITE, 4, 5, moveB); 32 | 33 | const primaryKiller = table.getPrimaryKiller(1); 34 | const secondaryKiller = table.getSecondaryKiller(1); 35 | 36 | expect(primaryKiller).toBe(moveB, "primary move set"); 37 | expect(secondaryKiller).toBe(moveA, "secondary move set"); 38 | }) 39 | 40 | it("Same move is not stored in primary and secondary slot", () => { 41 | const table = new HistoryHeuristics(); 42 | const moveA = encodeMove(QUEEN, 1, 2); 43 | const moveB = encodeMove(ROOK, 4, 5); 44 | table.update(1, WHITE, 4, 5, moveB); 45 | table.update(1, WHITE, 1, 2, moveA); 46 | table.update(1, WHITE, 1, 2, moveA); 47 | 48 | const primaryKiller = table.getPrimaryKiller(1); 49 | const secondaryKiller = table.getSecondaryKiller(1); 50 | 51 | expect(primaryKiller).toBe(moveA, "primary move set"); 52 | expect(secondaryKiller).toBe(moveB, "secondary move set"); 53 | }) 54 | 55 | }); 56 | 57 | -------------------------------------------------------------------------------- /assembly/perft.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { Board } from './board'; 20 | import { 21 | decodeEndIndex, 22 | decodePiece, 23 | decodeStartIndex, 24 | generateMoves, 25 | } from './move-generation'; 26 | 27 | 28 | /* Perft (performance test, move path enumeration) test helper function to verify the move generator. 29 | It generates all possible moves up to the specified depth and counts the number of leaf nodes. 30 | This number can then be compared to precalculated numbers that are known to be correct 31 | (see __tests__/perft.spec.ts). 32 | 33 | Another use for this function is to test the performance of the move generator (see __tests__/perft.performance.ts). 34 | */ 35 | export function perft(board: Board, depth: i32): u64 { 36 | if (depth == 0) { 37 | return 1 38 | } 39 | 40 | let nodes: u64 = 0; 41 | 42 | const activePlayer = board.getActivePlayer(); 43 | const moves = generateMoves(board, activePlayer); 44 | 45 | for (let i: i32 = 0; i < moves.length; i++) { 46 | const move = unchecked(moves[i]); 47 | 48 | const targetPieceId = decodePiece(move); 49 | const moveStart = decodeStartIndex(move); 50 | const moveEnd = decodeEndIndex(move); 51 | const previousPiece = board.getItem(moveStart); 52 | 53 | const removedPiece = board.performMove(targetPieceId, moveStart, moveEnd); 54 | 55 | if (!board.isInCheck(activePlayer)) { 56 | nodes += perft(board, depth - 1); 57 | } 58 | board.undoMove(previousPiece, moveStart, moveEnd, removedPiece); 59 | } 60 | 61 | return nodes; 62 | } 63 | 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## :sushi: Wasabi Chess Engine 2 | 3 | ![Release](https://img.shields.io/github/v/release/mhonert/chess) 4 | ![Test](https://img.shields.io/github/actions/workflow/status/mhonert/chess/test.yml?label=Test&logo=github) 5 | [![Website mhonert.github.io./chess](https://img.shields.io/website?url=https%3A%2F%2Fmhonert.github.io%2Fchess)](https://mhonert.github.io/chess) 6 | [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) 7 | 8 | [Screenshot](screenshots/chess_mobile.png?raw=true) 9 | 10 | **Wasabi Chess** is a web-based chess engine, written in AssemblyScript that runs directly in the Browser. 11 | 12 | > [**Play here!**](https://mhonert.github.io/chess) 13 | 14 | The React web application embeds the engine using [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) 15 | to compute the AI moves in the background without blocking the main thread for the UI. 16 | 17 | ### Features 18 | 19 | - Chess engine implemented in [AssemblyScript](https://github.com/AssemblyScript/assemblyscript) 20 | - Computer opponent with 6 difficulty levels 21 | - Opening Database for varied gameplay 22 | - Move history to undo player moves 23 | - Drag'n'Drop support to move chess pieces 24 | - Touch support for mobile devices 25 | 26 | ### Built With 27 | * [AssemblyScript](https://github.com/AssemblyScript/assemblyscript) - for the chess engine 28 | * [as-pect](https://github.com/jtenner/as-pect) - to test the engine 29 | * [react](https://reactjs.org/) - for the user interface 30 | * [react-dnd](https://github.com/react-dnd/react-dnd) - for Drag and Drop support 31 | * [styled-components](https://www.styled-components.com/) - to style React components in JS 32 | * [react-fontawesome](https://github.com/FortAwesome/react-fontawesome) - for some font icons 33 | * [workerize-loader](https://github.com/developit/workerize-loader) - to load modules as Web Workers 34 | * [as-wasi](https://github.com/jedisct1/as-wasi) - for WASI system calls (only for the standalone UCI engine) 35 | 36 | ### License 37 | This project is licensed under the GNU General Public License - see the [LICENSE](LICENSE) for details. 38 | 39 | ### Attributions 40 | * Images for the chess pieces come from [Wikimedia Commons](https://commons.wikimedia.org/wiki/Category:SVG_chess_pieces) 41 | * The opening book was generated from a selection of chess games from the [FICS Games Database](https://www.ficsgames.org) 42 | * A set of 725000 [test positions](https://bitbucket.org/zurichess/tuner/downloads/) collected by the author of Zurichess was used to tune all evaluation parameters 43 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 35 | 36 | 45 | Chess 46 | 47 | 48 | 49 |
50 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /assembly/__tests__/transposition-table.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { 20 | getDepth, 21 | getScoredMove, getScoreType, 22 | ScoreType, 23 | TRANSPOSITION_MAX_DEPTH, 24 | TranspositionTable 25 | } from '../transposition-table'; 26 | import { decodeScore, encodeMove, encodeScoredMove } from '../move-generation'; 27 | import { MAX_SCORE, MIN_SCORE } from '../engine'; 28 | 29 | describe("Transposition table", () => { 30 | 31 | it("writes entry correctly", () => { 32 | const hash: u64 = u64.MAX_VALUE; 33 | const depth = TRANSPOSITION_MAX_DEPTH; 34 | const move = encodeMove(5, 32, 33); 35 | const score: i32 = -10; 36 | const type = ScoreType.EXACT; 37 | 38 | const tt = new TranspositionTable(); 39 | tt.writeEntry(hash, depth, encodeScoredMove(move, score), type); 40 | 41 | const entry: u64 = tt.getEntry(hash); 42 | 43 | expect(getScoredMove(entry)).toBe(encodeScoredMove(move, score), "move does not match"); 44 | expect(getDepth(entry)).toBe(depth, "Depth does not match"); 45 | expect(getScoreType(entry)).toBe(type, "Type does not match"); 46 | }); 47 | 48 | it("encodes negative score correctly", () => { 49 | const hash: u64 = u64.MAX_VALUE; 50 | const score: i32 = MIN_SCORE; 51 | 52 | const tt = new TranspositionTable(); 53 | tt.writeEntry(hash, 1, encodeScoredMove(0, score), ScoreType.EXACT); 54 | 55 | const entry: u64 = tt.getEntry(hash); 56 | expect(decodeScore(getScoredMove(entry))).toBe(score, "Score does not match"); 57 | }); 58 | 59 | it("encodes positive score correctly", () => { 60 | const hash: u64 = u64.MAX_VALUE; 61 | const score: i32 = MAX_SCORE; 62 | 63 | const tt = new TranspositionTable(); 64 | tt.writeEntry(hash, 1, encodeScoredMove(0, score), ScoreType.EXACT); 65 | 66 | const entry: u64 = tt.getEntry(hash); 67 | expect(decodeScore(getScoredMove(entry))).toBe(score, "Score does not match"); 68 | }); 69 | 70 | }); 71 | 72 | -------------------------------------------------------------------------------- /src/ui/img/white_knight.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 29 | 49 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/ui/Board.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import React from 'react'; 20 | import Field from './Field'; 21 | import Piece from './Piece'; 22 | import styled from 'styled-components/macro'; 23 | import { BLACK, K, WHITE } from '../engine/constants'; 24 | 25 | const BoardGrid = styled.div` 26 | display: grid; 27 | grid-template-columns: repeat(8, 1fr); 28 | grid-template-rows: repeat(8, 1fr); 29 | 30 | @media (min-aspect-ratio: 99/100) { 31 | width: 100vh; 32 | height: 100vh; 33 | } 34 | @media (max-aspect-ratio: 100/99) { 35 | width: 100vw; 36 | height: 100vw; 37 | } 38 | 39 | box-shadow: 3px 3px 3px #586e75; 40 | `; 41 | 42 | const Board = ({board, isRotated, inCheck, lastMove, currentPieceMoves, handlePlayerMove, updatePossibleMoves, clearPossibleMoves}) => { 43 | return ( 44 | 45 | {board.slice(0, 64).map((_, idx) => { 46 | const rotatedIndex = isRotated ? 63 - idx : idx; 47 | const item = board[rotatedIndex]; 48 | 49 | return ( 50 | > 3)) % 2 === 0} 55 | isStart={rotatedIndex === lastMove.start} 56 | isEnd={rotatedIndex === lastMove.end} 57 | isPossibleTarget={currentPieceMoves.has(rotatedIndex)} 58 | isInCheck={Math.abs(item) === K && Math.sign(item) === inCheck} 59 | > 60 | {item !== 0 && ( 61 | 68 | )} 69 | 70 | ); 71 | })} 72 | 73 | ); 74 | }; 75 | 76 | export default Board; -------------------------------------------------------------------------------- /assembly/move-ordering.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { decodeScore } from './move-generation'; 20 | 21 | const CAPTURE_ORDER_SCORES = createCaptureOrderScores(); 22 | 23 | // Returns a higher score for capturing more valuable victims using less valuable attackers (MVV-LVA). 24 | @inline 25 | export function getCaptureOrderScore(attackerPieceId: i32, victimPieceId: i32): i32 { 26 | return unchecked(CAPTURE_ORDER_SCORES[((attackerPieceId - 1) * 8 + (victimPieceId - 1))]); 27 | } 28 | 29 | @inline 30 | export function sortByScoreDescending(moves: StaticArray): void { 31 | // Basic insertion sort 32 | for (let i = 1; i < moves.length; i++) { 33 | const x = unchecked(moves[i]); 34 | const xScore = decodeScore(x); 35 | let j = i - 1; 36 | while (j >= 0) { 37 | const y = unchecked(moves[j]); 38 | if (decodeScore(y) >= xScore) { 39 | break; 40 | } 41 | unchecked(moves[j + 1] = y); 42 | j--; 43 | } 44 | unchecked(moves[j + 1] = x); 45 | } 46 | } 47 | 48 | @inline 49 | export function sortByScoreAscending(moves: StaticArray): void { 50 | // Basic insertion sort 51 | for (let i = 1; i < moves.length; i++) { 52 | const x = unchecked(moves[i]); 53 | const xScore = decodeScore(x); 54 | let j = i - 1; 55 | while (j >= 0) { 56 | const y = unchecked(moves[j]); 57 | if (decodeScore(y) <= xScore) { 58 | break; 59 | } 60 | unchecked(moves[j + 1] = y); 61 | j--; 62 | } 63 | unchecked(moves[j + 1] = x); 64 | } 65 | } 66 | 67 | 68 | // Order capture moves first by most valuable victim and then by least valuable attacker (MVV-LVA) 69 | function createCaptureOrderScores(): StaticArray { 70 | const scores = new StaticArray(5 + 5 * 8 + 1); 71 | 72 | let orderScore: i32 = 0; 73 | for (let victim = 0; victim <= 5; victim++) { 74 | for (let attacker = 5; attacker >= 0; attacker--) { 75 | scores[victim + attacker * 8] = orderScore * 64; 76 | orderScore++; 77 | } 78 | } 79 | 80 | return scores; 81 | } 82 | 83 | -------------------------------------------------------------------------------- /src/ui/PromotionPieceSelection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2019 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import styled from 'styled-components/macro'; 20 | import { B, BLACK, N, Q, R, WHITE } from '../engine/constants'; 21 | import { PieceImage } from './Piece'; 22 | import React from 'react'; 23 | 24 | const Overlay = styled.div` 25 | outline: 1px solid #586e75; 26 | box-shadow: 3px 3px 3px #586e75; 27 | margin: 0; 28 | padding: 0; 29 | position: absolute; 30 | z-index: 1; 31 | 32 | @media (min-aspect-ratio: 99/100) { 33 | top: ${props => props.row * 12.5}vh; 34 | left: ${props => props.column * 12.5}vh; 35 | width: 12.5vh; 36 | height: 50vh; 37 | } 38 | @media (max-aspect-ratio: 100/99) { 39 | top: ${props => props.row * 12.5}vw; 40 | left: ${props => props.column * 12.5}vw; 41 | width: 12.5vw; 42 | height: 50vw; 43 | } 44 | `; 45 | 46 | const FieldDiv = styled.div` 47 | position: absolute; 48 | background-color: ${props => props.isEven ? '#fdf6e3' : '#eee8d5'}; 49 | 50 | @media (min-aspect-ratio: 99/100) { 51 | top: ${props => props.row * 12.5}vh; 52 | width: 12.5vh; 53 | height: 12.5vh; 54 | } 55 | @media (max-aspect-ratio: 100/99) { 56 | top: ${props => props.row * 12.5}vw; 57 | width: 12.5vw; 58 | height: 12.5vw; 59 | } 60 | `; 61 | 62 | const PromotionPieceSelection = ({ column, playerColor, onSelection, isRotated }) => { 63 | const rotatedColumn = isRotated ? 7 - column : column; 64 | 65 | const top = playerColor === WHITE 66 | ? isRotated ? 4 : 0 67 | : isRotated ? 0 : 4; 68 | 69 | let pieces = (isRotated && playerColor !== BLACK) || (!isRotated && playerColor === BLACK) ? [N, B, R, Q] : [Q, R, B, N]; 70 | 71 | return ( 72 | 73 | { 74 | pieces.map((piece, index) => ( 75 | onSelection(piece)}> 76 | 77 | 78 | )) 79 | } 80 | 81 | ); 82 | 83 | }; 84 | 85 | export default PromotionPieceSelection; 86 | -------------------------------------------------------------------------------- /src/ui/img/black_bishop.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 29 | 49 | 53 | 56 | 60 | 64 | 68 | 69 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/ui/img/white_bishop.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 29 | 49 | 53 | 56 | 60 | 64 | 68 | 69 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /assembly/__tests__/engine.performance.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { fromFEN } from '../fen'; 20 | import EngineControl from '../engine'; 21 | import { calculatePieceSquareTables } from '../board'; 22 | 23 | describe('Engine performance', () => { 24 | it('plays against itself', () => { 25 | const start = Date.now(); 26 | let nodeCount: u64 = 0; 27 | nodeCount += measureEnginePerformance("2r2rk1/1b3p1p/p5p1/q2pb3/Pp6/3BP2P/1P1NQPP1/3R1RK1 w - - 0 1", 12); // bm Nf3 28 | nodeCount += measureEnginePerformance("2rq1rk1/p2nbppp/b1p1p3/8/2pPP3/2B3P1/P2N1PBP/R2QR1K1 w - - 0 1", 12); // bm Qa4 29 | nodeCount += measureEnginePerformance("r1qr2k1/p3bppp/1p1Bpn2/1P6/2PQb3/5NP1/P3PPBP/R2R2K1 w - - 0 1", 12); // bm c5 30 | nodeCount += measureEnginePerformance("r1r2k2/pp2ppbp/3pb1p1/3N4/2P1P1n1/1P6/P2BBPPP/1R3RK1 w - - 0 1", 12); // bm Bg5 31 | nodeCount += measureEnginePerformance("r1r3k1/1p1nppbp/p2pb1p1/8/N1P1P3/1P2BP2/P2KB1PP/2R4R w - - 0 1", 12); // bm g4 32 | nodeCount += measureEnginePerformance("r1r3k1/2q1bppp/pn1p1n2/4pP2/2b1P3/1NN1B3/1PP1B1PP/R2Q1R1K w - - 0 1", 12); // bm Na5 33 | nodeCount += measureEnginePerformance("r2q1k1r/pb3pp1/4p2p/1BnnP3/1p6/5N2/PP3PPP/R1BQ1RK1 w - - 0 1", 12); // bm a3 34 | nodeCount += measureEnginePerformance("r2q1nk1/pp2r1pp/1np1bp2/3p4/3P1P2/2NBPN2/PPQ3PP/4RRK1 w - - 0 1", 12); // bm Kh1 35 | nodeCount += measureEnginePerformance("r2q1r1k/pb1nbppB/2p1p2p/4P3/1p1P4/5N2/PPQ2PPP/R1B2RK1 w - - 0 1", 12); // bm Be4 36 | 37 | const duration = Date.now() - start; 38 | trace("-------------------------------------------------------"); 39 | trace("Total nodes : " + nodeCount.toString()); 40 | trace("Duration (ms) : " + duration.toString()); 41 | const nodesPerSecond = duration > 0 ? nodeCount * 1000 / duration : 0; 42 | trace("Nodes per second: " + nodesPerSecond.toString()); 43 | 44 | }); 45 | 46 | }); 47 | 48 | 49 | function measureEnginePerformance(fen: string, depth: i32): u64 { 50 | calculatePieceSquareTables(); 51 | EngineControl.reset(); 52 | EngineControl.setBoard(fromFEN(fen)); 53 | const move = EngineControl.findBestMove(depth, 0, true); 54 | 55 | return EngineControl.getNodeCount(); 56 | } 57 | 58 | 59 | -------------------------------------------------------------------------------- /tools/bookgen/zobrist.py: -------------------------------------------------------------------------------- 1 | # A free and open source chess game using AssemblyScript and React 2 | # Copyright (C) 2020 mhonert (https://github.com/mhonert) 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import chess 18 | import numpy as np 19 | 20 | from util import square_to_bb, Random 21 | 22 | WHITE_KING_SIDE_CASTLING = 1 << 0 23 | BLACK_KING_SIDE_CASTLING = 1 << 1 24 | WHITE_QUEEN_SIDE_CASTLING = 1 << 2 25 | BLACK_QUEEN_SIDE_CASTLING = 1 << 3 26 | 27 | BIT_POS = [square_to_bb(sq) for sq in range(64)] 28 | 29 | 30 | # Calculates the zobrist hash for the current board position 31 | def calc_hash(board): 32 | hash = np.uint64(0) 33 | for idx, bitPos in enumerate(BIT_POS): 34 | piece = board.piece_at(bitPos) 35 | if piece: 36 | if piece.color: 37 | hash ^= (PIECE_RNG_NUMBERS[(piece.piece_type + 6) * 64 + idx]) 38 | 39 | else: 40 | hash ^= (PIECE_RNG_NUMBERS[(-piece.piece_type + 6) * 64 + idx]) 41 | 42 | if not board.turn: 43 | hash ^= PLAYER_RNG_NUMBER 44 | 45 | castling_bits = 0 46 | if board.castling_rights & chess.BB_A8: 47 | castling_bits |= BLACK_QUEEN_SIDE_CASTLING 48 | 49 | if board.castling_rights & chess.BB_H8: 50 | castling_bits |= BLACK_KING_SIDE_CASTLING 51 | 52 | if board.castling_rights & chess.BB_H1: 53 | castling_bits |= WHITE_KING_SIDE_CASTLING 54 | 55 | if board.castling_rights & chess.BB_A1: 56 | castling_bits |= WHITE_QUEEN_SIDE_CASTLING 57 | 58 | hash ^= CASTLING_RNG_NUMBERS[castling_bits] 59 | 60 | if board.ep_square is not None: 61 | ep_bit = 0 62 | if chess.A3 <= board.ep_square <= chess.H3: 63 | ep_bit |= ((board.ep_square - 16) + 8) 64 | 65 | if chess.A6 <= board.ep_square <= chess.H6: 66 | ep_bit |= (board.ep_square - 40) 67 | 68 | hash ^= EN_PASSANT_RNG_NUMBERS[ep_bit] 69 | 70 | return hash 71 | 72 | 73 | rnd = Random() 74 | 75 | 76 | def rand_array(count): 77 | numbers = [] 78 | for i in range(count): 79 | numbers.append(rnd.rand64()) 80 | return numbers 81 | 82 | 83 | def last_element_zero(elements): 84 | elements[len(elements) - 1] = np.uint64(0) 85 | return elements 86 | 87 | 88 | PIECE_RNG_NUMBERS = rand_array(13 * 64) 89 | PLAYER_RNG_NUMBER = rnd.rand64() 90 | EN_PASSANT_RNG_NUMBERS = rand_array(16) 91 | 92 | CASTLING_RNG_NUMBERS = last_element_zero(rand_array(16)) 93 | -------------------------------------------------------------------------------- /src/ui/img/black_knight.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 29 | 49 | 53 | 57 | 61 | 65 | 70 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /tools/bookgen/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "7e30dd3518ae2677056069bc5f19f3b91a23312d8e875012cefc8cb96e22caa9" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.9" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "chess": { 20 | "hashes": [ 21 | "sha256:590c979f25a26b8c621c6c312278fb5ba6cd6bdf39af9bd2553a7f1732295120", 22 | "sha256:823ab1bc8b1674e37e77e8d4ff8e758f37551dcdee4d106438fa22f12d3a7d9b" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.4.0" 26 | }, 27 | "numpy": { 28 | "hashes": [ 29 | "sha256:032be656d89bbf786d743fee11d01ef318b0781281241997558fa7950028dd29", 30 | "sha256:104f5e90b143dbf298361a99ac1af4cf59131218a045ebf4ee5990b83cff5fab", 31 | "sha256:125a0e10ddd99a874fd357bfa1b636cd58deb78ba4a30b5ddb09f645c3512e04", 32 | "sha256:12e4ba5c6420917571f1a5becc9338abbde71dd811ce40b37ba62dec7b39af6d", 33 | "sha256:13adf545732bb23a796914fe5f891a12bd74cf3d2986eed7b7eba2941eea1590", 34 | "sha256:2d7e27442599104ee08f4faed56bb87c55f8b10a5494ac2ead5c98a4b289e61f", 35 | "sha256:3bc63486a870294683980d76ec1e3efc786295ae00128f9ea38e2c6e74d5a60a", 36 | "sha256:3d3087e24e354c18fb35c454026af3ed8997cfd4997765266897c68d724e4845", 37 | "sha256:4ed8e96dc146e12c1c5cdd6fb9fd0757f2ba66048bf94c5126b7efebd12d0090", 38 | "sha256:60759ab15c94dd0e1ed88241fd4fa3312db4e91d2c8f5a2d4cf3863fad83d65b", 39 | "sha256:65410c7f4398a0047eea5cca9b74009ea61178efd78d1be9847fac1d6716ec1e", 40 | "sha256:66b467adfcf628f66ea4ac6430ded0614f5cc06ba530d09571ea404789064adc", 41 | "sha256:7199109fa46277be503393be9250b983f325880766f847885607d9b13848f257", 42 | "sha256:72251e43ac426ff98ea802a931922c79b8d7596480300eb9f1b1e45e0543571e", 43 | "sha256:89e5336f2bec0c726ac7e7cdae181b325a9c0ee24e604704ed830d241c5e47ff", 44 | "sha256:89f937b13b8dd17b0099c7c2e22066883c86ca1575a975f754babc8fbf8d69a9", 45 | "sha256:9c94cab5054bad82a70b2e77741271790304651d584e2cdfe2041488e753863b", 46 | "sha256:9eb551d122fadca7774b97db8a112b77231dcccda8e91a5bc99e79890797175e", 47 | "sha256:a1d7995d1023335e67fb070b2fae6f5968f5be3802b15ad6d79d81ecaa014fe0", 48 | "sha256:ae61f02b84a0211abb56462a3b6cd1e7ec39d466d3160eb4e1da8bf6717cdbeb", 49 | "sha256:b9410c0b6fed4a22554f072a86c361e417f0258838957b78bd063bde2c7f841f", 50 | "sha256:c26287dfc888cf1e65181f39ea75e11f42ffc4f4529e5bd19add57ad458996e2", 51 | "sha256:c91ec9569facd4757ade0888371eced2ecf49e7982ce5634cc2cf4e7331a4b14", 52 | "sha256:ecb5b74c702358cdc21268ff4c37f7466357871f53a30e6f84c686952bef16a9" 53 | ], 54 | "index": "pypi", 55 | "version": "==1.20.1" 56 | } 57 | }, 58 | "develop": {} 59 | } 60 | -------------------------------------------------------------------------------- /src/ui/img/black_king.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 29 | 49 | 53 | 58 | 63 | 68 | 73 | 78 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/ui/img/white_king.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 29 | 49 | 53 | 58 | 63 | 68 | 73 | 78 | 83 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: "v*" 6 | 7 | jobs: 8 | release: 9 | if: github.repository == 'mhonert/chess' 10 | name: Publish release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | 16 | - name: Setup node 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: '14.x' 20 | 21 | - name: Build UCI engine 22 | env: 23 | CI: true 24 | run: | 25 | npm ci 26 | npm test 27 | npm run build:uci 28 | cp build/engine.wasm artifacts/linux 29 | cp build/engine.wasm artifacts/windows 30 | cp LICENSE artifacts/linux 31 | cp LICENSE artifacts/windows 32 | 33 | - name: Get WAVM 34 | env: 35 | WAVM_VERSION: "2020-05-13" 36 | run: | 37 | mkdir -p artifacts/linux/wavm 38 | curl -L -o artifacts/wavm-linux.tar.gz https://github.com/WAVM/WAVM/releases/download/nightly%2F${WAVM_VERSION}/wavm-0.0.0-prerelease-linux.tar.gz 39 | 40 | mkdir -p artifacts/windows/wavm 41 | curl -L -o artifacts/wavm-windows.zip https://github.com/WAVM/WAVM/releases/download/nightly%2F${WAVM_VERSION}/wavm-0.0.0-prerelease-windows.zip 42 | 43 | sha256sum -c artifacts/wavm_checksums.txt 44 | 45 | tar xf artifacts/wavm-linux.tar.gz -C artifacts/linux/wavm 46 | rm -rf artifacts/linux/wavm/examples 47 | rm -rf artifacts/linux/wavm/include 48 | rm -rf artifacts/linux/wavm/lib 49 | rm artifacts/wavm-linux.tar.gz 50 | 51 | unzip artifacts/wavm-windows.zip -d artifacts/windows/wavm 52 | rm -rf artifacts/windows/wavm/examples 53 | rm -rf artifacts/windows/wavm/include 54 | rm -rf artifacts/windows/wavm/lib 55 | rm artifacts/wavm-windows.zip 56 | cp artifacts/windows/wavm/LICENSE.txt artifacts/linux/wavm 57 | 58 | - name: Package artifacts 59 | run: | 60 | tag_name="${GITHUB_REF##*/}" 61 | 62 | pushd artifacts/linux 63 | tar czvf ../wasabi-${tag_name}-linux.tar.gz * 64 | popd 65 | 66 | pushd artifacts/windows 67 | zip -r ../wasabi-${tag_name}-windows.zip * 68 | popd 69 | 70 | cp artifacts/linux/engine.wasm artifacts/wasabi-${tag_name}-engine.wasm 71 | 72 | pushd artifacts 73 | sha256sum wasabi* > checksums.txt 74 | popd 75 | 76 | - name: Test linux executable 77 | timeout-minutes: 1 78 | run: | 79 | ./artifacts/linux/wasabi < ./artifacts/engine_selftest.txt 80 | 81 | - name: Create Release 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | run: | 85 | pushd artifacts 86 | tag_name="${GITHUB_REF##*/}" 87 | echo "$tag_name" > release_description.txt 88 | cat release_notes.md >> release_description.txt 89 | hub release create -a "checksums.txt#Checksums" -a "wasabi-${tag_name}-linux.tar.gz#Wasabi Chess for Linux 64-bit" \ 90 | -a "wasabi-${tag_name}-windows.zip#Wasabi Chess for Windows 64-bit" -F release_description.txt "$tag_name" \ 91 | -a "wasabi-${tag_name}-engine.wasm#Standalone engine.wasm" -F release_description.txt "$tag_name" 92 | -------------------------------------------------------------------------------- /assembly/__tests__/move-ordering.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { decodeScore, encodeScoredMove } from '../move-generation'; 20 | import { sortByScoreAscending, sortByScoreDescending } from '../move-ordering'; 21 | 22 | describe("Move list sorting", () => { 23 | 24 | it("sorts moves descending by score", () => { 25 | const moves: StaticArray = StaticArray.fromArray([encodeScoredMove(0, 12), encodeScoredMove(1, 5), encodeScoredMove(2, 27), encodeScoredMove(3, 15)]); 26 | 27 | sortByScoreDescending(moves); 28 | 29 | expect(decodeScore(moves[0])).toBe(27); 30 | expect(decodeScore(moves[1])).toBe(15); 31 | expect(decodeScore(moves[2])).toBe(12); 32 | expect(decodeScore(moves[3])).toBe(5); 33 | }); 34 | 35 | it("sorts empty move list descending", () => { 36 | sortByScoreDescending(new StaticArray(0)); 37 | }); 38 | 39 | it("sorts moves with 1 element descending", () => { 40 | const moves: StaticArray = StaticArray.fromArray([encodeScoredMove(0, 12)]); 41 | sortByScoreDescending(moves); 42 | 43 | expect(decodeScore(moves[0])).toBe(12); 44 | }); 45 | 46 | it("sorts moves with 2 elements descending", () => { 47 | const moves: StaticArray = StaticArray.fromArray([encodeScoredMove(0, 5), encodeScoredMove(1, 12)]); 48 | sortByScoreDescending(moves); 49 | 50 | expect(decodeScore(moves[0])).toBe(12); 51 | expect(decodeScore(moves[1])).toBe(5); 52 | }); 53 | 54 | it("sorts moves ascending by score for black player", () => { 55 | const moves: StaticArray = StaticArray.fromArray([encodeScoredMove(0, 12), encodeScoredMove(1, 5), encodeScoredMove(2, 27), encodeScoredMove(3, 15)]); 56 | 57 | sortByScoreAscending(moves); 58 | 59 | expect(decodeScore(moves[0])).toBe(5); 60 | expect(decodeScore(moves[1])).toBe(12); 61 | expect(decodeScore(moves[2])).toBe(15); 62 | expect(decodeScore(moves[3])).toBe(27); 63 | }); 64 | 65 | it("sorts empty move list ascending", () => { 66 | sortByScoreAscending(new StaticArray(0)); 67 | }); 68 | 69 | it("sorts moves with 1 element ascending", () => { 70 | const moves: StaticArray = StaticArray.fromArray([encodeScoredMove(0, 12)]); 71 | sortByScoreAscending(moves); 72 | 73 | expect(decodeScore(moves[0])).toBe(12); 74 | }); 75 | 76 | it("sorts moves with 2 elements ascending", () => { 77 | const moves: StaticArray = StaticArray.fromArray([encodeScoredMove(0, 12), encodeScoredMove(1, 5)]); 78 | sortByScoreAscending(moves); 79 | 80 | expect(decodeScore(moves[0])).toBe(5); 81 | expect(decodeScore(moves[1])).toBe(12); 82 | }); 83 | }); 84 | 85 | -------------------------------------------------------------------------------- /src/ui/AnimatedSpinner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import styled from 'styled-components/macro'; 20 | import React from 'react'; 21 | 22 | /* Animated spinner adapted from https://loading.io/css/ */ 23 | const Spinner = styled.div` 24 | color: #073642; 25 | display: inline-block; 26 | position: relative; 27 | width: 80px; 28 | height: 80px; 29 | 30 | margin-top: 1rem; 31 | margin-right: 0.5rem; 32 | margin-left: 1.5rem; 33 | 34 | div { 35 | transform-origin: 40px 40px; 36 | animation: lds-spinner 1.2s linear infinite; 37 | } 38 | div:after { 39 | content: " "; 40 | display: block; 41 | position: absolute; 42 | top: 3px; 43 | left: 37px; 44 | width: 6px; 45 | height: 18px; 46 | border-radius: 20%; 47 | background: #073642; 48 | } 49 | div:nth-child(1) { 50 | transform: rotate(0deg); 51 | animation-delay: -1.1s; 52 | } 53 | div:nth-child(2) { 54 | transform: rotate(30deg); 55 | animation-delay: -1s; 56 | } 57 | div:nth-child(3) { 58 | transform: rotate(60deg); 59 | animation-delay: -0.9s; 60 | } 61 | div:nth-child(4) { 62 | transform: rotate(90deg); 63 | animation-delay: -0.8s; 64 | } 65 | div:nth-child(5) { 66 | transform: rotate(120deg); 67 | animation-delay: -0.7s; 68 | } 69 | div:nth-child(6) { 70 | transform: rotate(150deg); 71 | animation-delay: -0.6s; 72 | } 73 | div:nth-child(7) { 74 | transform: rotate(180deg); 75 | animation-delay: -0.5s; 76 | } 77 | div:nth-child(8) { 78 | transform: rotate(210deg); 79 | animation-delay: -0.4s; 80 | } 81 | div:nth-child(9) { 82 | transform: rotate(240deg); 83 | animation-delay: -0.3s; 84 | } 85 | div:nth-child(10) { 86 | transform: rotate(270deg); 87 | animation-delay: -0.2s; 88 | } 89 | div:nth-child(11) { 90 | transform: rotate(300deg); 91 | animation-delay: -0.1s; 92 | } 93 | div:nth-child(12) { 94 | transform: rotate(330deg); 95 | animation-delay: 0s; 96 | } 97 | @keyframes lds-spinner { 98 | 0% { 99 | opacity: 1; 100 | } 101 | 100% { 102 | opacity: 0; 103 | } 104 | } 105 | ` 106 | 107 | export const AnimatedSpinner = () => ( 108 | 109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | ); 123 | 124 | export default AnimatedSpinner; 125 | -------------------------------------------------------------------------------- /assembly/pieces.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | export const PAWN: i32 = 1; 20 | export const KNIGHT: i32 = 2; 21 | export const BISHOP: i32 = 3; 22 | export const ROOK: i32 = 4; 23 | export const QUEEN: i32 = 5; 24 | export const KING: i32 = 6; 25 | 26 | const KING_VALUE = 1500; 27 | export let EG_QUEEN_VALUE = 991; 28 | export let QUEEN_VALUE = 1376; 29 | export let EG_ROOK_VALUE = 568; 30 | export let ROOK_VALUE = 659; 31 | export let EG_BISHOP_VALUE = 335; 32 | export let BISHOP_VALUE = 489; 33 | export let EG_KNIGHT_VALUE = 267; 34 | export let KNIGHT_VALUE = 456; 35 | export let EG_PAWN_VALUE = 107; 36 | export let PAWN_VALUE = 102; 37 | 38 | export const PIECE_VALUES: StaticArray = StaticArray.fromArray([0, PAWN_VALUE, KNIGHT_VALUE, BISHOP_VALUE, ROOK_VALUE, QUEEN_VALUE, KING_VALUE]); 39 | export const EG_PIECE_VALUES: StaticArray = StaticArray.fromArray([0, EG_PAWN_VALUE, EG_KNIGHT_VALUE, EG_BISHOP_VALUE, EG_ROOK_VALUE, EG_QUEEN_VALUE, KING_VALUE]); 40 | 41 | export function resetPieceValues(): void { 42 | unchecked(PIECE_VALUES[PAWN] = PAWN_VALUE); 43 | unchecked(PIECE_VALUES[KNIGHT] = KNIGHT_VALUE); 44 | unchecked(PIECE_VALUES[BISHOP] = BISHOP_VALUE); 45 | unchecked(PIECE_VALUES[ROOK] = ROOK_VALUE); 46 | unchecked(PIECE_VALUES[QUEEN] = QUEEN_VALUE); 47 | unchecked(PIECE_VALUES[KING] = KING_VALUE); 48 | 49 | unchecked(EG_PIECE_VALUES[PAWN] = EG_PAWN_VALUE); 50 | unchecked(EG_PIECE_VALUES[KNIGHT] = EG_KNIGHT_VALUE); 51 | unchecked(EG_PIECE_VALUES[BISHOP] = EG_BISHOP_VALUE); 52 | unchecked(EG_PIECE_VALUES[ROOK] = EG_ROOK_VALUE); 53 | unchecked(EG_PIECE_VALUES[QUEEN] = EG_QUEEN_VALUE); 54 | unchecked(EG_PIECE_VALUES[KING] = KING_VALUE); 55 | } 56 | 57 | export const P = PAWN; 58 | export const N = KNIGHT; 59 | export const B = BISHOP; 60 | export const R = ROOK; 61 | export const Q = QUEEN; 62 | export const K = KING; 63 | 64 | export const KNIGHT_DIRECTIONS: StaticArray = StaticArray.fromArray([17, 15, 10, 6, -10, -6, -15, -17]); 65 | export const KING_DIRECTIONS: StaticArray = StaticArray.fromArray([1, 8, -1, -8, 7, 9, -7, -9]); 66 | 67 | export const BISHOP_DIRECTIONS: StaticArray = StaticArray.fromArray([-7, -9, 7, 9]); 68 | 69 | export const WHITE_QUEEN_SIDE_ROOK_START = 56; 70 | export const WHITE_KING_SIDE_ROOK_START = 63; 71 | export const BLACK_QUEEN_SIDE_ROOK_START = 0; 72 | export const BLACK_KING_SIDE_ROOK_START = 7; 73 | 74 | export const WHITE_PAWNS_BASELINE_START = 48; 75 | export const WHITE_PAWNS_BASELINE_END = 55; 76 | export const BLACK_PAWNS_BASELINE_START = 8; 77 | export const BLACK_PAWNS_BASELINE_END = 15; 78 | 79 | export const WHITE_ENPASSANT_LINE_START = 16; 80 | export const WHITE_ENPASSANT_LINE_END = 23 81 | export const BLACK_ENPASSANT_LINE_START = 40; 82 | export const BLACK_ENPASSANT_LINE_END = 47 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/ui/Piece.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import React from 'react'; 20 | import { useDrag } from 'react-dnd'; 21 | import styled from 'styled-components/macro'; 22 | 23 | import whitePawn from './img/white_pawn.svg'; 24 | import blackPawn from './img/black_pawn.svg'; 25 | import whiteKnight from './img/white_knight.svg'; 26 | import blackKnight from './img/black_knight.svg'; 27 | import whiteBishop from './img/white_bishop.svg'; 28 | import blackBishop from './img/black_bishop.svg'; 29 | import whiteRook from './img/white_rook.svg'; 30 | import blackRook from './img/black_rook.svg'; 31 | import whiteQueen from './img/white_queen.svg'; 32 | import blackQueen from './img/black_queen.svg'; 33 | import whiteKing from './img/white_king.svg'; 34 | import blackKing from './img/black_king.svg'; 35 | import { BLACK } from '../engine/constants'; 36 | 37 | const whiteImages = [ 38 | whitePawn, 39 | whiteKnight, 40 | whiteBishop, 41 | whiteRook, 42 | whiteQueen, 43 | whiteKing 44 | ]; 45 | 46 | const blackImages = [ 47 | blackPawn, 48 | blackKnight, 49 | blackBishop, 50 | blackRook, 51 | blackQueen, 52 | blackKing 53 | ]; 54 | 55 | const pieceNames = [ 'Pawn', 'Knight', 'Bishop', 'Rook', 'Queen', 'King' ] 56 | 57 | const Image = styled.img` 58 | display: block; 59 | margin: 13%; 60 | height: 74%; 61 | width: 74%; 62 | 63 | &.dragging { 64 | visibility: hidden; 65 | } 66 | 67 | // Workaround for wrong Drag'n'Drop preview image rendering in Chrome (see https://github.com/react-dnd/react-dnd/issues/832) 68 | -webkit-transform: rotateZ(0deg); 69 | `; 70 | 71 | const isFirefox = typeof InstallTrigger !== 'undefined'; 72 | 73 | export const PieceImage = ({color, pieceId, ...props}) => { 74 | const img = color === BLACK ? blackImages[pieceId - 1] : whiteImages[pieceId - 1]; 75 | return {pieceNames[pieceId; 76 | } 77 | 78 | const Piece = ({ boardIndex, color, piece, onPickup, onDrop }) => { 79 | const pieceId = Math.abs(piece); 80 | const img = color === BLACK ? blackImages[pieceId - 1] : whiteImages[pieceId - 1]; 81 | 82 | const [{ isDragging }, drag] = useDrag({ 83 | item: { 84 | type: 'PIECE', 85 | pieceId, 86 | boardIndex 87 | }, 88 | begin: monitor => onPickup(boardIndex), 89 | end: dropResult => onDrop(boardIndex), 90 | collect: monitor => ({ 91 | isDragging: !!monitor.isDragging() 92 | }) 93 | }); 94 | 95 | const pieceImage = 96 | ; 101 | 102 | // Workaround for wrong Drag'n'Drop preview image rendering in Firefox 103 | return isFirefox 104 | ?
{pieceImage}
105 | : pieceImage; 106 | }; 107 | 108 | export default Piece; 109 | -------------------------------------------------------------------------------- /src/engine/engine.worker.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import {instantiate} from "assemblyscript/lib/loader"; 20 | import { BLACK, WHITE } from './constants'; 21 | import { Move } from './move'; 22 | 23 | let engine; 24 | 25 | // Initializes the wasm engine 26 | export async function init() { 27 | if (engine) { 28 | return; // already initialized 29 | } 30 | const result = await instantiate(fetch("./as-api.wasm")); 31 | engine = result.exports; 32 | console.log("Engine initialized"); 33 | }; 34 | 35 | export function newGame() { 36 | engine.newGame(); 37 | } 38 | 39 | export function calculateMove(difficultyLevel) { 40 | console.log('Start calculation of move ...'); 41 | 42 | const moveEncoded = engine.calculateMove(difficultyLevel); 43 | const move = Move.fromEncodedMove(moveEncoded); 44 | 45 | console.log('Calculation finished'); 46 | return move; 47 | } 48 | 49 | export function performMove(move) { 50 | const gameStatePtr = engine.__pin(engine.performMove(move.encodedMove)); 51 | const gameState = engine.__getArray(gameStatePtr); 52 | engine.__unpin(gameStatePtr); 53 | 54 | return decodeGameState(gameState); 55 | } 56 | 57 | export function setPosition(fen, moves) { 58 | const fenStr = engine.__pin(engine.__newString(fen)); 59 | const movesArray = engine.__pin(engine.__newArray(engine.INT32ARRAY_ID, moves.map(move => move.encodedMove))); 60 | 61 | const gameStatePtr = engine.__pin(engine.setPosition(fenStr, movesArray)); 62 | 63 | engine.__unpin(movesArray); 64 | engine.__unpin(fenStr); 65 | 66 | const gameState = engine.__getArray(gameStatePtr); 67 | engine.__unpin(gameStatePtr); 68 | 69 | return decodeGameState(gameState); 70 | } 71 | 72 | const GAME_ENDED = 1 73 | const CHECK_MATE = 2; 74 | const STALE_MATE = 4; 75 | const THREEFOLD_REPETITION_DRAW = 8; 76 | const FIFTYMOVE_DRAW = 16; 77 | const INSUFFICIENT_MATERIAL_DRAW = 32; 78 | const ACTIVE_PLAYER = 64; // 0 - White, 1 - Black 79 | const WHITE_IN_CHECK = 128; 80 | const BLACK_IN_CHECK = 256; 81 | 82 | function decodeGameState(gameState) { 83 | const board = gameState.slice(0, 64); 84 | const state = gameState[64]; 85 | const moves = gameState.length > 65 ? gameState.slice(65).map(Move.fromEncodedMove) : []; 86 | 87 | return { 88 | board, 89 | moves, 90 | gameEnded: (state & GAME_ENDED) !== 0, 91 | checkMate: (state & CHECK_MATE) !== 0, 92 | staleMate: (state & STALE_MATE) !== 0, 93 | whiteInCheck: (state & WHITE_IN_CHECK) !== 0, 94 | blackInCheck: (state & BLACK_IN_CHECK) !== 0, 95 | threefoldRepetition: (state & THREEFOLD_REPETITION_DRAW) !== 0, 96 | fiftyMoveDraw: (state & FIFTYMOVE_DRAW) !== 0, 97 | insufficientMaterial: (state & INSUFFICIENT_MATERIAL_DRAW) !== 0, 98 | activePlayer: (state & ACTIVE_PLAYER) !== 0 ? BLACK : WHITE 99 | } 100 | } 101 | 102 | -------------------------------------------------------------------------------- /src/ui/img/white_queen.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 29 | 49 | 53 | 58 | 63 | 68 | 73 | 78 | 83 | 88 | 93 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/ui/Field.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import React from 'react'; 20 | import { useDrop } from 'react-dnd'; 21 | import styled from 'styled-components/macro'; 22 | 23 | export const FieldDiv = styled.div` 24 | position: relative; 25 | background-color: ${props => props.isEven ? "#fdf6e3" : "#eee8d5"}; 26 | 27 | &.mark:before { 28 | position: absolute; 29 | content: ''; 30 | display: block; 31 | border-radius: 50%; 32 | } 33 | 34 | // always keep board size rectangular and maximized to the smaller axis 35 | @media (min-aspect-ratio: 99/100) { 36 | width: 12.5vh; 37 | height: 12.5vh; 38 | 39 | &.mark:before { 40 | top: 0.7vh; 41 | left: 0.7vh; 42 | bottom: 0.7vh; 43 | right: 0.7vh; 44 | } 45 | 46 | &.move:before { 47 | border: 0.6vh solid ${props => props.markColor}; 48 | box-shadow: 0 0 0.2vh ${props => props.markColor}; 49 | } 50 | 51 | &.check:before { 52 | top: 6vh; 53 | left: 6vh; 54 | bottom: 6vh; 55 | right: 6vh; 56 | background-color: ${props => props.markColor}; 57 | box-shadow: 0 0 2.8vh 4vh ${props => props.markColor}; 58 | } 59 | } 60 | 61 | @media (max-aspect-ratio: 100/99) { 62 | width: 12.5vw; 63 | height: 12.5vw; 64 | 65 | &.mark:before { 66 | top: 0.7vw; 67 | left: 0.7vw; 68 | bottom: 0.7vw; 69 | right: 0.7vw; 70 | } 71 | 72 | &.move:before { 73 | border: 0.6vw solid ${props => props.markColor}; 74 | box-shadow: 0 0 0.2vw ${props => props.markColor}; 75 | } 76 | 77 | &.check:before { 78 | top: 6vw; 79 | left: 6vw; 80 | bottom: 6vw; 81 | right: 6vw; 82 | background-color: ${props => props.markColor}; 83 | box-shadow: 0 0 2.8vw 4vw ${props => props.markColor}; 84 | } 85 | } 86 | 87 | `; 88 | 89 | const Field = ({ 90 | boardIndex, 91 | children, 92 | movePiece, 93 | isEven, 94 | isStart, 95 | isEnd, 96 | isPossibleTarget, 97 | isInCheck 98 | }) => { 99 | const [, dropRef] = useDrop({ 100 | accept: 'PIECE', 101 | drop: (item, monitor) => { 102 | movePiece(item.pieceId, item.boardIndex, boardIndex); 103 | } 104 | }); 105 | 106 | const fieldMarkStyle = isStart || isEnd || isPossibleTarget ? ' mark move' 107 | : isInCheck ? 'mark check' 108 | : ''; 109 | 110 | const markColor = isPossibleTarget ? 'rgba(169, 189, 0, 0.69)' 111 | : isStart ? '#dc322f9f' 112 | : isEnd ? '#dc322faf' 113 | : isInCheck ? '#ff322f' 114 | : 'white'; 115 | 116 | return ( 117 | 123 | {children} 124 | 125 | ); 126 | }; 127 | 128 | export default Field; 129 | -------------------------------------------------------------------------------- /assembly/__tests__/bitboard.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 20 | import { antiDiagonalAttacks, diagonalAttacks, horizontalAttacks, verticalAttacks } from '../bitboard'; 21 | import { fromBitBoardString, toBitBoardString } from '../util'; 22 | 23 | describe("Bitboard ray attacks", () => { 24 | it("finds diagonal attacks", () => { 25 | expect(toBitBoardString(diagonalAttacks(0, 7))).toBe("00000000/00000010/00000100/00001000/00010000/00100000/01000000/10000000"); 26 | expect(toBitBoardString(diagonalAttacks(0, 56))).toBe("00000001/00000010/00000100/00001000/00010000/00100000/01000000/00000000"); 27 | }) 28 | 29 | it("finds diagonal attacks with blockers", () => { 30 | const blockers = fromBitBoardString("00000000/00000000/00000100/00000000/00000000/00000000/00000000/00000000") 31 | expect(toBitBoardString(diagonalAttacks(blockers, 7))).toBe("00000000/00000010/00000100/00000000/00000000/00000000/00000000/00000000"); 32 | expect(toBitBoardString(diagonalAttacks(blockers, 56))).toBe("00000000/00000000/00000100/00001000/00010000/00100000/01000000/00000000"); 33 | }) 34 | 35 | it("finds anti-diagonal attacks", () => { 36 | expect(toBitBoardString(antiDiagonalAttacks(0, 0))).toBe("00000000/01000000/00100000/00010000/00001000/00000100/00000010/00000001"); 37 | expect(toBitBoardString(antiDiagonalAttacks(0, 63))).toBe("10000000/01000000/00100000/00010000/00001000/00000100/00000010/00000000"); 38 | }) 39 | 40 | it("finds anti-diagonal attacks with blockers", () => { 41 | const blockers = fromBitBoardString("00000000/00000000/00000100/00000000/00000000/00000000/00000010/00000000") 42 | expect(toBitBoardString(antiDiagonalAttacks(blockers, 0))).toBe("00000000/01000000/00100000/00010000/00001000/00000100/00000010/00000000"); 43 | expect(toBitBoardString(antiDiagonalAttacks(blockers, 63))).toBe("00000000/00000000/00000000/00000000/00000000/00000000/00000010/00000000"); 44 | 45 | expect(toBitBoardString(antiDiagonalAttacks(blockers, 63))).toBe("00000000/00000000/00000000/00000000/00000000/00000000/00000010/00000000"); 46 | }) 47 | 48 | it("finds anti-diagonal attacks with multiple blockers", () => { 49 | const blockers = fromBitBoardString("10000110/01101111/10110100/00101010/00101010/10110100/01101111/10000110") 50 | expect(toBitBoardString(antiDiagonalAttacks(blockers, 30))).toBe("00000000/00000000/00000100/00000000/00000001/00000000/00000000/00000000"); 51 | }); 52 | 53 | it("finds horizontal attacks", () => { 54 | expect(toBitBoardString(horizontalAttacks(0, 0))).toBe("01111111/00000000/00000000/00000000/00000000/00000000/00000000/00000000"); 55 | expect(toBitBoardString(horizontalAttacks(0, 7))).toBe("11111110/00000000/00000000/00000000/00000000/00000000/00000000/00000000"); 56 | }) 57 | 58 | it("finds vertical attacks", () => { 59 | expect(toBitBoardString(verticalAttacks(0, 0))).toBe("00000000/10000000/10000000/10000000/10000000/10000000/10000000/10000000"); 60 | expect(toBitBoardString(verticalAttacks(0, 63))).toBe("00000001/00000001/00000001/00000001/00000001/00000001/00000001/00000000"); 61 | }) 62 | }); 63 | -------------------------------------------------------------------------------- /assembly/__tests__/perft.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { fromFEN } from '../fen'; 20 | import { perft } from '../perft'; 21 | 22 | 23 | // Compare number of computed nodes for various positions and depths with results listed here: https://www.chessprogramming.org/Perft_Results 24 | describe('Perft - move generation validation', () => { 25 | it('generates correct number of nodes for initial position', () => { 26 | const fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; 27 | expect(perft(fromFEN(fen), 0)).toBe(1, "depth 0"); 28 | expect(perft(fromFEN(fen), 1)).toBe(20, "depth 1"); 29 | expect(perft(fromFEN(fen), 2)).toBe(400, "depth 2"); 30 | expect(perft(fromFEN(fen), 3)).toBe(8902, "depth 3"); 31 | expect(perft(fromFEN(fen), 4)).toBe(197281, "depth 4"); 32 | }); 33 | 34 | it('generates correct number of nodes for "Test Position 2"', () => { 35 | const fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"; 36 | expect(perft(fromFEN(fen), 1)).toBe(48, "depth 1"); 37 | expect(perft(fromFEN(fen), 2)).toBe(2039, "depth 2"); 38 | expect(perft(fromFEN(fen), 3)).toBe(97862, "depth 3"); 39 | }); 40 | 41 | it('generates correct number of nodes for "Test Position 3"', () => { 42 | const fen = "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1"; 43 | expect(perft(fromFEN(fen), 1)).toBe(14, "depth 1"); 44 | expect(perft(fromFEN(fen), 2)).toBe(191, "depth 2"); 45 | expect(perft(fromFEN(fen), 3)).toBe(2812, "depth 3"); 46 | expect(perft(fromFEN(fen), 4)).toBe(43238, "depth 4"); 47 | expect(perft(fromFEN(fen), 5)).toBe(674624, "depth 5"); 48 | }); 49 | 50 | it('generates correct number of nodes for "Test Position 4"', () => { 51 | const fen = "r2q1rk1/pP1p2pp/Q4n2/bbp1p3/Np6/1B3NBn/pPPP1PPP/R3K2R b KQ - 0 1"; 52 | expect(perft(fromFEN(fen), 1)).toBe(6, "depth 1"); 53 | expect(perft(fromFEN(fen), 2)).toBe(264, "depth 2"); 54 | expect(perft(fromFEN(fen), 3)).toBe(9467, "depth 3"); 55 | expect(perft(fromFEN(fen), 4)).toBe(422333, "depth 4"); 56 | }); 57 | 58 | it('generates correct number of nodes for "Test Position 5"', () => { 59 | expect(perft(fromFEN("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8"), 1)).toBe(44, "depth 1"); 60 | expect(perft(fromFEN("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8"), 2)).toBe(1486, "depth 2"); 61 | expect(perft(fromFEN("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8"), 3)).toBe(62379, "depth 3"); 62 | expect(perft(fromFEN("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8"), 4)).toBe(2103487, "depth 4"); 63 | }); 64 | 65 | it('generates correct number of nodes for "Test Position 6"', () => { 66 | expect(perft(fromFEN("r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10"), 1)).toBe(46, "depth 1"); 67 | expect(perft(fromFEN("r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10"), 2)).toBe(2079, "depth 2"); 68 | expect(perft(fromFEN("r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10"), 3)).toBe(89890, "depth 3"); 69 | }); 70 | 71 | }); 72 | 73 | 74 | -------------------------------------------------------------------------------- /assembly/util.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { BLACK, Board, WHITE } from './board'; 20 | 21 | @inline 22 | export function sign(value: i32): i32 { 23 | if (value == 0) { 24 | return 0; 25 | } 26 | 27 | return value < 0 ? -1 : 1; 28 | } 29 | 30 | // Checks whether the given values have the same color, by comparing the sign (+/-) 31 | @inline 32 | export function sameColor(a: i32, b: i32): bool { 33 | return (a ^ b) >= 0; 34 | } 35 | 36 | // Checks whether the given values have different colors, by comparing the sign (+/-) 37 | @inline 38 | export function differentColor(a: i32, b: i32): bool { 39 | return (a ^ b) < 0; 40 | } 41 | 42 | // Only parses 0s and 1s. All other characters are ignored and can for example be used as separators (10001000/00110011/...) 43 | // Note: the printing order starts with the least significant bit (index 0) up to the most significant bit (index 63), so 44 | // the string representation is reversed (i.e. for value 1: "10000000/00000000/[...]" instead of "[...]/00000000/00000001") 45 | export function fromBitBoardString(bits: string): u64 { 46 | let result: u64 = 0; 47 | let bitCount = 0; 48 | for (let i = bits.length - 1; i >= 0; i--) { 49 | const char = bits.charAt(i); 50 | 51 | if (char == '1') { 52 | bitCount++; 53 | if (bitCount > 64) { 54 | throw new Error("Can not parse bit string with more than 64 bits"); 55 | } 56 | result <<= 1; 57 | result |= 1; 58 | } else if (char == '0') { 59 | bitCount++; 60 | if (bitCount > 64) { 61 | throw new Error("Can not parse bit string with more than 64 bits"); 62 | } 63 | result <<= 1; 64 | } 65 | } 66 | 67 | return result; 68 | } 69 | 70 | export function toBitBoardString(value: u64, separator: string = '/'): string { 71 | let result = ""; 72 | for (let i = 0; i < 64; i++) { 73 | if (i != 0 && i % 8 == 0) { 74 | result += separator 75 | } 76 | if ((value & (1 << i)) != 0) { 77 | result += "1"; 78 | } else { 79 | result += "0"; 80 | } 81 | } 82 | return result; 83 | } 84 | 85 | export function moveKing(board: Board, piece: i32, location: i32): void { 86 | const color = piece < 0 ? BLACK : WHITE; 87 | 88 | const kingPos = board.findKingPosition(color); 89 | board.removePiece(kingPos); 90 | 91 | board.addPiece(color, abs(piece), location); 92 | const state = board.getState(); 93 | board.updateKingPosition(color, location); 94 | board.setState(state); 95 | } 96 | 97 | // Packs two scores (i16) into a single value (u32) 98 | @inline 99 | export function packScores(a: i16, b: i16): u32 { 100 | return (u32(b) << 16) | (u32(a) & 0xFFFF); 101 | } 102 | 103 | // Unpacks the first score from a packed value (see packScore) 104 | @inline 105 | export function unpackFirstScore(packed: u32): i16 { 106 | return i16(packed & 0xFFFF); 107 | } 108 | 109 | // Unpacks the second score from a packed value (see packScore) 110 | @inline 111 | export function unpackSecondScore(packed: u32): i16 { 112 | return i16(packed >> 16); 113 | } 114 | -------------------------------------------------------------------------------- /src/ui/img/black_queen.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 29 | 49 | 53 | 56 | 61 | 66 | 71 | 76 | 81 | 82 | 87 | 92 | 97 | 102 | 107 | 112 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /assembly/transposition-table.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | export const MAX_HASH_SIZE_MB = 768; 20 | 21 | // Transposition table entry 22 | // Bits 63 - 23: 41 highest bits of the hash 23 | const HASHCHECK_BITSIZE = 41; 24 | const HASHCHECK_MASK: u64 = 0b1111111111111111111111111111111111111111100000000000000000000000; 25 | 26 | // Bits 22 - 17: Depth 27 | export const TRANSPOSITION_MAX_DEPTH = 63; 28 | const DEPTH_BITSHIFT = 17; 29 | const DEPTH_MASK: u64 = 0b111111; 30 | 31 | // Bits 16 - 15: Score Type 32 | export enum ScoreType { 33 | EXACT = 0, 34 | UPPER_BOUND, 35 | LOWER_BOUND 36 | } 37 | 38 | const SCORE_TYPE_BITSHIFT = 15; 39 | const SCORE_TYPE_MASK: u64 = 0b11; 40 | 41 | // Bits 14 - 0: Age 42 | const AGE_MASK: u64 = 0b111111111111111; 43 | 44 | 45 | export const DEFAULT_SIZE_MB = 32; 46 | const perEntryByteSize = 8 + 4; 47 | 48 | export class TranspositionTable { 49 | private indexMask: u64; 50 | private entries: StaticArray = new StaticArray(0); 51 | private moves: StaticArray = new StaticArray(0); 52 | private age: i32; 53 | 54 | constructor() { 55 | this.resize(DEFAULT_SIZE_MB, true); 56 | } 57 | 58 | resize(sizeInMB: u32, initialize: bool = false): void { 59 | // Calculate table size as close to the desired sizeInMB as possible, but never above it 60 | const sizeInBytes = sizeInMB * 1_048_576; 61 | const entryCount = sizeInBytes / perEntryByteSize; 62 | const indexBitCount = 31 - clz(entryCount | 1); 63 | 64 | const size = (1 << indexBitCount); 65 | if (initialize || size != this.entries.length) { 66 | this.indexMask = size - 1; 67 | this.entries = new StaticArray(size); 68 | this.moves = new StaticArray(size); 69 | } 70 | } 71 | 72 | increaseAge(): void { 73 | this.age = (this.age + 1) & i32(AGE_MASK); 74 | } 75 | 76 | writeEntry(hash: u64, depth: i32, scoredMove: i32, type: ScoreType): void { 77 | const index = this.calculateIndex(hash); 78 | 79 | const entry = unchecked(this.entries[index]); 80 | if (entry != 0 && i32(entry & AGE_MASK) == this.age && depth < i32((entry >> DEPTH_BITSHIFT) & DEPTH_MASK)) { 81 | return; 82 | } 83 | 84 | let newEntry: u64 = hash & HASHCHECK_MASK; 85 | newEntry |= (depth << DEPTH_BITSHIFT); 86 | newEntry |= (type << SCORE_TYPE_BITSHIFT); 87 | newEntry |= this.age; 88 | 89 | unchecked(this.entries[index] = newEntry); 90 | unchecked(this.moves[index] = scoredMove); 91 | } 92 | 93 | getEntry(hash: u64): u64 { 94 | const index = this.calculateIndex(hash); 95 | 96 | const entry = unchecked(this.entries[index]); 97 | const ageDiff = this.age - i32(entry & AGE_MASK); 98 | 99 | if (entry == 0 || ageDiff < 0 || ageDiff > 1 || (entry & HASHCHECK_MASK) != (hash & HASHCHECK_MASK)) { 100 | return 0; 101 | } 102 | 103 | return u64(unchecked(this.moves[index])) << 32 | (entry & ~HASHCHECK_MASK); 104 | } 105 | 106 | private calculateIndex(hash: u64): i32 { 107 | return i32(hash & this.indexMask); 108 | } 109 | 110 | clear(): void { 111 | for (let i = 0; i < this.entries.length; i++) { 112 | unchecked(this.entries[i] = 0); 113 | unchecked(this.moves[i] = 0); 114 | } 115 | this.age = 0; 116 | } 117 | 118 | } 119 | 120 | 121 | export function getScoredMove(entry: u64): i32 { 122 | return i32(entry >> 32); 123 | } 124 | 125 | export function getDepth(entry: u64): i32 { 126 | return i32((entry >> DEPTH_BITSHIFT) & DEPTH_MASK); 127 | } 128 | 129 | export function getScoreType(entry: u64): ScoreType { 130 | return i32((entry >> SCORE_TYPE_BITSHIFT) & SCORE_TYPE_MASK); 131 | } 132 | 133 | -------------------------------------------------------------------------------- /assembly/history-heuristics.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { TRANSPOSITION_MAX_DEPTH } from './transposition-table'; 20 | import { WHITE } from './board'; 21 | 22 | const PLAYED_MOVE_THRESHOLDS = calculateMoveThresholds(); 23 | 24 | /* Stores information about non-capture moves, which caused a cut-off during search */ 25 | export class HistoryHeuristics { 26 | private primaryKillers: StaticArray = new StaticArray(TRANSPOSITION_MAX_DEPTH); 27 | private secondaryKillers: StaticArray = new StaticArray(TRANSPOSITION_MAX_DEPTH); 28 | private cutOffHistory: StaticArray = new StaticArray(2 * 64 * 64); 29 | private playedMoveHistory: StaticArray = new StaticArray(2 * 64 * 64); 30 | 31 | clear(): void { 32 | for (let i = 0; i < this.primaryKillers.length; i++) { 33 | unchecked(this.primaryKillers[i] = 0); 34 | unchecked(this.secondaryKillers[i] = 0); 35 | } 36 | this.clearHistory(); 37 | } 38 | 39 | clearHistory(): void { 40 | for (let i = 0; i < this.cutOffHistory.length; i++) { 41 | unchecked(this.cutOffHistory[i] = 0); 42 | unchecked(this.playedMoveHistory[i] = 0); 43 | } 44 | } 45 | 46 | @inline 47 | getPrimaryKiller(ply: i32): i32 { 48 | return unchecked(this.primaryKillers[ply]); 49 | } 50 | 51 | @inline 52 | getSecondaryKiller(ply: i32): i32 { 53 | return unchecked(this.secondaryKillers[ply]); 54 | } 55 | 56 | @inline 57 | update(ply: i32, color: i32, moveStart: i32, moveEnd: i32, move: i32): void { 58 | const colOffset = color == WHITE ? 0 : 64 * 64; 59 | unchecked(this.cutOffHistory[colOffset + moveStart + moveEnd * 64]++); 60 | 61 | this.updateKillerMoveHistory(ply, move); 62 | } 63 | 64 | @inline 65 | private updateKillerMoveHistory(ply: i32, move: i32): void { 66 | const currentPrimary = unchecked(this.primaryKillers[ply]); 67 | if (currentPrimary != move) { 68 | unchecked(this.primaryKillers[ply] = move); 69 | unchecked(this.secondaryKillers[ply] = currentPrimary); 70 | } 71 | } 72 | 73 | @inline 74 | updatePlayedMoves(color: i32, moveStart: i32, moveEnd: i32): void { 75 | const colOffset = color == WHITE ? 0 : 64 * 64; 76 | unchecked(this.playedMoveHistory[colOffset + moveStart + moveEnd * 64]++); 77 | } 78 | 79 | // Returns a score between 0 and 512, which indicates how likely it is to cause a cut-off during search (higher scores = more likely). 80 | @inline 81 | getHistoryScore(color: i32, moveStart: i32, moveEnd: i32): i32 { 82 | const colOffset = color == WHITE ? 0 : 64 * 64; 83 | const index = colOffset + moveStart + moveEnd * 64; 84 | const playedMoveCount = unchecked(this.playedMoveHistory[index]); 85 | if (playedMoveCount == 0) { 86 | return 0; 87 | } 88 | 89 | return i32(unchecked(this.cutOffHistory[index]) * 512 / playedMoveCount); 90 | } 91 | 92 | // Returns true, if the history contains sufficient information about the given move, to indicate 93 | // that it is very unlikely to cause a cut-off during search 94 | @inline 95 | hasNegativeHistory(color: i32, depth: i32, moveStart: i32, moveEnd: i32): bool { 96 | const colOffset = color == WHITE ? 0 : 64 * 64; 97 | const index = colOffset + moveStart + moveEnd * 64; 98 | 99 | const playedMoveCount = unchecked(this.playedMoveHistory[index]); 100 | if (playedMoveCount < unchecked(PLAYED_MOVE_THRESHOLDS[depth])) { 101 | return false; 102 | } 103 | 104 | return (unchecked(this.cutOffHistory[index]) * 512 / playedMoveCount) == 0; 105 | } 106 | } 107 | 108 | function calculateMoveThresholds(): StaticArray { 109 | const thresholds = new StaticArray(TRANSPOSITION_MAX_DEPTH); 110 | let threshold: f32 = 2; 111 | for (let depth = 0; depth < TRANSPOSITION_MAX_DEPTH; depth++) { 112 | unchecked(thresholds[depth] = u64(threshold)); 113 | threshold *= 1.6; 114 | } 115 | 116 | return thresholds; 117 | } 118 | 119 | 120 | -------------------------------------------------------------------------------- /assembly/uci-move-notation.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { Board, EMPTY } from './board'; 20 | import { BISHOP, KNIGHT, QUEEN, ROOK } from './pieces'; 21 | import { decodeEndIndex, decodePiece, decodeStartIndex, encodeMove } from './move-generation'; 22 | 23 | const CHARCODE_A: i32 = 'a'.charCodeAt(0); 24 | const CHARCODE_H: i32 = 'h'.charCodeAt(0); 25 | 26 | const CHARCODE_1: i32 = '1'.charCodeAt(0); 27 | const CHARCODE_8: i32 = '8'.charCodeAt(0); 28 | 29 | // Returns column 0 to 7 for chars 'a' to 'h' 30 | function toNumericColumn(colChar: i32): i32 { 31 | if (colChar < CHARCODE_A || colChar > CHARCODE_H) { 32 | throw new Error("Invalid uci move colum char: " + String.fromCharCode(colChar)); 33 | } 34 | 35 | return colChar - CHARCODE_A; 36 | } 37 | 38 | // Returns row 7 to 0 for chars '1' to '8' 39 | // Note: bitboard representation for a8 is 0, while h1 is 63 40 | function toNumericRow(rowChar: i32): i32 { 41 | if (rowChar < CHARCODE_1 || rowChar > CHARCODE_8) { 42 | throw new Error("Invalid uci move row char: " + String.fromCharCode(rowChar)); 43 | } 44 | 45 | return CHARCODE_8 - rowChar; 46 | } 47 | 48 | function toNumericPiece(pieceChar: string): i32 { 49 | if (pieceChar == 'n') { 50 | return KNIGHT; 51 | } else if (pieceChar == 'b') { 52 | return BISHOP; 53 | } else if (pieceChar == 'r') { 54 | return ROOK; 55 | } else if (pieceChar == 'q') { 56 | return QUEEN; 57 | } 58 | 59 | throw new Error("Invalid uci move promotion piece: " + pieceChar); 60 | } 61 | 62 | function toUCISquare(row: i32, column: i32): string { 63 | return String.fromCharCode(CHARCODE_A + column) + String.fromCharCode(CHARCODE_8 - row); 64 | } 65 | 66 | function toUCIPromotionPiece(piece: i32): string { 67 | switch (piece) { 68 | case EMPTY: return ""; 69 | case KNIGHT: return 'n'; 70 | case BISHOP: return 'b'; 71 | case ROOK: return 'r'; 72 | case QUEEN: return 'q'; 73 | default: throw new Error("Unexpected promotion piece id: " + piece.toString()); 74 | } 75 | } 76 | 77 | export class UCIMove { 78 | start: i32; 79 | end: i32; 80 | promotionPiece: i32; 81 | 82 | constructor(start: i32, end: i32, promotionPiece: i32 = EMPTY) { 83 | this.start = start; 84 | this.end = end; 85 | this.promotionPiece = promotionPiece; 86 | } 87 | 88 | toEncodedMove(board: Board): i32 { 89 | const piece = this.promotionPiece != EMPTY ? this.promotionPiece : abs(board.getItem(this.start)); 90 | return encodeMove(piece, this.start, this.end); 91 | } 92 | 93 | toUCINotation(): string { 94 | const startColumn = this.start & 7; 95 | const startRow = this.start / 8; 96 | 97 | const endColumn = this.end & 7; 98 | const endRow = this.end / 8 ; 99 | 100 | return toUCISquare(startRow, startColumn) + toUCISquare(endRow, endColumn) + toUCIPromotionPiece(this.promotionPiece); 101 | } 102 | 103 | static fromUCINotation(str: string): UCIMove { 104 | if (str.length < 4) { 105 | throw new Error("Invalid uci move notation: " + str); 106 | } 107 | 108 | const startColumn = toNumericColumn(str.charCodeAt(0)); 109 | const startRow = toNumericRow(str.charCodeAt(1)); 110 | 111 | const endColumn = toNumericColumn(str.charCodeAt(2)); 112 | const endRow = toNumericRow(str.charCodeAt(3)); 113 | 114 | const promotionPiece = str.length == 5 ? toNumericPiece(str.charAt(4)) : EMPTY; 115 | 116 | return new UCIMove((startRow * 8) + startColumn, endRow * 8 + endColumn, promotionPiece); 117 | } 118 | 119 | static fromEncodedMove(board: Board, move: i32): UCIMove { 120 | const targetPiece = decodePiece(move); 121 | const startIndex = decodeStartIndex(move); 122 | const endIndex = decodeEndIndex(move); 123 | 124 | const currentPiece = abs(board.getItem(startIndex)); 125 | const promotionPiece = targetPiece != currentPiece ? targetPiece : EMPTY; 126 | 127 | return new UCIMove(startIndex, endIndex, promotionPiece); 128 | } 129 | } 130 | 131 | -------------------------------------------------------------------------------- /assembly/__tests__/util.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { 20 | differentColor, 21 | fromBitBoardString, 22 | packScores, 23 | sameColor, 24 | toBitBoardString, 25 | unpackFirstScore, 26 | unpackSecondScore 27 | } from '../util'; 28 | 29 | 30 | describe("fromBitBoardString", () => { 31 | 32 | it("parses bit board string correctly", () => { 33 | expect(fromBitBoardString("11000000/00000000/00000000/00000000/00000000/00000000/00000000/00000000")).toBe(0x3); 34 | expect(fromBitBoardString("11110000/00000000/00000000/00000000/00000000/00000000/00000000/00000000")).toBe(0xF); 35 | expect(fromBitBoardString("01010101/00000000/00000000/00000000/00000000/00000000/00000000/00000000")).toBe(0xAA); 36 | expect(fromBitBoardString("00000000/00000000/00000000/00000000/00000000/00000000/00000000/00000000")).toBe(0); 37 | expect(fromBitBoardString("11111111/11111111/11111111/11111111/11111111/11111111/11111111/11111111")).toBe(0xFFFFFFFFFFFFFFFF); 38 | expect(fromBitBoardString("00000000/00000000/00000000/00000000/00000000/00000000/00000000/00000001")).toBe(0x8000000000000000) 39 | }); 40 | 41 | }); 42 | 43 | 44 | describe("toBitBoardString", () => { 45 | 46 | it("writes correct bit board string", () => { 47 | expect(toBitBoardString(0x3)).toBe(("11000000/00000000/00000000/00000000/00000000/00000000/00000000/00000000")); 48 | expect(toBitBoardString(0xF)).toBe(("11110000/00000000/00000000/00000000/00000000/00000000/00000000/00000000")); 49 | expect(toBitBoardString(0xAA)).toBe(("01010101/00000000/00000000/00000000/00000000/00000000/00000000/00000000")); 50 | expect(toBitBoardString(0)).toBe(("00000000/00000000/00000000/00000000/00000000/00000000/00000000/00000000")); 51 | expect(toBitBoardString(0xFFFFFFFFFFFFFFFF)).toBe(("11111111/11111111/11111111/11111111/11111111/11111111/11111111/11111111")); 52 | expect(toBitBoardString(0x8000000000000000)).toBe("00000000/00000000/00000000/00000000/00000000/00000000/00000000/00000001"); 53 | }); 54 | 55 | }); 56 | 57 | 58 | describe("sameColor", () => { 59 | 60 | it("returns true for the same color (black)", () => { 61 | expect(sameColor(-2, -3)).toBeTruthy(); 62 | expect(sameColor(-4, -1)).toBeTruthy(); 63 | }); 64 | 65 | it("returns true for the same color (white)", () => { 66 | expect(sameColor(2, 3)).toBeTruthy(); 67 | expect(sameColor(4, 1)).toBeTruthy(); 68 | }); 69 | 70 | it("returns false for different colors", () => { 71 | expect(sameColor(-2, 3)).toBeFalsy(); 72 | expect(sameColor(4, -1)).toBeFalsy(); 73 | }); 74 | 75 | }); 76 | 77 | 78 | describe("differentColor", () => { 79 | 80 | it("returns false for the same color (black)", () => { 81 | expect(differentColor(-2, -3)).toBeFalsy(); 82 | expect(differentColor(-4, -1)).toBeFalsy(); 83 | }); 84 | 85 | it("returns false for the same color (white)", () => { 86 | expect(differentColor(2, 3)).toBeFalsy(); 87 | expect(differentColor(4, 1)).toBeFalsy(); 88 | }); 89 | 90 | it("returns true for different colors", () => { 91 | expect(differentColor(-2, 3)).toBeTruthy(); 92 | expect(differentColor(4, -1)).toBeTruthy(); 93 | }); 94 | 95 | }); 96 | 97 | 98 | describe("packScores", () => { 99 | it("packs two i16 values into one u32 value", () => { 100 | const a: i16 = 1; 101 | const b: i16 = 2; 102 | const packed = packScores(a, b); 103 | expect(unpackFirstScore(packed)).toBe(a, "first"); 104 | expect(unpackSecondScore(packed)).toBe(b, "second"); 105 | }); 106 | 107 | it("packs two maximum i16 values into one u32 value", () => { 108 | const a = i16.MAX_VALUE; 109 | const b = i16.MAX_VALUE; 110 | const packed = packScores(a, b); 111 | expect(unpackFirstScore(packed)).toBe(a, "first"); 112 | expect(unpackSecondScore(packed)).toBe(b, "second"); 113 | }); 114 | 115 | it("packs two minimum i16 values into one u32 value", () => { 116 | const a = i16.MIN_VALUE; 117 | const b = i16.MIN_VALUE; 118 | const packed = packScores(a, b); 119 | expect(unpackFirstScore(packed)).toBe(a, "first"); 120 | expect(unpackSecondScore(packed)).toBe(b, "second"); 121 | }); 122 | }); 123 | 124 | -------------------------------------------------------------------------------- /assembly/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | // The entry file of your WebAssembly module. 20 | /// 21 | /// 22 | 23 | import EngineControl from './engine'; 24 | import { isCheckMate as isCheckMateFn } from './move-generation'; 25 | import { BLACK, calculatePieceSquareTables, WHITE } from './board'; 26 | import { randomizeOpeningBookMoves } from './opening-book'; 27 | import { stdio } from './io'; 28 | import { VERSION } from '../version'; 29 | 30 | const DIFFICULTY_LEVELS: Array> = [ 31 | [1, 0], 32 | [3, 0], 33 | [5, 0], 34 | [7, 0], 35 | [9, 0], 36 | [11, 1250] 37 | ] 38 | 39 | export const INT32ARRAY_ID = idof(); 40 | 41 | const GAME_ENDED = 1; 42 | const CHECK_MATE = 2; 43 | const STALE_MATE = 4; 44 | const THREEFOLD_REPETITION_DRAW = 8; 45 | const FIFTYMOVE_DRAW = 16; 46 | const INSUFFICIENT_MATERIAL_DRAW = 32; 47 | const ACTIVE_PLAYER = 64; // 0 - White, 1 - Black 48 | const WHITE_IN_CHECK = 128; 49 | const BLACK_IN_CHECK = 256; 50 | 51 | let isInitialized = false; 52 | 53 | // Resets the engine state for a new game 54 | export function newGame(): void { 55 | EngineControl.reset(); 56 | 57 | if (!isInitialized) { 58 | stdio.writeLine("Wasabi " + VERSION); 59 | calculatePieceSquareTables(); 60 | 61 | EngineControl.resizeTranspositionTable(64); 62 | EngineControl.setUseOpeningBook(true); 63 | randomizeOpeningBookMoves(); 64 | isInitialized = true; 65 | } 66 | } 67 | 68 | // Sets the current board to the given position and returns the encoded game state 69 | export function setPosition(fen: string, moves: Int32Array): Int32Array { 70 | EngineControl.setPosition(fen); 71 | 72 | for (let i = 0; i < moves.length; i++) { 73 | EngineControl.performMove(moves[i]); 74 | } 75 | 76 | return encodeChessState(false); 77 | } 78 | 79 | // Calculates the best move for the current player using the given difficulty level 80 | export function calculateMove(difficultyLevel: i32): i32 { 81 | const levelSettings = DIFFICULTY_LEVELS[difficultyLevel - 1]; 82 | 83 | const maxTime = levelSettings[1]; 84 | const minimumSearchDepth = levelSettings[0]; 85 | 86 | return EngineControl.findBestMove(minimumSearchDepth, maxTime, maxTime === 0); 87 | } 88 | 89 | // Applies the given move to the current board and returns the encoded game state 90 | export function performMove(encodedMove: i32): Int32Array { 91 | EngineControl.performMove(encodedMove); 92 | 93 | return encodeChessState(true); 94 | } 95 | 96 | 97 | // Encodes the board (index 0-63), the game state (index 64) and all possible moves for the current player (index 65+) 98 | function encodeChessState(checkThreefoldRepetition: bool): Int32Array { 99 | const board = EngineControl.getBoard(); 100 | const moves = EngineControl.generateAvailableMoves(); 101 | 102 | const stateArray = new Int32Array(64 + 1 + moves.length); 103 | 104 | for (let i = 0; i < 64; i++) { 105 | stateArray[i] = board.getItem(i); 106 | } 107 | 108 | const isCheckMate: bool = isCheckMateFn(board, board.getActivePlayer()); 109 | const isStaleMate: bool = moves.length == 0; 110 | const isThreefoldRepetition: bool = checkThreefoldRepetition && board.isThreefoldRepetion(); 111 | const isFiftyMoveDraw: bool = board.isFiftyMoveDraw(); 112 | const isInsufficientMaterialDraw: bool = board.isInsufficientMaterialDraw(); 113 | 114 | const hasGameEnded: bool = isCheckMate || isStaleMate || isThreefoldRepetition || isFiftyMoveDraw || isInsufficientMaterialDraw; 115 | 116 | stateArray[64] = (hasGameEnded ? GAME_ENDED : 0) 117 | | (isCheckMate ? CHECK_MATE : 0) 118 | | (isStaleMate ? STALE_MATE : 0) 119 | | (isThreefoldRepetition ? THREEFOLD_REPETITION_DRAW : 0) 120 | | (isFiftyMoveDraw ? FIFTYMOVE_DRAW : 0) 121 | | (isInsufficientMaterialDraw ? INSUFFICIENT_MATERIAL_DRAW : 0) 122 | | ((board.getActivePlayer() == BLACK) ? ACTIVE_PLAYER : 0) 123 | | (board.isInCheck(WHITE) ? WHITE_IN_CHECK : 0) 124 | | (board.isInCheck(BLACK) ? BLACK_IN_CHECK : 0) 125 | 126 | for (let i = 0; i < moves.length; i++) { 127 | stateArray[i + 65] = moves[i]; 128 | } 129 | 130 | return stateArray; 131 | } 132 | 133 | -------------------------------------------------------------------------------- /src/ui/GameMenu.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import React from 'react'; 20 | import styled from 'styled-components/macro'; 21 | import { WHITE } from '../engine/constants'; 22 | import AnimatedSpinner from './AnimatedSpinner'; 23 | import { 24 | faBalanceScale, 25 | faDiceFive, 26 | faDiceFour, 27 | faDiceOne, 28 | faDiceSix, 29 | faDiceThree, 30 | faDiceTwo, 31 | faExchangeAlt, 32 | faMedal, 33 | faPlus, 34 | faRetweet, 35 | faRobot, 36 | faUndo 37 | } from '@fortawesome/free-solid-svg-icons'; 38 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 39 | 40 | const MenuBar = styled.div` 41 | display: flex; 42 | flex-direction: column; 43 | margin-top: 0.7rem; 44 | margin-left: 1rem; 45 | text-align: center; 46 | align-items: center; 47 | 48 | // center menubar below board, if window width is < window height 49 | @media (max-aspect-ratio: 100/99) { 50 | margin-left: auto; 51 | margin-right: auto; 52 | flex-flow: column-reverse; 53 | } 54 | `; 55 | 56 | const MenuItem = styled.div` 57 | position: relative; 58 | display: ${props => (props.hidden ? 'none' : 'flex')}; 59 | padding-bottom: 0.2rem; 60 | flex-direction: column; 61 | 62 | @media (max-aspect-ratio: 100/99) { 63 | flex-direction: row; 64 | align-self: center; 65 | } 66 | `; 67 | 68 | const GameButton = styled.button` 69 | background: white; 70 | color: #073642; 71 | border: 1px solid #073642; 72 | border-radius: 0.3rem; 73 | font-size: 1rem; 74 | font-weight: bold; 75 | padding: 0.5rem 0.3rem; 76 | width: 2.5rem; 77 | margin: 0.2rem; 78 | box-shadow: 1px 1px 1px #073642; 79 | 80 | &[disabled] { 81 | display: none; 82 | } 83 | 84 | & :hover { 85 | background: #073642; 86 | color: white; 87 | cursor: pointer; 88 | } 89 | `; 90 | 91 | const GameResult = styled(MenuItem)` 92 | margin-top: 0.3rem; 93 | font-weight: bold; 94 | font-size: 1.5rem; 95 | color: #073642; 96 | width: 100%; 97 | align-items: center; 98 | 99 | svg { 100 | margin-left: 1rem; 101 | margin-right: 1rem; 102 | } 103 | `; 104 | 105 | const IconRadioInput = styled.input` 106 | display: none; 107 | 108 | &:checked + label { 109 | opacity: 1; 110 | } 111 | `; 112 | 113 | const IconRadioLabel = styled.label` 114 | color: #073642; 115 | margin: 0.06rem 0.2rem; 116 | opacity: 0.2; 117 | 118 | & :hover { 119 | opacity: 0.5; 120 | cursor: pointer; 121 | } 122 | ` 123 | 124 | const colorName = color => (color === WHITE ? 'White' : 'Black'); 125 | 126 | const getGameResultIcon = (winningPlayerColor, humanPlayerColor) => { 127 | if (!winningPlayerColor) { 128 | return faBalanceScale; 129 | } 130 | 131 | return winningPlayerColor === humanPlayerColor 132 | ? faMedal 133 | : faRobot; 134 | } 135 | 136 | const GameMenu = ({ 137 | isAiThinking, 138 | firstMovePlayed, 139 | humanPlayerColor, 140 | gameEnded, 141 | winningPlayerColor, 142 | startNewGame, 143 | switchSides, 144 | rotateBoard, 145 | difficultyLevel, 146 | setDifficultyLevel, 147 | canUndoMove, 148 | undoMove, 149 | }) => ( 150 | 151 | {gameEnded && 152 | 153 | 154 | {winningPlayerColor ? colorName(winningPlayerColor) + ' wins!' : 'Draw!'} 155 | 156 | } 157 | 158 | 177 | 178 | 189 | 190 | {isAiThinking && } 191 | 192 | 193 | ); 194 | 195 | const IconRadioButtons = ({currentValue, name, options, onChange}) => ( 196 | <> 197 | {options.map(({value, description, icon}) => ( 198 | 199 | onChange(e.target.value)} /> 200 | 201 | 202 | 203 | 204 | ))} 205 | 206 | ); 207 | 208 | export default GameMenu; 209 | -------------------------------------------------------------------------------- /assembly/__tests__/engine.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 20 | import { B, K, N, P, Q, QUEEN, R } from '../pieces'; 21 | import EngineControl from '../engine'; 22 | import { 23 | BLACK, 24 | Board, calculatePieceSquareTables, NO_CASTLING_RIGHTS, 25 | WHITE 26 | } from '../board'; 27 | import { decodeMove, decodeScore, encodeMove, encodeScoredMove, isCheckMate } from '../move-generation'; 28 | 29 | beforeAll(() => { 30 | calculatePieceSquareTables(); 31 | }); 32 | 33 | describe('Encode and decode scored moves', () => { 34 | it('Zero score', () => { 35 | const score = 0; 36 | const move = encodeMove(QUEEN, 2, 63); 37 | 38 | const scoredMove = encodeScoredMove(move, score); 39 | 40 | expect(decodeMove(scoredMove)).toBe(move, "Correctly extracted move"); 41 | expect(decodeScore(scoredMove)).toBe(score, "Correctly extracted score"); 42 | }); 43 | 44 | it('Positive scores', () => { 45 | const score = 16383; 46 | const move = encodeMove(QUEEN, 2, 63); 47 | 48 | const scoredMove = encodeScoredMove(move, score); 49 | 50 | expect(decodeMove(scoredMove)).toBe(move, "Correctly extracted move"); 51 | expect(decodeScore(scoredMove)).toBe(score, "Correctly extracted score"); 52 | }); 53 | 54 | it('Negative score', () => { 55 | const score = -16383; 56 | const move = encodeMove(QUEEN, 2, 63); 57 | 58 | const scoredMove = encodeScoredMove(move, score); 59 | 60 | expect(decodeMove(scoredMove)).toBe(move, "Correctly extracted move"); 61 | expect(decodeScore(scoredMove)).toBe(score, "Correctly extracted score"); 62 | }); 63 | 64 | }); 65 | 66 | describe('Finds moves', () => { 67 | it('Finds mate in 1 move', () => { 68 | // prettier-ignore 69 | const board: Board = new Board([ 70 | 0, 0, 0, 0, +K, 0, 0, 0, 71 | -R, 0, 0, 0, 0, 0, 0, 0, 72 | 0, 0, 0, 0, 0, 0, 0, 0, 73 | 0, 0, 0, 0, 0, 0, 0, 0, 74 | 0, 0, 0, 0, 0, 0, 0, 0, 75 | 0, 0, 0, 0, 0, 0, 0, 0, 76 | 0, 0, 0, -K, 0, 0, 0, 0, 77 | 0, 0, 0, 0, 0, 0, 0, -R, 78 | 0, 0, NO_CASTLING_RIGHTS 79 | ]); 80 | 81 | board.performEncodedMove(findBestMove(board, BLACK, 2)); 82 | 83 | expect(isCheckMate(board, WHITE)).toBe(true); 84 | }); 85 | 86 | it('Finds mate in two moves', () => { 87 | // prettier-ignore 88 | const board: Board = new Board([ 89 | 0, 0, 0, 0, -K, 0, 0, 0, 90 | 0, 0, 0, 0, 0, 0, 0, 0, 91 | 0, 0, 0, 0, 0, 0, 0, 0, 92 | 0, 0, 0, 0, 0, 0, 0, 0, 93 | 0, 0, 0, 0, 0, 0, 0, 0, 94 | 0, 0, 0, 0, 0, 0, 0, 0, 95 | 0, 0, 0, 0, 0, 0, 0, 0, 96 | +R, 0, 0, 0, +K, 0, 0, +R, 97 | 0, 0, NO_CASTLING_RIGHTS 98 | ]); 99 | 100 | board.performEncodedMove(findBestMove(board, WHITE, 3)); 101 | board.performEncodedMove(findBestMove(board, BLACK, 2)); 102 | board.performEncodedMove(findBestMove(board, WHITE, 1)); 103 | 104 | expect(isCheckMate(board, BLACK)).toBe(true); 105 | }); 106 | 107 | it('Finds another mate in two moves', () => { 108 | // prettier-ignore 109 | const board: Board = new Board([ 110 | 0, 0, 0, -B, -R, -R, -B, 0, 111 | 0, 0, +N, 0, 0, 0, 0, +B, 112 | 0, 0, 0, 0, 0, 0, 0, 0, 113 | 0, 0, -P, 0, 0, 0, 0, +Q, 114 | 0, 0, -P, 0, 0, -K, 0, 0, 115 | 0, 0, 0, 0, 0, +P, 0, 0, 116 | 0, 0, 0, 0, +P, 0, +K, +R, 117 | 0, 0, +N, 0, 0, +R, +B, 0, 118 | 0, 0, NO_CASTLING_RIGHTS 119 | ]); 120 | 121 | board.performEncodedMove(findBestMove(board, WHITE, 5)); 122 | board.performEncodedMove(findBestMove(board, BLACK, 2)); 123 | board.performEncodedMove(findBestMove(board, WHITE, 1)); 124 | 125 | expect(isCheckMate(board, BLACK)).toBe(true); 126 | }); 127 | 128 | it('Finds opening move', () => { 129 | // prettier-ignore 130 | const board: Board = new Board([ 131 | -R, -N, -B, -Q, -K, -B, -N, -R, 132 | -P, -P, -P, -P, -P, -P, -P, -P, 133 | 0, 0, 0, 0, 0, 0, 0, 0, 134 | 0, 0, 0, 0, 0, 0, 0, 0, 135 | 0, 0, 0, 0, 0, 0, 0, 0, 136 | 0, 0, 0, 0, 0, 0, 0, 0, 137 | +P, +P, +P, +P, +P, +P, +P, +P, 138 | +R, +N, +B, +Q, +K, +B, +N, +R, 139 | 0, 0, 0 140 | ]); 141 | 142 | const move = findBestMove(board, WHITE, 4); 143 | expect(move).toBeGreaterThan(0, "An encoded move"); 144 | }); 145 | 146 | it('Does not sacrifice queen', () => { 147 | // prettier-ignore 148 | const board: Board = new Board([ 149 | -R, 0, -B, -Q, 0, -R, 0, -K, 150 | 0, -P, -P, 0, -N, -P, 0, 0, 151 | -P, 0, 0, -P, -P, 0, 0, -P, 152 | 0, 0, 0, 0, 0, -P, 0, 0, 153 | +B, 0, +P, 0, +P, 0, 0, 0, 154 | 0, 0, 0, 0, 0, 0, +Q, 0, 155 | +P, +P, +P, +N, 0, +P, +P, +P, 156 | +R, 0, 0, 0, 0, +R, +K, 0, 157 | 0, 0, 0 158 | ]); 159 | 160 | const move = findBestMove(board, WHITE, 2); 161 | board.performEncodedMove(move); 162 | expect(move).not.toBe(encodeMove(5, 46, 14), "Must not sacrifice queen @14"); 163 | expect(move).not.toBe(encodeMove(5, 46, 19), "Must not sacrifice queen @19"); 164 | }); 165 | 166 | 167 | it('Avoids stalemate when it is ahead of the opponent', () => { 168 | // prettier-ignore 169 | const board: Board = new Board([ 170 | 0, 0, 0, 0, 0, 0, 0, 0, 171 | 0, 0, 0, 0, 0, 0, 0, R, 172 | 0, 0, 0, 0, 0, B, 0, 0, 173 | 0, 0, 0, N, 0, P, 0, 0, 174 | 0, 0, 0, 0, P, 0, -K, 0, 175 | 0, 0, 0, 0, 0, 0, 0, -R, 176 | 0, 0, 0, 0, 0, 0, P, K, 177 | 0, 0, 0, 0, 0, 0, 0, 0, 178 | 0, 0, NO_CASTLING_RIGHTS 179 | ]); 180 | 181 | const move = findBestMove(board, WHITE, 2); 182 | expect(move).not.toBe(encodeMove(4, 15, 47), "Using the rook to capture the black rook causes a stalemate"); 183 | expect(move).toBe(encodeMove(1, 54, 47), "Using the pawn for the capture lets the game proceed"); 184 | }); 185 | 186 | it('Avoids threefold repetition', () => { 187 | // prettier-ignore 188 | const board: Board = new Board([ 189 | 0, -K, 0, 0, 0, 0, 0, 0, 190 | 0, 0, 0, 0, 0, 0, R, 0, 191 | 0, 0, 0, 0, 0, 0, 0, 0, 192 | 0, 0, 0, 0, 0, 0, 0, 0, 193 | 0, 0, 0, 0, 0, 0, 0, 0, 194 | 0, 0, 0, 0, 0, 0, 0, 0, 195 | 0, 0, 0, 0, 0, 0, 0, 0, 196 | 0, 0, 0, 0, 0, 0, K, 0, 197 | 0, 0, NO_CASTLING_RIGHTS 198 | ]); 199 | 200 | board.performEncodedMove(findBestMove(board, WHITE, 1)); 201 | const boardState1 = board.getHash(); 202 | 203 | board.performEncodedMove(findBestMove(board, BLACK, 1)); 204 | board.performEncodedMove(findBestMove(board, WHITE, 1)); 205 | const boardState2 = board.getHash(); 206 | 207 | board.performEncodedMove(findBestMove(board, BLACK, 1)); 208 | board.performEncodedMove(findBestMove(board, WHITE, 1)); 209 | const boardState3 = board.getHash(); 210 | 211 | board.performEncodedMove(findBestMove(board, BLACK, 1)); 212 | board.performEncodedMove(findBestMove(board, WHITE, 1)); 213 | const boardState4 = board.getHash(); 214 | 215 | board.performEncodedMove(findBestMove(board, BLACK, 1)); 216 | board.performEncodedMove(findBestMove(board, WHITE, 1)); 217 | const boardState5 = board.getHash(); 218 | 219 | expect(boardState5 != boardState1).toBeTruthy("Threefold repetion!"); 220 | 221 | }); 222 | }); 223 | 224 | 225 | // Test helper functions 226 | function findBestMove(board: Board, playerColor: i32, exactDepth: i32): i32 { 227 | EngineControl.setBoard(board); 228 | if (board.getActivePlayer() != playerColor) { 229 | board.performNullMove(); 230 | } 231 | return EngineControl.findBestMove(exactDepth, 0, true); 232 | } 233 | 234 | function findBestMoveIncrementally(board: Board, playerColor: i32, minimumDepth: i32, timeLimitMillis: i32): i32 { 235 | EngineControl.setBoard(board); 236 | if (board.getActivePlayer() != playerColor) { 237 | board.performNullMove(); 238 | } 239 | return EngineControl.findBestMove(minimumDepth, timeLimitMillis, true); 240 | } 241 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /src/ui/Game.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import React, { useCallback, useEffect, useState } from 'react'; 20 | import styled from 'styled-components/macro'; 21 | 22 | import engineWorkerLoader from 'workerize-loader!../engine/engine.worker'; // eslint-disable-line import/no-webpack-loader-syntax 23 | import { BLACK, P, WHITE } from '../engine/constants'; 24 | import AnimatedSpinner from './AnimatedSpinner'; 25 | import { Move } from '../engine/move'; 26 | import Board from './Board'; 27 | import GameMenu from './GameMenu'; 28 | import PromotionPieceSelection from './PromotionPieceSelection'; 29 | 30 | const engine = engineWorkerLoader(); 31 | 32 | const GameArea = styled.div` 33 | display: flex; 34 | flex-wrap: wrap; 35 | `; 36 | 37 | const startPosition = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; 38 | 39 | const nextPlayer = playerColor => -playerColor; 40 | 41 | const Game = () => { 42 | const [rotateBoard, setRotateBoard] = useState(false); 43 | const [activePlayer, setActivePlayer] = useState(WHITE); 44 | const [humanPlayerColor, setHumanPlayerColor] = useState(WHITE); 45 | const [isAiTurn, setAiTurn] = useState(false); 46 | const [board, setBoard] = useState(); 47 | const [gameEnded, setGameEnded] = useState(false); 48 | const [availableMoves, setAvailableMoves] = useState([]); 49 | const [currentPieceMoves, setCurrentPieceMoves] = useState(new Set()); 50 | const [winningPlayer, setWinningPlayer] = useState(); 51 | const [difficultyLevel, setDifficultyLevel] = useState(6); 52 | const [moveHistory, setMoveHistory] = useState([]); 53 | const [promotion, setPromotion] = useState(undefined); 54 | const [inCheck, setInCheck] = useState(0); 55 | 56 | const lastMove = moveHistory.length > 0 ? moveHistory[moveHistory.length - 1] : { start: -1, end: -1 }; 57 | 58 | const clearAvailableMoves = () => setAvailableMoves([]); 59 | 60 | const addMove = useCallback(move => { 61 | setMoveHistory([...moveHistory, move]) 62 | }, [setMoveHistory, moveHistory]); 63 | 64 | const updateGame = useCallback(async state => { 65 | setBoard(state.board); 66 | setAvailableMoves(state.moves); 67 | 68 | if (state.whiteInCheck) { 69 | setInCheck(WHITE); 70 | } else if (state.blackInCheck) { 71 | setInCheck(BLACK); 72 | } else { 73 | setInCheck(0); 74 | } 75 | 76 | if (state.gameEnded) { 77 | setGameEnded(true); 78 | 79 | if (state.checkMate) { 80 | setWinningPlayer(nextPlayer(state.activePlayer)); 81 | } 82 | } else { 83 | setActivePlayer(state.activePlayer); 84 | } 85 | 86 | }, []); 87 | 88 | // Initialize chess engine and game state 89 | useEffect(() => { 90 | (async () => { 91 | await engine.init(); 92 | await engine.newGame(); 93 | const gameState = await engine.setPosition(startPosition, []); 94 | await updateGame(gameState); 95 | })(); 96 | }, [updateGame]); 97 | 98 | const canMove = useCallback((start, end) => { 99 | return availableMoves.some( 100 | move => move.start === start && move.end === end 101 | ); 102 | }, [availableMoves]); 103 | 104 | const asyncDelay = (millis) => new Promise(resolve => setTimeout(resolve, millis)); 105 | 106 | const calculateAIMove = useCallback(async () => { 107 | clearAvailableMoves(); 108 | setAiTurn(true); 109 | 110 | const [move] = await Promise.all([engine.calculateMove(difficultyLevel), asyncDelay(150)]); 111 | 112 | const gameState = await engine.performMove(move); 113 | setAiTurn(false); 114 | await updateGame(gameState); 115 | addMove(move); 116 | }, [difficultyLevel, addMove, updateGame]); 117 | 118 | const switchSides = async () => { 119 | setRotateBoard(true); 120 | setHumanPlayerColor(-humanPlayerColor); 121 | setAiTurn(true); 122 | }; 123 | 124 | // Calculate next AI move whenever isAiTurn is set to true 125 | useEffect( () => { 126 | if (isAiTurn) { 127 | (async () => { 128 | await calculateAIMove(); 129 | })(); 130 | } 131 | }, [isAiTurn, calculateAIMove]); 132 | 133 | const startNewGame = async () => { 134 | setGameEnded(false); 135 | setWinningPlayer(undefined); 136 | setActivePlayer(WHITE); 137 | setHumanPlayerColor(WHITE); 138 | setMoveHistory([]); 139 | setCurrentPieceMoves(new Set()); 140 | 141 | await engine.newGame(); 142 | const gameState = await engine.setPosition(startPosition, []); 143 | await updateGame(gameState); 144 | 145 | setRotateBoard(false); 146 | }; 147 | 148 | const undoMove = useCallback(async () => { 149 | const previousMoveHistory = moveHistory.slice(0, moveHistory.length - 2); 150 | const gameState = await engine.setPosition(startPosition, previousMoveHistory); 151 | setMoveHistory(previousMoveHistory); 152 | await updateGame(gameState); 153 | }, [moveHistory, setMoveHistory, updateGame]); 154 | 155 | const handlePlayerMove = useCallback(async (piece, start, end) => { 156 | let pieceId = Math.abs(piece); 157 | if (gameEnded || isAiTurn) { 158 | return; 159 | } 160 | 161 | if (!canMove(start, end)) { 162 | return; 163 | } 164 | 165 | setCurrentPieceMoves(new Set()); 166 | 167 | if (pieceId === P && ((activePlayer === WHITE && end < 8) || (activePlayer === BLACK && end >= 56))) { 168 | // Promotion 169 | 170 | clearAvailableMoves(); 171 | setPromotion({ 172 | start: start, 173 | end: end, 174 | column: end & 7, 175 | color: activePlayer 176 | }); 177 | } else { 178 | 179 | // Standard move 180 | const interimBoard = board.slice(); 181 | interimBoard[start] = 0; 182 | setBoard(interimBoard); 183 | 184 | const move = new Move(pieceId, start, end); 185 | const gameState = await engine.performMove(move); 186 | await updateGame(gameState); 187 | addMove(move); 188 | 189 | if (!gameState.gameEnded) { 190 | setAiTurn(true); 191 | } 192 | } 193 | 194 | }, [activePlayer, addMove, board, canMove, gameEnded, isAiTurn, updateGame]); 195 | 196 | const handlePromotion = useCallback(async (pieceId) => { 197 | const { start, end } = promotion; 198 | setPromotion(undefined); 199 | const interimBoard = board.slice(); 200 | interimBoard[start] = 0; 201 | setBoard(interimBoard); 202 | 203 | const move = new Move(pieceId, start, end); 204 | const gameState = await engine.performMove(move); 205 | await updateGame(gameState); 206 | addMove(move); 207 | 208 | if (!gameState.gameEnded) { 209 | setAiTurn(true); 210 | } 211 | }, [addMove, board, promotion, setPromotion, updateGame]); 212 | 213 | const updatePossibleMoves = start => { 214 | const possibleMoves = availableMoves 215 | .filter(move => move.start === start) 216 | .map(move => move.end); 217 | setCurrentPieceMoves(new Set(possibleMoves)); 218 | }; 219 | 220 | const clearPossibleMoves = () => { 221 | setCurrentPieceMoves(new Set()); 222 | }; 223 | 224 | return ( 225 | board 226 | ? ( 227 | 228 | {promotion && 229 | } 231 | 241 | setRotateBoard(!rotateBoard)} 250 | difficultyLevel={difficultyLevel} 251 | setDifficultyLevel={setDifficultyLevel} 252 | canUndoMove={!gameEnded && moveHistory.length > 1} 253 | undoMove={undoMove} 254 | /> 255 | 256 | ) 257 | : ( 258 | 259 | ) 260 | ); 261 | }; 262 | 263 | const Centered = styled.div` 264 | position: absolute; 265 | left: calc(50% - 60px); 266 | top: calc(50% - 60px); 267 | `; 268 | 269 | export default Game; 270 | -------------------------------------------------------------------------------- /assembly/fen.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { 20 | BLACK, BLACK_KING_SIDE_CASTLING, BLACK_QUEEN_SIDE_CASTLING, 21 | Board, 22 | EMPTY, NO_CASTLING_RIGHTS, 23 | WHITE, WHITE_KING_SIDE_CASTLING, WHITE_QUEEN_SIDE_CASTLING 24 | } from './board'; 25 | import { 26 | BLACK_ENPASSANT_LINE_END, 27 | BLACK_ENPASSANT_LINE_START, BLACK_PAWNS_BASELINE_START, 28 | WHITE_ENPASSANT_LINE_END, 29 | WHITE_ENPASSANT_LINE_START, WHITE_PAWNS_BASELINE_START 30 | } from './pieces'; 31 | 32 | export const STARTPOS = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; 33 | 34 | /* Transforms the given board to a string representation of FEN (see https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation) 35 | */ 36 | export function toFEN(board: Board): string { 37 | return piecePlacement(board) + ' ' + activeColor(board) + ' ' + castlingAvailability(board) + 38 | ' ' + enPassantTargetSquare(board) + ' ' + halfMoveClock(board) + ' ' + fullMoveNumber(board); 39 | } 40 | 41 | // Black pieces go from -6 to -1, white pieces from 1 to 6 42 | // add 6 to get the index to the FEN character for the piece: 43 | const PIECE_FEN_CHARS: string = "kqrbnp/PNBRQK"; 44 | 45 | const COLUMN_LETTERS: string = "abcdefgh"; 46 | 47 | function piecePlacement(board: Board): string { 48 | let result: string = ""; 49 | let emptyFieldCount = 0; 50 | 51 | for (let pos = 0; pos < 64; pos++) { 52 | const piece = board.getItem(pos); 53 | 54 | if (piece == EMPTY) { 55 | emptyFieldCount++; 56 | if (pos % 8 == 7) { 57 | result += emptyFieldCount.toString(); 58 | if (pos != 63) { 59 | result += "/"; 60 | } 61 | emptyFieldCount = 0; 62 | } 63 | continue; 64 | } 65 | 66 | if (emptyFieldCount > 0) { 67 | result += emptyFieldCount.toString(); 68 | emptyFieldCount = 0; 69 | } 70 | 71 | const pieceFenCode = PIECE_FEN_CHARS.charAt(piece + 6); 72 | result += pieceFenCode; 73 | 74 | if (pos != 63 && pos % 8 == 7 ) { 75 | result += "/"; 76 | } 77 | 78 | } 79 | 80 | return result; 81 | } 82 | 83 | function activeColor(board: Board): string { 84 | return board.getActivePlayer() == WHITE ? "w" : "b"; 85 | } 86 | 87 | function castlingAvailability(board: Board): string { 88 | let result: string = ""; 89 | if (board.canWhiteCastleKingSide()) { 90 | result += "K"; 91 | } 92 | 93 | if (board.canWhiteCastleQueenSide()) { 94 | result += "Q"; 95 | } 96 | 97 | if (board.canBlackCastleKingSide()) { 98 | result += "k"; 99 | } 100 | 101 | if (board.canBlackCastleQueenSide()) { 102 | result += "q"; 103 | } 104 | 105 | return result.length == 0 ? "-" : result; 106 | } 107 | 108 | function enPassantTargetSquare(board: Board): string { 109 | for (let i = WHITE_ENPASSANT_LINE_START; i <= WHITE_ENPASSANT_LINE_END; i++) { 110 | if (board.isEnPassentPossible(WHITE, i)) { 111 | const columnNum = i % 8; 112 | return COLUMN_LETTERS.charAt(columnNum) + "6"; 113 | } 114 | } 115 | 116 | for (let i = BLACK_ENPASSANT_LINE_START; i <= BLACK_ENPASSANT_LINE_END; i++) { 117 | if (board.isEnPassentPossible(BLACK, i)) { 118 | const columnNum = i % 8; 119 | return COLUMN_LETTERS.charAt(columnNum) + "3"; 120 | } 121 | } 122 | 123 | return "-"; 124 | } 125 | 126 | function halfMoveClock(board: Board): string { 127 | return board.getHalfMoveClock().toString(); 128 | } 129 | 130 | function fullMoveNumber(board: Board): string { 131 | return board.getFullMoveCount().toString(); 132 | } 133 | 134 | 135 | /* Creates a Board instance from a FEN string (see https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation) 136 | */ 137 | export function fromFEN(fen: string): Board { 138 | const boardItems = new Array(67); 139 | boardItems[64] = 0; 140 | boardItems[65] = 0; 141 | boardItems[66] = 0; 142 | 143 | const fenParts = fen.split(" "); 144 | if (fenParts.length != 6) { 145 | throw new Error("Invalid FEN string: expected 6 parts, but got " + fenParts.length.toString()); 146 | } 147 | 148 | readPiecePlacement(boardItems, fenParts[0]); 149 | const board = new Board(boardItems); 150 | 151 | const activeColor = readActiveColor(fenParts[1]); 152 | readCastlingAvailability(board, fenParts[2]); 153 | readEnPassantTargetSquare(board, fenParts[3]); 154 | readHalfMoveClock(board,fenParts[4]); 155 | readFullMoveNumber(board, activeColor, fenParts[5]); 156 | 157 | board.recalculateHash(); 158 | 159 | return board; 160 | } 161 | 162 | const DIGIT_ONE_CHARCODE = "1".charCodeAt(0); 163 | const DIGIT_EIGHT_CHARCODE = "8".charCodeAt(0); 164 | 165 | function readPiecePlacement(boardItems: Array, fenPart: string): void { 166 | const piecePlacements = fenPart.split("/"); 167 | if (piecePlacements.length != 8) { 168 | throw new Error("Invalid FEN string: invalid piece placement part"); 169 | } 170 | 171 | let boardPos = 0; 172 | for (let i = 0; i < piecePlacements.length; i++) { 173 | const rowChars = piecePlacements[i]; 174 | for (let j = 0; j < rowChars.length; j++) { 175 | const pieceCharCode = rowChars.charCodeAt(j); 176 | if (pieceCharCode >= DIGIT_ONE_CHARCODE && pieceCharCode <= DIGIT_EIGHT_CHARCODE) { 177 | // it's a digit indicating the number of empty fields 178 | const numberOfEmptyFields = pieceCharCode - DIGIT_ONE_CHARCODE + 1; 179 | boardItems.fill(EMPTY, boardPos, boardPos + numberOfEmptyFields); 180 | boardPos += numberOfEmptyFields; 181 | continue; 182 | } 183 | 184 | const pieceIndex = PIECE_FEN_CHARS.indexOf(String.fromCharCode(pieceCharCode)); 185 | if (pieceIndex == -1) { 186 | throw new Error("Invalid FEN string: unknown piece character: " + String.fromCharCode(pieceCharCode)); 187 | } 188 | 189 | const piece = pieceIndex - 6; 190 | boardItems[boardPos] = piece; 191 | boardPos++; 192 | } 193 | } 194 | } 195 | 196 | function readActiveColor(fenPart: string): i32 { 197 | if (fenPart == "w") { 198 | return WHITE; 199 | } else if (fenPart == "b") { 200 | return BLACK; 201 | } 202 | 203 | throw new Error("Invalid FEN string: unexpected character in color part: " + fenPart); 204 | } 205 | 206 | function readCastlingAvailability(board: Board, fenPart: string): void { 207 | let state = NO_CASTLING_RIGHTS; 208 | 209 | if (fenPart == "-") { 210 | board.setState(state); 211 | return; 212 | } 213 | 214 | for (let i = 0; i < fenPart.length; i++) { 215 | const castlingChar = fenPart.charAt(i); 216 | if (castlingChar == "K") { 217 | state |= WHITE_KING_SIDE_CASTLING; 218 | } else if (castlingChar == "Q") { 219 | state |= WHITE_QUEEN_SIDE_CASTLING; 220 | } else if (castlingChar == "k") { 221 | state |= BLACK_KING_SIDE_CASTLING; 222 | } else if (castlingChar == "q") { 223 | state |= BLACK_QUEEN_SIDE_CASTLING; 224 | } else { 225 | throw new Error("Invalid FEN string: unexpected character in castling availability string: " + castlingChar); 226 | } 227 | } 228 | 229 | board.setState(state); 230 | } 231 | 232 | 233 | const LETTER_A_CHARCODE = "a".charCodeAt(0); 234 | const LETTER_H_CHARCODE = "h".charCodeAt(0); 235 | 236 | function readEnPassantTargetSquare(board: Board, fenPart: string): void { 237 | if (fenPart == "-") { 238 | return; 239 | } 240 | 241 | if (fenPart.length != 2) { 242 | throw new Error("Invalid FEN string: unexpected en passant part: " + fenPart); 243 | } 244 | 245 | const colChar = fenPart.charCodeAt(0); 246 | if (colChar < LETTER_A_CHARCODE || colChar > LETTER_H_CHARCODE) { 247 | throw new Error("Invalid FEN string: unexpected en passant part: " + fenPart); 248 | } 249 | 250 | const rowChar = fenPart.charAt(1); 251 | 252 | const colOffset = (colChar - LETTER_A_CHARCODE); // 0-7 253 | 254 | if (rowChar == "3") { 255 | board.setEnPassantPossible(WHITE_PAWNS_BASELINE_START + colOffset); 256 | } else if (rowChar == "6") { 257 | board.setEnPassantPossible(BLACK_PAWNS_BASELINE_START + colOffset); 258 | } else { 259 | throw new Error("Invalid FEN string: unexpected en passant part: " + fenPart); 260 | } 261 | 262 | board.updateHashForEnPassent(0); 263 | } 264 | 265 | function readHalfMoveClock(board: Board, fenPart: string): void { 266 | const halfMoveClock = i16(parseInt(fenPart)); 267 | if (halfMoveClock < 0) { 268 | throw new Error("Invalid FEN string: unexpected halfmove clock part: " + fenPart); 269 | } 270 | 271 | board.setHalfMoveClock(halfMoveClock); 272 | } 273 | 274 | function readFullMoveNumber(board: Board, activeColor: i32, fenPart: string): void { 275 | const fullMoveNumber = i16(parseInt(fenPart)); 276 | if (fullMoveNumber < 1) { 277 | throw new Error("Invalid FEN string: unexpected fullmove number part: " + fenPart); 278 | } 279 | 280 | const halfMoveCount = (fullMoveNumber - 1) * 2 + (activeColor == WHITE ? 0 : 1); 281 | board.initializeHalfMoveCount(halfMoveCount); 282 | } 283 | 284 | -------------------------------------------------------------------------------- /tools/bookgen/bookgen.py: -------------------------------------------------------------------------------- 1 | # A free and open source chess game using AssemblyScript and React 2 | # Copyright (C) 2020 mhonert (https://github.com/mhonert) 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import chess.pgn 18 | import zobrist 19 | import numpy as np 20 | import statistics 21 | 22 | from util import encode_move 23 | from pathlib import Path 24 | 25 | # Generates an opening book from chess games stored in PGN format. 26 | # Each game in the PGN files must contain the following header information 27 | # - Result (e.g. "1-0" if the white player won) 28 | # - WhiteElo 29 | # - BlackElo 30 | # 31 | # The generator will only take games from players with at least 2000 ELO into account. 32 | # The generation result is an AssemblyScript source file. 33 | 34 | PLY_BOOK_LIMIT = 16 35 | ply_moves = [{} for _ in range(PLY_BOOK_LIMIT + 1)] 36 | 37 | move_occurences = [{} for _ in range(PLY_BOOK_LIMIT + 1)] 38 | 39 | max_ply = 0 40 | 41 | print("Parsing chess games from pgn file ...") 42 | game_num = 0 43 | 44 | books = [ 45 | "pgn/fics2400.pgn", 46 | "pgn/fics2600.pgn", 47 | "pgn/fics2600ur.pgn", 48 | "pgn/fics2400ur.pgn" 49 | ] 50 | 51 | move_thresholds = [2 for i in range(PLY_BOOK_LIMIT + 1)] 52 | move_thresholds[0] = 10 53 | move_thresholds[1] = 10 54 | 55 | # First pass: count occurrences of moves to filter out unusual or rarely played openings 56 | for book in books: 57 | pgn = open(book) 58 | print("Analyzing games from ", book) 59 | while True: 60 | game = chess.pgn.read_game(pgn) 61 | if not game: 62 | break 63 | 64 | result = game.headers['Result'] 65 | whiteElo = int(game.headers['WhiteElo']) 66 | blackElo = int(game.headers['BlackElo']) 67 | skipWhite = result == '0-1' 68 | skipBlack = result == '1-0' 69 | 70 | if whiteElo < 2000 or blackElo < 2000 or abs(whiteElo - blackElo) > 50: 71 | continue 72 | 73 | game_num += 1 74 | print("- analyzing game #", game_num) 75 | board = game.board() 76 | ply = 0 77 | for move in game.mainline_moves(): 78 | zobrist_hash = zobrist.calc_hash(board) 79 | moveFrom = move.from_square 80 | moveTo = move.to_square 81 | encoded_move = encode_move(board.piece_at(moveFrom).piece_type, moveFrom, moveTo) 82 | 83 | isWhiteTurn = board.turn == chess.WHITE 84 | isBlackTurn = not isWhiteTurn 85 | 86 | board.push(move) 87 | 88 | if (isWhiteTurn and not skipWhite) or (isBlackTurn and not skipBlack): 89 | # Only include moves that were played in multiple games 90 | if encoded_move in move_occurences[ply]: 91 | move_occurences[ply][encoded_move] += 1 92 | 93 | if move_occurences[ply][encoded_move] >= move_thresholds[ply]: 94 | if ply > max_ply: 95 | max_ply = ply 96 | 97 | else: 98 | move_occurences[ply][encoded_move] = 1 99 | 100 | ply += 1 101 | 102 | if ply > PLY_BOOK_LIMIT: 103 | break 104 | pgn.close() 105 | 106 | # Second pass: extract opening lines 107 | for book in books: 108 | pgn = open(book) 109 | print("Reading games from ", book) 110 | while True: 111 | game = chess.pgn.read_game(pgn) 112 | if not game: 113 | break 114 | 115 | result = game.headers['Result'] 116 | whiteElo = int(game.headers['WhiteElo']) 117 | blackElo = int(game.headers['BlackElo']) 118 | skipWhite = result == '0-1' 119 | skipBlack = result == '1-0' 120 | 121 | if whiteElo < 2000 or blackElo < 2000 or abs(whiteElo - blackElo) > 50: 122 | continue 123 | 124 | game_num += 1 125 | print("- extracting moves from game #", game_num) 126 | board = game.board() 127 | ply = 0 128 | for move in game.mainline_moves(): 129 | zobrist_hash = zobrist.calc_hash(board) 130 | moveFrom = move.from_square 131 | moveTo = move.to_square 132 | encoded_move = encode_move(board.piece_at(moveFrom).piece_type, moveFrom, moveTo) 133 | 134 | isWhiteTurn = board.turn == chess.WHITE 135 | isBlackTurn = not isWhiteTurn 136 | 137 | board.push(move) 138 | 139 | if (isWhiteTurn and not skipWhite) or (isBlackTurn and not skipBlack): 140 | # Only include moves that were played in multiple games 141 | if encoded_move in move_occurences[ply] and move_occurences[ply][encoded_move] >= move_thresholds[ply]: 142 | if ply > max_ply: 143 | max_ply = ply 144 | 145 | if zobrist_hash in ply_moves[ply]: 146 | ply_moves[ply][zobrist_hash].add(encoded_move) 147 | else: 148 | ply_moves[ply][zobrist_hash] = {encoded_move} 149 | 150 | else: 151 | break 152 | 153 | ply += 1 154 | 155 | if ply > PLY_BOOK_LIMIT: 156 | break 157 | pgn.close() 158 | 159 | print("Preparing opening book list...") 160 | 161 | book = [0 for _ in range(max_ply + 1)] 162 | book[0] = max_ply 163 | 164 | for idx in range(max_ply): 165 | if len(ply_moves[idx]) == 0: 166 | break 167 | book[idx + 1] = len(book) # Start index for the move list of the current ply 168 | book.append(len(ply_moves[idx])) # Number of entries (positions) 169 | for zobrist_hash, moves in ply_moves[idx].items(): 170 | # Split 64 bit hash into 2 32-bit entries 171 | for i in range(2): 172 | book.append(zobrist_hash & np.uint64(0xFFFFFFFF)) 173 | zobrist_hash = np.uint64(zobrist_hash >> np.uint64(32)) 174 | 175 | book.append(len(moves)) 176 | book.extend(moves) 177 | 178 | print("Writing opening book list...") 179 | 180 | out = open("../../assembly/opening-book-data.ts", "w") 181 | out.write("/*\n") 182 | out.write(" * A free and open source chess game using AssemblyScript and React\n") 183 | out.write(" * Copyright (C) 2020 mhonert (https://github.com/mhonert)\n") 184 | out.write(" *\n") 185 | out.write(" * This program is free software: you can redistribute it and/or modify\n") 186 | out.write(" * it under the terms of the GNU General Public License as published by\n") 187 | out.write(" * the Free Software Foundation, either version 3 of the License, or\n") 188 | out.write(" * (at your option) any later version.\n") 189 | out.write(" *\n") 190 | out.write(" * This program is distributed in the hope that it will be useful,\n") 191 | out.write(" * but WITHOUT ANY WARRANTY; without even the implied warranty of\n") 192 | out.write(" * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n") 193 | out.write(" * GNU General Public License for more details.\n") 194 | out.write(" *\n") 195 | out.write(" * You should have received a copy of the GNU General Public License\n") 196 | out.write(" * along with this program. If not, see .\n*/\n\n") 197 | 198 | out.write("/* _________________________________________________________________________\n\n") 199 | out.write(" * Auto-generated opening book data file\n") 200 | out.write(" * Format:\n") 201 | out.write(" * Index\n") 202 | out.write(" * 0: Number of plies in this book (BOOK_PLIES)\n") 203 | out.write(" * 1 - BOOK_PLIES: Start index for the moves for this ply\n\n") 204 | out.write(" * For each ply:\n") 205 | out.write(" * - Number of entries for this ply\n") 206 | out.write(" * For each entry:\n") 207 | out.write(" * - Zobrist hash\n") 208 | out.write(" * - Number of moves for this position\n") 209 | out.write(" * For each move:\n") 210 | out.write(" * - Encoded move\n*/\n\n") 211 | 212 | 213 | out.write("@inline\n") 214 | out.write("export function getOpeningBookU32(index: u32): u32 {\n") 215 | out.write(" return load(openingBookData + index * 4);\n") 216 | out.write("}\n\n") 217 | 218 | out.write("@inline\n") 219 | out.write("export function getOpeningBookI32(index: u32): i32 {\n") 220 | out.write(" return load(openingBookData + index * 4);\n") 221 | out.write("}\n\n") 222 | 223 | out.write("const openingBookData = memory.data([ ") 224 | 225 | charsWritten = 0 226 | for idx, entry in enumerate(book): 227 | if idx > 0: 228 | charsWritten += 2 229 | out.write(", ") 230 | 231 | if idx == 1: 232 | out.write("\n ") 233 | 234 | if idx > max_ply and (charsWritten >= 100 or entry > 65535 and charsWritten >= 88): 235 | charsWritten = 0 236 | out.write("\n ") 237 | 238 | text_entry = hex(entry) if entry > 65535 else str(entry) 239 | 240 | out.write(text_entry) 241 | charsWritten += len(text_entry) 242 | 243 | out.write("\n]);\n") 244 | out.close() 245 | print("Success!") 246 | 247 | for ply in range(max_ply): 248 | occurences = move_occurences[ply] 249 | if len(occurences) > 0: 250 | values = move_occurences[ply].values() 251 | print(ply, max(values), int(statistics.median(values)), int(statistics.mean(values))) 252 | 253 | print(move_thresholds) 254 | 255 | print("Memory usage for book data: ", (len(book) * 4) / 1024, "KB") 256 | -------------------------------------------------------------------------------- /assembly/bitboard.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A free and open source chess game using AssemblyScript and React 3 | * Copyright (C) 2020 mhonert (https://github.com/mhonert) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 20 | import { BLACK, indexFromColor, MAX_FIELD_DISTANCE, WHITE } from './board'; 21 | 22 | 23 | // Patterns to check, whether the fields between king and rook are empty 24 | export const WHITE_KING_SIDE_CASTLING_BIT_PATTERN: u64 = 0b01100000_00000000_00000000_00000000_00000000_00000000_00000000_00000000; 25 | export const WHITE_QUEEN_SIDE_CASTLING_BIT_PATTERN: u64 = 0b00001110_00000000_00000000_00000000_00000000_00000000_00000000_00000000; 26 | 27 | export const BLACK_KING_SIDE_CASTLING_BIT_PATTERN: u64 = 0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100000; 28 | export const BLACK_QUEEN_SIDE_CASTLING_BIT_PATTERN: u64 = 0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_00001110; 29 | 30 | // Patterns to check, whether a piece is on a light or dark field 31 | export const LIGHT_COLORED_FIELD_PATTERN: u64 = 0b01010101_01010101_01010101_01010101_01010101_01010101_01010101_01010101; 32 | export const DARK_COLORED_FIELD_PATTERN: u64 = 0b10101010_10101010_10101010_10101010_10101010_10101010_10101010_10101010; 33 | 34 | export const KING_DANGER_ZONE_SIZE: i32 = 2; 35 | 36 | function isBorder(boardPos: i32): bool { 37 | if (boardPos < 21 || boardPos > 98) { 38 | return true; 39 | } 40 | 41 | return boardPos % 10 == 0 || boardPos % 10 == 9; 42 | } 43 | 44 | 45 | function calculateSingleMovePatterns(directions: StaticArray): StaticArray { 46 | const patterns = new StaticArray(64); 47 | let index = 0; 48 | for (let boardPos = 21; boardPos <= 98; boardPos++) { 49 | if (isBorder(boardPos)) { 50 | continue; 51 | } 52 | 53 | let pattern: u64 = 0; 54 | for (let i = 0; i < directions.length; i++) { 55 | const dir = directions[i]; 56 | const targetPos = boardPos + dir; 57 | if (!isBorder(targetPos)) { 58 | let row = (targetPos - 21) / 10; 59 | let col = (targetPos - 21) % 10; 60 | const bitIndex = col + (row * 8); 61 | pattern |= 1 << bitIndex; 62 | } 63 | } 64 | 65 | patterns[index++] = pattern; 66 | } 67 | 68 | return patterns; 69 | } 70 | 71 | // Use letterbox board (10 columns * 12 rows) for simpler border detection during pattern calculation: 72 | const LETTERBOX_KNIGHT_DIRECTIONS: StaticArray = StaticArray.fromArray([21, 19, 12, 8, -12, -21, -19, -8]); 73 | export const KNIGHT_PATTERNS: StaticArray = calculateSingleMovePatterns(LETTERBOX_KNIGHT_DIRECTIONS); 74 | 75 | const LETTERBOX_KING_DIRECTIONS: StaticArray = StaticArray.fromArray([1, 10, -1, -10, 9, 11, -9, -11]); 76 | export const KING_PATTERNS: StaticArray = calculateSingleMovePatterns(LETTERBOX_KING_DIRECTIONS); 77 | 78 | 79 | export const PAWN_DOUBLE_MOVE_LINE: StaticArray = createDoubleMoveLine(); 80 | 81 | function createDoubleMoveLine(): StaticArray { 82 | const lines = new StaticArray(2); 83 | lines[indexFromColor(BLACK)] = 0b0000000000000000000000000000000000000000111111110000000000000000; 84 | lines[indexFromColor(WHITE)] = 0b0000000000000000111111110000000000000000000000000000000000000000; 85 | 86 | return lines; 87 | } 88 | 89 | 90 | enum Direction { 91 | NORTH_WEST, NORTH, NORTH_EAST, 92 | EAST, 93 | SOUTH_EAST, SOUTH, SOUTH_WEST, 94 | WEST 95 | } 96 | 97 | const DIRECTION_COL_OFFSET: Array = [-1, 0, +1, +1, +1, 0, -1, -1]; 98 | const DIRECTION_ROW_OFFSET: Array = [-1, -1, -1, 0, +1, +1, +1, 0]; 99 | 100 | function computeRayAttackBitboards(): StaticArray { 101 | const rayAttacks = new StaticArray(65 * 8); // (64 squares + 1 for empty attack bitboard) * 8 directions 102 | let index = 0; 103 | for (let dir = Direction.NORTH_WEST; dir <= Direction.WEST; dir++) { 104 | for (let pos = 0; pos < 64; pos++) { 105 | let col = pos % 8; 106 | let row = pos / 8; 107 | 108 | let attackBitboard: u64 = 0; 109 | 110 | for (let distance = 1; distance <= MAX_FIELD_DISTANCE; distance++) { 111 | col += DIRECTION_COL_OFFSET[dir]; 112 | row += DIRECTION_ROW_OFFSET[dir]; 113 | if (col < 0 || col > 7 || row < 0 || row > 7) { 114 | break; // border 115 | } 116 | 117 | const patternIndex = row * 8 + col; 118 | attackBitboard |= (1 << patternIndex); 119 | } 120 | 121 | rayAttacks[index++] = attackBitboard; 122 | } 123 | rayAttacks[index++] = 0; // empty attack bitboard 124 | } 125 | return rayAttacks; 126 | } 127 | 128 | const RAY_ATTACKS: StaticArray = computeRayAttackBitboards(); 129 | 130 | @inline 131 | function getPositiveRayAttacks(occupied: u64, dir: Direction, pos: i32): u64 { 132 | const dirOffset = dir * 65; 133 | let attacks = unchecked(RAY_ATTACKS[dirOffset + pos]); 134 | const blocker = attacks & occupied; 135 | if (blocker == 0) { 136 | return attacks; 137 | } 138 | const firstBlockerPos = 63 - i32(clz(blocker)); 139 | attacks ^= unchecked(RAY_ATTACKS[dirOffset + firstBlockerPos]); 140 | return attacks; 141 | } 142 | 143 | @inline 144 | function getNegativeRayAttacks(occupied: u64, dir: Direction, pos: i32): u64 { 145 | const dirOffset = dir * 65; 146 | let attacks = unchecked(RAY_ATTACKS[dirOffset + pos]); 147 | const blocker = attacks & occupied; 148 | const firstBlockerPos = i32(ctz(blocker)); 149 | attacks ^= unchecked(RAY_ATTACKS[dirOffset + firstBlockerPos]); 150 | return attacks; 151 | } 152 | 153 | @inline 154 | export function diagonalAttacks(occupied: u64, pos: i32): u64 { 155 | return getPositiveRayAttacks(occupied, Direction.NORTH_EAST, pos) | getNegativeRayAttacks(occupied, Direction.SOUTH_WEST, pos); 156 | } 157 | 158 | @inline 159 | export function antiDiagonalAttacks(occupied: u64, pos: i32): u64 { 160 | return getPositiveRayAttacks(occupied, Direction.NORTH_WEST, pos) | getNegativeRayAttacks(occupied, Direction.SOUTH_EAST, pos); 161 | } 162 | 163 | @inline 164 | export function horizontalAttacks(occupied: u64, pos: i32): u64 { 165 | return getPositiveRayAttacks(occupied, Direction.WEST, pos) | getNegativeRayAttacks(occupied, Direction.EAST, pos); 166 | } 167 | 168 | @inline 169 | export function verticalAttacks(occupied: u64, pos: i32): u64 { 170 | return getPositiveRayAttacks(occupied, Direction.NORTH, pos) | getNegativeRayAttacks(occupied, Direction.SOUTH, pos); 171 | } 172 | 173 | 174 | @inline 175 | export function blackPawnAttacks(pawns: u64): u64 { 176 | return blackLeftPawnAttacks(pawns) | blackRightPawnAttacks(pawns); 177 | } 178 | 179 | @inline 180 | export function whitePawnAttacks(pawns: u64): u64 { 181 | return whiteLeftPawnAttacks(pawns) | whiteRightPawnAttacks(pawns); 182 | } 183 | 184 | @inline 185 | export function whiteLeftPawnAttacks(pawns: u64): u64 { 186 | return (pawns & 0xfefefefefefefefe) >> 9 // mask right column 187 | } 188 | 189 | @inline 190 | export function whiteRightPawnAttacks(pawns: u64): u64 { 191 | return (pawns & 0x7f7f7f7f7f7f7f7f) >> 7 // mask right column 192 | } 193 | 194 | @inline 195 | export function blackLeftPawnAttacks(pawns: u64): u64 { 196 | return (pawns & 0xfefefefefefefefe) << 7 // mask right column 197 | } 198 | 199 | @inline 200 | export function blackRightPawnAttacks(pawns: u64): u64 { 201 | return (pawns & 0x7f7f7f7f7f7f7f7f) << 9 // mask right column 202 | } 203 | 204 | export const WHITE_KING_SHIELD_PATTERNS = createKingShieldPatterns(-1); 205 | export const BLACK_KING_SHIELD_PATTERNS = createKingShieldPatterns(1); 206 | 207 | function createKingShieldPatterns(direction: i32): StaticArray { 208 | const patterns = new StaticArray(64); 209 | 210 | for (let pos: u32 = 0; pos < 64; pos++) { 211 | const row = pos / 8; 212 | const col = pos & 7; 213 | 214 | let pattern: u64 = 0; 215 | for (let distance = 1; distance <= 2; distance++) { 216 | const shieldRow = row + (direction * distance); 217 | if (shieldRow < 0 || shieldRow > 7) { // Outside the board 218 | continue; 219 | } 220 | 221 | let frontPawnPos = shieldRow * 8 + col; 222 | pattern |= (1 << frontPawnPos); 223 | if (col > 0) { 224 | let frontWestPawnPos = shieldRow * 8 + col - 1; 225 | pattern |= (1 << frontWestPawnPos); 226 | } 227 | if (col < 7) { 228 | let frontEastPawnPos = shieldRow * 8 + col + 1; 229 | pattern |= (1 << frontEastPawnPos); 230 | } 231 | } 232 | 233 | unchecked(patterns[pos] = pattern); 234 | } 235 | 236 | return patterns; 237 | } 238 | 239 | 240 | export const KING_DANGER_ZONE_PATTERNS = createKingDangerZonePatterns(); 241 | 242 | function createKingDangerZonePatterns(): StaticArray { 243 | const patterns = new StaticArray(64); 244 | 245 | for (let pos: u32 = 0; pos < 64; pos++) { 246 | const row = pos / 8; 247 | const col = pos & 7; 248 | 249 | let pattern: u64 = 0; 250 | for (let rowOffset = -KING_DANGER_ZONE_SIZE; rowOffset <= KING_DANGER_ZONE_SIZE; rowOffset++) { 251 | const zoneRow = row + rowOffset; 252 | if (zoneRow < 0 || zoneRow > 7) { // Outside the board 253 | continue; 254 | } 255 | for (let colOffset = -KING_DANGER_ZONE_SIZE; colOffset <= KING_DANGER_ZONE_SIZE; colOffset++) { 256 | const zoneCol = col + colOffset; 257 | if (zoneCol < 0 || zoneCol > 7) { // Outside the board 258 | continue; 259 | } 260 | 261 | const patternPos = zoneRow * 8 + zoneCol; 262 | pattern |= (1 << patternPos); 263 | } 264 | } 265 | 266 | unchecked(patterns[pos] = pattern); 267 | } 268 | 269 | return patterns; 270 | } 271 | 272 | // Patterns to check, whether the path in front of the pawn is free (i.e. not blocked by opponent pieces) 273 | export const WHITE_PAWN_FREEPATH_PATTERNS = createPawnFreePathPatterns(-1); 274 | export const BLACK_PAWN_FREEPATH_PATTERNS = createPawnFreePathPatterns(1); 275 | 276 | function createPawnFreePathPatterns(direction: i32): StaticArray { 277 | const patterns = new StaticArray(64); 278 | 279 | for (let pos: u32 = 0; pos < 64; pos++) { 280 | let row = pos / 8; 281 | const col = pos & 7; 282 | 283 | let pattern: u64 = 0; 284 | while (row >= 1 && row <= 6) { 285 | row += direction; 286 | 287 | pattern |= u64(1) << (row * 8 + col); 288 | } 289 | 290 | unchecked(patterns[pos] = pattern); 291 | } 292 | 293 | return patterns; 294 | } 295 | --------------------------------------------------------------------------------