From 0355cd5e211cd6f0ab4149071654c7539f21752d Mon Sep 17 00:00:00 2001 From: jonty007 Date: Mon, 4 Nov 2024 16:27:04 +0530 Subject: [PATCH 1/5] Base chess app --- packages/chess-app/.env.example | 11 + packages/chess-app/.gitignore | 22 + packages/chess-app/.prettierrc | 6 + packages/chess-app/README.md | 92 + packages/chess-app/eslint.config.js | 28 + packages/chess-app/index.html | 13 + packages/chess-app/package.json | 43 + packages/chess-app/postcss.config.js | 6 + .../chess-app/public/BitcoinComputer-Logo.png | Bin 0 -> 15614 bytes packages/chess-app/public/logo.png | Bin 0 -> 43566 bytes packages/chess-app/public/vite.svg | 1 + packages/chess-app/scripts/deploy.ts | 68 + packages/chess-app/src/App.css | 38 + packages/chess-app/src/App.test.tsx | 10 + packages/chess-app/src/App.tsx | 39 + packages/chess-app/src/assets/react.svg | 1 + packages/chess-app/src/components/Assets.tsx | 11 + .../chess-app/src/components/ChessBoard.tsx | 247 ++ .../src/components/CreateNewGame.tsx | 188 ++ packages/chess-app/src/components/Gallery.tsx | 313 +++ packages/chess-app/src/components/Navbar.tsx | 278 ++ .../chess-app/src/components/utils/index.ts | 70 + packages/chess-app/src/constants/modSpecs.ts | 2 + .../chess-app/src/contracts/chess-game.ts | 71 + .../chess-app/src/contracts/chess-module.ts | 2439 +++++++++++++++++ packages/chess-app/src/contracts/chess.mjs | 1929 +++++++++++++ packages/chess-app/src/index.css | 14 + packages/chess-app/src/main.tsx | 10 + packages/chess-app/src/setupTests.ts | 3 + packages/chess-app/src/types/common.ts | 2 + packages/chess-app/src/vite-env.d.ts | 1 + packages/chess-app/tailwind.config.js | 20 + packages/chess-app/tsconfig.build.json | 10 + packages/chess-app/tsconfig.json | 25 + packages/chess-app/vite.config.ts | 36 + scripts/check-obfuscation.sh | 2 +- 36 files changed, 6048 insertions(+), 1 deletion(-) create mode 100644 packages/chess-app/.env.example create mode 100644 packages/chess-app/.gitignore create mode 100644 packages/chess-app/.prettierrc create mode 100644 packages/chess-app/README.md create mode 100644 packages/chess-app/eslint.config.js create mode 100644 packages/chess-app/index.html create mode 100644 packages/chess-app/package.json create mode 100644 packages/chess-app/postcss.config.js create mode 100644 packages/chess-app/public/BitcoinComputer-Logo.png create mode 100644 packages/chess-app/public/logo.png create mode 100644 packages/chess-app/public/vite.svg create mode 100644 packages/chess-app/scripts/deploy.ts create mode 100644 packages/chess-app/src/App.css create mode 100644 packages/chess-app/src/App.test.tsx create mode 100644 packages/chess-app/src/App.tsx create mode 100644 packages/chess-app/src/assets/react.svg create mode 100644 packages/chess-app/src/components/Assets.tsx create mode 100644 packages/chess-app/src/components/ChessBoard.tsx create mode 100644 packages/chess-app/src/components/CreateNewGame.tsx create mode 100644 packages/chess-app/src/components/Gallery.tsx create mode 100644 packages/chess-app/src/components/Navbar.tsx create mode 100644 packages/chess-app/src/components/utils/index.ts create mode 100644 packages/chess-app/src/constants/modSpecs.ts create mode 100644 packages/chess-app/src/contracts/chess-game.ts create mode 100644 packages/chess-app/src/contracts/chess-module.ts create mode 100644 packages/chess-app/src/contracts/chess.mjs create mode 100644 packages/chess-app/src/index.css create mode 100644 packages/chess-app/src/main.tsx create mode 100644 packages/chess-app/src/setupTests.ts create mode 100644 packages/chess-app/src/types/common.ts create mode 100644 packages/chess-app/src/vite-env.d.ts create mode 100644 packages/chess-app/tailwind.config.js create mode 100644 packages/chess-app/tsconfig.build.json create mode 100644 packages/chess-app/tsconfig.json create mode 100644 packages/chess-app/vite.config.ts diff --git a/packages/chess-app/.env.example b/packages/chess-app/.env.example new file mode 100644 index 000000000..34d733588 --- /dev/null +++ b/packages/chess-app/.env.example @@ -0,0 +1,11 @@ +# Application configuration +VITE_CHAIN=LTC +VITE_NETWORK=regtest +VITE_URL=http://127.0.0.1:1031 + +# Application Port +VITE_PORT=1032 + +# Smart Contract Locations +# Run 'npm run deploy' and copy the output here +VITE_COUNTER_MOD_SPEC=fddcf4fe11f7460ea2efa2912cb3feee73773beffcd9216e6bd6b28ad9c59518:0 diff --git a/packages/chess-app/.gitignore b/packages/chess-app/.gitignore new file mode 100644 index 000000000..9aad0ad64 --- /dev/null +++ b/packages/chess-app/.gitignore @@ -0,0 +1,22 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build +/dist + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +*.log* diff --git a/packages/chess-app/.prettierrc b/packages/chess-app/.prettierrc new file mode 100644 index 000000000..b7a412b57 --- /dev/null +++ b/packages/chess-app/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 100, + "semi": false, + "singleQuote": false, + "trailingComma": "none" +} diff --git a/packages/chess-app/README.md b/packages/chess-app/README.md new file mode 100644 index 000000000..34cc2597d --- /dev/null +++ b/packages/chess-app/README.md @@ -0,0 +1,92 @@ +
+

TBC CRA Template

+

+ A template for Create React App with TypeScript and the Bitcoin Computer +
+ website · docs +

+
+ +## Prerequisites + +You need to have [git](https://www.git-scm.com/) and [node.js](https://nodejs.org/) installed. + +## Installation + + + +```sh +# Download the monorepo +git clone https://github.com/bitcoin-computer/monorepo.git + +# Move into monorepo folder +cd monorepo + +# Install the dependencies +npm install +``` + + + +## Usage + +Most of the api is documented in the [Create React App Readme](https://github.com/facebook/create-react-app). + +### Start the Application + +To start the application run the command below and open [http://localhost:3000](http://localhost:3000). + + + +```bash +# Move to the package +cd packages/cra-template + +# Install the dependencies +npm install + +# Use the default environment variables +cp .env.example .env + +# Start the app +npm run start +``` + + + +## Documentation + +Have a look at the [docs](https://docs.bitcoincomputer.io/) for the Bitcoin Computer. + +## Getting Help + +If you have any questions, please let us know on Telegram, Twitter, or by email clemens@bitcoincomputer.io. + +## Development Status +See [here](https://github.com/bitcoin-computer/monorepo/tree/main/packages/lib#development-status). + +## Price + +See [here](https://github.com/bitcoin-computer/monorepo/tree/main/packages/lib#price). + +## Contributing + +This project is intended as a starting point for new development so we want to keep it simple. If you have found a bug please create an [issue](https://github.com/bitcoin-computer/monorepo/issues). If you have a bug fix or a UX improvement please create a pull request [here](https://github.com/bitcoin-computer/monorepo/pulls). + +If you want to add a feature we recommend to create a fork. Let us know if you have built something cool and we can link to your project. + +## Legal Notice + +See [here](https://github.com/bitcoin-computer/monorepo/tree/main/packages/lib#legal-notice). + +## MIT License + +Copyright (c) 2022 BCDB Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +[node]: https://github.com/bitcoin-computer/monorepo/tree/main/packages/node diff --git a/packages/chess-app/eslint.config.js b/packages/chess-app/eslint.config.js new file mode 100644 index 000000000..092408a9f --- /dev/null +++ b/packages/chess-app/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/packages/chess-app/index.html b/packages/chess-app/index.html new file mode 100644 index 000000000..daf4078a7 --- /dev/null +++ b/packages/chess-app/index.html @@ -0,0 +1,13 @@ + + + + + + + TBC Chess + + +
+ + + diff --git a/packages/chess-app/package.json b/packages/chess-app/package.json new file mode 100644 index 000000000..7855db5b2 --- /dev/null +++ b/packages/chess-app/package.json @@ -0,0 +1,43 @@ +{ + "name": "@bitcoin-computer/chess-app", + "private": true, + "version": "0.22.0-beta.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "test": "vitest", + "deploy": "npm run build-contract && node --loader ts-node/esm ./scripts/deploy.ts", + "build-contract": "tsc --project tsconfig.build.json && mv ./build-contract/chess-module.js ./src/contracts/chess.mjs && rm -rf build-contract" + }, + "dependencies": { + "@bitcoin-computer/components": "^0.22.0-beta.0", + "@bitcoin-computer/lib": "^0.22.0-beta.0", + "flowbite": "^2.3.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@eslint/js": "^9.9.0", + "@testing-library/jest-dom": "^6.5.0", + "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.20", + "eslint": "^9.9.0", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", + "globals": "^15.9.0", + "jsdom": "^25.0.0", + "postcss": "^8.4.44", + "tailwindcss": "^3.4.10", + "typescript": "^5.5.3", + "typescript-eslint": "^8.0.1", + "vite": "^5.4.1", + "vitest": "^2.0.5" + } +} diff --git a/packages/chess-app/postcss.config.js b/packages/chess-app/postcss.config.js new file mode 100644 index 000000000..2e7af2b7f --- /dev/null +++ b/packages/chess-app/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/packages/chess-app/public/BitcoinComputer-Logo.png b/packages/chess-app/public/BitcoinComputer-Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..539aeba1adbac0b1ee13860ad965bb7c5d8e53cd GIT binary patch literal 15614 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�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 literal 0 HcmV?d00001 diff --git a/packages/chess-app/public/vite.svg b/packages/chess-app/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/packages/chess-app/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/chess-app/scripts/deploy.ts b/packages/chess-app/scripts/deploy.ts new file mode 100644 index 000000000..a50a79afd --- /dev/null +++ b/packages/chess-app/scripts/deploy.ts @@ -0,0 +1,68 @@ +import { Computer } from "@bitcoin-computer/lib" +import { ChessGame } from "../src/contracts/chess-game.js" +import { config } from "dotenv" +import * as readline from "node:readline/promises" +import { stdin as input, stdout as output } from "node:process" +import * as fs from "fs" + +config() + +const rl = readline.createInterface({ input, output }) + +const { VITE_CHAIN: chain, VITE_NETWORK: network, VITE_URL: url, MNEMONIC: mnemonic } = process.env + +if (network !== "regtest") { + if (!mnemonic) throw new Error("Please set MNEMONIC in the .env file") +} + +const computer = new Computer({ chain, network, mnemonic, url }) +await computer.faucet(2e8) +const balance = await computer.wallet.getBalance() + +// Summary +console.log(`Chain \x1b[2m${chain}\x1b[0m +Network \x1b[2m${network}\x1b[0m +Node Url \x1b[2m${url}\x1b[0m +Address \x1b[2m${computer.wallet.address}\x1b[0m +Mnemonic \x1b[2m${mnemonic}\x1b[0m +Balance \x1b[2m${balance.balance / 1e8}\x1b[0m`) + +const answer = await rl.question("\nDo you want to deploy the contracts? \x1b[2m(y/n)\x1b[0m") +if (answer === "n") { + console.log("\n Aborting...\n") +} else { + fs.readFile("./src/contracts/chess.mjs", "utf-8", async (err, chessFile) => { + if (err) { + console.error("Error reading file:", err) + return + } + console.log("\n * Deploying Chess contract...") + const chessModSpec = await computer.deploy(`${chessFile}`) + const chessGameModSpec = await computer.deploy(` + import { Chess } from "${chessModSpec}" + export ${ChessGame} + `) + + console.log(` + import { Chess } from "${chessModSpec}" + export ${ChessGame} + `) + + console.log(` +Successfully deployed smart contracts. + +----------------- + ACTION REQUIRED +----------------- + +(1) Update the following rows in your .env file. + +VITE_CHESS_MODULE_MOD_SPEC\x1b[2m=${chessModSpec}\x1b[0m +VITE_CHESS_GAME_MOD_SPEC\x1b[2m=${chessGameModSpec}\x1b[0m + +(2) Run 'npm start' to start the application. +`) + }) +} + +rl.close() diff --git a/packages/chess-app/src/App.css b/packages/chess-app/src/App.css new file mode 100644 index 000000000..74b5e0534 --- /dev/null +++ b/packages/chess-app/src/App.css @@ -0,0 +1,38 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/packages/chess-app/src/App.test.tsx b/packages/chess-app/src/App.test.tsx new file mode 100644 index 000000000..c53ac5c74 --- /dev/null +++ b/packages/chess-app/src/App.test.tsx @@ -0,0 +1,10 @@ +import { screen, render } from "@testing-library/react" +import App from "./App" + +describe("App", () => { + it("renders the App component", () => { + render() + const linkElement = screen.getByText(/All Counters/i) + expect(linkElement).toBeInTheDocument() + }) +}) diff --git a/packages/chess-app/src/App.tsx b/packages/chess-app/src/App.tsx new file mode 100644 index 000000000..9e03f97f6 --- /dev/null +++ b/packages/chess-app/src/App.tsx @@ -0,0 +1,39 @@ +import "./App.css" +import { useEffect, useState } from "react" +import { BrowserRouter, Route, Routes, Navigate } from "react-router-dom" +import { initFlowbite } from "flowbite" +import { Auth, UtilsContext, Wallet, ComputerContext } from "@bitcoin-computer/components" +import { ChessBoard } from "./components/ChessBoard" + +import { Navbar } from "./components/Navbar" +import { MyGames } from "./components/Assets" +import CreateNewGame from "./components/CreateNewGame" + +export default function App() { + const [computer] = useState(Auth.getComputer()) + + useEffect(() => { + initFlowbite() + }, []) + + return ( + + + + + + + +

+ + + + ) +} diff --git a/packages/chess-app/src/assets/react.svg b/packages/chess-app/src/assets/react.svg new file mode 100644 index 000000000..6c87de9bb --- /dev/null +++ b/packages/chess-app/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/chess-app/src/components/Assets.tsx b/packages/chess-app/src/components/Assets.tsx new file mode 100644 index 000000000..bfff101aa --- /dev/null +++ b/packages/chess-app/src/components/Assets.tsx @@ -0,0 +1,11 @@ +import { VITE_CHESS_GAME_MOD_SPEC } from "../constants/modSpecs" +import { Gallery } from "./Gallery" + +export function MyGames() { + return ( + <> +

All Games

+ + + ) +} diff --git a/packages/chess-app/src/components/ChessBoard.tsx b/packages/chess-app/src/components/ChessBoard.tsx new file mode 100644 index 000000000..92ecb98dd --- /dev/null +++ b/packages/chess-app/src/components/ChessBoard.tsx @@ -0,0 +1,247 @@ +import { ComputerContext, Modal, UtilsContext } from "@bitcoin-computer/components" +import { useContext, useEffect, useState } from "react" +import { useParams, Link } from "react-router-dom" +import { Chessboard } from "react-chessboard" +import { Chess, Square } from "../contracts/chess-module" +import { ChessGame } from "../contracts/chess-game" +import { currentPlayer, getGameState, getOrientation, getWinnerPubKey } from "./utils" + +function ListLayout(props: { listOfMoves: string[] }) { + const { listOfMoves } = props + + const rows = [] + + for (let i = 0; i < listOfMoves.length; i += 2) { + const item1 = listOfMoves[i] + const item2 = listOfMoves[i + 1] + + if (item1 && item2) { + rows.push( +
+
{i / 2 + 1}.
+
{item1}
+
{item2}
+
+ ) + } else if (item1) { + rows.push( +
+
{i / 2 + 1}.
+
{item1}
+
+ ) + } + } + + return
{rows}
+} + +function WinnerModal(data: any) { + return ( + <> +
+
+ {data.winnerPubKey === data.userPubKey + ? `Congratiolations! You have won the game. ` + : `Sorry! You have lost the game. `} + Click{" "} + { + Modal.hideModal("winner-modal") + }} + > + here + {" "} + to start a new game. +
+
+
+ +
+ + ) +} + +export function ChessBoard() { + const params = useParams() + const { showSnackBar } = UtilsContext.useUtilsComponents() + const [gameId] = useState(params.id || "") + const [orientation, setOrientation] = useState<"white" | "black">("white") + const [sans, setSans] = useState([]) + const [skipSync, setSkipSync] = useState(false) + const [winnerData, setWinnerData] = useState({}) + + const fetchChessContract = async (): Promise => { + const [latestRev] = await computer.query({ ids: [gameId] }) + const cc = (await computer.sync(latestRev)) as ChessGame + return cc + } + const computer = useContext(ComputerContext) + const [game, setGame] = useState(null) + const [chessContract, setChessContract] = useState(null) + + const setWinner = async () => { + if (!game || !chessContract) return + const winnerPubKey = getWinnerPubKey(game, chessContract) + if (!winnerPubKey) { + return + } + setWinnerData({ winnerPubKey: winnerPubKey, userPubKey: computer.getPublicKey() }) + Modal.showModal("winner-modal") + } + + const syncChessContract = async () => { + try { + const cc = await fetchChessContract() + setSans(cc.sans) + if (skipSync) { + setSkipSync(false) + return + } + setChessContract(cc) + setGame(new Chess(cc.fen)) + await setWinner() + } catch (error) { + console.error("Error fetching contract:", error) + } + } + + useEffect(() => { + const fetch = async () => { + const cc = await fetchChessContract() + setSans(cc.sans) + setChessContract(cc) + setGame(new Chess(cc.fen)) + setOrientation( + getOrientation(cc.firstPlayerColor, cc.firstUserPubKey === computer.getPublicKey()) + ) + await setWinner() + } + fetch() + }, [computer, gameId]) + + // Update the chess state by polling + useEffect(() => { + const intervalId = setInterval(() => { + syncChessContract() + }, 6000) + + return () => clearInterval(intervalId) + }, [chessContract, skipSync]) + + // OnDrop action for chess game + function onDrop(sourceSquare: Square, targetSquare: Square) { + if (!chessContract) return false + + try { + const chessGame = new Chess(chessContract.fen) + const result = chessGame.move({ + from: sourceSquare, + to: targetSquare, + promotion: "q" // always promote to a queen for example simplicity + }) + const chessMovePromise = chessContract.move(result.san) as unknown as Promise + chessMovePromise.catch((err: any) => { + showSnackBar(err.message, false) + syncChessContract() + }) + setSkipSync(true) + const newSan = [...sans] + newSan.push(result.san) + setSans(newSan) + setGame(chessGame) + return true + } catch (error) { + showSnackBar(error instanceof Error ? error.message : "Error Occurred", false) + return false + } + } + + return ( +
+
+ {/* Game Info Column */} + {game && ( +
+
+
+
+
+ Current Player +
+
{currentPlayer(game.fen())}
+
+
+
State
+
{getGameState(game)}
+
+
+
+
+ )} + + {/* Chessboard Column */} +
+ {game && ( + <> +
+
+
+
+ {chessContract?.firstPlayerColor === orientation[0] + ? chessContract.secondPlayerName + : chessContract?.firstPlayerColor} +
+
+
+
+ +
+ +
+ +
+
+
+
+ {chessContract?.firstPlayerColor === orientation[0] + ? chessContract.firstPlayerName + : chessContract?.secondPlayerName} +
+
+
+
+ + )} +
+ + {/* Moves List Column */} + {game && ( +
+
+

Move History

+ +
+
+ )} +
+ +
+ ) +} diff --git a/packages/chess-app/src/components/CreateNewGame.tsx b/packages/chess-app/src/components/CreateNewGame.tsx new file mode 100644 index 000000000..a340fd576 --- /dev/null +++ b/packages/chess-app/src/components/CreateNewGame.tsx @@ -0,0 +1,188 @@ +import { useContext, useState } from "react" +import { ComputerContext, Modal, UtilsContext } from "@bitcoin-computer/components" +import { Link } from "react-router-dom" +import { Computer } from "@bitcoin-computer/lib" +import { VITE_CHESS_GAME_MOD_SPEC } from "../constants/modSpecs" + +function SuccessContent(id: string) { + return ( + <> +
+
+ Congratiolations! You have created a new game. Click{" "} + { + Modal.hideModal("success-modal") + }} + > + here + {" "} + to start playing it. +
+
+
+ +
+ + ) +} + +function ErrorContent(msg: string) { + return ( + <> +
+
+ Something went wrong. +
+
+ {msg} +
+
+
+ +
+ + ) +} + +function MintForm(props: { + computer: Computer + setSuccessRev: React.Dispatch> + setErrorMsg: React.Dispatch> +}) { + const { computer, setSuccessRev, setErrorMsg } = props + const [name, setName] = useState("") + const [color, setColor] = useState("") + const [secondPlayerPublicKey, setSecondPlayerPublicKey] = useState("") + const [secondPlayerUserName, setSecondPlayerUserName] = useState("") + const { showLoader } = UtilsContext.useUtilsComponents() + + const onSubmit = async (e: React.SyntheticEvent) => { + e.preventDefault() + try { + showLoader(true) + console.log( + `new ChessGame("${color}", "${computer.getPublicKey()}", "${secondPlayerPublicKey}", "${name}", "${secondPlayerUserName}")` + ) + const { tx, effect }: any = await computer.encode({ + exp: `new ChessGame("${color}", "${computer.getPublicKey()}", "${secondPlayerPublicKey}", "${name}", "${secondPlayerUserName}")`, + mod: VITE_CHESS_GAME_MOD_SPEC + }) + + const data = await computer.broadcast(tx) + console.log(data, effect) + setSuccessRev(effect.res?._id) + showLoader(false) + Modal.showModal("success-modal") + } catch (err) { + showLoader(false) + if (err instanceof Error) { + setErrorMsg(err.message) + Modal.showModal("error-modal") + } + } + } + return ( + <> +
+
+

Let's Play

+

Start a new game and invite your friend.

+ +
+ + setName(e.target.value)} + className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + /> +
+
+ + setSecondPlayerUserName(e.target.value)} + className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + /> +
+
+ + setSecondPlayerPublicKey(e.target.value)} + className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + /> +
+ + +
+ +
+ + ) +} + +export default function CreateNewGame() { + const computer = useContext(ComputerContext) + const [successRev, setSuccessRev] = useState("") + const [errorMsg, setErrorMsg] = useState("") + + return ( + <> + + + + + ) +} diff --git a/packages/chess-app/src/components/Gallery.tsx b/packages/chess-app/src/components/Gallery.tsx new file mode 100644 index 000000000..89d40498b --- /dev/null +++ b/packages/chess-app/src/components/Gallery.tsx @@ -0,0 +1,313 @@ +import { Computer } from "@bitcoin-computer/lib" +import { useContext, useEffect, useState } from "react" +import { Link, useLocation, useNavigate } from "react-router-dom" +import { initFlowbite } from "flowbite" +import { Auth, ComputerContext, UtilsContext } from "@bitcoin-computer/components" +import { BiGitCompare } from "react-icons/bi" +import { ChessGame } from "../contracts/chess-game" +import { Chess } from "../contracts/chess-module" +import { getGameState, truncateName } from "./utils" + +export type Class = new (...args: any) => any + +export type UserQuery = Partial<{ + mod: string + publicKey: string + limit: number + offset: number + order: "ASC" | "DESC" + ids: string[] + contract: { + class: T + args?: ConstructorParameters + } +}> + +function GameCard({ chessGame }: { chessGame: ChessGame }) { + const c = new Chess(chessGame.fen) + const publicKey = Auth.getComputer().getPublicKey() + return ( + <> +
+
+

+ {getGameState(c)} +

+

+ {truncateName(chessGame.firstPlayerName)} +

+
+ +
+

+ {truncateName(chessGame.secondPlayerName)} +

+
+
+ + ) +} + +function HomePageCard({ content }: any) { + return ( +
+
+        {typeof content === "function" ? content() : ""}
+      
+
+ ) +} + +function ValueComponent({ rev, computer }: { rev: string; computer: Computer }) { + const [value, setValue] = useState("loading...") + const [errorMsg, setMsgError] = useState("") + const [loading, setLoading] = useState(true) + + useEffect(() => { + const fetch = async () => { + try { + const synced: ChessGame = (await computer.sync(rev)) as ChessGame + setValue(synced) + } catch (err) { + if (err instanceof Error) setMsgError(`Error: ${err.message}`) + } + setLoading(false) + } + fetch() + }, [computer, rev]) + + const loadingContent = () => ( + <> + +  Loading... + + ) + + if (loading) { + return + } + + if (errorMsg) { + return + } + + return ( + + + + ) +} + +function FromRevs({ revs, computer }: { revs: string[]; computer: any }) { + const cols: string[][] = [[], [], [], []] + + revs.forEach((rev, index) => { + const colNumber = index % 4 + cols[colNumber].push(rev) + }) + return ( + <> +
+
+ {cols[0].map((rev) => ( +
+ +
+ ))} +
+
+ {cols[1].map((rev) => ( +
+ +
+ ))} +
+
+ {cols[2].map((rev) => ( +
+ +
+ ))} +
+
+ {cols[3].map((rev) => ( +
+ +
+ ))} +
+
+ + ) +} + +function Pagination({ isPrevAvailable, handlePrev, isNextAvailable, handleNext }: any) { + return ( + + ) +} + +export default function WithPagination(q: UserQuery) { + const navigate = useNavigate() + const { showLoader } = UtilsContext.useUtilsComponents() + const contractsPerPage = 12 + const computer = useContext(ComputerContext) + const [pageNum, setPageNum] = useState(0) + const [isNextAvailable, setIsNextAvailable] = useState(true) + const [isPrevAvailable, setIsPrevAvailable] = useState(pageNum > 0) + const [showNoAsset, setShowNoAsset] = useState(false) + const [revs, setRevs] = useState([]) + const location = useLocation() + const params = Object.fromEntries(new URLSearchParams(location.search)) + + useEffect(() => { + initFlowbite() + }, []) + + useEffect(() => { + const fetch = async () => { + showLoader(true) + const query = { ...q, ...params } + query.offset = contractsPerPage * pageNum + query.limit = contractsPerPage + 1 + query.order = "DESC" + const result = await computer.query(query) + setIsNextAvailable(result.length > contractsPerPage) + setRevs(result.slice(0, contractsPerPage)) + if (pageNum === 0 && result?.length === 0) { + setShowNoAsset(true) + } + showLoader(false) + } + fetch() + }, [computer, pageNum]) + + const handleNext = async () => { + setIsPrevAvailable(true) + setPageNum(pageNum + 1) + } + + const handlePrev = async () => { + setIsNextAvailable(true) + if (pageNum - 1 === 0) setIsPrevAvailable(false) + setPageNum(pageNum - 1) + } + + return ( +
+ + {!(pageNum === 0 && revs && revs.length === 0) && ( + + )} + {pageNum === 0 && revs && revs.length === 0 && showNoAsset && ( +
+ +
+ )} +
+ ) +} + +export const Gallery = { + FromRevs, + WithPagination +} diff --git a/packages/chess-app/src/components/Navbar.tsx b/packages/chess-app/src/components/Navbar.tsx new file mode 100644 index 000000000..205dcbb00 --- /dev/null +++ b/packages/chess-app/src/components/Navbar.tsx @@ -0,0 +1,278 @@ +import { Link } from "react-router-dom" +import { Modal, Auth, UtilsContext, Drawer } from "@bitcoin-computer/components" +import { useEffect, useState } from "react" +import { initFlowbite } from "flowbite" +import { Chain, Network } from "../types/common" + +const modalTitle = "Connect to Node" +const modalId = "unsupported-config-modal" + +function formatChainAndNetwork(chain: Chain, network: Network) { + const map = { + mainnet: "", + testnet: "t", + regtest: "r" + } + const prefix = map[network] + return `${prefix}${chain}` +} + +function ModalContent() { + const [url, setUrl] = useState("") + function setNetwork(e: React.SyntheticEvent) { + e.preventDefault() + localStorage.setItem("URL", url) + } + + function closeModal() { + Modal.get(modalId).hide() + } + + return ( +
+
+
+ + + setUrl(e.target.value)} + value={url} + type="text" + name="url" + id="url" + className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" + placeholder="http://127.0.0.1:1031" + required + /> + + +
+
+ +
+ + +
+
+ ) +} + +function SignInItem() { + return ( +
  • + +
  • + ) +} + +export function NotLoggedMenu() { + const [dropDownLabel, setDropDownLabel] = useState("LTC") + const { showSnackBar } = UtilsContext.useUtilsComponents() + + useEffect(() => { + initFlowbite() + + const { chain, network } = Auth.defaultConfiguration() + setDropDownLabel(formatChainAndNetwork(chain, network)) + }, []) + + const setChainAndNetwork = (chain: Chain, network: Network) => { + try { + localStorage.setItem("CHAIN", chain) + localStorage.setItem("NETWORK", network) + setDropDownLabel(formatChainAndNetwork(chain, network)) + window.location.href = "/" + } catch (err) { + showSnackBar("Error setting chain and network", false) + Modal.get(modalId).show() + } + } + + function CoinSelectionItem({ chain, network }: { chain: Chain; network: Network }) { + return ( +
  • +
    setChainAndNetwork(chain, network)} + className="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" + > + {chain} {network} +
    +
  • + ) + } + + return ( + <> + +
      +
    • + + +
    • + + +
    + + ) +} + +function WalletItem() { + return ( +
  • + +
  • + ) +} + +const capitalizeFirstLetter = (s: string) => s.charAt(0).toUpperCase() + s.slice(1) + +function Item({ dest }: { dest: string }) { + return ( + + + {capitalizeFirstLetter(dest)} + + + ) +} + +export function LoggedInMenu() { + return ( +
      + + +
    + ) +} + +function NavbarDropdownButton() { + return ( + + ) +} + +export function Logo({ name = "TBC Chess" }) { + return ( + + Bitcoin Computer Logo + + {name} + + + ) +} + +export function Navbar() { + useEffect(() => { + initFlowbite() + }, []) + + return ( + <> + + + ) +} diff --git a/packages/chess-app/src/components/utils/index.ts b/packages/chess-app/src/components/utils/index.ts new file mode 100644 index 000000000..3c6726b42 --- /dev/null +++ b/packages/chess-app/src/components/utils/index.ts @@ -0,0 +1,70 @@ +import { Chess } from "../../contracts/chess-module" +import { ChessGame } from "../../contracts/chess-game" + +export function currentPlayer(fen: string) { + const parts = fen.split(" ") + const activeColor = parts[1] + + if (activeColor === "w") { + return "White" + } + if (activeColor === "b") { + return "Black" + } + throw new Error("Invalid FEN: Unknown active color") +} +export function getOrientation(firstPlayerColor: string, isFirst: boolean): "white" | "black" { + let result: "white" | "black" + if (isFirst) { + if (firstPlayerColor === "w") { + result = "white" + } else { + result = "black" + } + } else if (firstPlayerColor === "w") { + result = "black" + } else { + result = "white" + } + return result +} + +export function getWinnerPubKey(chessInstance: Chess, cc: ChessGame) { + if (!chessInstance || !cc) { + return null + } + if (chessInstance.isCheckmate()) { + const winner = chessInstance.turn() === "w" ? "b" : "w" + if (cc.firstPlayerColor === winner) { + return cc.firstUserPubKey + } + return cc.secondUserPubKey + } + return null +} + +export function getGameState(chessInstance: Chess): string { + if (chessInstance.isCheckmate()) { + return `${chessInstance.turn() === "w" ? "Black" : "White"} wins by checkmate!` + } + if ( + chessInstance.isDraw() || + chessInstance.isStalemate() || + chessInstance.isThreefoldRepetition() + ) { + return "Game is Draw!" + } + if (chessInstance.isCheck()) { + return `${chessInstance.turn() === "w" ? "White" : "Black"} is in check!` + } + if (chessInstance.isInsufficientMaterial()) { + return "Insufficient material for checkmate." + } + if (chessInstance.isGameOver()) { + return "Game over!" + } + return "In Progress" +} + +export const truncateName = (name: string, maxLength: number = 15) => + name.length > maxLength ? name.slice(0, maxLength) + "..." : name diff --git a/packages/chess-app/src/constants/modSpecs.ts b/packages/chess-app/src/constants/modSpecs.ts new file mode 100644 index 000000000..713217a2f --- /dev/null +++ b/packages/chess-app/src/constants/modSpecs.ts @@ -0,0 +1,2 @@ +const { VITE_CHESS_MODULE_MOD_SPEC, VITE_CHESS_GAME_MOD_SPEC } = import.meta.env +export { VITE_CHESS_MODULE_MOD_SPEC, VITE_CHESS_GAME_MOD_SPEC } diff --git a/packages/chess-app/src/contracts/chess-game.ts b/packages/chess-app/src/contracts/chess-game.ts new file mode 100644 index 000000000..8f3ea3fab --- /dev/null +++ b/packages/chess-app/src/contracts/chess-game.ts @@ -0,0 +1,71 @@ +export class ChessGame extends Contract { + firstPlayerColor!: string + sans!: string[] + firstUserPubKey!: string + secondUserPubKey!: string + firstPlayerName!: string + secondPlayerName!: string + fen!: string + constructor( + color: string, + firstUserPubKey: string, + secondUserPubKey: string, + firstPlayerName: string, + secondPlayerName: string + ) { + super({ + firstPlayerColor: color.toLocaleLowerCase() === "black" ? "b" : "w", + sans: [], + firstUserPubKey: firstUserPubKey, + secondUserPubKey: secondUserPubKey, + fen: + color.toLocaleLowerCase() === "black" + ? "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1" + : "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", + firstPlayerName: firstPlayerName, + secondPlayerName: secondPlayerName + }) + } + + addFirstPlayer(pubKey: string) { + this.firstUserPubKey = pubKey + } + + addSecondPlayer(pubKey: string) { + this.secondUserPubKey = pubKey + } + + move(san: string) { + this.sans.push(san) + // @ts-expect-error type error + const game = new Chess(this.fen) + game.move(san) + this.fen = game.fen() + if (this._owners[0] === this.firstUserPubKey) { + this._owners = [this.secondUserPubKey] + } else { + this._owners = [this.firstUserPubKey] + } + } + + changeOwner() { + if (this._owners[0] === this.firstUserPubKey) { + this._owners = [this.secondUserPubKey] + } else { + this._owners = [this.firstUserPubKey] + } + } + + isGameOver() { + // @ts-expect-error type error + return new Chess(this.fen).isGameOver() + } + + getSans() { + return this.sans + } + + getFen() { + return this.fen + } +} diff --git a/packages/chess-app/src/contracts/chess-module.ts b/packages/chess-app/src/contracts/chess-module.ts new file mode 100644 index 000000000..40627f51c --- /dev/null +++ b/packages/chess-app/src/contracts/chess-module.ts @@ -0,0 +1,2439 @@ +/** + * @license + * Copyright (c) 2023, Jeff Hlywa (jhlywa@gmail.com) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + + export const WHITE = 'w' + export const BLACK = 'b' + + export const PAWN = 'p' + export const KNIGHT = 'n' + export const BISHOP = 'b' + export const ROOK = 'r' + export const QUEEN = 'q' + export const KING = 'k' + + export type Color = 'w' | 'b' + export type PieceSymbol = 'p' | 'n' | 'b' | 'r' | 'q' | 'k' + + // prettier-ignore + export type Square = + 'a8' | 'b8' | 'c8' | 'd8' | 'e8' | 'f8' | 'g8' | 'h8' | + 'a7' | 'b7' | 'c7' | 'd7' | 'e7' | 'f7' | 'g7' | 'h7' | + 'a6' | 'b6' | 'c6' | 'd6' | 'e6' | 'f6' | 'g6' | 'h6' | + 'a5' | 'b5' | 'c5' | 'd5' | 'e5' | 'f5' | 'g5' | 'h5' | + 'a4' | 'b4' | 'c4' | 'd4' | 'e4' | 'f4' | 'g4' | 'h4' | + 'a3' | 'b3' | 'c3' | 'd3' | 'e3' | 'f3' | 'g3' | 'h3' | + 'a2' | 'b2' | 'c2' | 'd2' | 'e2' | 'f2' | 'g2' | 'h2' | + 'a1' | 'b1' | 'c1' | 'd1' | 'e1' | 'f1' | 'g1' | 'h1' + + export const DEFAULT_POSITION = + 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1' + + export type Piece = { + color: Color + type: PieceSymbol + } + + type InternalMove = { + color: Color + from: number + to: number + piece: PieceSymbol + captured?: PieceSymbol + promotion?: PieceSymbol + flags: number + } + + interface History { + move: InternalMove + kings: Record + turn: Color + castling: Record + epSquare: number + halfMoves: number + moveNumber: number + } + + export type Move = { + color: Color + from: Square + to: Square + piece: PieceSymbol + captured?: PieceSymbol + promotion?: PieceSymbol + flags: string + san: string + lan: string + before: string + after: string + } + + const EMPTY = -1 + + const FLAGS: Record = { + NORMAL: 'n', + CAPTURE: 'c', + BIG_PAWN: 'b', + EP_CAPTURE: 'e', + PROMOTION: 'p', + KSIDE_CASTLE: 'k', + QSIDE_CASTLE: 'q', + } + + // prettier-ignore + export const SQUARES: Square[] = [ + 'a8', 'b8', 'c8', 'd8', 'e8', 'f8', 'g8', 'h8', + 'a7', 'b7', 'c7', 'd7', 'e7', 'f7', 'g7', 'h7', + 'a6', 'b6', 'c6', 'd6', 'e6', 'f6', 'g6', 'h6', + 'a5', 'b5', 'c5', 'd5', 'e5', 'f5', 'g5', 'h5', + 'a4', 'b4', 'c4', 'd4', 'e4', 'f4', 'g4', 'h4', + 'a3', 'b3', 'c3', 'd3', 'e3', 'f3', 'g3', 'h3', + 'a2', 'b2', 'c2', 'd2', 'e2', 'f2', 'g2', 'h2', + 'a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1' + ] + + const BITS: Record = { + NORMAL: 1, + CAPTURE: 2, + BIG_PAWN: 4, + EP_CAPTURE: 8, + PROMOTION: 16, + KSIDE_CASTLE: 32, + QSIDE_CASTLE: 64, + } + + /* + * NOTES ABOUT 0x88 MOVE GENERATION ALGORITHM + * ---------------------------------------------------------------------------- + * From https://github.com/jhlywa/chess.js/issues/230 + * + * A lot of people are confused when they first see the internal representation + * of chess.js. It uses the 0x88 Move Generation Algorithm which internally + * stores the board as an 8x16 array. This is purely for efficiency but has a + * couple of interesting benefits: + * + * 1. 0x88 offers a very inexpensive "off the board" check. Bitwise AND (&) any + * square with 0x88, if the result is non-zero then the square is off the + * board. For example, assuming a knight square A8 (0 in 0x88 notation), + * there are 8 possible directions in which the knight can move. These + * directions are relative to the 8x16 board and are stored in the + * PIECE_OFFSETS map. One possible move is A8 - 18 (up one square, and two + * squares to the left - which is off the board). 0 - 18 = -18 & 0x88 = 0x88 + * (because of two-complement representation of -18). The non-zero result + * means the square is off the board and the move is illegal. Take the + * opposite move (from A8 to C7), 0 + 18 = 18 & 0x88 = 0. A result of zero + * means the square is on the board. + * + * 2. The relative distance (or difference) between two squares on a 8x16 board + * is unique and can be used to inexpensively determine if a piece on a + * square can attack any other arbitrary square. For example, let's see if a + * pawn on E7 can attack E2. The difference between E7 (20) - E2 (100) is + * -80. We add 119 to make the ATTACKS array index non-negative (because the + * worst case difference is A8 - H1 = -119). The ATTACKS array contains a + * bitmask of pieces that can attack from that distance and direction. + * ATTACKS[-80 + 119=39] gives us 24 or 0b11000 in binary. Look at the + * PIECE_MASKS map to determine the mask for a given piece type. In our pawn + * example, we would check to see if 24 & 0x1 is non-zero, which it is + * not. So, naturally, a pawn on E7 can't attack a piece on E2. However, a + * rook can since 24 & 0x8 is non-zero. The only thing left to check is that + * there are no blocking pieces between E7 and E2. That's where the RAYS + * array comes in. It provides an offset (in this case 16) to add to E7 (20) + * to check for blocking pieces. E7 (20) + 16 = E6 (36) + 16 = E5 (52) etc. + */ + + // prettier-ignore + // eslint-disable-next-line + const Ox88: Record = { + a8: 0, b8: 1, c8: 2, d8: 3, e8: 4, f8: 5, g8: 6, h8: 7, + a7: 16, b7: 17, c7: 18, d7: 19, e7: 20, f7: 21, g7: 22, h7: 23, + a6: 32, b6: 33, c6: 34, d6: 35, e6: 36, f6: 37, g6: 38, h6: 39, + a5: 48, b5: 49, c5: 50, d5: 51, e5: 52, f5: 53, g5: 54, h5: 55, + a4: 64, b4: 65, c4: 66, d4: 67, e4: 68, f4: 69, g4: 70, h4: 71, + a3: 80, b3: 81, c3: 82, d3: 83, e3: 84, f3: 85, g3: 86, h3: 87, + a2: 96, b2: 97, c2: 98, d2: 99, e2: 100, f2: 101, g2: 102, h2: 103, + a1: 112, b1: 113, c1: 114, d1: 115, e1: 116, f1: 117, g1: 118, h1: 119 + } + + const PAWN_OFFSETS = { + b: [16, 32, 17, 15], + w: [-16, -32, -17, -15], + } + + const PIECE_OFFSETS = { + n: [-18, -33, -31, -14, 18, 33, 31, 14], + b: [-17, -15, 17, 15], + r: [-16, 1, 16, -1], + q: [-17, -16, -15, 1, 17, 16, 15, -1], + k: [-17, -16, -15, 1, 17, 16, 15, -1], + } + + // prettier-ignore + const ATTACKS = [ + 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0,20, 0, + 0,20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0,20, 0, 0, + 0, 0,20, 0, 0, 0, 0, 24, 0, 0, 0, 0,20, 0, 0, 0, + 0, 0, 0,20, 0, 0, 0, 24, 0, 0, 0,20, 0, 0, 0, 0, + 0, 0, 0, 0,20, 0, 0, 24, 0, 0,20, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0,20, 2, 24, 2,20, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2,53, 56, 53, 2, 0, 0, 0, 0, 0, 0, + 24,24,24,24,24,24,56, 0, 56,24,24,24,24,24,24, 0, + 0, 0, 0, 0, 0, 2,53, 56, 53, 2, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0,20, 2, 24, 2,20, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0,20, 0, 0, 24, 0, 0,20, 0, 0, 0, 0, 0, + 0, 0, 0,20, 0, 0, 0, 24, 0, 0, 0,20, 0, 0, 0, 0, + 0, 0,20, 0, 0, 0, 0, 24, 0, 0, 0, 0,20, 0, 0, 0, + 0,20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0,20, 0, 0, + 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0,20 + ]; + + // prettier-ignore + const RAYS = [ + 17, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 15, 0, + 0, 17, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 15, 0, 0, + 0, 0, 17, 0, 0, 0, 0, 16, 0, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 17, 0, 0, 0, 16, 0, 0, 0, 15, 0, 0, 0, 0, + 0, 0, 0, 0, 17, 0, 0, 16, 0, 0, 15, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 17, 0, 16, 0, 15, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 17, 16, 15, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 0, -1, -1, -1,-1, -1, -1, -1, 0, + 0, 0, 0, 0, 0, 0,-15,-16,-17, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0,-15, 0,-16, 0,-17, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0,-15, 0, 0,-16, 0, 0,-17, 0, 0, 0, 0, 0, + 0, 0, 0,-15, 0, 0, 0,-16, 0, 0, 0,-17, 0, 0, 0, 0, + 0, 0,-15, 0, 0, 0, 0,-16, 0, 0, 0, 0,-17, 0, 0, 0, + 0,-15, 0, 0, 0, 0, 0,-16, 0, 0, 0, 0, 0,-17, 0, 0, + -15, 0, 0, 0, 0, 0, 0,-16, 0, 0, 0, 0, 0, 0,-17 + ]; + + const PIECE_MASKS = { p: 0x1, n: 0x2, b: 0x4, r: 0x8, q: 0x10, k: 0x20 } + + const SYMBOLS = 'pnbrqkPNBRQK' + + const PROMOTIONS: PieceSymbol[] = [KNIGHT, BISHOP, ROOK, QUEEN] + + const RANK_1 = 7 + const RANK_2 = 6 + /* + * const RANK_3 = 5 + * const RANK_4 = 4 + * const RANK_5 = 3 + * const RANK_6 = 2 + */ + const RANK_7 = 1 + const RANK_8 = 0 + + const SIDES = { + [KING]: BITS.KSIDE_CASTLE, + [QUEEN]: BITS.QSIDE_CASTLE, + } + + const ROOKS = { + w: [ + { square: Ox88.a1, flag: BITS.QSIDE_CASTLE }, + { square: Ox88.h1, flag: BITS.KSIDE_CASTLE }, + ], + b: [ + { square: Ox88.a8, flag: BITS.QSIDE_CASTLE }, + { square: Ox88.h8, flag: BITS.KSIDE_CASTLE }, + ], + } + + const SECOND_RANK = { b: RANK_7, w: RANK_2 } + + const TERMINATION_MARKERS = ['1-0', '0-1', '1/2-1/2', '*'] + + // Extracts the zero-based rank of an 0x88 square. + function rank(square: number): number { + return square >> 4 + } + + // Extracts the zero-based file of an 0x88 square. + function file(square: number): number { + return square & 0xf + } + + function isDigit(c: string): boolean { + return '0123456789'.indexOf(c) !== -1 + } + + // Converts a 0x88 square to algebraic notation. + function algebraic(square: number): Square { + const f = file(square) + const r = rank(square) + return ('abcdefgh'.substring(f, f + 1) + + '87654321'.substring(r, r + 1)) as Square + } + + function swapColor(color: Color): Color { + return color === WHITE ? BLACK : WHITE + } + + export function validateFen(fen: string) { + // 1st criterion: 6 space-seperated fields? + const tokens = fen.split(/\s+/) + if (tokens.length !== 6) { + return { + ok: false, + error: 'Invalid FEN: must contain six space-delimited fields', + } + } + + // 2nd criterion: move number field is a integer value > 0? + const moveNumber = parseInt(tokens[5], 10) + if (isNaN(moveNumber) || moveNumber <= 0) { + return { + ok: false, + error: 'Invalid FEN: move number must be a positive integer', + } + } + + // 3rd criterion: half move counter is an integer >= 0? + const halfMoves = parseInt(tokens[4], 10) + if (isNaN(halfMoves) || halfMoves < 0) { + return { + ok: false, + error: + 'Invalid FEN: half move counter number must be a non-negative integer', + } + } + + // 4th criterion: 4th field is a valid e.p.-string? + if (!/^(-|[abcdefgh][36])$/.test(tokens[3])) { + return { ok: false, error: 'Invalid FEN: en-passant square is invalid' } + } + + // 5th criterion: 3th field is a valid castle-string? + if (/[^kKqQ-]/.test(tokens[2])) { + return { ok: false, error: 'Invalid FEN: castling availability is invalid' } + } + + // 6th criterion: 2nd field is "w" (white) or "b" (black)? + if (!/^(w|b)$/.test(tokens[1])) { + return { ok: false, error: 'Invalid FEN: side-to-move is invalid' } + } + + // 7th criterion: 1st field contains 8 rows? + const rows = tokens[0].split('/') + if (rows.length !== 8) { + return { + ok: false, + error: "Invalid FEN: piece data does not contain 8 '/'-delimited rows", + } + } + + // 8th criterion: every row is valid? + for (let i = 0; i < rows.length; i++) { + // check for right sum of fields AND not two numbers in succession + let sumFields = 0 + let previousWasNumber = false + + for (let k = 0; k < rows[i].length; k++) { + if (isDigit(rows[i][k])) { + if (previousWasNumber) { + return { + ok: false, + error: 'Invalid FEN: piece data is invalid (consecutive number)', + } + } + sumFields += parseInt(rows[i][k], 10) + previousWasNumber = true + } else { + if (!/^[prnbqkPRNBQK]$/.test(rows[i][k])) { + return { + ok: false, + error: 'Invalid FEN: piece data is invalid (invalid piece)', + } + } + sumFields += 1 + previousWasNumber = false + } + } + if (sumFields !== 8) { + return { + ok: false, + error: 'Invalid FEN: piece data is invalid (too many squares in rank)', + } + } + } + + // 9th criterion: is en-passant square legal? + if ( + (tokens[3][1] == '3' && tokens[1] == 'w') || + (tokens[3][1] == '6' && tokens[1] == 'b') + ) { + return { ok: false, error: 'Invalid FEN: illegal en-passant square' } + } + + // 10th criterion: does chess position contain exact two kings? + const kings = [ + { color: 'white', regex: /K/g }, + { color: 'black', regex: /k/g }, + ] + + for (const { color, regex } of kings) { + if (!regex.test(tokens[0])) { + return { ok: false, error: `Invalid FEN: missing ${color} king` } + } + + if ((tokens[0].match(regex) || []).length > 1) { + return { ok: false, error: `Invalid FEN: too many ${color} kings` } + } + } + + // 11th criterion: are any pawns on the first or eighth rows? + if ( + Array.from(rows[0] + rows[7]).some((char) => char.toUpperCase() === 'P') + ) { + return { + ok: false, + error: 'Invalid FEN: some pawns are on the edge rows', + } + } + + return { ok: true } + } + + // this function is used to uniquely identify ambiguous moves + function getDisambiguator(move: InternalMove, moves: InternalMove[]) { + const from = move.from + const to = move.to + const piece = move.piece + + let ambiguities = 0 + let sameRank = 0 + let sameFile = 0 + + for (let i = 0, len = moves.length; i < len; i++) { + const ambigFrom = moves[i].from + const ambigTo = moves[i].to + const ambigPiece = moves[i].piece + + /* + * if a move of the same piece type ends on the same to square, we'll need + * to add a disambiguator to the algebraic notation + */ + if (piece === ambigPiece && from !== ambigFrom && to === ambigTo) { + ambiguities++ + + if (rank(from) === rank(ambigFrom)) { + sameRank++ + } + + if (file(from) === file(ambigFrom)) { + sameFile++ + } + } + } + + if (ambiguities > 0) { + if (sameRank > 0 && sameFile > 0) { + /* + * if there exists a similar moving piece on the same rank and file as + * the move in question, use the square as the disambiguator + */ + return algebraic(from) + } else if (sameFile > 0) { + /* + * if the moving piece rests on the same file, use the rank symbol as the + * disambiguator + */ + return algebraic(from).charAt(1) + } else { + // else use the file symbol + return algebraic(from).charAt(0) + } + } + + return '' + } + + function addMove( + moves: InternalMove[], + color: Color, + from: number, + to: number, + piece: PieceSymbol, + captured: PieceSymbol | undefined = undefined, + flags: number = BITS.NORMAL, + ) { + const r = rank(to) + + if (piece === PAWN && (r === RANK_1 || r === RANK_8)) { + for (let i = 0; i < PROMOTIONS.length; i++) { + const promotion = PROMOTIONS[i] + moves.push({ + color, + from, + to, + piece, + captured, + promotion, + flags: flags | BITS.PROMOTION, + }) + } + } else { + moves.push({ + color, + from, + to, + piece, + captured, + flags, + }) + } + } + + function inferPieceType(san: string) { + let pieceType = san.charAt(0) + if (pieceType >= 'a' && pieceType <= 'h') { + const matches = san.match(/[a-h]\d.*[a-h]\d/) + if (matches) { + return undefined + } + return PAWN + } + pieceType = pieceType.toLowerCase() + if (pieceType === 'o') { + return KING + } + return pieceType as PieceSymbol + } + + // parses all of the decorators out of a SAN string + function strippedSan(move: string) { + return move.replace(/=/, '').replace(/[+#]?[?!]*$/, '') + } + + function trimFen(fen: string): string { + /* + * remove last two fields in FEN string as they're not needed when checking + * for repetition + */ + return fen.split(' ').slice(0, 4).join(' ') + } + + export class Chess { + private _board = new Array(128) + private _turn: Color = WHITE + private _header: Record = {} + private _kings: Record = { w: EMPTY, b: EMPTY } + private _epSquare = -1 + private _halfMoves = 0 + private _moveNumber = 0 + private _history: History[] = [] + private _comments: Record = {} + private _castling: Record = { w: 0, b: 0 } + + // tracks number of times a position has been seen for repetition checking + private _positionCount: Record = {} + + constructor(fen = DEFAULT_POSITION) { + this.load(fen) + } + + clear({ preserveHeaders = false } = {}) { + this._board = new Array(128) + this._kings = { w: EMPTY, b: EMPTY } + this._turn = WHITE + this._castling = { w: 0, b: 0 } + this._epSquare = EMPTY + this._halfMoves = 0 + this._moveNumber = 1 + this._history = [] + this._comments = {} + this._header = preserveHeaders ? this._header : {} + this._positionCount = {} + + /* + * Delete the SetUp and FEN headers (if preserved), the board is empty and + * these headers don't make sense in this state. They'll get added later + * via .load() or .put() + */ + delete this._header['SetUp'] + delete this._header['FEN'] + } + + removeHeader(key: string) { + if (key in this._header) { + delete this._header[key] + } + } + + load(fen: string, { skipValidation = false, preserveHeaders = false } = {}) { + let tokens = fen.split(/\s+/) + + // append commonly omitted fen tokens + if (tokens.length >= 2 && tokens.length < 6) { + const adjustments = ['-', '-', '0', '1'] + fen = tokens.concat(adjustments.slice(-(6 - tokens.length))).join(' ') + } + + tokens = fen.split(/\s+/) + + if (!skipValidation) { + const { ok, error } = validateFen(fen) + if (!ok) { + throw new Error(error) + } + } + + const position = tokens[0] + let square = 0 + + this.clear({ preserveHeaders }) + + for (let i = 0; i < position.length; i++) { + const piece = position.charAt(i) + + if (piece === '/') { + square += 8 + } else if (isDigit(piece)) { + square += parseInt(piece, 10) + } else { + const color = piece < 'a' ? WHITE : BLACK + this._put( + { type: piece.toLowerCase() as PieceSymbol, color }, + algebraic(square), + ) + square++ + } + } + + this._turn = tokens[1] as Color + + if (tokens[2].indexOf('K') > -1) { + this._castling.w |= BITS.KSIDE_CASTLE + } + if (tokens[2].indexOf('Q') > -1) { + this._castling.w |= BITS.QSIDE_CASTLE + } + if (tokens[2].indexOf('k') > -1) { + this._castling.b |= BITS.KSIDE_CASTLE + } + if (tokens[2].indexOf('q') > -1) { + this._castling.b |= BITS.QSIDE_CASTLE + } + + this._epSquare = tokens[3] === '-' ? EMPTY : Ox88[tokens[3] as Square] + this._halfMoves = parseInt(tokens[4], 10) + this._moveNumber = parseInt(tokens[5], 10) + + this._updateSetup(fen) + this._incPositionCount(fen) + } + + fen() { + let empty = 0 + let fen = '' + + for (let i = Ox88.a8; i <= Ox88.h1; i++) { + if (this._board[i]) { + if (empty > 0) { + fen += empty + empty = 0 + } + const { color, type: piece } = this._board[i] + + fen += color === WHITE ? piece.toUpperCase() : piece.toLowerCase() + } else { + empty++ + } + + if ((i + 1) & 0x88) { + if (empty > 0) { + fen += empty + } + + if (i !== Ox88.h1) { + fen += '/' + } + + empty = 0 + i += 8 + } + } + + let castling = '' + if (this._castling[WHITE] & BITS.KSIDE_CASTLE) { + castling += 'K' + } + if (this._castling[WHITE] & BITS.QSIDE_CASTLE) { + castling += 'Q' + } + if (this._castling[BLACK] & BITS.KSIDE_CASTLE) { + castling += 'k' + } + if (this._castling[BLACK] & BITS.QSIDE_CASTLE) { + castling += 'q' + } + + // do we have an empty castling flag? + castling = castling || '-' + + let epSquare = '-' + /* + * only print the ep square if en passant is a valid move (pawn is present + * and ep capture is not pinned) + */ + if (this._epSquare !== EMPTY) { + const bigPawnSquare = this._epSquare + (this._turn === WHITE ? 16 : -16) + const squares = [bigPawnSquare + 1, bigPawnSquare - 1] + + for (const square of squares) { + // is the square off the board? + if (square & 0x88) { + continue + } + + const color = this._turn + + // is there a pawn that can capture the epSquare? + if ( + this._board[square]?.color === color && + this._board[square]?.type === PAWN + ) { + // if the pawn makes an ep capture, does it leave it's king in check? + this._makeMove({ + color, + from: square, + to: this._epSquare, + piece: PAWN, + captured: PAWN, + flags: BITS.EP_CAPTURE, + }) + const isLegal = !this._isKingAttacked(color) + this._undoMove() + + // if ep is legal, break and set the ep square in the FEN output + if (isLegal) { + epSquare = algebraic(this._epSquare) + break + } + } + } + } + + return [ + fen, + this._turn, + castling, + epSquare, + this._halfMoves, + this._moveNumber, + ].join(' ') + } + + /* + * Called when the initial board setup is changed with put() or remove(). + * modifies the SetUp and FEN properties of the header object. If the FEN + * is equal to the default position, the SetUp and FEN are deleted the setup + * is only updated if history.length is zero, ie moves haven't been made. + */ + private _updateSetup(fen: string) { + if (this._history.length > 0) return + + if (fen !== DEFAULT_POSITION) { + this._header['SetUp'] = '1' + this._header['FEN'] = fen + } else { + delete this._header['SetUp'] + delete this._header['FEN'] + } + } + + reset() { + this.load(DEFAULT_POSITION) + } + + get(square: Square) { + return this._board[Ox88[square]] || false + } + + put({ type, color }: { type: PieceSymbol; color: Color }, square: Square) { + if (this._put({ type, color }, square)) { + this._updateCastlingRights() + this._updateEnPassantSquare() + this._updateSetup(this.fen()) + return true + } + return false + } + + private _put( + { type, color }: { type: PieceSymbol; color: Color }, + square: Square, + ) { + // check for piece + if (SYMBOLS.indexOf(type.toLowerCase()) === -1) { + return false + } + + // check for valid square + if (!(square in Ox88)) { + return false + } + + const sq = Ox88[square] + + // don't let the user place more than one king + if ( + type == KING && + !(this._kings[color] == EMPTY || this._kings[color] == sq) + ) { + return false + } + + const currentPieceOnSquare = this._board[sq] + + // if one of the kings will be replaced by the piece from args, set the `_kings` respective entry to `EMPTY` + if (currentPieceOnSquare && currentPieceOnSquare.type === KING) { + this._kings[currentPieceOnSquare.color] = EMPTY + } + + this._board[sq] = { type: type as PieceSymbol, color: color as Color } + + if (type === KING) { + this._kings[color] = sq + } + + return true + } + + remove(square: Square) { + const piece = this.get(square) + delete this._board[Ox88[square]] + if (piece && piece.type === KING) { + this._kings[piece.color] = EMPTY + } + + this._updateCastlingRights() + this._updateEnPassantSquare() + this._updateSetup(this.fen()) + + return piece + } + + private _updateCastlingRights() { + const whiteKingInPlace = + this._board[Ox88.e1]?.type === KING && + this._board[Ox88.e1]?.color === WHITE + const blackKingInPlace = + this._board[Ox88.e8]?.type === KING && + this._board[Ox88.e8]?.color === BLACK + + if ( + !whiteKingInPlace || + this._board[Ox88.a1]?.type !== ROOK || + this._board[Ox88.a1]?.color !== WHITE + ) { + this._castling.w &= ~BITS.QSIDE_CASTLE + } + + if ( + !whiteKingInPlace || + this._board[Ox88.h1]?.type !== ROOK || + this._board[Ox88.h1]?.color !== WHITE + ) { + this._castling.w &= ~BITS.KSIDE_CASTLE + } + + if ( + !blackKingInPlace || + this._board[Ox88.a8]?.type !== ROOK || + this._board[Ox88.a8]?.color !== BLACK + ) { + this._castling.b &= ~BITS.QSIDE_CASTLE + } + + if ( + !blackKingInPlace || + this._board[Ox88.h8]?.type !== ROOK || + this._board[Ox88.h8]?.color !== BLACK + ) { + this._castling.b &= ~BITS.KSIDE_CASTLE + } + } + + private _updateEnPassantSquare() { + if (this._epSquare === EMPTY) { + return + } + + const startSquare = this._epSquare + (this._turn === WHITE ? -16 : 16) + const currentSquare = this._epSquare + (this._turn === WHITE ? 16 : -16) + const attackers = [currentSquare + 1, currentSquare - 1] + + if ( + this._board[startSquare] !== null || + this._board[this._epSquare] !== null || + this._board[currentSquare]?.color !== swapColor(this._turn) || + this._board[currentSquare]?.type !== PAWN + ) { + this._epSquare = EMPTY + return + } + + const canCapture = (square: number) => + !(square & 0x88) && + this._board[square]?.color === this._turn && + this._board[square]?.type === PAWN + + if (!attackers.some(canCapture)) { + this._epSquare = EMPTY + } + } + + private _attacked(color: Color, square: number): boolean + private _attacked(color: Color, square: number, verbose: false): boolean + private _attacked(color: Color, square: number, verbose: true): Square[] + private _attacked(color: Color, square: number, verbose?: boolean) { + const attackers: Square[] = [] + for (let i = Ox88.a8; i <= Ox88.h1; i++) { + // did we run off the end of the board + if (i & 0x88) { + i += 7 + continue + } + + // if empty square or wrong color + if (this._board[i] === undefined || this._board[i].color !== color) { + continue + } + + const piece = this._board[i] + const difference = i - square + + // skip - to/from square are the same + if (difference === 0) { + continue + } + + const index = difference + 119 + + if (ATTACKS[index] & PIECE_MASKS[piece.type]) { + if (piece.type === PAWN) { + if ( + (difference > 0 && piece.color === WHITE) || + (difference <= 0 && piece.color === BLACK) + ) { + if (!verbose) { + return true + } else { + attackers.push(algebraic(i)) + } + } + continue + } + + // if the piece is a knight or a king + if (piece.type === 'n' || piece.type === 'k') { + if (!verbose) { + return true + } else { + attackers.push(algebraic(i)) + continue + } + } + + const offset = RAYS[index] + let j = i + offset + + let blocked = false + while (j !== square) { + if (this._board[j] != null) { + blocked = true + break + } + j += offset + } + + if (!blocked) { + if (!verbose) { + return true + } else { + attackers.push(algebraic(i)) + continue + } + } + } + } + + if (verbose) { + return attackers + } else { + return false + } + } + + attackers(square: Square, attackedBy?: Color) { + if (!attackedBy) { + return this._attacked(this._turn, Ox88[square], true) + } else { + return this._attacked(attackedBy, Ox88[square], true) + } + } + + private _isKingAttacked(color: Color) { + const square = this._kings[color] + return square === -1 ? false : this._attacked(swapColor(color), square) + } + + isAttacked(square: Square, attackedBy: Color) { + return this._attacked(attackedBy, Ox88[square]) + } + + isCheck() { + return this._isKingAttacked(this._turn) + } + + inCheck() { + return this.isCheck() + } + + isCheckmate() { + return this.isCheck() && this._moves().length === 0 + } + + isStalemate() { + return !this.isCheck() && this._moves().length === 0 + } + + isInsufficientMaterial() { + /* + * k.b. vs k.b. (of opposite colors) with mate in 1: + * 8/8/8/8/1b6/8/B1k5/K7 b - - 0 1 + * + * k.b. vs k.n. with mate in 1: + * 8/8/8/8/1n6/8/B7/K1k5 b - - 2 1 + */ + const pieces: Record = { + b: 0, + n: 0, + r: 0, + q: 0, + k: 0, + p: 0, + } + const bishops = [] + let numPieces = 0 + let squareColor = 0 + + for (let i = Ox88.a8; i <= Ox88.h1; i++) { + squareColor = (squareColor + 1) % 2 + if (i & 0x88) { + i += 7 + continue + } + + const piece = this._board[i] + if (piece) { + pieces[piece.type] = piece.type in pieces ? pieces[piece.type] + 1 : 1 + if (piece.type === BISHOP) { + bishops.push(squareColor) + } + numPieces++ + } + } + + // k vs. k + if (numPieces === 2) { + return true + } else if ( + // k vs. kn .... or .... k vs. kb + numPieces === 3 && + (pieces[BISHOP] === 1 || pieces[KNIGHT] === 1) + ) { + return true + } else if (numPieces === pieces[BISHOP] + 2) { + // kb vs. kb where any number of bishops are all on the same color + let sum = 0 + const len = bishops.length + for (let i = 0; i < len; i++) { + sum += bishops[i] + } + if (sum === 0 || sum === len) { + return true + } + } + + return false + } + + isThreefoldRepetition(): boolean { + return this._getPositionCount(this.fen()) >= 3 + } + + isDraw() { + return ( + this._halfMoves >= 100 || // 50 moves per side = 100 half moves + this.isStalemate() || + this.isInsufficientMaterial() || + this.isThreefoldRepetition() + ) + } + + isGameOver() { + return this.isCheckmate() || this.isStalemate() || this.isDraw() + } + + moves(): string[] + moves({ square }: { square: Square }): string[] + moves({ piece }: { piece: PieceSymbol }): string[] + + moves({ square, piece }: { square: Square; piece: PieceSymbol }): string[] + + moves({ verbose, square }: { verbose: true; square?: Square }): Move[] + moves({ verbose, square }: { verbose: false; square?: Square }): string[] + moves({ + verbose, + square, + }: { + verbose?: boolean + square?: Square + }): string[] | Move[] + + moves({ verbose, piece }: { verbose: true; piece?: PieceSymbol }): Move[] + moves({ verbose, piece }: { verbose: false; piece?: PieceSymbol }): string[] + moves({ + verbose, + piece, + }: { + verbose?: boolean + piece?: PieceSymbol + }): string[] | Move[] + + moves({ + verbose, + square, + piece, + }: { + verbose: true + square?: Square + piece?: PieceSymbol + }): Move[] + moves({ + verbose, + square, + piece, + }: { + verbose: false + square?: Square + piece?: PieceSymbol + }): string[] + moves({ + verbose, + square, + piece, + }: { + verbose?: boolean + square?: Square + piece?: PieceSymbol + }): string[] | Move[] + + moves({ square, piece }: { square?: Square; piece?: PieceSymbol }): Move[] + + moves({ + verbose = false, + square = undefined, + piece = undefined, + }: { verbose?: boolean; square?: Square; piece?: PieceSymbol } = {}) { + const moves = this._moves({ square, piece }) + + if (verbose) { + return moves.map((move) => this._makePretty(move)) + } else { + return moves.map((move) => this._moveToSan(move, moves)) + } + } + + private _moves({ + legal = true, + piece = undefined, + square = undefined, + }: { + legal?: boolean + piece?: PieceSymbol + square?: Square + } = {}) { + const forSquare = square ? (square.toLowerCase() as Square) : undefined + const forPiece = piece?.toLowerCase() + + const moves: InternalMove[] = [] + const us = this._turn + const them = swapColor(us) + + let firstSquare = Ox88.a8 + let lastSquare = Ox88.h1 + let singleSquare = false + + // are we generating moves for a single square? + if (forSquare) { + // illegal square, return empty moves + if (!(forSquare in Ox88)) { + return [] + } else { + firstSquare = lastSquare = Ox88[forSquare] + singleSquare = true + } + } + + for (let from = firstSquare; from <= lastSquare; from++) { + // did we run off the end of the board + if (from & 0x88) { + from += 7 + continue + } + + // empty square or opponent, skip + if (!this._board[from] || this._board[from].color === them) { + continue + } + const { type } = this._board[from] + + let to: number + if (type === PAWN) { + if (forPiece && forPiece !== type) continue + + // single square, non-capturing + to = from + PAWN_OFFSETS[us][0] + if (!this._board[to]) { + addMove(moves, us, from, to, PAWN) + + // double square + to = from + PAWN_OFFSETS[us][1] + if (SECOND_RANK[us] === rank(from) && !this._board[to]) { + addMove(moves, us, from, to, PAWN, undefined, BITS.BIG_PAWN) + } + } + + // pawn captures + for (let j = 2; j < 4; j++) { + to = from + PAWN_OFFSETS[us][j] + if (to & 0x88) continue + + if (this._board[to]?.color === them) { + addMove( + moves, + us, + from, + to, + PAWN, + this._board[to].type, + BITS.CAPTURE, + ) + } else if (to === this._epSquare) { + addMove(moves, us, from, to, PAWN, PAWN, BITS.EP_CAPTURE) + } + } + } else { + if (forPiece && forPiece !== type) continue + + for (let j = 0, len = PIECE_OFFSETS[type].length; j < len; j++) { + const offset = PIECE_OFFSETS[type][j] + to = from + + while (true) { + to += offset + if (to & 0x88) break + + if (!this._board[to]) { + addMove(moves, us, from, to, type) + } else { + // own color, stop loop + if (this._board[to].color === us) break + + addMove( + moves, + us, + from, + to, + type, + this._board[to].type, + BITS.CAPTURE, + ) + break + } + + /* break, if knight or king */ + if (type === KNIGHT || type === KING) break + } + } + } + } + + /* + * check for castling if we're: + * a) generating all moves, or + * b) doing single square move generation on the king's square + */ + + if (forPiece === undefined || forPiece === KING) { + if (!singleSquare || lastSquare === this._kings[us]) { + // king-side castling + if (this._castling[us] & BITS.KSIDE_CASTLE) { + const castlingFrom = this._kings[us] + const castlingTo = castlingFrom + 2 + + if ( + !this._board[castlingFrom + 1] && + !this._board[castlingTo] && + !this._attacked(them, this._kings[us]) && + !this._attacked(them, castlingFrom + 1) && + !this._attacked(them, castlingTo) + ) { + addMove( + moves, + us, + this._kings[us], + castlingTo, + KING, + undefined, + BITS.KSIDE_CASTLE, + ) + } + } + + // queen-side castling + if (this._castling[us] & BITS.QSIDE_CASTLE) { + const castlingFrom = this._kings[us] + const castlingTo = castlingFrom - 2 + + if ( + !this._board[castlingFrom - 1] && + !this._board[castlingFrom - 2] && + !this._board[castlingFrom - 3] && + !this._attacked(them, this._kings[us]) && + !this._attacked(them, castlingFrom - 1) && + !this._attacked(them, castlingTo) + ) { + addMove( + moves, + us, + this._kings[us], + castlingTo, + KING, + undefined, + BITS.QSIDE_CASTLE, + ) + } + } + } + } + + /* + * return all pseudo-legal moves (this includes moves that allow the king + * to be captured) + */ + if (!legal || this._kings[us] === -1) { + return moves + } + + // filter out illegal moves + const legalMoves = [] + + for (let i = 0, len = moves.length; i < len; i++) { + this._makeMove(moves[i]) + if (!this._isKingAttacked(us)) { + legalMoves.push(moves[i]) + } + this._undoMove() + } + + return legalMoves + } + + move( + move: string | { from: string; to: string; promotion?: string }, + { strict = false }: { strict?: boolean } = {}, + ) { + /* + * The move function can be called with in the following parameters: + * + * .move('Nxb7') <- argument is a case-sensitive SAN string + * + * .move({ from: 'h7', <- argument is a move object + * to :'h8', + * promotion: 'q' }) + * + * + * An optional strict argument may be supplied to tell chess.js to + * strictly follow the SAN specification. + */ + + let moveObj = null + + if (typeof move === 'string') { + moveObj = this._moveFromSan(move, strict) + } else if (typeof move === 'object') { + const moves = this._moves() + + // convert the pretty move object to an ugly move object + for (let i = 0, len = moves.length; i < len; i++) { + if ( + move.from === algebraic(moves[i].from) && + move.to === algebraic(moves[i].to) && + (!('promotion' in moves[i]) || move.promotion === moves[i].promotion) + ) { + moveObj = moves[i] + break + } + } + } + + // failed to find move + if (!moveObj) { + if (typeof move === 'string') { + throw new Error(`Invalid move: ${move}`) + } else { + throw new Error(`Invalid move: ${JSON.stringify(move)}`) + } + } + + /* + * need to make a copy of move because we can't generate SAN after the move + * is made + */ + const prettyMove = this._makePretty(moveObj) + + this._makeMove(moveObj) + this._incPositionCount(prettyMove.after) + return prettyMove + } + + private _push(move: InternalMove) { + this._history.push({ + move, + kings: { b: this._kings.b, w: this._kings.w }, + turn: this._turn, + castling: { b: this._castling.b, w: this._castling.w }, + epSquare: this._epSquare, + halfMoves: this._halfMoves, + moveNumber: this._moveNumber, + }) + } + + private _makeMove(move: InternalMove) { + const us = this._turn + const them = swapColor(us) + this._push(move) + + this._board[move.to] = this._board[move.from] + delete this._board[move.from] + + // if ep capture, remove the captured pawn + if (move.flags & BITS.EP_CAPTURE) { + if (this._turn === BLACK) { + delete this._board[move.to - 16] + } else { + delete this._board[move.to + 16] + } + } + + // if pawn promotion, replace with new piece + if (move.promotion) { + this._board[move.to] = { type: move.promotion, color: us } + } + + // if we moved the king + if (this._board[move.to].type === KING) { + this._kings[us] = move.to + + // if we castled, move the rook next to the king + if (move.flags & BITS.KSIDE_CASTLE) { + const castlingTo = move.to - 1 + const castlingFrom = move.to + 1 + this._board[castlingTo] = this._board[castlingFrom] + delete this._board[castlingFrom] + } else if (move.flags & BITS.QSIDE_CASTLE) { + const castlingTo = move.to + 1 + const castlingFrom = move.to - 2 + this._board[castlingTo] = this._board[castlingFrom] + delete this._board[castlingFrom] + } + + // turn off castling + this._castling[us] = 0 + } + + // turn off castling if we move a rook + if (this._castling[us]) { + for (let i = 0, len = ROOKS[us].length; i < len; i++) { + if ( + move.from === ROOKS[us][i].square && + this._castling[us] & ROOKS[us][i].flag + ) { + this._castling[us] ^= ROOKS[us][i].flag + break + } + } + } + + // turn off castling if we capture a rook + if (this._castling[them]) { + for (let i = 0, len = ROOKS[them].length; i < len; i++) { + if ( + move.to === ROOKS[them][i].square && + this._castling[them] & ROOKS[them][i].flag + ) { + this._castling[them] ^= ROOKS[them][i].flag + break + } + } + } + + // if big pawn move, update the en passant square + if (move.flags & BITS.BIG_PAWN) { + if (us === BLACK) { + this._epSquare = move.to - 16 + } else { + this._epSquare = move.to + 16 + } + } else { + this._epSquare = EMPTY + } + + // reset the 50 move counter if a pawn is moved or a piece is captured + if (move.piece === PAWN) { + this._halfMoves = 0 + } else if (move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE)) { + this._halfMoves = 0 + } else { + this._halfMoves++ + } + + if (us === BLACK) { + this._moveNumber++ + } + + this._turn = them + } + + undo() { + const move = this._undoMove() + if (move) { + const prettyMove = this._makePretty(move) + this._decPositionCount(prettyMove.after) + return prettyMove + } + return null + } + + private _undoMove() { + const old = this._history.pop() + if (old === undefined) { + return null + } + + const move = old.move + + this._kings = old.kings + this._turn = old.turn + this._castling = old.castling + this._epSquare = old.epSquare + this._halfMoves = old.halfMoves + this._moveNumber = old.moveNumber + + const us = this._turn + const them = swapColor(us) + + this._board[move.from] = this._board[move.to] + this._board[move.from].type = move.piece // to undo any promotions + delete this._board[move.to] + + if (move.captured) { + if (move.flags & BITS.EP_CAPTURE) { + // en passant capture + let index: number + if (us === BLACK) { + index = move.to - 16 + } else { + index = move.to + 16 + } + this._board[index] = { type: PAWN, color: them } + } else { + // regular capture + this._board[move.to] = { type: move.captured, color: them } + } + } + + if (move.flags & (BITS.KSIDE_CASTLE | BITS.QSIDE_CASTLE)) { + let castlingTo: number, castlingFrom: number + if (move.flags & BITS.KSIDE_CASTLE) { + castlingTo = move.to + 1 + castlingFrom = move.to - 1 + } else { + castlingTo = move.to - 2 + castlingFrom = move.to + 1 + } + + this._board[castlingTo] = this._board[castlingFrom] + delete this._board[castlingFrom] + } + + return move + } + + pgn({ + newline = '\n', + maxWidth = 0, + }: { newline?: string; maxWidth?: number } = {}) { + /* + * using the specification from http://www.chessclub.com/help/PGN-spec + * example for html usage: .pgn({ max_width: 72, newline_char: "
    " }) + */ + + const result: string[] = [] + let headerExists = false + + /* add the PGN header information */ + for (const i in this._header) { + /* + * TODO: order of enumerated properties in header object is not + * guaranteed, see ECMA-262 spec (section 12.6.4) + */ + result.push('[' + i + ' "' + this._header[i] + '"]' + newline) + headerExists = true + } + + if (headerExists && this._history.length) { + result.push(newline) + } + + const appendComment = (moveString: string) => { + const comment = this._comments[this.fen()] + if (typeof comment !== 'undefined') { + const delimiter = moveString.length > 0 ? ' ' : '' + moveString = `${moveString}${delimiter}{${comment}}` + } + return moveString + } + + // pop all of history onto reversed_history + const reversedHistory = [] + while (this._history.length > 0) { + reversedHistory.push(this._undoMove()) + } + + const moves = [] + let moveString = '' + + // special case of a commented starting position with no moves + if (reversedHistory.length === 0) { + moves.push(appendComment('')) + } + + // build the list of moves. a move_string looks like: "3. e3 e6" + while (reversedHistory.length > 0) { + moveString = appendComment(moveString) + const move = reversedHistory.pop() + + // make TypeScript stop complaining about move being undefined + if (!move) { + break + } + + // if the position started with black to move, start PGN with #. ... + if (!this._history.length && move.color === 'b') { + const prefix = `${this._moveNumber}. ...` + // is there a comment preceding the first move? + moveString = moveString ? `${moveString} ${prefix}` : prefix + } else if (move.color === 'w') { + // store the previous generated move_string if we have one + if (moveString.length) { + moves.push(moveString) + } + moveString = this._moveNumber + '.' + } + + moveString = + moveString + ' ' + this._moveToSan(move, this._moves({ legal: true })) + this._makeMove(move) + } + + // are there any other leftover moves? + if (moveString.length) { + moves.push(appendComment(moveString)) + } + + // is there a result? + if (typeof this._header.Result !== 'undefined') { + moves.push(this._header.Result) + } + + /* + * history should be back to what it was before we started generating PGN, + * so join together moves + */ + if (maxWidth === 0) { + return result.join('') + moves.join(' ') + } + + // TODO (jah): huh? + const strip = function () { + if (result.length > 0 && result[result.length - 1] === ' ') { + result.pop() + return true + } + return false + } + + // NB: this does not preserve comment whitespace. + const wrapComment = function (width: number, move: string) { + for (const token of move.split(' ')) { + if (!token) { + continue + } + if (width + token.length > maxWidth) { + while (strip()) { + width-- + } + result.push(newline) + width = 0 + } + result.push(token) + width += token.length + result.push(' ') + width++ + } + if (strip()) { + width-- + } + return width + } + + // wrap the PGN output at max_width + let currentWidth = 0 + for (let i = 0; i < moves.length; i++) { + if (currentWidth + moves[i].length > maxWidth) { + if (moves[i].includes('{')) { + currentWidth = wrapComment(currentWidth, moves[i]) + continue + } + } + // if the current move will push past max_width + if (currentWidth + moves[i].length > maxWidth && i !== 0) { + // don't end the line with whitespace + if (result[result.length - 1] === ' ') { + result.pop() + } + + result.push(newline) + currentWidth = 0 + } else if (i !== 0) { + result.push(' ') + currentWidth++ + } + result.push(moves[i]) + currentWidth += moves[i].length + } + + return result.join('') + } + + header(...args: string[]) { + for (let i = 0; i < args.length; i += 2) { + if (typeof args[i] === 'string' && typeof args[i + 1] === 'string') { + this._header[args[i]] = args[i + 1] + } + } + return this._header + } + + loadPgn( + pgn: string, + { + strict = false, + newlineChar = '\r?\n', + }: { strict?: boolean; newlineChar?: string } = {}, + ) { + function mask(str: string): string { + return str.replace(/\\/g, '\\') + } + + function parsePgnHeader(header: string): { [key: string]: string } { + const headerObj: Record = {} + const headers = header.split(new RegExp(mask(newlineChar))) + let key = '' + let value = '' + + for (let i = 0; i < headers.length; i++) { + const regex = /^\s*\[\s*([A-Za-z]+)\s*"(.*)"\s*\]\s*$/ + key = headers[i].replace(regex, '$1') + value = headers[i].replace(regex, '$2') + if (key.trim().length > 0) { + headerObj[key] = value + } + } + + return headerObj + } + + // strip whitespace from head/tail of PGN block + pgn = pgn.trim() + + /* + * RegExp to split header. Takes advantage of the fact that header and movetext + * will always have a blank line between them (ie, two newline_char's). Handles + * case where movetext is empty by matching newlineChar until end of string is + * matched - effectively trimming from the end extra newlineChar. + * + * With default newline_char, will equal: + * /^(\[((?:\r?\n)|.)*\])((?:\s*\r?\n){2}|(?:\s*\r?\n)*$)/ + */ + const headerRegex = new RegExp( + '^(\\[((?:' + + mask(newlineChar) + + ')|.)*\\])' + + '((?:\\s*' + + mask(newlineChar) + + '){2}|(?:\\s*' + + mask(newlineChar) + + ')*$)', + ) + + // If no header given, begin with moves. + const headerRegexResults = headerRegex.exec(pgn) + const headerString = headerRegexResults + ? headerRegexResults.length >= 2 + ? headerRegexResults[1] + : '' + : '' + + // Put the board in the starting position + this.reset() + + // parse PGN header + const headers = parsePgnHeader(headerString) + let fen = '' + + for (const key in headers) { + // check to see user is including fen (possibly with wrong tag case) + if (key.toLowerCase() === 'fen') { + fen = headers[key] + } + + this.header(key, headers[key]) + } + + /* + * the permissive parser should attempt to load a fen tag, even if it's the + * wrong case and doesn't include a corresponding [SetUp "1"] tag + */ + if (!strict) { + if (fen) { + this.load(fen, { preserveHeaders: true }) + } + } else { + /* + * strict parser - load the starting position indicated by [Setup '1'] + * and [FEN position] + */ + if (headers['SetUp'] === '1') { + if (!('FEN' in headers)) { + throw new Error( + 'Invalid PGN: FEN tag must be supplied with SetUp tag', + ) + } + // don't clear the headers when loading + this.load(headers['FEN'], { preserveHeaders: true }) + } + } + + /* + * NB: the regexes below that delete move numbers, recursive annotations, + * and numeric annotation glyphs may also match text in comments. To + * prevent this, we transform comments by hex-encoding them in place and + * decoding them again after the other tokens have been deleted. + * + * While the spec states that PGN files should be ASCII encoded, we use + * {en,de}codeURIComponent here to support arbitrary UTF8 as a convenience + * for modern users + */ + + function toHex(s: string): string { + return Array.from(s) + .map(function (c) { + /* + * encodeURI doesn't transform most ASCII characters, so we handle + * these ourselves + */ + return c.charCodeAt(0) < 128 + ? c.charCodeAt(0).toString(16) + : encodeURIComponent(c).replace(/%/g, '').toLowerCase() + }) + .join('') + } + + function fromHex(s: string): string { + return s.length == 0 + ? '' + : decodeURIComponent('%' + (s.match(/.{1,2}/g) || []).join('%')) + } + + const encodeComment = function (s: string) { + s = s.replace(new RegExp(mask(newlineChar), 'g'), ' ') + return `{${toHex(s.slice(1, s.length - 1))}}` + } + + const decodeComment = function (s: string) { + if (s.startsWith('{') && s.endsWith('}')) { + return fromHex(s.slice(1, s.length - 1)) + } + } + + // delete header to get the moves + let ms = pgn + .replace(headerString, '') + .replace( + // encode comments so they don't get deleted below + new RegExp(`({[^}]*})+?|;([^${mask(newlineChar)}]*)`, 'g'), + function (_match, bracket, semicolon) { + return bracket !== undefined + ? encodeComment(bracket) + : ' ' + encodeComment(`{${semicolon.slice(1)}}`) + }, + ) + .replace(new RegExp(mask(newlineChar), 'g'), ' ') + + // delete recursive annotation variations + const ravRegex = /(\([^()]+\))+?/g + while (ravRegex.test(ms)) { + ms = ms.replace(ravRegex, '') + } + + // delete move numbers + ms = ms.replace(/\d+\.(\.\.)?/g, '') + + // delete ... indicating black to move + ms = ms.replace(/\.\.\./g, '') + + /* delete numeric annotation glyphs */ + ms = ms.replace(/\$\d+/g, '') + + // trim and get array of moves + let moves = ms.trim().split(new RegExp(/\s+/)) + + // delete empty entries + moves = moves.filter((move) => move !== '') + + let result = '' + + for (let halfMove = 0; halfMove < moves.length; halfMove++) { + const comment = decodeComment(moves[halfMove]) + if (comment !== undefined) { + this._comments[this.fen()] = comment + continue + } + + const move = this._moveFromSan(moves[halfMove], strict) + + // invalid move + if (move == null) { + // was the move an end of game marker + if (TERMINATION_MARKERS.indexOf(moves[halfMove]) > -1) { + result = moves[halfMove] + } else { + throw new Error(`Invalid move in PGN: ${moves[halfMove]}`) + } + } else { + // reset the end of game marker if making a valid move + result = '' + this._makeMove(move) + this._incPositionCount(this.fen()) + } + } + + /* + * Per section 8.2.6 of the PGN spec, the Result tag pair must match match + * the termination marker. Only do this when headers are present, but the + * result tag is missing + */ + + if (result && Object.keys(this._header).length && !this._header['Result']) { + this.header('Result', result) + } + } + + /* + * Convert a move from 0x88 coordinates to Standard Algebraic Notation + * (SAN) + * + * @param {boolean} strict Use the strict SAN parser. It will throw errors + * on overly disambiguated moves (see below): + * + * r1bqkbnr/ppp2ppp/2n5/1B1pP3/4P3/8/PPPP2PP/RNBQK1NR b KQkq - 2 4 + * 4. ... Nge7 is overly disambiguated because the knight on c6 is pinned + * 4. ... Ne7 is technically the valid SAN + */ + + private _moveToSan(move: InternalMove, moves: InternalMove[]) { + let output = '' + + if (move.flags & BITS.KSIDE_CASTLE) { + output = 'O-O' + } else if (move.flags & BITS.QSIDE_CASTLE) { + output = 'O-O-O' + } else { + if (move.piece !== PAWN) { + const disambiguator = getDisambiguator(move, moves) + output += move.piece.toUpperCase() + disambiguator + } + + if (move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE)) { + if (move.piece === PAWN) { + output += algebraic(move.from)[0] + } + output += 'x' + } + + output += algebraic(move.to) + + if (move.promotion) { + output += '=' + move.promotion.toUpperCase() + } + } + + this._makeMove(move) + if (this.isCheck()) { + if (this.isCheckmate()) { + output += '#' + } else { + output += '+' + } + } + this._undoMove() + + return output + } + + // convert a move from Standard Algebraic Notation (SAN) to 0x88 coordinates + private _moveFromSan(move: string, strict = false): InternalMove | null { + // strip off any move decorations: e.g Nf3+?! becomes Nf3 + const cleanMove = strippedSan(move) + + let pieceType = inferPieceType(cleanMove) + let moves = this._moves({ legal: true, piece: pieceType }) + + // strict parser + for (let i = 0, len = moves.length; i < len; i++) { + if (cleanMove === strippedSan(this._moveToSan(moves[i], moves))) { + return moves[i] + } + } + + // the strict parser failed + if (strict) { + return null + } + + let piece = undefined + let matches = undefined + let from = undefined + let to = undefined + let promotion = undefined + + /* + * The default permissive (non-strict) parser allows the user to parse + * non-standard chess notations. This parser is only run after the strict + * Standard Algebraic Notation (SAN) parser has failed. + * + * When running the permissive parser, we'll run a regex to grab the piece, the + * to/from square, and an optional promotion piece. This regex will + * parse common non-standard notation like: Pe2-e4, Rc1c4, Qf3xf7, + * f7f8q, b1c3 + * + * NOTE: Some positions and moves may be ambiguous when using the permissive + * parser. For example, in this position: 6k1/8/8/B7/8/8/8/BN4K1 w - - 0 1, + * the move b1c3 may be interpreted as Nc3 or B1c3 (a disambiguated bishop + * move). In these cases, the permissive parser will default to the most + * basic interpretation (which is b1c3 parsing to Nc3). + */ + + let overlyDisambiguated = false + + matches = cleanMove.match( + /([pnbrqkPNBRQK])?([a-h][1-8])x?-?([a-h][1-8])([qrbnQRBN])?/, + // piece from to promotion + ) + + if (matches) { + piece = matches[1] + from = matches[2] as Square + to = matches[3] as Square + promotion = matches[4] + + if (from.length == 1) { + overlyDisambiguated = true + } + } else { + /* + * The [a-h]?[1-8]? portion of the regex below handles moves that may be + * overly disambiguated (e.g. Nge7 is unnecessary and non-standard when + * there is one legal knight move to e7). In this case, the value of + * 'from' variable will be a rank or file, not a square. + */ + + matches = cleanMove.match( + /([pnbrqkPNBRQK])?([a-h]?[1-8]?)x?-?([a-h][1-8])([qrbnQRBN])?/, + ) + + if (matches) { + piece = matches[1] + from = matches[2] as Square + to = matches[3] as Square + promotion = matches[4] + + if (from.length == 1) { + overlyDisambiguated = true + } + } + } + + pieceType = inferPieceType(cleanMove) + moves = this._moves({ + legal: true, + piece: piece ? (piece as PieceSymbol) : pieceType, + }) + + if (!to) { + return null + } + + for (let i = 0, len = moves.length; i < len; i++) { + if (!from) { + // if there is no from square, it could be just 'x' missing from a capture + if ( + cleanMove === + strippedSan(this._moveToSan(moves[i], moves)).replace('x', '') + ) { + return moves[i] + } + // hand-compare move properties with the results from our permissive regex + } else if ( + (!piece || piece.toLowerCase() == moves[i].piece) && + Ox88[from] == moves[i].from && + Ox88[to] == moves[i].to && + (!promotion || promotion.toLowerCase() == moves[i].promotion) + ) { + return moves[i] + } else if (overlyDisambiguated) { + /* + * SPECIAL CASE: we parsed a move string that may have an unneeded + * rank/file disambiguator (e.g. Nge7). The 'from' variable will + */ + + const square = algebraic(moves[i].from) + if ( + (!piece || piece.toLowerCase() == moves[i].piece) && + Ox88[to] == moves[i].to && + (from == square[0] || from == square[1]) && + (!promotion || promotion.toLowerCase() == moves[i].promotion) + ) { + return moves[i] + } + } + } + + return null + } + + ascii() { + let s = ' +------------------------+\n' + for (let i = Ox88.a8; i <= Ox88.h1; i++) { + // display the rank + if (file(i) === 0) { + s += ' ' + '87654321'[rank(i)] + ' |' + } + + if (this._board[i]) { + const piece = this._board[i].type + const color = this._board[i].color + const symbol = + color === WHITE ? piece.toUpperCase() : piece.toLowerCase() + s += ' ' + symbol + ' ' + } else { + s += ' . ' + } + + if ((i + 1) & 0x88) { + s += '|\n' + i += 8 + } + } + s += ' +------------------------+\n' + s += ' a b c d e f g h' + + return s + } + + perft(depth: number) { + const moves = this._moves({ legal: false }) + let nodes = 0 + const color = this._turn + + for (let i = 0, len = moves.length; i < len; i++) { + this._makeMove(moves[i]) + if (!this._isKingAttacked(color)) { + if (depth - 1 > 0) { + nodes += this.perft(depth - 1) + } else { + nodes++ + } + } + this._undoMove() + } + + return nodes + } + + // pretty = external move object + private _makePretty(uglyMove: InternalMove): Move { + const { color, piece, from, to, flags, captured, promotion } = uglyMove + + let prettyFlags = '' + + for (const flag in BITS) { + if (BITS[flag] & flags) { + prettyFlags += FLAGS[flag] + } + } + + const fromAlgebraic = algebraic(from) + const toAlgebraic = algebraic(to) + + const move: Move = { + color, + piece, + from: fromAlgebraic, + to: toAlgebraic, + san: this._moveToSan(uglyMove, this._moves({ legal: true })), + flags: prettyFlags, + lan: fromAlgebraic + toAlgebraic, + before: this.fen(), + after: '', + } + + // generate the FEN for the 'after' key + this._makeMove(uglyMove) + move.after = this.fen() + this._undoMove() + + if (captured) { + move.captured = captured + } + if (promotion) { + move.promotion = promotion + move.lan += promotion + } + + return move + } + + turn() { + return this._turn + } + + board() { + const output = [] + let row = [] + + for (let i = Ox88.a8; i <= Ox88.h1; i++) { + if (this._board[i] == null) { + row.push(null) + } else { + row.push({ + square: algebraic(i), + type: this._board[i].type, + color: this._board[i].color, + }) + } + if ((i + 1) & 0x88) { + output.push(row) + row = [] + i += 8 + } + } + + return output + } + + squareColor(square: Square) { + if (square in Ox88) { + const sq = Ox88[square] + return (rank(sq) + file(sq)) % 2 === 0 ? 'light' : 'dark' + } + + return null + } + + history(): string[] + history({ verbose }: { verbose: true }): Move[] + history({ verbose }: { verbose: false }): string[] + history({ verbose }: { verbose: boolean }): string[] | Move[] + history({ verbose = false }: { verbose?: boolean } = {}) { + const reversedHistory = [] + const moveHistory = [] + + while (this._history.length > 0) { + reversedHistory.push(this._undoMove()) + } + + while (true) { + const move = reversedHistory.pop() + if (!move) { + break + } + + if (verbose) { + moveHistory.push(this._makePretty(move)) + } else { + moveHistory.push(this._moveToSan(move, this._moves())) + } + this._makeMove(move) + } + + return moveHistory + } + + /* + * Keeps track of position occurrence counts for the purpose of repetition + * checking. All three methods (`_inc`, `_dec`, and `_get`) trim the + * irrelevent information from the fen, initialising new positions, and + * removing old positions from the record if their counts are reduced to 0. + */ + private _getPositionCount(fen: string) { + const trimmedFen = trimFen(fen) + return this._positionCount[trimmedFen] || 0 + } + + private _incPositionCount(fen: string) { + const trimmedFen = trimFen(fen) + if (this._positionCount[trimmedFen] === undefined) { + this._positionCount[trimmedFen] = 0 + } + this._positionCount[trimmedFen] += 1 + } + + private _decPositionCount(fen: string) { + const trimmedFen = trimFen(fen) + if (this._positionCount[trimmedFen] === 1) { + delete this._positionCount[trimmedFen] + } else { + this._positionCount[trimmedFen] -= 1 + } + } + + private _pruneComments() { + const reversedHistory = [] + const currentComments: Record = {} + + const copyComment = (fen: string) => { + if (fen in this._comments) { + currentComments[fen] = this._comments[fen] + } + } + + while (this._history.length > 0) { + reversedHistory.push(this._undoMove()) + } + + copyComment(this.fen()) + + while (true) { + const move = reversedHistory.pop() + if (!move) { + break + } + this._makeMove(move) + copyComment(this.fen()) + } + this._comments = currentComments + } + + getComment() { + return this._comments[this.fen()] + } + + setComment(comment: string) { + this._comments[this.fen()] = comment.replace('{', '[').replace('}', ']') + } + + deleteComment() { + const comment = this._comments[this.fen()] + delete this._comments[this.fen()] + return comment + } + + getComments() { + this._pruneComments() + return Object.keys(this._comments).map((fen: string) => { + return { fen: fen, comment: this._comments[fen] } + }) + } + + deleteComments() { + this._pruneComments() + return Object.keys(this._comments).map((fen) => { + const comment = this._comments[fen] + delete this._comments[fen] + return { fen: fen, comment: comment } + }) + } + + setCastlingRights( + color: Color, + rights: Partial>, + ) { + for (const side of [KING, QUEEN] as const) { + if (rights[side] !== undefined) { + if (rights[side]) { + this._castling[color] |= SIDES[side] + } else { + this._castling[color] &= ~SIDES[side] + } + } + } + + this._updateCastlingRights() + const result = this.getCastlingRights(color) + + return ( + (rights[KING] === undefined || rights[KING] === result[KING]) && + (rights[QUEEN] === undefined || rights[QUEEN] === result[QUEEN]) + ) + } + + getCastlingRights(color: Color) { + return { + [KING]: (this._castling[color] & SIDES[KING]) !== 0, + [QUEEN]: (this._castling[color] & SIDES[QUEEN]) !== 0, + } + } + + moveNumber() { + return this._moveNumber + } + } \ No newline at end of file diff --git a/packages/chess-app/src/contracts/chess.mjs b/packages/chess-app/src/contracts/chess.mjs new file mode 100644 index 000000000..fca67e0fe --- /dev/null +++ b/packages/chess-app/src/contracts/chess.mjs @@ -0,0 +1,1929 @@ +/** + * @license + * Copyright (c) 2023, Jeff Hlywa (jhlywa@gmail.com) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +export const WHITE = 'w'; +export const BLACK = 'b'; +export const PAWN = 'p'; +export const KNIGHT = 'n'; +export const BISHOP = 'b'; +export const ROOK = 'r'; +export const QUEEN = 'q'; +export const KING = 'k'; +export const DEFAULT_POSITION = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; +const EMPTY = -1; +const FLAGS = { + NORMAL: 'n', + CAPTURE: 'c', + BIG_PAWN: 'b', + EP_CAPTURE: 'e', + PROMOTION: 'p', + KSIDE_CASTLE: 'k', + QSIDE_CASTLE: 'q', +}; +// prettier-ignore +export const SQUARES = [ + 'a8', 'b8', 'c8', 'd8', 'e8', 'f8', 'g8', 'h8', + 'a7', 'b7', 'c7', 'd7', 'e7', 'f7', 'g7', 'h7', + 'a6', 'b6', 'c6', 'd6', 'e6', 'f6', 'g6', 'h6', + 'a5', 'b5', 'c5', 'd5', 'e5', 'f5', 'g5', 'h5', + 'a4', 'b4', 'c4', 'd4', 'e4', 'f4', 'g4', 'h4', + 'a3', 'b3', 'c3', 'd3', 'e3', 'f3', 'g3', 'h3', + 'a2', 'b2', 'c2', 'd2', 'e2', 'f2', 'g2', 'h2', + 'a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1' +]; +const BITS = { + NORMAL: 1, + CAPTURE: 2, + BIG_PAWN: 4, + EP_CAPTURE: 8, + PROMOTION: 16, + KSIDE_CASTLE: 32, + QSIDE_CASTLE: 64, +}; +/* + * NOTES ABOUT 0x88 MOVE GENERATION ALGORITHM + * ---------------------------------------------------------------------------- + * From https://github.com/jhlywa/chess.js/issues/230 + * + * A lot of people are confused when they first see the internal representation + * of chess.js. It uses the 0x88 Move Generation Algorithm which internally + * stores the board as an 8x16 array. This is purely for efficiency but has a + * couple of interesting benefits: + * + * 1. 0x88 offers a very inexpensive "off the board" check. Bitwise AND (&) any + * square with 0x88, if the result is non-zero then the square is off the + * board. For example, assuming a knight square A8 (0 in 0x88 notation), + * there are 8 possible directions in which the knight can move. These + * directions are relative to the 8x16 board and are stored in the + * PIECE_OFFSETS map. One possible move is A8 - 18 (up one square, and two + * squares to the left - which is off the board). 0 - 18 = -18 & 0x88 = 0x88 + * (because of two-complement representation of -18). The non-zero result + * means the square is off the board and the move is illegal. Take the + * opposite move (from A8 to C7), 0 + 18 = 18 & 0x88 = 0. A result of zero + * means the square is on the board. + * + * 2. The relative distance (or difference) between two squares on a 8x16 board + * is unique and can be used to inexpensively determine if a piece on a + * square can attack any other arbitrary square. For example, let's see if a + * pawn on E7 can attack E2. The difference between E7 (20) - E2 (100) is + * -80. We add 119 to make the ATTACKS array index non-negative (because the + * worst case difference is A8 - H1 = -119). The ATTACKS array contains a + * bitmask of pieces that can attack from that distance and direction. + * ATTACKS[-80 + 119=39] gives us 24 or 0b11000 in binary. Look at the + * PIECE_MASKS map to determine the mask for a given piece type. In our pawn + * example, we would check to see if 24 & 0x1 is non-zero, which it is + * not. So, naturally, a pawn on E7 can't attack a piece on E2. However, a + * rook can since 24 & 0x8 is non-zero. The only thing left to check is that + * there are no blocking pieces between E7 and E2. That's where the RAYS + * array comes in. It provides an offset (in this case 16) to add to E7 (20) + * to check for blocking pieces. E7 (20) + 16 = E6 (36) + 16 = E5 (52) etc. + */ +// prettier-ignore +// eslint-disable-next-line +const Ox88 = { + a8: 0, b8: 1, c8: 2, d8: 3, e8: 4, f8: 5, g8: 6, h8: 7, + a7: 16, b7: 17, c7: 18, d7: 19, e7: 20, f7: 21, g7: 22, h7: 23, + a6: 32, b6: 33, c6: 34, d6: 35, e6: 36, f6: 37, g6: 38, h6: 39, + a5: 48, b5: 49, c5: 50, d5: 51, e5: 52, f5: 53, g5: 54, h5: 55, + a4: 64, b4: 65, c4: 66, d4: 67, e4: 68, f4: 69, g4: 70, h4: 71, + a3: 80, b3: 81, c3: 82, d3: 83, e3: 84, f3: 85, g3: 86, h3: 87, + a2: 96, b2: 97, c2: 98, d2: 99, e2: 100, f2: 101, g2: 102, h2: 103, + a1: 112, b1: 113, c1: 114, d1: 115, e1: 116, f1: 117, g1: 118, h1: 119 +}; +const PAWN_OFFSETS = { + b: [16, 32, 17, 15], + w: [-16, -32, -17, -15], +}; +const PIECE_OFFSETS = { + n: [-18, -33, -31, -14, 18, 33, 31, 14], + b: [-17, -15, 17, 15], + r: [-16, 1, 16, -1], + q: [-17, -16, -15, 1, 17, 16, 15, -1], + k: [-17, -16, -15, 1, 17, 16, 15, -1], +}; +// prettier-ignore +const ATTACKS = [ + 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 20, 0, + 0, 20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 20, 0, 0, + 0, 0, 20, 0, 0, 0, 0, 24, 0, 0, 0, 0, 20, 0, 0, 0, + 0, 0, 0, 20, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, 0, + 0, 0, 0, 0, 20, 0, 0, 24, 0, 0, 20, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 20, 2, 24, 2, 20, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2, 53, 56, 53, 2, 0, 0, 0, 0, 0, 0, + 24, 24, 24, 24, 24, 24, 56, 0, 56, 24, 24, 24, 24, 24, 24, 0, + 0, 0, 0, 0, 0, 2, 53, 56, 53, 2, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 20, 2, 24, 2, 20, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 20, 0, 0, 24, 0, 0, 20, 0, 0, 0, 0, 0, + 0, 0, 0, 20, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, 0, + 0, 0, 20, 0, 0, 0, 0, 24, 0, 0, 0, 0, 20, 0, 0, 0, + 0, 20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 20, 0, 0, + 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 20 +]; +// prettier-ignore +const RAYS = [ + 17, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 15, 0, + 0, 17, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 15, 0, 0, + 0, 0, 17, 0, 0, 0, 0, 16, 0, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 17, 0, 0, 0, 16, 0, 0, 0, 15, 0, 0, 0, 0, + 0, 0, 0, 0, 17, 0, 0, 16, 0, 0, 15, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 17, 0, 16, 0, 15, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 17, 16, 15, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 0, -1, -1, -1, -1, -1, -1, -1, 0, + 0, 0, 0, 0, 0, 0, -15, -16, -17, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, -15, 0, -16, 0, -17, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -15, 0, 0, -16, 0, 0, -17, 0, 0, 0, 0, 0, + 0, 0, 0, -15, 0, 0, 0, -16, 0, 0, 0, -17, 0, 0, 0, 0, + 0, 0, -15, 0, 0, 0, 0, -16, 0, 0, 0, 0, -17, 0, 0, 0, + 0, -15, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, -17, 0, 0, + -15, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, -17 +]; +const PIECE_MASKS = { p: 0x1, n: 0x2, b: 0x4, r: 0x8, q: 0x10, k: 0x20 }; +const SYMBOLS = 'pnbrqkPNBRQK'; +const PROMOTIONS = [KNIGHT, BISHOP, ROOK, QUEEN]; +const RANK_1 = 7; +const RANK_2 = 6; +/* + * const RANK_3 = 5 + * const RANK_4 = 4 + * const RANK_5 = 3 + * const RANK_6 = 2 + */ +const RANK_7 = 1; +const RANK_8 = 0; +const SIDES = { + [KING]: BITS.KSIDE_CASTLE, + [QUEEN]: BITS.QSIDE_CASTLE, +}; +const ROOKS = { + w: [ + { square: Ox88.a1, flag: BITS.QSIDE_CASTLE }, + { square: Ox88.h1, flag: BITS.KSIDE_CASTLE }, + ], + b: [ + { square: Ox88.a8, flag: BITS.QSIDE_CASTLE }, + { square: Ox88.h8, flag: BITS.KSIDE_CASTLE }, + ], +}; +const SECOND_RANK = { b: RANK_7, w: RANK_2 }; +const TERMINATION_MARKERS = ['1-0', '0-1', '1/2-1/2', '*']; +// Extracts the zero-based rank of an 0x88 square. +function rank(square) { + return square >> 4; +} +// Extracts the zero-based file of an 0x88 square. +function file(square) { + return square & 0xf; +} +function isDigit(c) { + return '0123456789'.indexOf(c) !== -1; +} +// Converts a 0x88 square to algebraic notation. +function algebraic(square) { + const f = file(square); + const r = rank(square); + return ('abcdefgh'.substring(f, f + 1) + + '87654321'.substring(r, r + 1)); +} +function swapColor(color) { + return color === WHITE ? BLACK : WHITE; +} +export function validateFen(fen) { + // 1st criterion: 6 space-seperated fields? + const tokens = fen.split(/\s+/); + if (tokens.length !== 6) { + return { + ok: false, + error: 'Invalid FEN: must contain six space-delimited fields', + }; + } + // 2nd criterion: move number field is a integer value > 0? + const moveNumber = parseInt(tokens[5], 10); + if (isNaN(moveNumber) || moveNumber <= 0) { + return { + ok: false, + error: 'Invalid FEN: move number must be a positive integer', + }; + } + // 3rd criterion: half move counter is an integer >= 0? + const halfMoves = parseInt(tokens[4], 10); + if (isNaN(halfMoves) || halfMoves < 0) { + return { + ok: false, + error: 'Invalid FEN: half move counter number must be a non-negative integer', + }; + } + // 4th criterion: 4th field is a valid e.p.-string? + if (!/^(-|[abcdefgh][36])$/.test(tokens[3])) { + return { ok: false, error: 'Invalid FEN: en-passant square is invalid' }; + } + // 5th criterion: 3th field is a valid castle-string? + if (/[^kKqQ-]/.test(tokens[2])) { + return { ok: false, error: 'Invalid FEN: castling availability is invalid' }; + } + // 6th criterion: 2nd field is "w" (white) or "b" (black)? + if (!/^(w|b)$/.test(tokens[1])) { + return { ok: false, error: 'Invalid FEN: side-to-move is invalid' }; + } + // 7th criterion: 1st field contains 8 rows? + const rows = tokens[0].split('/'); + if (rows.length !== 8) { + return { + ok: false, + error: "Invalid FEN: piece data does not contain 8 '/'-delimited rows", + }; + } + // 8th criterion: every row is valid? + for (let i = 0; i < rows.length; i++) { + // check for right sum of fields AND not two numbers in succession + let sumFields = 0; + let previousWasNumber = false; + for (let k = 0; k < rows[i].length; k++) { + if (isDigit(rows[i][k])) { + if (previousWasNumber) { + return { + ok: false, + error: 'Invalid FEN: piece data is invalid (consecutive number)', + }; + } + sumFields += parseInt(rows[i][k], 10); + previousWasNumber = true; + } + else { + if (!/^[prnbqkPRNBQK]$/.test(rows[i][k])) { + return { + ok: false, + error: 'Invalid FEN: piece data is invalid (invalid piece)', + }; + } + sumFields += 1; + previousWasNumber = false; + } + } + if (sumFields !== 8) { + return { + ok: false, + error: 'Invalid FEN: piece data is invalid (too many squares in rank)', + }; + } + } + // 9th criterion: is en-passant square legal? + if ((tokens[3][1] == '3' && tokens[1] == 'w') || + (tokens[3][1] == '6' && tokens[1] == 'b')) { + return { ok: false, error: 'Invalid FEN: illegal en-passant square' }; + } + // 10th criterion: does chess position contain exact two kings? + const kings = [ + { color: 'white', regex: /K/g }, + { color: 'black', regex: /k/g }, + ]; + for (const { color, regex } of kings) { + if (!regex.test(tokens[0])) { + return { ok: false, error: `Invalid FEN: missing ${color} king` }; + } + if ((tokens[0].match(regex) || []).length > 1) { + return { ok: false, error: `Invalid FEN: too many ${color} kings` }; + } + } + // 11th criterion: are any pawns on the first or eighth rows? + if (Array.from(rows[0] + rows[7]).some((char) => char.toUpperCase() === 'P')) { + return { + ok: false, + error: 'Invalid FEN: some pawns are on the edge rows', + }; + } + return { ok: true }; +} +// this function is used to uniquely identify ambiguous moves +function getDisambiguator(move, moves) { + const from = move.from; + const to = move.to; + const piece = move.piece; + let ambiguities = 0; + let sameRank = 0; + let sameFile = 0; + for (let i = 0, len = moves.length; i < len; i++) { + const ambigFrom = moves[i].from; + const ambigTo = moves[i].to; + const ambigPiece = moves[i].piece; + /* + * if a move of the same piece type ends on the same to square, we'll need + * to add a disambiguator to the algebraic notation + */ + if (piece === ambigPiece && from !== ambigFrom && to === ambigTo) { + ambiguities++; + if (rank(from) === rank(ambigFrom)) { + sameRank++; + } + if (file(from) === file(ambigFrom)) { + sameFile++; + } + } + } + if (ambiguities > 0) { + if (sameRank > 0 && sameFile > 0) { + /* + * if there exists a similar moving piece on the same rank and file as + * the move in question, use the square as the disambiguator + */ + return algebraic(from); + } + else if (sameFile > 0) { + /* + * if the moving piece rests on the same file, use the rank symbol as the + * disambiguator + */ + return algebraic(from).charAt(1); + } + else { + // else use the file symbol + return algebraic(from).charAt(0); + } + } + return ''; +} +function addMove(moves, color, from, to, piece, captured = undefined, flags = BITS.NORMAL) { + const r = rank(to); + if (piece === PAWN && (r === RANK_1 || r === RANK_8)) { + for (let i = 0; i < PROMOTIONS.length; i++) { + const promotion = PROMOTIONS[i]; + moves.push({ + color, + from, + to, + piece, + captured, + promotion, + flags: flags | BITS.PROMOTION, + }); + } + } + else { + moves.push({ + color, + from, + to, + piece, + captured, + flags, + }); + } +} +function inferPieceType(san) { + let pieceType = san.charAt(0); + if (pieceType >= 'a' && pieceType <= 'h') { + const matches = san.match(/[a-h]\d.*[a-h]\d/); + if (matches) { + return undefined; + } + return PAWN; + } + pieceType = pieceType.toLowerCase(); + if (pieceType === 'o') { + return KING; + } + return pieceType; +} +// parses all of the decorators out of a SAN string +function strippedSan(move) { + return move.replace(/=/, '').replace(/[+#]?[?!]*$/, ''); +} +function trimFen(fen) { + /* + * remove last two fields in FEN string as they're not needed when checking + * for repetition + */ + return fen.split(' ').slice(0, 4).join(' '); +} +export class Chess { + constructor(fen = DEFAULT_POSITION) { + this._board = new Array(128); + this._turn = WHITE; + this._header = {}; + this._kings = { w: EMPTY, b: EMPTY }; + this._epSquare = -1; + this._halfMoves = 0; + this._moveNumber = 0; + this._history = []; + this._comments = {}; + this._castling = { w: 0, b: 0 }; + // tracks number of times a position has been seen for repetition checking + this._positionCount = {}; + this.load(fen); + } + clear({ preserveHeaders = false } = {}) { + this._board = new Array(128); + this._kings = { w: EMPTY, b: EMPTY }; + this._turn = WHITE; + this._castling = { w: 0, b: 0 }; + this._epSquare = EMPTY; + this._halfMoves = 0; + this._moveNumber = 1; + this._history = []; + this._comments = {}; + this._header = preserveHeaders ? this._header : {}; + this._positionCount = {}; + /* + * Delete the SetUp and FEN headers (if preserved), the board is empty and + * these headers don't make sense in this state. They'll get added later + * via .load() or .put() + */ + delete this._header['SetUp']; + delete this._header['FEN']; + } + removeHeader(key) { + if (key in this._header) { + delete this._header[key]; + } + } + load(fen, { skipValidation = false, preserveHeaders = false } = {}) { + let tokens = fen.split(/\s+/); + // append commonly omitted fen tokens + if (tokens.length >= 2 && tokens.length < 6) { + const adjustments = ['-', '-', '0', '1']; + fen = tokens.concat(adjustments.slice(-(6 - tokens.length))).join(' '); + } + tokens = fen.split(/\s+/); + if (!skipValidation) { + const { ok, error } = validateFen(fen); + if (!ok) { + throw new Error(error); + } + } + const position = tokens[0]; + let square = 0; + this.clear({ preserveHeaders }); + for (let i = 0; i < position.length; i++) { + const piece = position.charAt(i); + if (piece === '/') { + square += 8; + } + else if (isDigit(piece)) { + square += parseInt(piece, 10); + } + else { + const color = piece < 'a' ? WHITE : BLACK; + this._put({ type: piece.toLowerCase(), color }, algebraic(square)); + square++; + } + } + this._turn = tokens[1]; + if (tokens[2].indexOf('K') > -1) { + this._castling.w |= BITS.KSIDE_CASTLE; + } + if (tokens[2].indexOf('Q') > -1) { + this._castling.w |= BITS.QSIDE_CASTLE; + } + if (tokens[2].indexOf('k') > -1) { + this._castling.b |= BITS.KSIDE_CASTLE; + } + if (tokens[2].indexOf('q') > -1) { + this._castling.b |= BITS.QSIDE_CASTLE; + } + this._epSquare = tokens[3] === '-' ? EMPTY : Ox88[tokens[3]]; + this._halfMoves = parseInt(tokens[4], 10); + this._moveNumber = parseInt(tokens[5], 10); + this._updateSetup(fen); + this._incPositionCount(fen); + } + fen() { + let empty = 0; + let fen = ''; + for (let i = Ox88.a8; i <= Ox88.h1; i++) { + if (this._board[i]) { + if (empty > 0) { + fen += empty; + empty = 0; + } + const { color, type: piece } = this._board[i]; + fen += color === WHITE ? piece.toUpperCase() : piece.toLowerCase(); + } + else { + empty++; + } + if ((i + 1) & 0x88) { + if (empty > 0) { + fen += empty; + } + if (i !== Ox88.h1) { + fen += '/'; + } + empty = 0; + i += 8; + } + } + let castling = ''; + if (this._castling[WHITE] & BITS.KSIDE_CASTLE) { + castling += 'K'; + } + if (this._castling[WHITE] & BITS.QSIDE_CASTLE) { + castling += 'Q'; + } + if (this._castling[BLACK] & BITS.KSIDE_CASTLE) { + castling += 'k'; + } + if (this._castling[BLACK] & BITS.QSIDE_CASTLE) { + castling += 'q'; + } + // do we have an empty castling flag? + castling = castling || '-'; + let epSquare = '-'; + /* + * only print the ep square if en passant is a valid move (pawn is present + * and ep capture is not pinned) + */ + if (this._epSquare !== EMPTY) { + const bigPawnSquare = this._epSquare + (this._turn === WHITE ? 16 : -16); + const squares = [bigPawnSquare + 1, bigPawnSquare - 1]; + for (const square of squares) { + // is the square off the board? + if (square & 0x88) { + continue; + } + const color = this._turn; + // is there a pawn that can capture the epSquare? + if (this._board[square]?.color === color && + this._board[square]?.type === PAWN) { + // if the pawn makes an ep capture, does it leave it's king in check? + this._makeMove({ + color, + from: square, + to: this._epSquare, + piece: PAWN, + captured: PAWN, + flags: BITS.EP_CAPTURE, + }); + const isLegal = !this._isKingAttacked(color); + this._undoMove(); + // if ep is legal, break and set the ep square in the FEN output + if (isLegal) { + epSquare = algebraic(this._epSquare); + break; + } + } + } + } + return [ + fen, + this._turn, + castling, + epSquare, + this._halfMoves, + this._moveNumber, + ].join(' '); + } + /* + * Called when the initial board setup is changed with put() or remove(). + * modifies the SetUp and FEN properties of the header object. If the FEN + * is equal to the default position, the SetUp and FEN are deleted the setup + * is only updated if history.length is zero, ie moves haven't been made. + */ + _updateSetup(fen) { + if (this._history.length > 0) + return; + if (fen !== DEFAULT_POSITION) { + this._header['SetUp'] = '1'; + this._header['FEN'] = fen; + } + else { + delete this._header['SetUp']; + delete this._header['FEN']; + } + } + reset() { + this.load(DEFAULT_POSITION); + } + get(square) { + return this._board[Ox88[square]] || false; + } + put({ type, color }, square) { + if (this._put({ type, color }, square)) { + this._updateCastlingRights(); + this._updateEnPassantSquare(); + this._updateSetup(this.fen()); + return true; + } + return false; + } + _put({ type, color }, square) { + // check for piece + if (SYMBOLS.indexOf(type.toLowerCase()) === -1) { + return false; + } + // check for valid square + if (!(square in Ox88)) { + return false; + } + const sq = Ox88[square]; + // don't let the user place more than one king + if (type == KING && + !(this._kings[color] == EMPTY || this._kings[color] == sq)) { + return false; + } + const currentPieceOnSquare = this._board[sq]; + // if one of the kings will be replaced by the piece from args, set the `_kings` respective entry to `EMPTY` + if (currentPieceOnSquare && currentPieceOnSquare.type === KING) { + this._kings[currentPieceOnSquare.color] = EMPTY; + } + this._board[sq] = { type: type, color: color }; + if (type === KING) { + this._kings[color] = sq; + } + return true; + } + remove(square) { + const piece = this.get(square); + delete this._board[Ox88[square]]; + if (piece && piece.type === KING) { + this._kings[piece.color] = EMPTY; + } + this._updateCastlingRights(); + this._updateEnPassantSquare(); + this._updateSetup(this.fen()); + return piece; + } + _updateCastlingRights() { + const whiteKingInPlace = this._board[Ox88.e1]?.type === KING && + this._board[Ox88.e1]?.color === WHITE; + const blackKingInPlace = this._board[Ox88.e8]?.type === KING && + this._board[Ox88.e8]?.color === BLACK; + if (!whiteKingInPlace || + this._board[Ox88.a1]?.type !== ROOK || + this._board[Ox88.a1]?.color !== WHITE) { + this._castling.w &= ~BITS.QSIDE_CASTLE; + } + if (!whiteKingInPlace || + this._board[Ox88.h1]?.type !== ROOK || + this._board[Ox88.h1]?.color !== WHITE) { + this._castling.w &= ~BITS.KSIDE_CASTLE; + } + if (!blackKingInPlace || + this._board[Ox88.a8]?.type !== ROOK || + this._board[Ox88.a8]?.color !== BLACK) { + this._castling.b &= ~BITS.QSIDE_CASTLE; + } + if (!blackKingInPlace || + this._board[Ox88.h8]?.type !== ROOK || + this._board[Ox88.h8]?.color !== BLACK) { + this._castling.b &= ~BITS.KSIDE_CASTLE; + } + } + _updateEnPassantSquare() { + if (this._epSquare === EMPTY) { + return; + } + const startSquare = this._epSquare + (this._turn === WHITE ? -16 : 16); + const currentSquare = this._epSquare + (this._turn === WHITE ? 16 : -16); + const attackers = [currentSquare + 1, currentSquare - 1]; + if (this._board[startSquare] !== null || + this._board[this._epSquare] !== null || + this._board[currentSquare]?.color !== swapColor(this._turn) || + this._board[currentSquare]?.type !== PAWN) { + this._epSquare = EMPTY; + return; + } + const canCapture = (square) => !(square & 0x88) && + this._board[square]?.color === this._turn && + this._board[square]?.type === PAWN; + if (!attackers.some(canCapture)) { + this._epSquare = EMPTY; + } + } + _attacked(color, square, verbose) { + const attackers = []; + for (let i = Ox88.a8; i <= Ox88.h1; i++) { + // did we run off the end of the board + if (i & 0x88) { + i += 7; + continue; + } + // if empty square or wrong color + if (this._board[i] === undefined || this._board[i].color !== color) { + continue; + } + const piece = this._board[i]; + const difference = i - square; + // skip - to/from square are the same + if (difference === 0) { + continue; + } + const index = difference + 119; + if (ATTACKS[index] & PIECE_MASKS[piece.type]) { + if (piece.type === PAWN) { + if ((difference > 0 && piece.color === WHITE) || + (difference <= 0 && piece.color === BLACK)) { + if (!verbose) { + return true; + } + else { + attackers.push(algebraic(i)); + } + } + continue; + } + // if the piece is a knight or a king + if (piece.type === 'n' || piece.type === 'k') { + if (!verbose) { + return true; + } + else { + attackers.push(algebraic(i)); + continue; + } + } + const offset = RAYS[index]; + let j = i + offset; + let blocked = false; + while (j !== square) { + if (this._board[j] != null) { + blocked = true; + break; + } + j += offset; + } + if (!blocked) { + if (!verbose) { + return true; + } + else { + attackers.push(algebraic(i)); + continue; + } + } + } + } + if (verbose) { + return attackers; + } + else { + return false; + } + } + attackers(square, attackedBy) { + if (!attackedBy) { + return this._attacked(this._turn, Ox88[square], true); + } + else { + return this._attacked(attackedBy, Ox88[square], true); + } + } + _isKingAttacked(color) { + const square = this._kings[color]; + return square === -1 ? false : this._attacked(swapColor(color), square); + } + isAttacked(square, attackedBy) { + return this._attacked(attackedBy, Ox88[square]); + } + isCheck() { + return this._isKingAttacked(this._turn); + } + inCheck() { + return this.isCheck(); + } + isCheckmate() { + return this.isCheck() && this._moves().length === 0; + } + isStalemate() { + return !this.isCheck() && this._moves().length === 0; + } + isInsufficientMaterial() { + /* + * k.b. vs k.b. (of opposite colors) with mate in 1: + * 8/8/8/8/1b6/8/B1k5/K7 b - - 0 1 + * + * k.b. vs k.n. with mate in 1: + * 8/8/8/8/1n6/8/B7/K1k5 b - - 2 1 + */ + const pieces = { + b: 0, + n: 0, + r: 0, + q: 0, + k: 0, + p: 0, + }; + const bishops = []; + let numPieces = 0; + let squareColor = 0; + for (let i = Ox88.a8; i <= Ox88.h1; i++) { + squareColor = (squareColor + 1) % 2; + if (i & 0x88) { + i += 7; + continue; + } + const piece = this._board[i]; + if (piece) { + pieces[piece.type] = piece.type in pieces ? pieces[piece.type] + 1 : 1; + if (piece.type === BISHOP) { + bishops.push(squareColor); + } + numPieces++; + } + } + // k vs. k + if (numPieces === 2) { + return true; + } + else if ( + // k vs. kn .... or .... k vs. kb + numPieces === 3 && + (pieces[BISHOP] === 1 || pieces[KNIGHT] === 1)) { + return true; + } + else if (numPieces === pieces[BISHOP] + 2) { + // kb vs. kb where any number of bishops are all on the same color + let sum = 0; + const len = bishops.length; + for (let i = 0; i < len; i++) { + sum += bishops[i]; + } + if (sum === 0 || sum === len) { + return true; + } + } + return false; + } + isThreefoldRepetition() { + return this._getPositionCount(this.fen()) >= 3; + } + isDraw() { + return (this._halfMoves >= 100 || // 50 moves per side = 100 half moves + this.isStalemate() || + this.isInsufficientMaterial() || + this.isThreefoldRepetition()); + } + isGameOver() { + return this.isCheckmate() || this.isStalemate() || this.isDraw(); + } + moves({ verbose = false, square = undefined, piece = undefined, } = {}) { + const moves = this._moves({ square, piece }); + if (verbose) { + return moves.map((move) => this._makePretty(move)); + } + else { + return moves.map((move) => this._moveToSan(move, moves)); + } + } + _moves({ legal = true, piece = undefined, square = undefined, } = {}) { + const forSquare = square ? square.toLowerCase() : undefined; + const forPiece = piece?.toLowerCase(); + const moves = []; + const us = this._turn; + const them = swapColor(us); + let firstSquare = Ox88.a8; + let lastSquare = Ox88.h1; + let singleSquare = false; + // are we generating moves for a single square? + if (forSquare) { + // illegal square, return empty moves + if (!(forSquare in Ox88)) { + return []; + } + else { + firstSquare = lastSquare = Ox88[forSquare]; + singleSquare = true; + } + } + for (let from = firstSquare; from <= lastSquare; from++) { + // did we run off the end of the board + if (from & 0x88) { + from += 7; + continue; + } + // empty square or opponent, skip + if (!this._board[from] || this._board[from].color === them) { + continue; + } + const { type } = this._board[from]; + let to; + if (type === PAWN) { + if (forPiece && forPiece !== type) + continue; + // single square, non-capturing + to = from + PAWN_OFFSETS[us][0]; + if (!this._board[to]) { + addMove(moves, us, from, to, PAWN); + // double square + to = from + PAWN_OFFSETS[us][1]; + if (SECOND_RANK[us] === rank(from) && !this._board[to]) { + addMove(moves, us, from, to, PAWN, undefined, BITS.BIG_PAWN); + } + } + // pawn captures + for (let j = 2; j < 4; j++) { + to = from + PAWN_OFFSETS[us][j]; + if (to & 0x88) + continue; + if (this._board[to]?.color === them) { + addMove(moves, us, from, to, PAWN, this._board[to].type, BITS.CAPTURE); + } + else if (to === this._epSquare) { + addMove(moves, us, from, to, PAWN, PAWN, BITS.EP_CAPTURE); + } + } + } + else { + if (forPiece && forPiece !== type) + continue; + for (let j = 0, len = PIECE_OFFSETS[type].length; j < len; j++) { + const offset = PIECE_OFFSETS[type][j]; + to = from; + while (true) { + to += offset; + if (to & 0x88) + break; + if (!this._board[to]) { + addMove(moves, us, from, to, type); + } + else { + // own color, stop loop + if (this._board[to].color === us) + break; + addMove(moves, us, from, to, type, this._board[to].type, BITS.CAPTURE); + break; + } + /* break, if knight or king */ + if (type === KNIGHT || type === KING) + break; + } + } + } + } + /* + * check for castling if we're: + * a) generating all moves, or + * b) doing single square move generation on the king's square + */ + if (forPiece === undefined || forPiece === KING) { + if (!singleSquare || lastSquare === this._kings[us]) { + // king-side castling + if (this._castling[us] & BITS.KSIDE_CASTLE) { + const castlingFrom = this._kings[us]; + const castlingTo = castlingFrom + 2; + if (!this._board[castlingFrom + 1] && + !this._board[castlingTo] && + !this._attacked(them, this._kings[us]) && + !this._attacked(them, castlingFrom + 1) && + !this._attacked(them, castlingTo)) { + addMove(moves, us, this._kings[us], castlingTo, KING, undefined, BITS.KSIDE_CASTLE); + } + } + // queen-side castling + if (this._castling[us] & BITS.QSIDE_CASTLE) { + const castlingFrom = this._kings[us]; + const castlingTo = castlingFrom - 2; + if (!this._board[castlingFrom - 1] && + !this._board[castlingFrom - 2] && + !this._board[castlingFrom - 3] && + !this._attacked(them, this._kings[us]) && + !this._attacked(them, castlingFrom - 1) && + !this._attacked(them, castlingTo)) { + addMove(moves, us, this._kings[us], castlingTo, KING, undefined, BITS.QSIDE_CASTLE); + } + } + } + } + /* + * return all pseudo-legal moves (this includes moves that allow the king + * to be captured) + */ + if (!legal || this._kings[us] === -1) { + return moves; + } + // filter out illegal moves + const legalMoves = []; + for (let i = 0, len = moves.length; i < len; i++) { + this._makeMove(moves[i]); + if (!this._isKingAttacked(us)) { + legalMoves.push(moves[i]); + } + this._undoMove(); + } + return legalMoves; + } + move(move, { strict = false } = {}) { + /* + * The move function can be called with in the following parameters: + * + * .move('Nxb7') <- argument is a case-sensitive SAN string + * + * .move({ from: 'h7', <- argument is a move object + * to :'h8', + * promotion: 'q' }) + * + * + * An optional strict argument may be supplied to tell chess.js to + * strictly follow the SAN specification. + */ + let moveObj = null; + if (typeof move === 'string') { + moveObj = this._moveFromSan(move, strict); + } + else if (typeof move === 'object') { + const moves = this._moves(); + // convert the pretty move object to an ugly move object + for (let i = 0, len = moves.length; i < len; i++) { + if (move.from === algebraic(moves[i].from) && + move.to === algebraic(moves[i].to) && + (!('promotion' in moves[i]) || move.promotion === moves[i].promotion)) { + moveObj = moves[i]; + break; + } + } + } + // failed to find move + if (!moveObj) { + if (typeof move === 'string') { + throw new Error(`Invalid move: ${move}`); + } + else { + throw new Error(`Invalid move: ${JSON.stringify(move)}`); + } + } + /* + * need to make a copy of move because we can't generate SAN after the move + * is made + */ + const prettyMove = this._makePretty(moveObj); + this._makeMove(moveObj); + this._incPositionCount(prettyMove.after); + return prettyMove; + } + _push(move) { + this._history.push({ + move, + kings: { b: this._kings.b, w: this._kings.w }, + turn: this._turn, + castling: { b: this._castling.b, w: this._castling.w }, + epSquare: this._epSquare, + halfMoves: this._halfMoves, + moveNumber: this._moveNumber, + }); + } + _makeMove(move) { + const us = this._turn; + const them = swapColor(us); + this._push(move); + this._board[move.to] = this._board[move.from]; + delete this._board[move.from]; + // if ep capture, remove the captured pawn + if (move.flags & BITS.EP_CAPTURE) { + if (this._turn === BLACK) { + delete this._board[move.to - 16]; + } + else { + delete this._board[move.to + 16]; + } + } + // if pawn promotion, replace with new piece + if (move.promotion) { + this._board[move.to] = { type: move.promotion, color: us }; + } + // if we moved the king + if (this._board[move.to].type === KING) { + this._kings[us] = move.to; + // if we castled, move the rook next to the king + if (move.flags & BITS.KSIDE_CASTLE) { + const castlingTo = move.to - 1; + const castlingFrom = move.to + 1; + this._board[castlingTo] = this._board[castlingFrom]; + delete this._board[castlingFrom]; + } + else if (move.flags & BITS.QSIDE_CASTLE) { + const castlingTo = move.to + 1; + const castlingFrom = move.to - 2; + this._board[castlingTo] = this._board[castlingFrom]; + delete this._board[castlingFrom]; + } + // turn off castling + this._castling[us] = 0; + } + // turn off castling if we move a rook + if (this._castling[us]) { + for (let i = 0, len = ROOKS[us].length; i < len; i++) { + if (move.from === ROOKS[us][i].square && + this._castling[us] & ROOKS[us][i].flag) { + this._castling[us] ^= ROOKS[us][i].flag; + break; + } + } + } + // turn off castling if we capture a rook + if (this._castling[them]) { + for (let i = 0, len = ROOKS[them].length; i < len; i++) { + if (move.to === ROOKS[them][i].square && + this._castling[them] & ROOKS[them][i].flag) { + this._castling[them] ^= ROOKS[them][i].flag; + break; + } + } + } + // if big pawn move, update the en passant square + if (move.flags & BITS.BIG_PAWN) { + if (us === BLACK) { + this._epSquare = move.to - 16; + } + else { + this._epSquare = move.to + 16; + } + } + else { + this._epSquare = EMPTY; + } + // reset the 50 move counter if a pawn is moved or a piece is captured + if (move.piece === PAWN) { + this._halfMoves = 0; + } + else if (move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE)) { + this._halfMoves = 0; + } + else { + this._halfMoves++; + } + if (us === BLACK) { + this._moveNumber++; + } + this._turn = them; + } + undo() { + const move = this._undoMove(); + if (move) { + const prettyMove = this._makePretty(move); + this._decPositionCount(prettyMove.after); + return prettyMove; + } + return null; + } + _undoMove() { + const old = this._history.pop(); + if (old === undefined) { + return null; + } + const move = old.move; + this._kings = old.kings; + this._turn = old.turn; + this._castling = old.castling; + this._epSquare = old.epSquare; + this._halfMoves = old.halfMoves; + this._moveNumber = old.moveNumber; + const us = this._turn; + const them = swapColor(us); + this._board[move.from] = this._board[move.to]; + this._board[move.from].type = move.piece; // to undo any promotions + delete this._board[move.to]; + if (move.captured) { + if (move.flags & BITS.EP_CAPTURE) { + // en passant capture + let index; + if (us === BLACK) { + index = move.to - 16; + } + else { + index = move.to + 16; + } + this._board[index] = { type: PAWN, color: them }; + } + else { + // regular capture + this._board[move.to] = { type: move.captured, color: them }; + } + } + if (move.flags & (BITS.KSIDE_CASTLE | BITS.QSIDE_CASTLE)) { + let castlingTo, castlingFrom; + if (move.flags & BITS.KSIDE_CASTLE) { + castlingTo = move.to + 1; + castlingFrom = move.to - 1; + } + else { + castlingTo = move.to - 2; + castlingFrom = move.to + 1; + } + this._board[castlingTo] = this._board[castlingFrom]; + delete this._board[castlingFrom]; + } + return move; + } + pgn({ newline = '\n', maxWidth = 0, } = {}) { + /* + * using the specification from http://www.chessclub.com/help/PGN-spec + * example for html usage: .pgn({ max_width: 72, newline_char: "
    " }) + */ + const result = []; + let headerExists = false; + /* add the PGN header information */ + for (const i in this._header) { + /* + * TODO: order of enumerated properties in header object is not + * guaranteed, see ECMA-262 spec (section 12.6.4) + */ + result.push('[' + i + ' "' + this._header[i] + '"]' + newline); + headerExists = true; + } + if (headerExists && this._history.length) { + result.push(newline); + } + const appendComment = (moveString) => { + const comment = this._comments[this.fen()]; + if (typeof comment !== 'undefined') { + const delimiter = moveString.length > 0 ? ' ' : ''; + moveString = `${moveString}${delimiter}{${comment}}`; + } + return moveString; + }; + // pop all of history onto reversed_history + const reversedHistory = []; + while (this._history.length > 0) { + reversedHistory.push(this._undoMove()); + } + const moves = []; + let moveString = ''; + // special case of a commented starting position with no moves + if (reversedHistory.length === 0) { + moves.push(appendComment('')); + } + // build the list of moves. a move_string looks like: "3. e3 e6" + while (reversedHistory.length > 0) { + moveString = appendComment(moveString); + const move = reversedHistory.pop(); + // make TypeScript stop complaining about move being undefined + if (!move) { + break; + } + // if the position started with black to move, start PGN with #. ... + if (!this._history.length && move.color === 'b') { + const prefix = `${this._moveNumber}. ...`; + // is there a comment preceding the first move? + moveString = moveString ? `${moveString} ${prefix}` : prefix; + } + else if (move.color === 'w') { + // store the previous generated move_string if we have one + if (moveString.length) { + moves.push(moveString); + } + moveString = this._moveNumber + '.'; + } + moveString = + moveString + ' ' + this._moveToSan(move, this._moves({ legal: true })); + this._makeMove(move); + } + // are there any other leftover moves? + if (moveString.length) { + moves.push(appendComment(moveString)); + } + // is there a result? + if (typeof this._header.Result !== 'undefined') { + moves.push(this._header.Result); + } + /* + * history should be back to what it was before we started generating PGN, + * so join together moves + */ + if (maxWidth === 0) { + return result.join('') + moves.join(' '); + } + // TODO (jah): huh? + const strip = function () { + if (result.length > 0 && result[result.length - 1] === ' ') { + result.pop(); + return true; + } + return false; + }; + // NB: this does not preserve comment whitespace. + const wrapComment = function (width, move) { + for (const token of move.split(' ')) { + if (!token) { + continue; + } + if (width + token.length > maxWidth) { + while (strip()) { + width--; + } + result.push(newline); + width = 0; + } + result.push(token); + width += token.length; + result.push(' '); + width++; + } + if (strip()) { + width--; + } + return width; + }; + // wrap the PGN output at max_width + let currentWidth = 0; + for (let i = 0; i < moves.length; i++) { + if (currentWidth + moves[i].length > maxWidth) { + if (moves[i].includes('{')) { + currentWidth = wrapComment(currentWidth, moves[i]); + continue; + } + } + // if the current move will push past max_width + if (currentWidth + moves[i].length > maxWidth && i !== 0) { + // don't end the line with whitespace + if (result[result.length - 1] === ' ') { + result.pop(); + } + result.push(newline); + currentWidth = 0; + } + else if (i !== 0) { + result.push(' '); + currentWidth++; + } + result.push(moves[i]); + currentWidth += moves[i].length; + } + return result.join(''); + } + header(...args) { + for (let i = 0; i < args.length; i += 2) { + if (typeof args[i] === 'string' && typeof args[i + 1] === 'string') { + this._header[args[i]] = args[i + 1]; + } + } + return this._header; + } + loadPgn(pgn, { strict = false, newlineChar = '\r?\n', } = {}) { + function mask(str) { + return str.replace(/\\/g, '\\'); + } + function parsePgnHeader(header) { + const headerObj = {}; + const headers = header.split(new RegExp(mask(newlineChar))); + let key = ''; + let value = ''; + for (let i = 0; i < headers.length; i++) { + const regex = /^\s*\[\s*([A-Za-z]+)\s*"(.*)"\s*\]\s*$/; + key = headers[i].replace(regex, '$1'); + value = headers[i].replace(regex, '$2'); + if (key.trim().length > 0) { + headerObj[key] = value; + } + } + return headerObj; + } + // strip whitespace from head/tail of PGN block + pgn = pgn.trim(); + /* + * RegExp to split header. Takes advantage of the fact that header and movetext + * will always have a blank line between them (ie, two newline_char's). Handles + * case where movetext is empty by matching newlineChar until end of string is + * matched - effectively trimming from the end extra newlineChar. + * + * With default newline_char, will equal: + * /^(\[((?:\r?\n)|.)*\])((?:\s*\r?\n){2}|(?:\s*\r?\n)*$)/ + */ + const headerRegex = new RegExp('^(\\[((?:' + + mask(newlineChar) + + ')|.)*\\])' + + '((?:\\s*' + + mask(newlineChar) + + '){2}|(?:\\s*' + + mask(newlineChar) + + ')*$)'); + // If no header given, begin with moves. + const headerRegexResults = headerRegex.exec(pgn); + const headerString = headerRegexResults + ? headerRegexResults.length >= 2 + ? headerRegexResults[1] + : '' + : ''; + // Put the board in the starting position + this.reset(); + // parse PGN header + const headers = parsePgnHeader(headerString); + let fen = ''; + for (const key in headers) { + // check to see user is including fen (possibly with wrong tag case) + if (key.toLowerCase() === 'fen') { + fen = headers[key]; + } + this.header(key, headers[key]); + } + /* + * the permissive parser should attempt to load a fen tag, even if it's the + * wrong case and doesn't include a corresponding [SetUp "1"] tag + */ + if (!strict) { + if (fen) { + this.load(fen, { preserveHeaders: true }); + } + } + else { + /* + * strict parser - load the starting position indicated by [Setup '1'] + * and [FEN position] + */ + if (headers['SetUp'] === '1') { + if (!('FEN' in headers)) { + throw new Error('Invalid PGN: FEN tag must be supplied with SetUp tag'); + } + // don't clear the headers when loading + this.load(headers['FEN'], { preserveHeaders: true }); + } + } + /* + * NB: the regexes below that delete move numbers, recursive annotations, + * and numeric annotation glyphs may also match text in comments. To + * prevent this, we transform comments by hex-encoding them in place and + * decoding them again after the other tokens have been deleted. + * + * While the spec states that PGN files should be ASCII encoded, we use + * {en,de}codeURIComponent here to support arbitrary UTF8 as a convenience + * for modern users + */ + function toHex(s) { + return Array.from(s) + .map(function (c) { + /* + * encodeURI doesn't transform most ASCII characters, so we handle + * these ourselves + */ + return c.charCodeAt(0) < 128 + ? c.charCodeAt(0).toString(16) + : encodeURIComponent(c).replace(/%/g, '').toLowerCase(); + }) + .join(''); + } + function fromHex(s) { + return s.length == 0 + ? '' + : decodeURIComponent('%' + (s.match(/.{1,2}/g) || []).join('%')); + } + const encodeComment = function (s) { + s = s.replace(new RegExp(mask(newlineChar), 'g'), ' '); + return `{${toHex(s.slice(1, s.length - 1))}}`; + }; + const decodeComment = function (s) { + if (s.startsWith('{') && s.endsWith('}')) { + return fromHex(s.slice(1, s.length - 1)); + } + }; + // delete header to get the moves + let ms = pgn + .replace(headerString, '') + .replace( + // encode comments so they don't get deleted below + new RegExp(`({[^}]*})+?|;([^${mask(newlineChar)}]*)`, 'g'), function (_match, bracket, semicolon) { + return bracket !== undefined + ? encodeComment(bracket) + : ' ' + encodeComment(`{${semicolon.slice(1)}}`); + }) + .replace(new RegExp(mask(newlineChar), 'g'), ' '); + // delete recursive annotation variations + const ravRegex = /(\([^()]+\))+?/g; + while (ravRegex.test(ms)) { + ms = ms.replace(ravRegex, ''); + } + // delete move numbers + ms = ms.replace(/\d+\.(\.\.)?/g, ''); + // delete ... indicating black to move + ms = ms.replace(/\.\.\./g, ''); + /* delete numeric annotation glyphs */ + ms = ms.replace(/\$\d+/g, ''); + // trim and get array of moves + let moves = ms.trim().split(new RegExp(/\s+/)); + // delete empty entries + moves = moves.filter((move) => move !== ''); + let result = ''; + for (let halfMove = 0; halfMove < moves.length; halfMove++) { + const comment = decodeComment(moves[halfMove]); + if (comment !== undefined) { + this._comments[this.fen()] = comment; + continue; + } + const move = this._moveFromSan(moves[halfMove], strict); + // invalid move + if (move == null) { + // was the move an end of game marker + if (TERMINATION_MARKERS.indexOf(moves[halfMove]) > -1) { + result = moves[halfMove]; + } + else { + throw new Error(`Invalid move in PGN: ${moves[halfMove]}`); + } + } + else { + // reset the end of game marker if making a valid move + result = ''; + this._makeMove(move); + this._incPositionCount(this.fen()); + } + } + /* + * Per section 8.2.6 of the PGN spec, the Result tag pair must match match + * the termination marker. Only do this when headers are present, but the + * result tag is missing + */ + if (result && Object.keys(this._header).length && !this._header['Result']) { + this.header('Result', result); + } + } + /* + * Convert a move from 0x88 coordinates to Standard Algebraic Notation + * (SAN) + * + * @param {boolean} strict Use the strict SAN parser. It will throw errors + * on overly disambiguated moves (see below): + * + * r1bqkbnr/ppp2ppp/2n5/1B1pP3/4P3/8/PPPP2PP/RNBQK1NR b KQkq - 2 4 + * 4. ... Nge7 is overly disambiguated because the knight on c6 is pinned + * 4. ... Ne7 is technically the valid SAN + */ + _moveToSan(move, moves) { + let output = ''; + if (move.flags & BITS.KSIDE_CASTLE) { + output = 'O-O'; + } + else if (move.flags & BITS.QSIDE_CASTLE) { + output = 'O-O-O'; + } + else { + if (move.piece !== PAWN) { + const disambiguator = getDisambiguator(move, moves); + output += move.piece.toUpperCase() + disambiguator; + } + if (move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE)) { + if (move.piece === PAWN) { + output += algebraic(move.from)[0]; + } + output += 'x'; + } + output += algebraic(move.to); + if (move.promotion) { + output += '=' + move.promotion.toUpperCase(); + } + } + this._makeMove(move); + if (this.isCheck()) { + if (this.isCheckmate()) { + output += '#'; + } + else { + output += '+'; + } + } + this._undoMove(); + return output; + } + // convert a move from Standard Algebraic Notation (SAN) to 0x88 coordinates + _moveFromSan(move, strict = false) { + // strip off any move decorations: e.g Nf3+?! becomes Nf3 + const cleanMove = strippedSan(move); + let pieceType = inferPieceType(cleanMove); + let moves = this._moves({ legal: true, piece: pieceType }); + // strict parser + for (let i = 0, len = moves.length; i < len; i++) { + if (cleanMove === strippedSan(this._moveToSan(moves[i], moves))) { + return moves[i]; + } + } + // the strict parser failed + if (strict) { + return null; + } + let piece = undefined; + let matches = undefined; + let from = undefined; + let to = undefined; + let promotion = undefined; + /* + * The default permissive (non-strict) parser allows the user to parse + * non-standard chess notations. This parser is only run after the strict + * Standard Algebraic Notation (SAN) parser has failed. + * + * When running the permissive parser, we'll run a regex to grab the piece, the + * to/from square, and an optional promotion piece. This regex will + * parse common non-standard notation like: Pe2-e4, Rc1c4, Qf3xf7, + * f7f8q, b1c3 + * + * NOTE: Some positions and moves may be ambiguous when using the permissive + * parser. For example, in this position: 6k1/8/8/B7/8/8/8/BN4K1 w - - 0 1, + * the move b1c3 may be interpreted as Nc3 or B1c3 (a disambiguated bishop + * move). In these cases, the permissive parser will default to the most + * basic interpretation (which is b1c3 parsing to Nc3). + */ + let overlyDisambiguated = false; + matches = cleanMove.match(/([pnbrqkPNBRQK])?([a-h][1-8])x?-?([a-h][1-8])([qrbnQRBN])?/); + if (matches) { + piece = matches[1]; + from = matches[2]; + to = matches[3]; + promotion = matches[4]; + if (from.length == 1) { + overlyDisambiguated = true; + } + } + else { + /* + * The [a-h]?[1-8]? portion of the regex below handles moves that may be + * overly disambiguated (e.g. Nge7 is unnecessary and non-standard when + * there is one legal knight move to e7). In this case, the value of + * 'from' variable will be a rank or file, not a square. + */ + matches = cleanMove.match(/([pnbrqkPNBRQK])?([a-h]?[1-8]?)x?-?([a-h][1-8])([qrbnQRBN])?/); + if (matches) { + piece = matches[1]; + from = matches[2]; + to = matches[3]; + promotion = matches[4]; + if (from.length == 1) { + overlyDisambiguated = true; + } + } + } + pieceType = inferPieceType(cleanMove); + moves = this._moves({ + legal: true, + piece: piece ? piece : pieceType, + }); + if (!to) { + return null; + } + for (let i = 0, len = moves.length; i < len; i++) { + if (!from) { + // if there is no from square, it could be just 'x' missing from a capture + if (cleanMove === + strippedSan(this._moveToSan(moves[i], moves)).replace('x', '')) { + return moves[i]; + } + // hand-compare move properties with the results from our permissive regex + } + else if ((!piece || piece.toLowerCase() == moves[i].piece) && + Ox88[from] == moves[i].from && + Ox88[to] == moves[i].to && + (!promotion || promotion.toLowerCase() == moves[i].promotion)) { + return moves[i]; + } + else if (overlyDisambiguated) { + /* + * SPECIAL CASE: we parsed a move string that may have an unneeded + * rank/file disambiguator (e.g. Nge7). The 'from' variable will + */ + const square = algebraic(moves[i].from); + if ((!piece || piece.toLowerCase() == moves[i].piece) && + Ox88[to] == moves[i].to && + (from == square[0] || from == square[1]) && + (!promotion || promotion.toLowerCase() == moves[i].promotion)) { + return moves[i]; + } + } + } + return null; + } + ascii() { + let s = ' +------------------------+\n'; + for (let i = Ox88.a8; i <= Ox88.h1; i++) { + // display the rank + if (file(i) === 0) { + s += ' ' + '87654321'[rank(i)] + ' |'; + } + if (this._board[i]) { + const piece = this._board[i].type; + const color = this._board[i].color; + const symbol = color === WHITE ? piece.toUpperCase() : piece.toLowerCase(); + s += ' ' + symbol + ' '; + } + else { + s += ' . '; + } + if ((i + 1) & 0x88) { + s += '|\n'; + i += 8; + } + } + s += ' +------------------------+\n'; + s += ' a b c d e f g h'; + return s; + } + perft(depth) { + const moves = this._moves({ legal: false }); + let nodes = 0; + const color = this._turn; + for (let i = 0, len = moves.length; i < len; i++) { + this._makeMove(moves[i]); + if (!this._isKingAttacked(color)) { + if (depth - 1 > 0) { + nodes += this.perft(depth - 1); + } + else { + nodes++; + } + } + this._undoMove(); + } + return nodes; + } + // pretty = external move object + _makePretty(uglyMove) { + const { color, piece, from, to, flags, captured, promotion } = uglyMove; + let prettyFlags = ''; + for (const flag in BITS) { + if (BITS[flag] & flags) { + prettyFlags += FLAGS[flag]; + } + } + const fromAlgebraic = algebraic(from); + const toAlgebraic = algebraic(to); + const move = { + color, + piece, + from: fromAlgebraic, + to: toAlgebraic, + san: this._moveToSan(uglyMove, this._moves({ legal: true })), + flags: prettyFlags, + lan: fromAlgebraic + toAlgebraic, + before: this.fen(), + after: '', + }; + // generate the FEN for the 'after' key + this._makeMove(uglyMove); + move.after = this.fen(); + this._undoMove(); + if (captured) { + move.captured = captured; + } + if (promotion) { + move.promotion = promotion; + move.lan += promotion; + } + return move; + } + turn() { + return this._turn; + } + board() { + const output = []; + let row = []; + for (let i = Ox88.a8; i <= Ox88.h1; i++) { + if (this._board[i] == null) { + row.push(null); + } + else { + row.push({ + square: algebraic(i), + type: this._board[i].type, + color: this._board[i].color, + }); + } + if ((i + 1) & 0x88) { + output.push(row); + row = []; + i += 8; + } + } + return output; + } + squareColor(square) { + if (square in Ox88) { + const sq = Ox88[square]; + return (rank(sq) + file(sq)) % 2 === 0 ? 'light' : 'dark'; + } + return null; + } + history({ verbose = false } = {}) { + const reversedHistory = []; + const moveHistory = []; + while (this._history.length > 0) { + reversedHistory.push(this._undoMove()); + } + while (true) { + const move = reversedHistory.pop(); + if (!move) { + break; + } + if (verbose) { + moveHistory.push(this._makePretty(move)); + } + else { + moveHistory.push(this._moveToSan(move, this._moves())); + } + this._makeMove(move); + } + return moveHistory; + } + /* + * Keeps track of position occurrence counts for the purpose of repetition + * checking. All three methods (`_inc`, `_dec`, and `_get`) trim the + * irrelevent information from the fen, initialising new positions, and + * removing old positions from the record if their counts are reduced to 0. + */ + _getPositionCount(fen) { + const trimmedFen = trimFen(fen); + return this._positionCount[trimmedFen] || 0; + } + _incPositionCount(fen) { + const trimmedFen = trimFen(fen); + if (this._positionCount[trimmedFen] === undefined) { + this._positionCount[trimmedFen] = 0; + } + this._positionCount[trimmedFen] += 1; + } + _decPositionCount(fen) { + const trimmedFen = trimFen(fen); + if (this._positionCount[trimmedFen] === 1) { + delete this._positionCount[trimmedFen]; + } + else { + this._positionCount[trimmedFen] -= 1; + } + } + _pruneComments() { + const reversedHistory = []; + const currentComments = {}; + const copyComment = (fen) => { + if (fen in this._comments) { + currentComments[fen] = this._comments[fen]; + } + }; + while (this._history.length > 0) { + reversedHistory.push(this._undoMove()); + } + copyComment(this.fen()); + while (true) { + const move = reversedHistory.pop(); + if (!move) { + break; + } + this._makeMove(move); + copyComment(this.fen()); + } + this._comments = currentComments; + } + getComment() { + return this._comments[this.fen()]; + } + setComment(comment) { + this._comments[this.fen()] = comment.replace('{', '[').replace('}', ']'); + } + deleteComment() { + const comment = this._comments[this.fen()]; + delete this._comments[this.fen()]; + return comment; + } + getComments() { + this._pruneComments(); + return Object.keys(this._comments).map((fen) => { + return { fen: fen, comment: this._comments[fen] }; + }); + } + deleteComments() { + this._pruneComments(); + return Object.keys(this._comments).map((fen) => { + const comment = this._comments[fen]; + delete this._comments[fen]; + return { fen: fen, comment: comment }; + }); + } + setCastlingRights(color, rights) { + for (const side of [KING, QUEEN]) { + if (rights[side] !== undefined) { + if (rights[side]) { + this._castling[color] |= SIDES[side]; + } + else { + this._castling[color] &= ~SIDES[side]; + } + } + } + this._updateCastlingRights(); + const result = this.getCastlingRights(color); + return ((rights[KING] === undefined || rights[KING] === result[KING]) && + (rights[QUEEN] === undefined || rights[QUEEN] === result[QUEEN])); + } + getCastlingRights(color) { + return { + [KING]: (this._castling[color] & SIDES[KING]) !== 0, + [QUEEN]: (this._castling[color] & SIDES[QUEEN]) !== 0, + }; + } + moveNumber() { + return this._moveNumber; + } +} diff --git a/packages/chess-app/src/index.css b/packages/chess-app/src/index.css new file mode 100644 index 000000000..e2ac8d522 --- /dev/null +++ b/packages/chess-app/src/index.css @@ -0,0 +1,14 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", + "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; +} diff --git a/packages/chess-app/src/main.tsx b/packages/chess-app/src/main.tsx new file mode 100644 index 000000000..6f4ac9bcc --- /dev/null +++ b/packages/chess-app/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App.tsx' +import './index.css' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/packages/chess-app/src/setupTests.ts b/packages/chess-app/src/setupTests.ts new file mode 100644 index 000000000..3f925457f --- /dev/null +++ b/packages/chess-app/src/setupTests.ts @@ -0,0 +1,3 @@ +import * as matchers from "@testing-library/jest-dom/matchers"; + +expect.extend(matchers); diff --git a/packages/chess-app/src/types/common.ts b/packages/chess-app/src/types/common.ts new file mode 100644 index 000000000..58276575f --- /dev/null +++ b/packages/chess-app/src/types/common.ts @@ -0,0 +1,2 @@ +export type Chain = "LTC" | "BTC" | "DOGE" | "PEPE" +export type Network = "testnet" | "mainnet" | "regtest" diff --git a/packages/chess-app/src/vite-env.d.ts b/packages/chess-app/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/packages/chess-app/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/chess-app/tailwind.config.js b/packages/chess-app/tailwind.config.js new file mode 100644 index 000000000..c53aed771 --- /dev/null +++ b/packages/chess-app/tailwind.config.js @@ -0,0 +1,20 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./src/**/*.{js,jsx,ts,tsx}", + "./node_modules/@bitcoin-computer/components/built/**/*.{js,jsx,ts,tsx}", + "../components/built/**/*.{js,jsx,ts,tsx}" + ], + darkMode: "media", + theme: { + extend: { + colors: { + "blue-1": "#000F38", + "blue-2": "#002A99", + "blue-3": "#0046FF", + "blue-4": "#A7BFFF", + }, + }, + }, + plugins: [], +} diff --git a/packages/chess-app/tsconfig.build.json b/packages/chess-app/tsconfig.build.json new file mode 100644 index 000000000..b8f46b428 --- /dev/null +++ b/packages/chess-app/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./build-contract", + "noEmit": false, + "declaration": false, + "allowImportingTsExtensions": false + }, + "include": ["src/contracts/chess-module.ts"] +} diff --git a/packages/chess-app/tsconfig.json b/packages/chess-app/tsconfig.json new file mode 100644 index 000000000..21d8316bc --- /dev/null +++ b/packages/chess-app/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": false, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "types": ["vitest/globals", "@testing-library/jest-dom"], + + /* Bundler mode */ + "moduleResolution": "node", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/packages/chess-app/vite.config.ts b/packages/chess-app/vite.config.ts new file mode 100644 index 000000000..d334a417a --- /dev/null +++ b/packages/chess-app/vite.config.ts @@ -0,0 +1,36 @@ +/// +/// + +import { defineConfig, loadEnv } from "vite" +import react from "@vitejs/plugin-react" +import path from "path" +import fs from "fs" + +// https://vitejs.dev/config/ +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), "") + + const primaryPath = path.resolve(__dirname, "./node_modules/@bitcoin-computer/lib/dist/bc-lib.browser.min.mjs"); + const pathInWorkspace = path.resolve(__dirname, "../lib/dist/bc-lib.browser.min.mjs"); + const filePath = fs.existsSync(primaryPath) ? primaryPath : pathInWorkspace; + + console.log(filePath); // This will log the path based on the file's existence. + + return { + plugins: [react()], + resolve: { + alias: { + // Define the alias pointing to the specific entry point in node_modules + "@bitcoin-computer/lib": path.resolve(__dirname, filePath) + } + }, + server: { + port: parseInt(env.VITE_PORT) + }, + test: { + globals: true, + environment: "jsdom", + setupFiles: ["./src/setupTests.ts"] + } + } +}) diff --git a/scripts/check-obfuscation.sh b/scripts/check-obfuscation.sh index 4e971cdec..73a5bf405 100755 --- a/scripts/check-obfuscation.sh +++ b/scripts/check-obfuscation.sh @@ -1,7 +1,7 @@ #!/bin/bash # List of folders to skip -skip_folders=("vite-template" "nft-vite" "explorer-vite" "wallet-vite" "chat-vite") +skip_folders=("vite-template" "nft-vite" "explorer-vite" "wallet-vite" "chat-vite", "chess-app") # Check if the obfuscation was successful on all dist folders msg="Checking obfuscation ..." From e0d3cc1135a8a62af8f31eb6c2be934ef131618a Mon Sep 17 00:00:00 2001 From: jonty007 Date: Mon, 4 Nov 2024 16:38:10 +0530 Subject: [PATCH 2/5] updated readme --- packages/chess-app/README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/chess-app/README.md b/packages/chess-app/README.md index 34cc2597d..e491a0b02 100644 --- a/packages/chess-app/README.md +++ b/packages/chess-app/README.md @@ -1,7 +1,7 @@
    -

    TBC CRA Template

    +

    TBC Chess

    - A template for Create React App with TypeScript and the Bitcoin Computer + A decentralised Chess App based on Bitcoin Computer.
    website · docs

    @@ -40,7 +40,7 @@ To start the application run the command below and open [http://localhost:3000]( ```bash # Move to the package -cd packages/cra-template +cd packages/chess-app # Install the dependencies npm install @@ -48,8 +48,13 @@ npm install # Use the default environment variables cp .env.example .env +# Deploy the contracts +npm run deploy + +# Copy the mod-specs to env + # Start the app -npm run start +npm run dev ``` @@ -63,6 +68,7 @@ Have a look at the [docs](https://docs.bitcoincomputer.io/) for the Bitcoin Comp If you have any questions, please let us know on Telegram, Twitter, or by email clemens@bitcoincomputer.io. ## Development Status + See [here](https://github.com/bitcoin-computer/monorepo/tree/main/packages/lib#development-status). ## Price From 18dcd79b4881b6acff8213fb1454ea78ea20f520 Mon Sep 17 00:00:00 2001 From: jonty007 Date: Mon, 4 Nov 2024 17:09:57 +0530 Subject: [PATCH 3/5] sync chess game fix --- packages/chess-app/src/components/ChessBoard.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/chess-app/src/components/ChessBoard.tsx b/packages/chess-app/src/components/ChessBoard.tsx index 92ecb98dd..8cc7500a1 100644 --- a/packages/chess-app/src/components/ChessBoard.tsx +++ b/packages/chess-app/src/components/ChessBoard.tsx @@ -150,6 +150,7 @@ export function ChessBoard() { const chessMovePromise = chessContract.move(result.san) as unknown as Promise chessMovePromise.catch((err: any) => { showSnackBar(err.message, false) + setSkipSync(false) syncChessContract() }) setSkipSync(true) From 5170c27d05b83818bfd277f14fa8968e70d35c18 Mon Sep 17 00:00:00 2001 From: jonty007 Date: Mon, 4 Nov 2024 17:30:43 +0530 Subject: [PATCH 4/5] UI Fixes --- packages/chess-app/src/components/CreateNewGame.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/chess-app/src/components/CreateNewGame.tsx b/packages/chess-app/src/components/CreateNewGame.tsx index a340fd576..2915611fe 100644 --- a/packages/chess-app/src/components/CreateNewGame.tsx +++ b/packages/chess-app/src/components/CreateNewGame.tsx @@ -66,7 +66,7 @@ function MintForm(props: { }) { const { computer, setSuccessRev, setErrorMsg } = props const [name, setName] = useState("") - const [color, setColor] = useState("") + const [color, setColor] = useState("black") const [secondPlayerPublicKey, setSecondPlayerPublicKey] = useState("") const [secondPlayerUserName, setSecondPlayerUserName] = useState("") const { showLoader } = UtilsContext.useUtilsComponents() @@ -98,10 +98,10 @@ function MintForm(props: { } return ( <> -
    +

    Let's Play

    -

    Start a new game and invite your friend.

    +

    Start a new game and invite your friend.

    diff --git a/packages/chess-app/src/components/Gallery.tsx b/packages/chess-app/src/components/Gallery.tsx index 89d40498b..b954b1e25 100644 --- a/packages/chess-app/src/components/Gallery.tsx +++ b/packages/chess-app/src/components/Gallery.tsx @@ -35,7 +35,7 @@ function GameCard({ chessGame }: { chessGame: ChessGame }) { : "border-gray-200 dark:border-gray-700" }`} > -
    +

    {getGameState(c)}