feat: implement 'makeMove', generate castling and en passant moves

This commit is contained in:
Cozma Rares
2023-06-02 16:29:43 +03:00
parent 5e8b88bee6
commit 3878b343b4
3 changed files with 245 additions and 82 deletions
+41 -3
View File
@@ -1,11 +1,11 @@
import { describe, expect, test } from "vitest";
import Chess, { MOVE_FLAGS, Move, Square } from "../chess/engine";
import Chess, { InternalMove, MOVE_FLAGS, Square } from "../chess/engine";
type ExpectedMoves = Record<
string,
{
square: Square;
moves: Move[];
moves: InternalMove[];
}[]
>;
@@ -18,7 +18,6 @@ function runTest(fen: string, expectedMoves: ExpectedMoves) {
const moves = chess.getMovesForSquare(square);
expect(moves).toHaveLength(expectedMoves.length);
moves.forEach(move => expect(expectedMoves).toContainEqual(move));
expectedMoves.forEach(expectedMove =>
expect(moves).toContainEqual(expectedMove)
@@ -489,3 +488,42 @@ describe("queen and king moves", () => {
runTests("7Q/2k5/8/8/8/6K1/q7/8 % - - 0 1", expectedMovesW, expectedMovesB);
});
describe("en passant", () => {
const expectedMoves: ExpectedMoves = {
white_sees_ep_square: [
{
square: "d5",
moves: [
{ from: "d5", to: "d6", flag: MOVE_FLAGS.NORMAL },
{ from: "d5", to: "e6", flag: MOVE_FLAGS.EN_PASSANT },
],
},
],
};
runTest(
"rnbqkbnr/pp1p1ppp/8/2pPp3/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 0 3",
expectedMoves
);
});
describe("castling", () => {
const expectedMoves: ExpectedMoves = {
white_castling: [
{
square: "e1",
moves: [
{ from: "e1", to: "g1", flag: MOVE_FLAGS.K_CASTLE },
{ from: "e1", to: "d2", flag: MOVE_FLAGS.NORMAL },
{ from: "e1", to: "f1", flag: MOVE_FLAGS.NORMAL },
],
},
],
};
runTest(
"rn1qkbnr/p4ppp/B1p5/1p1pp3/3PPBb1/2NQ3N/PPP2PPP/R3K2R w KQkq - 0 8",
expectedMoves
);
});
-7
View File
@@ -42,7 +42,6 @@ describe("valid FEN strings", () => {
builder.addPiece("h7", { color: COLOR.BLACK, type: PIECE.PAWN });
const expected = builder.build();
const chess = Chess.load();
for (let i = 0; i < 64; i++)
@@ -89,7 +88,6 @@ describe("valid FEN strings", () => {
builder.addPiece("h7", { color: COLOR.BLACK, type: PIECE.PAWN });
const expected = builder.build();
const chess = Chess.load(
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"
);
@@ -138,7 +136,6 @@ describe("valid FEN strings", () => {
builder.addPiece("h7", { color: COLOR.BLACK, type: PIECE.PAWN });
const expected = builder.build();
const chess = Chess.load(
"rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2"
);
@@ -187,7 +184,6 @@ describe("valid FEN strings", () => {
builder.addPiece("h7", { color: COLOR.BLACK, type: PIECE.PAWN });
const expected = builder.build();
const chess = Chess.load(
"rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPPKPPP/RNBQ1BNR b kq - 1 2"
);
@@ -224,7 +220,6 @@ describe("valid FEN strings", () => {
builder.addPiece("h7", { color: COLOR.BLACK, type: PIECE.PAWN });
const expected = builder.build();
const chess = Chess.load(
"r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 b - - 0 19"
);
@@ -253,7 +248,6 @@ describe("valid FEN strings", () => {
builder.addPiece("h5", { color: COLOR.BLACK, type: PIECE.PAWN });
const expected = builder.build();
const chess = Chess.load("1k6/1pp5/p7/5B1p/PP6/6K1/4p2r/4R3 b - - 3 43");
for (let i = 0; i < 64; i++)
@@ -273,7 +267,6 @@ describe("valid FEN strings", () => {
builder.addPiece("b2", { color: COLOR.BLACK, type: PIECE.PAWN });
const expected = builder.build();
const chess = Chess.load("8/8/2k2Q2/8/5P2/8/1p6/6K1 b - - 1 48");
for (let i = 0; i < 64; i++)
+204 -72
View File
@@ -48,7 +48,7 @@ export const SQUARES = Object.freeze([
'a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1',
] as const);
const EN_PASSANT_SQUARES = {
const EN_PASSANT_ATTACK_SQUARES = {
w: ["a6", "b6", "c6", "d6", "e6", "f6", "g6", "h6"],
b: ["a3", "b3", "c3", "d3", "e3", "f3", "g3", "h3"],
};
@@ -70,13 +70,12 @@ export type MoveFlag = (typeof MOVE_FLAGS)[keyof typeof MOVE_FLAGS];
export type Move = {
from: Square;
to: Square;
flag: MoveFlag;
promotion?: PiecePromotionType;
};
// type InternalMove = Move & {
// beforeFEN: string;
// };
export type InternalMove = Move & {
flag: MoveFlag;
};
export const PIECE_MASKS: Record<PieceType, number> = Object.freeze({
p: 0b000001,
@@ -229,7 +228,10 @@ export function validateFEN(fen: string): void {
};
const validateEnPassant = (enPassant: string, turn: Color) => {
if (enPassant != "-" && !EN_PASSANT_SQUARES[turn].includes(enPassant))
if (
enPassant != "-" &&
!EN_PASSANT_ATTACK_SQUARES[turn].includes(enPassant)
)
throw new Error("Invalid FEN - invalid en-passant square");
};
@@ -326,13 +328,14 @@ const PIECE_MOVE_INFO = Object.freeze({
});
function generatePawnMoves(
board: Readonly<Board>,
position: number,
color: Color
): Move[] {
color: Color,
board: Readonly<Board>,
enPassantSquare: Square
): InternalMove[] {
if (board[position] == null) return [];
const moves: Move[] = [];
const moves: InternalMove[] = [];
const generatePromotionMoves = (from: number, to: number) => {
const fromAlgebraic = algebraic(from);
@@ -351,7 +354,7 @@ function generatePawnMoves(
const offset = PAWN_MOVE_INFO[color].offset;
const nextPosition = position + offset;
if (board[nextPosition] == null) {
if (board[nextPosition] == null)
if (rank(nextPosition) == PAWN_MOVE_INFO[color].promotion)
generatePromotionMoves(position, nextPosition);
else {
@@ -372,18 +375,23 @@ function generatePawnMoves(
});
}
}
}
const enPassant = squareIndex(enPassantSquare);
PAWN_ATTACKS.forEach(({ offset, excludedFile }) => {
const attackPosition = nextPosition + offset;
const piece = board[attackPosition];
if (file(position) != excludedFile && piece != null && piece.color != color)
const isPiece = piece != null && piece.color != color;
const isEnPassant =
attackPosition == enPassant &&
EN_PASSANT_ATTACK_SQUARES[color].includes(enPassantSquare);
if (file(position) != excludedFile && (isPiece || isEnPassant))
moves.push({
from: algebraic(position),
to: algebraic(attackPosition),
flag: MOVE_FLAGS.CAPTURE,
flag: isPiece ? MOVE_FLAGS.CAPTURE : MOVE_FLAGS.EN_PASSANT,
});
});
@@ -391,16 +399,17 @@ function generatePawnMoves(
}
export function generatePieceMoves(
board: Readonly<Board>,
position: number,
piece: Piece
): Move[] {
piece: Piece,
board: Readonly<Board>,
enPassantSquare: Square
): InternalMove[] {
if (piece.type == PIECE.PAWN)
return generatePawnMoves(board, position, piece.color);
return generatePawnMoves(position, piece.color, board, enPassantSquare);
const type = piece.type; // hacked the type system
const generateOnce = () => {
const moves: Move[] = [];
const moves: InternalMove[] = [];
PIECE_MOVE_INFO[type].moves.forEach(({ offset, excludedFiles }) => {
if (excludedFiles.includes(file(position))) return;
@@ -429,7 +438,7 @@ export function generatePieceMoves(
};
const generateMultiple = () => {
const moves: Move[] = [];
const moves: InternalMove[] = [];
PIECE_MOVE_INFO[type].moves.forEach(({ offset, excludedFiles }) => {
if (excludedFiles.includes(file(position))) return;
@@ -490,10 +499,10 @@ export default class Chess {
private _enPassant;
private _halfMoves;
private _fullMoves;
private _kings;
private _moves: Move[] = [];
private _moves: InternalMove[] = [];
private _attacks: number[] = [];
private _kings: Record<Color, number> = { w: 0, b: 0 };
private constructor(
board: Board,
@@ -501,7 +510,8 @@ export default class Chess {
castling: Record<Color, number>,
enPassant: Square,
halfMoves: number,
fullMoves: number
fullMoves: number,
kings: Record<Color, number>
) {
this._board = board;
this._turn = turn;
@@ -509,23 +519,74 @@ export default class Chess {
this._enPassant = enPassant;
this._halfMoves = halfMoves;
this._fullMoves = fullMoves;
this._kings = kings;
this._computeMoves();
}
private _computeMoves() {
this._attacks = new Array(64).fill(0);
const currentMoves: {
piece: Piece;
moves: InternalMove[];
}[] = [];
for (let i = 0; i < this._board.length; i++) {
const { piece, moves } = this._getMovesForSquare(i);
this._moves = this._moves.concat(this._processMoves(piece, moves));
if (piece != null) {
const pieceColor = piece.color;
moves.forEach(
({ to }) =>
(this._attacks[squareIndex(to)] |= COLOR_MASKS[pieceColor])
);
}
if (piece == null) continue;
if (this._turn == piece.color) currentMoves.push({ piece, moves });
moves.forEach(
({ to }) => (this._attacks[squareIndex(to)] |= COLOR_MASKS[piece.color])
);
}
this._moves = currentMoves.flatMap(({ piece, moves }) =>
this._processMoves(piece, moves)
);
const canCastleThrough = (square: number, attackedBy: Color) => {
return (
this._board[square] == null &&
!this.isSquareAttacked(square, attackedBy)
);
};
for (const color of Object.values(COLOR)) {
const otherColor = swapColor(color);
const castling = this._castling[color];
const kingPosition = this._kings[color];
const queensKnightPosition = kingPosition - 3;
const queensBishopPosition = kingPosition - 2;
const queenPosition = kingPosition - 1;
const kingsBishopPosition = kingPosition + 1;
const kingsKnightPosition = kingPosition + 2;
if (
castling & MOVE_FLAGS.K_CASTLE &&
canCastleThrough(kingsBishopPosition, otherColor) &&
canCastleThrough(kingsKnightPosition, otherColor)
)
this._moves.push({
from: algebraic(kingPosition),
to: algebraic(kingsKnightPosition),
flag: MOVE_FLAGS.K_CASTLE,
});
if (
castling & MOVE_FLAGS.Q_CASTLE &&
canCastleThrough(queenPosition, otherColor) &&
canCastleThrough(queensBishopPosition, otherColor) &&
canCastleThrough(queensKnightPosition, otherColor)
)
this._moves.push({
from: algebraic(kingPosition),
to: algebraic(queensBishopPosition),
flag: MOVE_FLAGS.Q_CASTLE,
});
}
}
@@ -537,17 +598,17 @@ export default class Chess {
const position = fields[0];
let square = 0;
for (let i = 0; i < position.length; i++) {
if (position[i] == "/") continue;
for (const char of position) {
if (char == "/") continue;
if (isDigit(position[i])) {
square += parseInt(position[i]);
if (isDigit(char)) {
square += parseInt(char);
continue;
}
const piece = {
color: position[i] < "a" ? COLOR.WHITE : COLOR.BLACK,
type: position[i].toLowerCase() as PieceType,
color: char < "a" ? COLOR.WHITE : COLOR.BLACK,
type: char.toLowerCase() as PieceType,
};
builder.addPiece(square++, piece);
}
@@ -646,41 +707,116 @@ export default class Chess {
return str;
}
getMoves(): Move[] {
getMoves(): InternalMove[] {
return this._moves;
}
private _getMovesForSquare(square: number) {
const piece = this.getPiece(square);
const moves =
piece == null ? [] : generatePieceMoves(this._board, square, piece);
piece == null
? []
: generatePieceMoves(square, piece, this._board, this._enPassant);
return { piece, moves };
}
// TODO: consider checks
private _processMoves(piece: Piece | null, moves: Move[]) {
// TODO: uncomment correct implementation after 'makeMove' and 'undo' are done
private _processMoves(
piece: Piece | null,
moves: InternalMove[]
): InternalMove[] {
if (piece?.type != PIECE.KING) return moves;
const other_color = COLOR_MASKS[swapColor(piece.color)];
const otherColor = swapColor(piece.color);
return moves.filter(
({ to }) => (this._attacks[squareIndex(to)] & other_color) == 0,
({ to }) => !this.isSquareAttacked(to, otherColor),
this
);
// return moves.fiter(
// move => {
// this.makeMove
// const check = this.isCheck
// this.undo
// return !check
// },
// this
// )
}
getMovesForSquare(square: Square | number): Move[] {
square = squareIndex(square);
if (square < 0 || square > 63) return [];
const { piece, moves } = this._getMovesForSquare(square);
return this._processMoves(piece, moves);
getMovesForSquare(square: Square): InternalMove[] {
return this._moves.filter(({ from }) => from == square);
}
makeMove() {}
makeMove(move: Move) {
const myColor = this._turn;
const theirColor = swapColor(this._turn);
let moveObj = null;
isSquareAttacked(square: Square | number, color: Color) {
for (const computedMove of this._moves) {
const found =
move.to == computedMove.to &&
move.from == computedMove.from &&
move.promotion == computedMove.promotion;
if (found) {
moveObj = computedMove;
break;
}
}
if (moveObj == null)
throw new Error(`Move ${JSON.stringify(move)} not found`);
this._board[squareIndex(moveObj.to)] =
this._board[squareIndex(moveObj.from)];
this._board[squareIndex(moveObj.from)] = null;
switch (moveObj.flag) {
case MOVE_FLAGS.PAWN_JUMP:
this._enPassant = algebraic(
squareIndex(moveObj.to) - PAWN_MOVE_INFO[myColor].offset
);
break;
case MOVE_FLAGS.EN_PASSANT:
this._board[
squareIndex(this._enPassant + PAWN_MOVE_INFO[theirColor].offset)
] = null;
this._enPassant = "-";
break;
case MOVE_FLAGS.PROMOTION:
this._board[squareIndex(moveObj.to)] = {
type: moveObj.promotion as PieceType,
color: myColor,
};
break;
case MOVE_FLAGS.K_CASTLE:
const kingsKnight = squareIndex(moveObj.to);
const kingsRook = kingsKnight + 1;
const kingsBishop = kingsKnight - 1;
this._board[kingsBishop] = this._board[kingsRook];
this._board[kingsRook] = null;
break;
case MOVE_FLAGS.Q_CASTLE:
const queensBishop = squareIndex(moveObj.to);
const queen = queensBishop + 1;
const queensRook = queensBishop - 2;
this._board[queen] = this._board[queensRook];
this._board[queensRook] = null;
break;
default:
break;
}
}
isSquareAttacked(square: Square | number, attackedBy: Color) {
square = squareIndex(square);
return square < 0 || square > 63
? false
: (this._attacks[square] & COLOR_MASKS[color]) != 0;
: (this._attacks[square] & COLOR_MASKS[attackedBy]) != 0;
}
isCheck() {
@@ -753,36 +889,31 @@ export default class Chess {
private _addToHistory() {}
// TODO: implement
// and a way to track how many moves were undo'ed
// mabye store the history in a tree
undo() {}
redo() {}
// TODO: implement
turn() {}
turn() {
return this._turn;
}
// TODO: implement
history() {}
static Builder = class {
private _board: Board;
private _turn: Color;
private _castling: Record<Color, number>;
private _enPassant: Square;
private _halfMoves: number;
private _fullMoves: number;
constructor() {
this._board = new Array(64).fill(null);
this._turn = COLOR.WHITE;
this._castling = { w: 0, b: 0 };
this._enPassant = EMPTY_SQUARE;
this._halfMoves = 0;
this._fullMoves = 1;
}
private _board: Board = new Array(64).fill(null);
private _turn: Color = COLOR.WHITE;
private _castling: Record<Color, number> = { w: 0, b: 0 };
private _enPassant: Square = EMPTY_SQUARE;
private _halfMoves: number = 0;
private _fullMoves: number = 1;
private _kings: Record<Color, number> = { w: 0, b: 0 };
addPiece(square: Square | number, piece: Piece) {
if (typeof square != "number") square = squareIndex(square);
if (square < 0 || square > 63) return;
this._board[square] = piece;
if (piece.type == PIECE.KING) this._kings[piece.color] = square;
}
setTurn(turn: Color) {
@@ -798,7 +929,7 @@ export default class Chess {
}
setEnPassant(enPassant: Square) {
if (EN_PASSANT_SQUARES[this._turn].includes(enPassant))
if (EN_PASSANT_ATTACK_SQUARES[this._turn].includes(enPassant))
this._enPassant = enPassant;
}
@@ -817,7 +948,8 @@ export default class Chess {
this._castling,
this._enPassant,
this._halfMoves,
this._fullMoves
this._fullMoves,
this._kings
);
}
};