feat: remove moves that leave the king in check
This commit is contained in:
+34
-16
@@ -14,7 +14,6 @@ import bk from "./assets/pieces/bk.png";
|
||||
|
||||
import Chess, {
|
||||
squareColor,
|
||||
COLOR,
|
||||
Piece,
|
||||
PieceType,
|
||||
Color,
|
||||
@@ -35,17 +34,12 @@ const PIECES: Record<Color, Record<PieceType, string>> = {
|
||||
b: { p: bp, n: bn, b: bb, r: br, q: bq, k: bk },
|
||||
};
|
||||
|
||||
function getKeyByValue(object: Record<string, any>, value: any) {
|
||||
return Object.keys(object).find((key) => object[key] === value);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [chess] = useState(Chess.load());
|
||||
const [_, setUpdate] = useState(false);
|
||||
|
||||
const sendMove = (move: Move) => {
|
||||
chess.makeMove(move);
|
||||
console.log(chess.getFEN());
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -73,12 +67,18 @@ const ChessBoard: React.FC<{
|
||||
piece: chess.getPiece(i),
|
||||
isAttacked: false,
|
||||
isPromotion: false,
|
||||
isActive: false,
|
||||
}));
|
||||
|
||||
if (activeTile != -1)
|
||||
chess
|
||||
.getMovesForSquare(algebraic(activeTile))
|
||||
.forEach(({ to }) => (tileProps[squareIndex(to)].isAttacked = true));
|
||||
if (activeTile != -1) {
|
||||
tileProps[activeTile].isActive = true;
|
||||
chess.getMovesForSquare(algebraic(activeTile)).forEach(({ to, flags }) => {
|
||||
const square = squareIndex(to);
|
||||
tileProps[square].isAttacked = true;
|
||||
tileProps[square].isPromotion =
|
||||
flags & MOVE_FLAGS.PROMOTION ? true : false;
|
||||
});
|
||||
}
|
||||
|
||||
const tiles = tileProps.map((props, i) => (
|
||||
<Tile key={i} {...props} blackPerspective={blackPerspective} />
|
||||
@@ -89,12 +89,16 @@ const ChessBoard: React.FC<{
|
||||
// @ts-ignore
|
||||
const tile = parseInt(e.target.dataset.tile);
|
||||
|
||||
if (isNaN(tile)) return;
|
||||
|
||||
if (activeTile != -1 && tileProps[tile].isAttacked) {
|
||||
sendMove({ from: algebraic(activeTile), to: algebraic(tile) });
|
||||
setActiveTile(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tileProps[tile].piece == null) return setActiveTile(-1);
|
||||
|
||||
if (tile == activeTile) setActiveTile(-1);
|
||||
else setActiveTile(tile);
|
||||
};
|
||||
@@ -111,16 +115,30 @@ const ChessBoard: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const TILE_COLORS = Object.freeze({
|
||||
w: {
|
||||
bg: "bg-white-tile",
|
||||
text: "text-black-tile",
|
||||
active: "bg-sky-400",
|
||||
},
|
||||
b: {
|
||||
bg: "bg-black-tile",
|
||||
text: "text-white-tile",
|
||||
active: "bg-sky-700",
|
||||
},
|
||||
} as const);
|
||||
|
||||
const Tile: React.FC<{
|
||||
tileNumber: number;
|
||||
piece: Piece | null;
|
||||
isActive: boolean;
|
||||
isAttacked: boolean;
|
||||
blackPerspective?: boolean;
|
||||
}> = ({ tileNumber, piece, isAttacked, blackPerspective }) => {
|
||||
const [bgColor, textColor] =
|
||||
squareColor(tileNumber) == COLOR.WHITE
|
||||
? ["bg-white-tile", "text-black-tile"]
|
||||
: ["bg-black-tile", "text-white-tile"];
|
||||
}> = ({ tileNumber, piece, isActive, isAttacked, blackPerspective }) => {
|
||||
const color = squareColor(tileNumber);
|
||||
|
||||
const { bg: bgColor, text: textColor } = TILE_COLORS[color];
|
||||
const activeColor = isActive ? TILE_COLORS[color].active : "";
|
||||
|
||||
const tileFile = file(tileNumber);
|
||||
const tileRank = rank(tileNumber);
|
||||
@@ -131,7 +149,7 @@ const Tile: React.FC<{
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative font-bold text-xl isolate group [&>*]:pointer-events-none ${bgColor}`}
|
||||
className={`relative font-bold text-xl isolate group [&>*]:pointer-events-none ${bgColor} ${activeColor}`}
|
||||
data-tile={tileNumber}
|
||||
>
|
||||
{piece == null ? <></> : <img src={PIECES[piece.color][piece.type]} />}
|
||||
|
||||
+104
-103
@@ -503,6 +503,7 @@ export default class Chess {
|
||||
private _moves: InternalMove[] = [];
|
||||
private _attacks: number[] = [];
|
||||
private _history: string[] = [];
|
||||
private _enableProcessMoves = true;
|
||||
|
||||
private constructor(
|
||||
board: Board,
|
||||
@@ -511,7 +512,8 @@ export default class Chess {
|
||||
enPassant: Square,
|
||||
halfMoves: number,
|
||||
fullMoves: number,
|
||||
kings: Record<Color, number>
|
||||
kings: Record<Color, number>,
|
||||
enableProcessMoves: boolean
|
||||
) {
|
||||
this._board = board;
|
||||
this._turn = turn;
|
||||
@@ -520,31 +522,26 @@ export default class Chess {
|
||||
this._halfMoves = halfMoves;
|
||||
this._fullMoves = fullMoves;
|
||||
this._kings = kings;
|
||||
this._enableProcessMoves = enableProcessMoves;
|
||||
this._computeMoves();
|
||||
}
|
||||
|
||||
private _computeMoves() {
|
||||
this._attacks = new Array(64).fill(0);
|
||||
|
||||
const currentMoves: {
|
||||
piece: Piece;
|
||||
moves: InternalMove[];
|
||||
}[] = [];
|
||||
this._moves = [];
|
||||
|
||||
for (let i = 0; i < this._board.length; i++) {
|
||||
const { piece, moves } = this._getMovesForSquare(i);
|
||||
|
||||
if (piece == null) continue;
|
||||
if (this._turn == piece.color) currentMoves.push({ piece, moves });
|
||||
if (this._turn == piece.color) this._moves = this._moves.concat(moves);
|
||||
|
||||
moves.forEach(
|
||||
({ to }) => (this._attacks[squareIndex(to)] |= COLOR_MASKS[piece.color])
|
||||
);
|
||||
}
|
||||
|
||||
this._moves = currentMoves.flatMap(({ piece, moves }) =>
|
||||
this._processMoves(piece, moves)
|
||||
);
|
||||
if (this._enableProcessMoves) this._processMoves();
|
||||
|
||||
const canCastleThrough = (square: number, attackedBy: Color) => {
|
||||
return (
|
||||
@@ -591,7 +588,7 @@ export default class Chess {
|
||||
}
|
||||
}
|
||||
|
||||
static load(fen = DEFAULT_POSITION) {
|
||||
static load(fen = DEFAULT_POSITION, enableProcessMoves = true) {
|
||||
validateFEN(fen);
|
||||
|
||||
const builder = new Chess.Builder();
|
||||
@@ -619,6 +616,9 @@ export default class Chess {
|
||||
builder.setEnPassant(fields[3] as Square);
|
||||
builder.setHalfMoves(fields[4]);
|
||||
builder.setFullMoves(fields[5]);
|
||||
|
||||
if (enableProcessMoves == false) builder.disableComputeMoves();
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@@ -721,35 +721,91 @@ export default class Chess {
|
||||
return { piece, moves };
|
||||
}
|
||||
|
||||
// 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 otherColor = swapColor(piece.color);
|
||||
return moves.filter(
|
||||
({ to }) => !this.isSquareAttacked(to, otherColor),
|
||||
this
|
||||
);
|
||||
// return moves.fiter(
|
||||
// move => {
|
||||
// this.makeMove
|
||||
// const check = this.isCheck
|
||||
// this.undo
|
||||
// return !check
|
||||
// },
|
||||
// this
|
||||
// )
|
||||
private _processMoves() {
|
||||
const currentFEN = this.getFEN();
|
||||
this._moves = this._moves.filter((move) => {
|
||||
const chess = Chess.load(currentFEN, false);
|
||||
chess._makeMove(move);
|
||||
return !chess._isKingAttacked(this._turn);
|
||||
}, this);
|
||||
}
|
||||
|
||||
getMovesForSquare(square: Square): InternalMove[] {
|
||||
return this._moves.filter(({ from }) => from == square);
|
||||
}
|
||||
|
||||
makeMove(move: Move | InternalMove) {
|
||||
private _makeMove(move: InternalMove) {
|
||||
const myColor = this._turn;
|
||||
const theirColor = swapColor(this._turn);
|
||||
|
||||
this._history.push(this.getFEN());
|
||||
|
||||
this._board[squareIndex(move.to)] =
|
||||
move.flags & MOVE_FLAGS.PROMOTION
|
||||
? {
|
||||
type: move.promotion as PieceType,
|
||||
color: myColor,
|
||||
}
|
||||
: this._board[squareIndex(move.from)];
|
||||
this._board[squareIndex(move.from)] = null;
|
||||
let keepEpSquare = false;
|
||||
|
||||
switch (move.flags) {
|
||||
case MOVE_FLAGS.PAWN_JUMP:
|
||||
this._enPassant = algebraic(
|
||||
squareIndex(move.to) - PAWN_MOVE_INFO[myColor].offset
|
||||
);
|
||||
keepEpSquare = true;
|
||||
break;
|
||||
|
||||
case MOVE_FLAGS.EN_PASSANT:
|
||||
this._board[
|
||||
squareIndex(this._enPassant) + PAWN_MOVE_INFO[theirColor].offset
|
||||
] = null;
|
||||
break;
|
||||
|
||||
case MOVE_FLAGS.K_CASTLE:
|
||||
const kingsKnight = squareIndex(move.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(move.to);
|
||||
const queen = queensBishop + 1;
|
||||
const queensRook = queensBishop - 2;
|
||||
this._board[queen] = this._board[queensRook];
|
||||
this._board[queensRook] = null;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (!keepEpSquare) this._enPassant = EMPTY_SQUARE;
|
||||
|
||||
const piece = this.getPiece(move.to)?.type as PieceType;
|
||||
|
||||
if (piece == PIECE.KING) this._kings[myColor] = squareIndex(move.to);
|
||||
|
||||
if (
|
||||
piece == PIECE.PAWN ||
|
||||
move.flags & (MOVE_FLAGS.CAPTURE | MOVE_FLAGS.EN_PASSANT)
|
||||
)
|
||||
this._halfMoves = 0;
|
||||
else this._halfMoves++;
|
||||
|
||||
if (myColor == COLOR.BLACK) this._fullMoves++;
|
||||
|
||||
this._turn = theirColor;
|
||||
this._updateCastling();
|
||||
|
||||
if (this.isGameOver()) this._moves = [];
|
||||
else this._computeMoves();
|
||||
}
|
||||
|
||||
makeMove(move: Move) {
|
||||
let moveObj = null;
|
||||
|
||||
for (const computedMove of this._moves) {
|
||||
@@ -767,71 +823,7 @@ export default class Chess {
|
||||
if (moveObj == null)
|
||||
throw new Error(`Move ${JSON.stringify(move)} not found`);
|
||||
|
||||
this._history.push(this.getFEN());
|
||||
|
||||
this._board[squareIndex(moveObj.to)] =
|
||||
moveObj.flags & MOVE_FLAGS.PROMOTION
|
||||
? {
|
||||
type: moveObj.promotion as PieceType,
|
||||
color: myColor,
|
||||
}
|
||||
: this._board[squareIndex(moveObj.from)];
|
||||
this._board[squareIndex(moveObj.from)] = null;
|
||||
let keepEpSquare = false;
|
||||
|
||||
switch (moveObj.flags) {
|
||||
case MOVE_FLAGS.PAWN_JUMP:
|
||||
this._enPassant = algebraic(
|
||||
squareIndex(moveObj.to) - PAWN_MOVE_INFO[myColor].offset
|
||||
);
|
||||
keepEpSquare = true;
|
||||
break;
|
||||
|
||||
case MOVE_FLAGS.EN_PASSANT:
|
||||
this._board[
|
||||
squareIndex(this._enPassant) + PAWN_MOVE_INFO[theirColor].offset
|
||||
] = null;
|
||||
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;
|
||||
}
|
||||
if (!keepEpSquare) this._enPassant = EMPTY_SQUARE;
|
||||
|
||||
const piece = this.getPiece(move.to)?.type as PieceType;
|
||||
|
||||
if (piece == PIECE.KING) this._kings[myColor] = squareIndex(moveObj.to);
|
||||
|
||||
if (
|
||||
piece == PIECE.PAWN ||
|
||||
moveObj.flags & (MOVE_FLAGS.CAPTURE | MOVE_FLAGS.EN_PASSANT)
|
||||
)
|
||||
this._halfMoves = 0;
|
||||
else this._halfMoves++;
|
||||
|
||||
if (myColor == COLOR.BLACK) this._fullMoves++;
|
||||
|
||||
this._turn = theirColor;
|
||||
this._updateCastling();
|
||||
|
||||
if (this.isGameOver()) this._moves = [];
|
||||
else this._computeMoves();
|
||||
this._makeMove(moveObj);
|
||||
}
|
||||
|
||||
private _updateCastling() {
|
||||
@@ -900,9 +892,13 @@ export default class Chess {
|
||||
: (this._attacks[square] & COLOR_MASKS[attackedBy]) != 0;
|
||||
}
|
||||
|
||||
_isKingAttacked(color: Color) {
|
||||
const square = this._kings[color];
|
||||
return this.isSquareAttacked(square, swapColor(color));
|
||||
}
|
||||
|
||||
isCheck() {
|
||||
const square = this._kings[this._turn];
|
||||
return this.isSquareAttacked(square, swapColor(this._turn));
|
||||
return this._isKingAttacked(this._turn);
|
||||
}
|
||||
|
||||
isCheckMate() {
|
||||
@@ -966,13 +962,12 @@ export default class Chess {
|
||||
return this.isCheckMate() || this.isDraw();
|
||||
}
|
||||
|
||||
// TODO: implement
|
||||
// mabye store the history in a tree
|
||||
// TODO: store the history in a tree
|
||||
undo() {
|
||||
const lastFen = this._history.pop();
|
||||
if (lastFen == undefined) return;
|
||||
|
||||
const chess = Chess.load();
|
||||
const chess = Chess.load(lastFen, this._enableProcessMoves);
|
||||
this._board = chess._board;
|
||||
this._turn = chess._turn;
|
||||
this._castling = chess._castling;
|
||||
@@ -998,6 +993,7 @@ export default class Chess {
|
||||
private _halfMoves: number = 0;
|
||||
private _fullMoves: number = 1;
|
||||
private _kings: Record<Color, number> = { w: 0, b: 0 };
|
||||
private _enableProcessMoves = true;
|
||||
|
||||
addPiece(square: Square | number, piece: Piece) {
|
||||
if (typeof square != "number") square = squareIndex(square);
|
||||
@@ -1031,6 +1027,10 @@ export default class Chess {
|
||||
this._fullMoves = parseInt(fullMoves);
|
||||
}
|
||||
|
||||
disableComputeMoves() {
|
||||
this._enableProcessMoves = false;
|
||||
}
|
||||
|
||||
build() {
|
||||
return new Chess(
|
||||
this._board,
|
||||
@@ -1039,7 +1039,8 @@ export default class Chess {
|
||||
this._enPassant,
|
||||
this._halfMoves,
|
||||
this._fullMoves,
|
||||
this._kings
|
||||
this._kings,
|
||||
this._enableProcessMoves
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user