UNPKG

svelte4-chess

Version:

Fully playable chess component for Svelte 4. Powered by Chess.js logic, Chessground chessboard and optionally Stockfish chess AI.

319 lines (318 loc) 11.3 kB
import { Chess as ChessJS, SQUARES } from "chess.js"; export class Api { cg; stateChangeCallback; promotionCallback; moveCallback; gameOverCallback; _orientation; ownColor; engine; chessJS; gameIsOver = false; initialised = false; constructor(cg, fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", stateChangeCallback = (api) => { }, // called when the game state (not visuals) changes promotionCallback = async (sq) => "q", // called before promotion moveCallback = (m) => { }, // called after move gameOverCallback = (go) => { }, // called after game-ending move _orientation = "w", ownColor = "white", engine = undefined) { this.cg = cg; this.stateChangeCallback = stateChangeCallback; this.promotionCallback = promotionCallback; this.moveCallback = moveCallback; this.gameOverCallback = gameOverCallback; this._orientation = _orientation; this.ownColor = ownColor; this.engine = engine; this.cg.set({ fen, orientation: Api._colorToCgColor(_orientation), movable: { free: false }, }); this.chessJS = new ChessJS(fen); } setOwnColor(color) { this.ownColor = color; } async init() { if (this.engine) { await this.engine.init(); this.load(this.chessJS.fen()); if (this._enginePlaysNextMove()) { this.playEngineMove(); } } else { this.load(this.chessJS.fen()); } this.initialised = true; } // Load FEN. Throws exception on invalid FEN. load(fen) { let engineStopSearchPromise; if (this.initialised && this.engine?.isSearching()) engineStopSearchPromise = this.engine.stopSearch(); this.chessJS.load(fen); this._checkForGameOver(); this.cg.set({ animation: { enabled: false } }); const cgColor = Api._colorToCgColor(this.chessJS.turn()); const enginePlaysNextMove = this._enginePlaysNextMove(); this.cg.set({ fen: fen, turnColor: cgColor, check: this.chessJS.inCheck(), lastMove: undefined, selected: undefined, movable: { free: false, color: cgColor, dests: enginePlaysNextMove ? new Map() : this.possibleMovesDests(), events: { after: (orig, dest) => { this._chessgroundMoveCallback(orig, dest); }, }, }, }); this.cg.set({ animation: { enabled: true } }); if (this.initialised && enginePlaysNextMove) { // Play immediate engine move, but wait until stopSearch has finished if (engineStopSearchPromise) { engineStopSearchPromise.then(() => { this.playEngineMove(); }); } else { this.playEngineMove(); } } this.stateChangeCallback(this); } /* * Making a move */ // called after a move is played on Chessground async _chessgroundMoveCallback(orig, dest) { if (orig === "a0" || dest === "a0") { // the Chessground square type (Key) includes a0 throw Error("invalid square"); } if (this.engine && this.engine.isSearching()) this.engine.stopSearch(); let cjsMove; if (this._moveIsPromotion(orig, dest)) { const promotion = await this.promotionCallback(dest); cjsMove = this.chessJS.move({ from: orig, to: dest, promotion }); } else { cjsMove = this.chessJS.move({ from: orig, to: dest }); } const move = Api._cjsMoveToMove(cjsMove); this._postMoveAdmin(move); } _moveIsPromotion(orig, dest) { return (this.chessJS.get(orig).type === "p" && (dest.charAt(1) == "1" || dest.charAt(1) == "8")); } set(config) { this.cg.set(config); } // Make a move programmatically // argument is either a short algebraic notation (SAN) string // or an object with from/to/promotion (see chess.js move()) move(moveSanOrObj) { if (!this.initialised) throw new Error("Called move before initialisation finished."); if (this.gameIsOver) throw new Error("Invalid move: Game is over."); if (this.engine && this.engine.isSearching()) this.engine.stopSearch(); const cjsMove = this.chessJS.move(moveSanOrObj); // throws on illegal move const move = Api._cjsMoveToMove(cjsMove); this.cg.move(move.from, move.to); this.cg.set({ turnColor: Api._colorToCgColor(this.chessJS.turn()) }); this._postMoveAdmin(move); } // Make a move programmatically from long algebraic notation (LAN) string, // as returned by UCI engines. moveLan(moveLan) { const from = moveLan.slice(0, 2); const to = moveLan.slice(2, 4); const promotion = moveLan.charAt(4) || undefined; this.move({ from, to, promotion }); } // Called after a move (chess.js or chessground) to: // - update chess-logic details Chessground doesn't handle // - dispatch events // - play engine move _postMoveAdmin(move) { const enginePlaysNextMove = this._enginePlaysNextMove(); // reload FEN after en-passant or promotion. TODO make promotion smoother if (move.flags.includes("e") || move.flags.includes("p")) { this.cg.set({ fen: this.chessJS.fen() }); } // highlight king if in check if (move.check) { this.cg.set({ check: true }); } // dispatch move event this.moveCallback(move); // dispatch gameOver event if applicable this._checkForGameOver(); // set legal moves if (enginePlaysNextMove) { this.cg.set({ movable: { dests: new Map() } }); // no legal moves } else { this._updateChessgroundWithPossibleMoves(); } // update state props this.stateChangeCallback(this); // engine move if (!this.gameIsOver && enginePlaysNextMove) { this.playEngineMove(); } } async playEngineMove() { if (!this.engine) throw new Error("playEngineMove called without initialised engine"); return this.engine.getMove(this.chessJS.fen()).then((lan) => { this.moveLan(lan); }); } _enginePlaysNextMove() { return (this.engine && (this.engine.getColor() === "both" || this.engine.getColor() === this.chessJS.turn())); } _updateChessgroundWithPossibleMoves() { const cgColor = Api._colorToCgColor(this.chessJS.turn()); this.cg.set({ turnColor: cgColor, movable: { color: this.ownColor, dests: this.ownColor === cgColor ? this.possibleMovesDests() : new Map(), free: false, }, }); } _checkForGameOver() { if (this.chessJS.isCheckmate()) { const result = this.chessJS.turn() == "w" ? 0 : 1; this.gameOverCallback({ reason: "checkmate", result }); this.gameIsOver = true; } else if (this.chessJS.isStalemate()) { this.gameOverCallback({ reason: "stalemate", result: 0.5 }); this.gameIsOver = true; } else if (this.chessJS.isInsufficientMaterial()) { this.gameOverCallback({ reason: "insufficient material", result: 0.5 }); this.gameIsOver = true; } else if (this.chessJS.isThreefoldRepetition()) { this.gameOverCallback({ reason: "repetition", result: 0.5 }); this.gameIsOver = true; } else if (this.chessJS.isDraw()) { // use isDraw until chess.js exposes isFiftyMoveDraw() this.gameOverCallback({ reason: "fifty-move rule", result: 0.5 }); this.gameIsOver = true; } else { this.gameIsOver = false; } } /* * */ // Find all legal moves in chessground "dests" format possibleMovesDests() { const dests = new Map(); if (!this.gameIsOver) { SQUARES.forEach((s) => { const ms = this.chessJS.moves({ square: s, verbose: true }); if (ms.length) dests.set(s, ms.map((m) => m.to)); }); } return dests; } // Reset board to the starting position reset() { this.load("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); } // Undo last move undo() { const cjsMove = this.chessJS.undo(); const move = cjsMove ? Api._cjsMoveToMove(cjsMove) : null; const turnColor = Api._colorToCgColor(this.chessJS.turn()); this.cg.set({ fen: this.chessJS.fen(), check: this.chessJS.inCheck() ? turnColor : undefined, turnColor: turnColor, lastMove: undefined, }); this.gameIsOver = false; this._updateChessgroundWithPossibleMoves(); this.stateChangeCallback(this); return move; } // Board orientation toggleOrientation() { this._orientation = this._orientation === "w" ? "b" : "w"; this.cg.set({ orientation: Api._colorToCgColor(this._orientation), }); this.stateChangeCallback(this); } orientation() { return this._orientation; } // Check if game is over (checkmate, stalemate, repetition, insufficient material, fifty-move rule) isGameOver() { return this.gameIsOver; } /* * Methods passed through to chess.js */ fen() { return this.chessJS.fen(); } turn() { return this.chessJS.turn(); } moveNumber() { return this.chessJS.moveNumber(); } inCheck() { return this.chessJS.inCheck(); } history({ verbose = false } = {}) { if (verbose) { return this.chessJS.history({ verbose }).map(Api._cjsMoveToMove); } else { return this.chessJS.history({ verbose }); } } board() { return this.chessJS.board(); } // Convert between chess.js color (w/b) and chessground color (white/black). // Chess.js color is always used internally. static _colorToCgColor(chessjsColor) { return chessjsColor === "w" ? "white" : "black"; } static _cgColorToColor(chessgroundColor) { return chessgroundColor === "white" ? "w" : "b"; } // Convert chess.js move (CjsMove) to svelte-chess Move. // Only difference is check:boolean and checkmate:boolean in the latter. static _cjsMoveToMove(cjsMove) { const lastSanChar = cjsMove.san.slice(-1); const checkmate = lastSanChar === "#"; const check = lastSanChar === "+" || checkmate; return { ...cjsMove, check, checkmate }; } }