UNPKG

chess

Version:

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

405 lines (344 loc) 9.6 kB
/** The Board is the representation of the current position of the pieces on the squares it contains. */ import { Piece, PieceType, SideType } from './piece.js'; import { EventEmitter } from 'events'; import { Square } from './square.js'; // types export var NeighborType = { Above : { offset : 8 }, AboveLeft : { offset : 7 }, AboveRight : { offset : 9 }, Below : { offset : -8 }, BelowLeft : { offset : -9 }, BelowRight : { offset : -7 }, KnightAboveLeft : { offset : 15 }, KnightAboveRight : { offset : 17 }, KnightBelowLeft : { offset : -17 }, KnightBelowRight : { offset : -15 }, KnightLeftAbove : { offset : 6 }, KnightLeftBelow : { offset : -10 }, KnightRightAbove : { offset : 10 }, KnightRightBelow : { offset : -6 }, Left : { offset : -1 }, Right : { offset : 1 } }; // ctor export class Board extends EventEmitter { constructor (squares) { super(); this.squares = squares; } static create () { let b = new Board([]), f = 0, i = 0, r = 0, sq = null; /* eslint no-magic-numbers:0 */ for (i = 0; i < 64; i++) { f = Math.floor(i % 8); r = Math.floor(i / 8) + 1; sq = Square.create('abcdefgh'[f], r); b.squares.push(sq); if (r === 1 || r === 8) { // Named pieces if (f === 0 || f === 7) { // Rookage sq.piece = Piece.createRook( r === 1 ? SideType.White : SideType.Black ); } else if (f === 1 || f === 6) { // Knights sq.piece = Piece.createKnight( r === 1 ? SideType.White : SideType.Black ); } else if (f === 2 || f === 5) { // Bish's sq.piece = Piece.createBishop( r === 1 ? SideType.White : SideType.Black ); } else if (f === 3) { sq.piece = Piece.createQueen( r === 1 ? SideType.White : SideType.Black ); } else { sq.piece = Piece.createKing( r === 1 ? SideType.White : SideType.Black ); } } else if (r === 2 || r === 7) { // Pawns sq.piece = Piece.createPawn( r === 2 ? SideType.White : SideType.Black ); } } return b; } static load (fen) { /* eslint sort-keys: 0 */ const pieces = { b: { arg: SideType.Black, method: 'createBishop' }, B: { arg: SideType.White, method: 'createBishop' }, k: { arg: SideType.Black, method: 'createKing' }, K: { arg: SideType.White, method: 'createKing' }, n: { arg: SideType.Black, method: 'createKnight' }, N: { arg: SideType.White, method: 'createKnight' }, p: { arg: SideType.Black, method: 'createPawn' }, P: { arg: SideType.White, method: 'createPawn' }, q: { arg: SideType.Black, method: 'createQueen' }, Q: { arg: SideType.White, method: 'createQueen' }, r: { arg: SideType.Black, method: 'createRook' }, R: { arg: SideType.White, method: 'createRook' } }; const [board/* , turn, castling, enPassant, halfs, moves */] = fen.split(' '); const lines = board.split('/') .map((line, rank) => { const arr = line.split(''); let file = 0; return arr.reduce((acc, cur) => { if (!isNaN(Number(cur))) { for (let i = 0; i < Number(cur); i += 1) { acc.push(Square.create('abcdefgh'[file], 8 - rank)); file = file < 7 ? file + 1 : 0; } } else { const square = Square.create('abcdefgh'[file], 8 - rank); square.piece = Piece[pieces[cur].method](pieces[cur].arg); acc.push(square); file = file < 7 ? file + 1 : 0; } return acc; }, []); }); return new Board(lines.reduce((acc, cur) => { acc.push(...cur); return acc; }, [])); } getFen () { const fen = []; const squares = this.squares .reduce((acc, cur, idx) => { const outerIdx = parseInt(idx / 8, 10); acc[outerIdx] = acc[outerIdx] || []; acc[outerIdx].push(cur); return acc; }, []) .flatMap((row) => row.reverse()) .reverse(); for (let i = 0; i < squares.length; i += 1) { const square = squares[i]; if (square.file === 'a' && square.rank < 8) { fen.push('/'); } if (square.piece) { const transform = `to${square.piece.side.name === 'white' ? 'Upp' : 'Low'}erCase`; fen.push((square.piece.notation || 'p')[transform]()); } else { if (isNaN(Number(fen[fen.length - 1]))) { fen.push(1); } else { fen[fen.length - 1] += 1; } } } return fen.join(''); } getNeighborSquare (sq, n) { if (sq && n) { // validate boundaries of board if (sq.file === 'a' && (n === NeighborType.AboveLeft || n === NeighborType.BelowLeft || n === NeighborType.Left)) { return null; } if (sq.file === 'h' && (n === NeighborType.AboveRight || n === NeighborType.BelowRight || n === NeighborType.Right)) { return null; } if (sq.rank === 1 && (n === NeighborType.Below || n === NeighborType.BelowLeft || n === NeighborType.BelowRight)) { return null; } if (sq.rank === 8 && (n === NeighborType.Above || n === NeighborType.AboveLeft || n === NeighborType.AboveRight)) { return null; } // validate file let fIndex = 'abcdefgh'.indexOf(sq.file), i = 0; if (fIndex !== -1 && sq.rank > 0 && sq.rank < 9) { // find the index i = 8 * (sq.rank - 1) + fIndex + n.offset; if (this.squares && this.squares.length > i && i > -1) { return this.squares[i]; } } } return null; } getSquare (f, r) { // check for shorthand if (typeof f === 'string' && f.length === 2 && !r) { r = parseInt(f.charAt(1), 10); f = f.charAt(0); } // validate file let fIndex = 'abcdefgh'.indexOf(f), i = 0; if (fIndex !== -1 && r > 0 && r < 9) { // Find the index i = 8 * (r - 1) + fIndex; if (this.squares && this.squares.length > i) { return this.squares[i]; } } return null; } getSquares (side) { const list = []; for (let i = 0; i < this.squares.length; i++) { if (this.squares[i].piece && this.squares[i].piece.side === side) { list.push(this.squares[i]); } } return list; } move (src, dest, n) { if (typeof src === 'string' && src.length === 2) { src = this.getSquare(src); } if (typeof dest === 'string' && dest.length === 2) { dest = this.getSquare(dest); } let simulate; if (typeof n === 'boolean') { simulate = n; n = null; } if (src && src.file && src.rank && dest && dest.file && dest.rank) { let move = { algebraic : n, capturedPiece : dest.piece, castle : false, enPassant : false, postSquare : dest, prevSquare : src }, p = src.piece, sq = null, undo = (b, m) => { return () => { if (!simulate) { // ensure no harm can be done if called multiple times if (m.undone) { throw new Error('cannot undo a move multiple times'); } } // backout move by returning the squares to their state prior to the move m.prevSquare.piece = m.postSquare.piece; m.postSquare.piece = m.capturedPiece; // handle standard scenario if (!m.enPassant) { m.postSquare.piece = m.capturedPiece; } // handle en-passant scenario if (m.enPassant) { b.getSquare( m.postSquare.file, m.prevSquare.rank ).piece = m.capturedPiece; // there is no piece on the post square in the event of // an en-passant, clear anything that me be present as // a result of the move (fix for issue #8) m.postSquare.piece = null; } // handle castle scenario if (m.castle) { sq = b.getSquare( move.postSquare.file === 'g' ? 'f' : 'd', move.postSquare.rank); b.getSquare( move.postSquare.file === 'g' ? 'h' : 'a', move.postSquare.rank ).piece = sq.piece; sq.piece = null; } // if not a simulation, reset the move count if (!simulate) { // correct the moveCount for the piece m.prevSquare.piece.moveCount = m.prevSquare.piece.moveCount - 1; // indicate move has been undone m.undone = true; // emit an undo event b.emit('undo', m); } }; }; dest.piece = p; move.castle = p.type === PieceType.King && p.moveCount === 0 && (move.postSquare.file === 'g' || move.postSquare.file === 'c'); move.enPassant = p.type === PieceType.Pawn && move.capturedPiece === null && move.postSquare.file !== move.prevSquare.file; move.prevSquare.piece = null; // check for en-passant if (move.enPassant) { sq = this.getSquare(move.postSquare.file, move.prevSquare.rank); move.capturedPiece = sq.piece; sq.piece = null; } // check for castle if (move.castle) { sq = this.getSquare( move.postSquare.file === 'g' ? 'h' : 'a', move.postSquare.rank ); if (sq.piece === null) { move.castle = false; } else { this.getSquare( move.postSquare.file === 'g' ? 'f' : 'd', move.postSquare.rank ).piece = sq.piece; sq.piece = null; } } if (!simulate) { p.moveCount++; this.lastMovedPiece = p; if (move.capturedPiece) { this.emit('capture', move); } if (move.castle) { this.emit('castle', move); } if (move.enPassant) { this.emit('enPassant', move); } this.emit('move', move); } return { move, undo : undo(this, move) }; } } promote (sq, p) { // update move count and last piece p.moveCount = sq.piece.moveCount; this.lastMovedPiece = p; // set to square sq.piece = p; this.emit('promote', sq); return sq; } } // exports export default { Board, NeighborType };