feat: generate legal moves for a square

This commit is contained in:
Cozma Rares
2023-04-15 02:10:46 +03:00
parent d5bfe6f5b2
commit 9c296d3429
+247 -82
View File
@@ -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<keyof typeof COLOR, number> = 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<Board>,
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<Board>) {}
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<string, number> = 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<Board>,
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<Board>,
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() {}