From 9c296d34297c22af62ee9455ed541d0a0b6d9abf Mon Sep 17 00:00:00 2001 From: Cozma Rares Date: Sat, 15 Apr 2023 02:10:46 +0300 Subject: [PATCH] feat: generate legal moves for a square --- server/src/chess/engine.ts | 329 ++++++++++++++++++++++++++++--------- 1 file changed, 247 insertions(+), 82 deletions(-) diff --git a/server/src/chess/engine.ts b/server/src/chess/engine.ts index 24fc5b6..37091ce 100644 --- a/server/src/chess/engine.ts +++ b/server/src/chess/engine.ts @@ -36,6 +36,8 @@ export type Piece = { color: Color; }; +export type Board = (Piece | null)[]; + // prettier-ignore export const SQUARES = Object.freeze([ 'a8', 'b8', 'c8', 'd8', 'e8', 'f8', 'g8', 'h8', @@ -99,95 +101,36 @@ export const COLOR_MASKS: Record = Object.freeze({ BLACK: 0b10, } as const); -const PAWN_PROMOTION_RANK = Object.freeze({ - w: 8, - b: 1, -}); - -const PAWN_OFFSETS = Object.freeze({ - w: 8, - b: -8, -}); - -const PIECE_OFFSETS = Object.freeze({ - n: [-10, -17, -15, -6, 10, 17, 15, 6], - b: [-9, -7, 9, 7], - r: [-8, 1, 8, -1], - q: [-9, -7, 9, 7, -8, 1, 8, -1], - k: [-9, -7, 9, 7, -8, 1, 8, -1], -}); - -export type Board = (Piece | null)[]; - -function generatePawnMoves( - board: Readonly, - position: number, - color: Color -) { - const moves: Move[] = []; - - const generatePromotionMoves = (from: number, to: number) => { - const fromAlgebraic = algebraic(from); - const toAlgebraic = algebraic(to); - - PIECE_PROMOTION.forEach(piece => - moves.push({ - from: fromAlgebraic, - to: toAlgebraic, - promotion: piece, - flag: MOVE_FLAGS.PROMOTION, - }) - ); - }; - - const offset = PAWN_OFFSETS[color]; - const nextPosition = position + offset; - - if (board[nextPosition] == null) { - if (rank(nextPosition) == PAWN_PROMOTION_RANK[color]) - generatePromotionMoves(position, nextPosition); - else { - moves.push({ - from: algebraic(position), - to: algebraic(nextPosition), - flag: MOVE_FLAGS.NORMAL, - }); - - const jumpPosition = nextPosition + offset; - - if (board[jumpPosition] == null) - moves.push({ - from: algebraic(position), - to: algebraic(jumpPosition), - flag: MOVE_FLAGS.PAWN_JUMP, - }); - } - } - - [1, -1].forEach(value => { - const attackPosition = nextPosition + value; - - if (board[attackPosition] != null) - moves.push({ - from: algebraic(position), - to: algebraic(attackPosition), - flag: MOVE_FLAGS.CAPTURE, - }); - }); - - return moves; -} - -function generatePieceMoves(board: Readonly) {} - export function rank(squareIdx: number): number { return squareIdx >> 3; } +export const RANK = Object.freeze({ + FIRST: 0, + SECOND: 1, + THIRD: 2, + FORTH: 3, + FIFTH: 4, + SIXTH: 5, + SEVENTH: 6, + EIGHTH: 7, +} as const); + export function file(squareIdx: number): number { return squareIdx & 0b111; } +export const FILE: Record = Object.freeze({ + A: 0, + B: 1, + C: 2, + D: 3, + E: 4, + F: 5, + G: 6, + H: 7, +}); + export function isDigit(c: string): boolean { return /^\d$/.test(c); } @@ -314,6 +257,222 @@ export function validateFEN(fen: string): void { validatePosition(fields[0]); } +const PAWN_MOVE_INFO = Object.freeze({ + w: { offset: 8, promotion: RANK.EIGHTH }, + b: { offset: -8, promotion: RANK.FIRST }, +}); + +const PAWN_ATTACKS = Object.freeze([ + { offset: 1, excludedFile: FILE.H }, + { offset: -1, excludedFile: FILE.A }, +]); + +const PIECE_MOVE_INFO = Object.freeze({ + n: { + generateMultiple: false, + moves: [ + { offset: -10, excludedFiles: [FILE.A, FILE.B] }, + { offset: -17, excludedFiles: [FILE.H] }, + { offset: -15, excludedFiles: [FILE.A] }, + { offset: -6, excludedFiles: [FILE.G, FILE.H] }, + { offset: 10, excludedFiles: [FILE.G, FILE.H] }, + { offset: 17, excludedFiles: [FILE.G] }, + { offset: 15, excludedFiles: [FILE.A] }, + { offset: 6, excludedFiles: [FILE.A, FILE.B] }, + ], + }, + b: { + generateMultiple: true, + moves: [ + { offset: -9, excludedFiles: [FILE.H] }, + { offset: -7, excludedFiles: [FILE.A] }, + { offset: 9, excludedFiles: [FILE.H] }, + { offset: 7, excludedFiles: [FILE.A] }, + ], + }, + r: { + generateMultiple: true, + moves: [ + { offset: -8, excludedFiles: [] }, + { offset: 1, excludedFiles: [FILE.H] }, + { offset: 8, excludedFiles: [] }, + { offset: -1, excludedFiles: [FILE.A] }, + ], + }, + q: { + generateMultiple: true, + moves: [ + { offset: -9, excludedFiles: [FILE.H] }, + { offset: -7, excludedFiles: [FILE.A] }, + { offset: 9, excludedFiles: [FILE.H] }, + { offset: 7, excludedFiles: [FILE.A] }, + { offset: -8, excludedFiles: [] }, + { offset: 1, excludedFiles: [FILE.H] }, + { offset: 8, excludedFiles: [] }, + { offset: -1, excludedFiles: [FILE.A] }, + ], + }, + k: { + generateMultiple: false, + moves: [ + { offset: -9, excludedFiles: [FILE.H] }, + { offset: -7, excludedFiles: [FILE.A] }, + { offset: 9, excludedFiles: [FILE.H] }, + { offset: 7, excludedFiles: [FILE.A] }, + { offset: -8, excludedFiles: [] }, + { offset: 1, excludedFiles: [FILE.H] }, + { offset: 8, excludedFiles: [] }, + { offset: -1, excludedFiles: [FILE.A] }, + ], + }, +}); + +function generatePawnMoves( + board: Readonly, + position: number, + color: Color +): Move[] { + if (board[position] == null) return []; + + const moves: Move[] = []; + + const generatePromotionMoves = (from: number, to: number) => { + const fromAlgebraic = algebraic(from); + const toAlgebraic = algebraic(to); + + PIECE_PROMOTION.forEach(piece => + moves.push({ + from: fromAlgebraic, + to: toAlgebraic, + promotion: piece, + flag: MOVE_FLAGS.PROMOTION, + }) + ); + }; + + const offset = PAWN_MOVE_INFO[color].offset; + const nextPosition = position + offset; + + if (board[nextPosition] == null) { + if (rank(nextPosition) == PAWN_MOVE_INFO[color].promotion) + generatePromotionMoves(position, nextPosition); + else { + moves.push({ + from: algebraic(position), + to: algebraic(nextPosition), + flag: MOVE_FLAGS.NORMAL, + }); + + const jumpPosition = nextPosition + offset; + + if (board[jumpPosition] == null) + moves.push({ + from: algebraic(position), + to: algebraic(jumpPosition), + flag: MOVE_FLAGS.PAWN_JUMP, + }); + } + } + + PAWN_ATTACKS.forEach(({ offset, excludedFile }) => { + const attackPosition = nextPosition + offset; + + if (file(position) != excludedFile && board[attackPosition] != null) + moves.push({ + from: algebraic(position), + to: algebraic(attackPosition), + flag: MOVE_FLAGS.CAPTURE, + }); + }); + + return moves; +} + +function generatePieceMoves( + board: Readonly, + position: number, + piece: Piece +): Move[] { + if (piece.type == PIECE.PAWN) + return generatePawnMoves(board, position, piece.color); + + const type = piece.type; // hacked the type system + + const generateOnce = () => { + const moves: Move[] = []; + + PIECE_MOVE_INFO[type].moves.forEach(({ offset, excludedFiles }) => { + const nextPosition = position + offset; + + if (nextPosition < 0 || nextPosition >= 64) return; + + if (excludedFiles.includes(file(nextPosition))) return; + + const attackedPiece = board[nextPosition]; + + if (attackedPiece == null) { + moves.push({ + from: algebraic(position), + to: algebraic(nextPosition), + flag: MOVE_FLAGS.NORMAL, + }); + return; + } + + if (attackedPiece.color == piece.color) return; + + moves.push({ + from: algebraic(position), + to: algebraic(nextPosition), + flag: MOVE_FLAGS.CAPTURE, + }); + }); + + return moves; + }; + + const generateMultiple = () => { + const moves: Move[] = []; + + PIECE_MOVE_INFO[type].moves.forEach(({ offset, excludedFiles }) => { + let nextPosition = position + offset; + + while ( + nextPosition >= 0 && + nextPosition < 64 && + !excludedFiles.includes(file(nextPosition)) + ) { + const attackedPiece = board[nextPosition]; + + if (attackedPiece == null) { + moves.push({ + from: algebraic(position), + to: algebraic(nextPosition), + flag: MOVE_FLAGS.NORMAL, + }); + nextPosition += offset; + continue; + } + + if (attackedPiece.color != piece.color) + moves.push({ + from: algebraic(position), + to: algebraic(nextPosition), + flag: MOVE_FLAGS.CAPTURE, + }); + + break; + } + }); + + return moves; + }; + + return PIECE_MOVE_INFO[piece.type].generateMultiple + ? generateMultiple() + : generateOnce(); +} + export default class Chess { private _board: Board; private _turn: Color; @@ -471,7 +630,13 @@ export default class Chess { getMoves() {} - getMovesForSquare(square: Square) {} + getMovesForSquare(square: Square | number): Move[] { + if (typeof square != "number") square = squareIndex(square); + + const piece = this.getPiece(square); + + return piece == null ? [] : generatePieceMoves(this._board, square, piece); + } makeMove() {}