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
JavaScript
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 };
}
}