chess
Version:
An algebraic notation driven chess engine that can validate board position and produce a list of viable moves (notated).
239 lines (195 loc) • 5.9 kB
JavaScript
/* eslint sort-imports: 0 */
import { EventEmitter } from 'events';
import { Game } from './game.js';
import { GameValidation } from './gameValidation.js';
import { Piece } from './piece.js';
import { PieceType } from './piece.js';
// private helpers
function parseUCI(uci) {
if (typeof uci !== 'string') {
return null;
}
// UCI format: e2e4, e7e8q (promotion), case-insensitive for promo
let
formatRegex = /^([a-h][1-8])([a-h][1-8])([qrbnQRBN])?$/,
uciMove = uci.trim().match(formatRegex);
if (!uciMove) {
return null;
}
let
dest = { file: uciMove[2][0], rank: Number(uciMove[2][1]) },
promo = uciMove[3] ? uciMove[3].toUpperCase() : '',
src = { file: uciMove[1][0], rank: Number(uciMove[1][1]) };
return { dest, promo, src };
}
function updateGameClient(gameClient) {
return gameClient.validation.start((err, result) => {
if (err) {
throw new Error(err);
}
gameClient.isCheck = result.isCheck;
gameClient.isCheckmate = result.isCheckmate;
gameClient.isRepetition = result.isRepetition;
gameClient.isStalemate = result.isStalemate;
gameClient.validMoves = result.validMoves;
gameClient.uciMoves = notateUCI(result.validMoves);
});
}
function notateUCI(validMoves) {
let
i = 0,
isPromotion = false,
notation = {};
// iterate through all valid moves and create UCI notation
for (; i < validMoves.length; i++) {
let
p = validMoves[i].src.piece,
src = validMoves[i].src;
// reset inner index for each piece's move list
for (let n = 0; n < validMoves[i].squares.length; n++) {
// get the destination square for this move
let sq = validMoves[i].squares[n];
// base notation
let base = `${src.file}${src.rank}${sq.file}${sq.rank}`;
// check for potential promotion
/* eslint no-magic-numbers: 0 */
isPromotion =
(sq.rank === 8 || sq.rank === 1) &&
p.type === PieceType.Pawn;
if (isPromotion) {
// add all promotion options
['q', 'r', 'b', 'n'].forEach((promo) => {
notation[`${base}${promo}`] = {
dest: sq,
src
};
});
continue
}
// regular move
notation[base] = {
dest: sq,
src
};
}
}
return notation;
}
export class UCIGameClient extends EventEmitter {
constructor(game) {
super();
this.game = game;
this.isCheck = false;
this.isCheckmate = false;
this.isRepetition = false;
this.isStalemate = false;
this.uciMoves = {};
this.validMoves = [];
this.validation = GameValidation.create(this.game);
// bubble the game and board events
['check', 'checkmate'].forEach((ev) => {
this.game.on(ev, (data) => this.emit(ev, data));
});
['capture', 'castle', 'enPassant', 'move', 'promote', 'undo'].forEach((ev) => {
this.game.board.on(ev, (data) => this.emit(ev, data));
});
const self = this;
this.on('undo', () => {
// force an update
self.getStatus(true);
});
}
static create() {
let
game = Game.create(),
gameClient = new UCIGameClient(game);
updateGameClient(gameClient);
return gameClient;
}
getStatus(forceUpdate) {
if (forceUpdate) {
updateGameClient(this);
}
return {
board: this.game.board,
isCheck: this.isCheck,
isCheckmate: this.isCheckmate,
isRepetition: this.isRepetition,
isStalemate: this.isStalemate,
uciMoves: this.uciMoves
};
}
getCaptureHistory() {
return this.game.captureHistory;
}
move(uci) {
let
canonical = null,
dest = null,
move = null,
parsed = parseUCI(uci),
promo = null,
requiresPromotion = false,
side = null,
src = null,
srcSquare = null;
if (!parsed) {
throw new Error(`UCI is invalid (${uci})`);
}
// destructure the parsed UCI move
({ src, dest, promo } = parsed);
// normalize UCI key to compare with generated map
canonical = promo
? `${src.file}${src.rank}${dest.file}${dest.rank}${promo.toLowerCase()}`
: `${src.file}${src.rank}${dest.file}${dest.rank}`;
// ensure move exactly matches a generated UCI move
if (!this.uciMoves || !this.uciMoves[canonical]) {
throw new Error(`Move is invalid (${uci})`);
}
// determine the current side
side = this.game.getCurrentSide();
// additional safety: enforce promotion semantics
srcSquare = this.game.board.getSquare(src.file, src.rank);
requiresPromotion =
srcSquare && srcSquare.piece && srcSquare.piece.type === PieceType.Pawn &&
(dest.rank === 8 || dest.rank === 1);
if (requiresPromotion && !promo) {
throw new Error(`Promotion required for move (${uci})`);
}
if (promo && !requiresPromotion) {
throw new Error(`Promotion flag not allowed for move (${uci})`);
}
// make the move
move = this.game.board.move(`${src.file}${src.rank}`, `${dest.file}${dest.rank}`);
if (move) {
// apply pawn promotion if applicable (already validated above)
if (promo) {
let piece;
switch (promo) {
case 'B':
piece = Piece.createBishop(side);
break;
case 'N':
piece = Piece.createKnight(side);
break;
case 'Q':
piece = Piece.createQueen(side);
break;
case 'R':
piece = Piece.createRook(side);
break;
default:
piece = null;
break;
}
if (piece) {
this.game.board.promote(move.move.postSquare, piece);
}
}
updateGameClient(this);
return move;
}
throw new Error(`Move is invalid (${uci})`);
}
}
export default { UCIGameClient };