UNPKG

chess

Version:

An algebraic notation driven chess engine that can validate board position and produce a list of viable moves (notated).

403 lines (337 loc) 9.79 kB
/* eslint sort-imports: 0 */ import { EventEmitter } from 'events'; import { Board } from './board.js'; import { Game } from './game.js'; import { GameValidation } from './gameValidation.js'; import { Piece } from './piece.js'; import { PieceType } from './piece.js'; import { SideType } from './piece.js'; // private methods function getNotationPrefix (src, dest, movesForPiece) { let containsDest = (squares) => { let n = 0; for (; n < squares.length; n++) { if (squares[n] === dest) { return true; } } return false; }, file = '', fileHash = {}, i = 0, prefix = src.piece.notation, rank = 0, rankHash = {}; for (; i < movesForPiece.length; i++) { if (containsDest(movesForPiece[i].squares)) { file = movesForPiece[i].src.file; rank = movesForPiece[i].src.rank; fileHash[file] = (typeof fileHash[file] !== 'undefined' ? fileHash[file] + 1 : 1); rankHash[rank] = (typeof rankHash[rank] !== 'undefined' ? rankHash[rank] + 1 : 1); } } if (Object.keys(fileHash).length > 1) { prefix += src.file; } if (Object.keys(rankHash).length > Object.keys(fileHash).length) { prefix += src.rank; } return prefix; } function getValidMovesByPieceType (pieceType, validMoves) { let byPiece = [], i = 0; for (; i < validMoves.length; i++) { if (validMoves[i].src.piece.type === pieceType) { byPiece.push(validMoves[i]); } } return byPiece; } function notate (validMoves, gameClient) { let algebraicNotation = {}, i = 0, isPromotion = false, movesForPiece = [], n = 0, p = null, prefix = '', sq = null, src = null, suffix = ''; // iterate through each starting squares valid moves for (; i < validMoves.length; i++) { src = validMoves[i].src; p = src.piece; // iterate each potential move and build prefix and suffix for notation for (n = 0; n < validMoves[i].squares.length; n++) { prefix = ''; sq = validMoves[i].squares[n]; // set suffix for notation suffix = (sq.piece ? 'x' : '') + sq.file + sq.rank; // check for potential promotion /* eslint no-magic-numbers: 0 */ isPromotion = (sq.rank === 8 || sq.rank === 1) && p.type === PieceType.Pawn; // squares with pawns if (sq.piece && p.type === PieceType.Pawn) { prefix = src.file; } // en passant // fix for #53 if (p.type === PieceType.Pawn && src.file !== sq.file && !sq.piece) { prefix = [src.file, 'x'].join(''); } // squares with Bishop, Knight, Queen or Rook pieces if (p.type === PieceType.Bishop || p.type === PieceType.Knight || p.type === PieceType.Queen || p.type === PieceType.Rook) { // if there is more than 1 of the specified piece on the board, // can more than 1 land on the specified square? movesForPiece = getValidMovesByPieceType(p.type, validMoves); if (movesForPiece.length > 1) { prefix = getNotationPrefix(src, sq, movesForPiece); } else { prefix = src.piece.notation; } } // squares with a King piece if (p.type === PieceType.King) { // look for castle left and castle right if (src.file === 'e' && sq.file === 'g') { // fix for issue #13 - if PGN is specified should be letters, not numbers prefix = gameClient.PGN ? 'O-O' : '0-0'; suffix = ''; } else if (src.file === 'e' && sq.file === 'c') { // fix for issue #13 - if PGN is specified should be letters, not numbers prefix = gameClient.PGN ? 'O-O-O' : '0-0-0'; suffix = ''; } else { prefix = src.piece.notation; } } // set the notation if (isPromotion) { // Rook promotion algebraicNotation[prefix + suffix + 'R'] = { dest : sq, src }; // Knight promotion algebraicNotation[prefix + suffix + 'N'] = { dest : sq, src }; // Bishop promotion algebraicNotation[prefix + suffix + 'B'] = { dest : sq, src }; // Queen promotion algebraicNotation[prefix + suffix + 'Q'] = { dest : sq, src }; } else { algebraicNotation[prefix + suffix] = { dest : sq, src }; } } } return algebraicNotation; } function parseNotation (notation) { let captureRegex = /^[a-h]x[a-h][1-8]$/, parseDest = ''; // try and parse the notation parseDest = notation.substring(notation.length - 2); if (notation.length > 2) { // check for preceding pawn capture style notation (i.e. a-h x) if (captureRegex.test(notation)) { return parseDest; } return notation.charAt(0) + parseDest; } return ''; } function updateGameClient (gameClient) { 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.notatedMoves = notate(result.validMoves, gameClient); gameClient.validMoves = result.validMoves; }); } export class AlgebraicGameClient extends EventEmitter { constructor (game, opts) { super(); this.game = game; this.isCheck = false; this.isCheckmate = false; this.isRepetition = false; this.isStalemate = false; this.notatedMoves = {}; // for issue #13, adding options allowing consumers to specify // PGN (Portable Game Notation)... essentially, this makes castle moves // appear as capital letter O rather than the number 0 this.PGN = (opts && typeof opts.PGN === 'boolean') ? opts.PGN : false; 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)); }); let self = this; this.on('undo', () => { // force an update self.getStatus(true); }); } static create (opts) { let game = Game.create(), gameClient = new AlgebraicGameClient(game, opts); updateGameClient(gameClient); return gameClient; } static fromFEN (fen, opts) { if (!fen || typeof fen !== 'string') { throw new Error('FEN must be a non-empty string'); } // create a standard game so listeners/history are wired let game = Game.create(), loadedBoard = Board.load(fen); // copy piece placement from loaded board to preserve board indexing and listeners for (let i = 0; i < game.board.squares.length; i++) { game.board.squares[i].piece = null; } for (let i = 0; i < loadedBoard.squares.length; i++) { let sq = loadedBoard.squares[i]; if (sq.piece) { let target = game.board.getSquare(sq.file, sq.rank); target.piece = sq.piece; } } game.board.lastMovedPiece = null; // derive side to move from FEN (default to White if missing) let parts = fen.split(' '); let active = parts[1] || 'w'; let baseSide = active === 'b' ? SideType.Black : SideType.White; // override getCurrentSide to honor FEN and alternate thereafter let whiteFirst = baseSide === SideType.White; /* eslint no-param-reassign: 0 */ game.getCurrentSide = function getCurrentSideAfterFENLoad () { return (this.moveHistory.length % 2 === 0) ? (whiteFirst ? SideType.White : SideType.Black) : (whiteFirst ? SideType.Black : SideType.White); }; const gameClient = new AlgebraicGameClient(game, opts); 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, notatedMoves : this.notatedMoves }; } getFen () { return this.game.board.getFen(); } getCaptureHistory () { return this.game.captureHistory; } move (notation, isFuzzy) { let move = null, notationRegex = /^[BKQNR]?[a-h]?[1-8]?[x-]?[a-h][1-8][+#]?$/, p = null, promo = '', side = this.game.getCurrentSide(); if (notation && typeof notation === 'string') { // clean notation of extra or alternate chars notation = notation .replace(/\!/g, '') .replace(/\+/g, '') .replace(/\#/g, '') .replace(/\=/g, '') .replace(/\\/g, ''); // fix for issue #13 - if PGN is specified, should be letters not numbers if (this.PGN) { notation = notation.replace(/0/g, 'O'); } else { notation = notation.replace(/O/g, '0'); } // check for pawn promotion if (notation.charAt(notation.length - 1).match(/[BNQR]/)) { promo = notation.charAt(notation.length - 1); } // use it directly or attempt to parse it if not found if (this.notatedMoves[notation]) { move = this.game.board.move( this.notatedMoves[notation].src, this.notatedMoves[notation].dest, notation); } else if (notation.match(notationRegex) && notation.length > 1 && !isFuzzy) { return this.move(parseNotation(notation), true); } else if (isFuzzy) { throw new Error(`Invalid move (${notation})`); } if (move) { // apply pawn promotion if (promo) { switch (promo) { case 'B': p = Piece.createBishop(side); break; case 'N': p = Piece.createKnight(side); break; case 'Q': p = Piece.createQueen(side); break; case 'R': p = Piece.createRook(side); break; default: p = Piece.createPawn(side); } if (p) { this.game.board.promote(move.move.postSquare, p); } } updateGameClient(this); return move; } } throw new Error(`Notation is invalid (${notation})`); } } export default { AlgebraicGameClient };