From b41dc5ab8fb803ae22d2bcc7fd27effac850cbcc Mon Sep 17 00:00:00 2001 From: Emily Date: Sun, 3 Dec 2023 21:50:28 +0100 Subject: [PATCH] WIP idk what --- src/board.ts | 27 +++++++++++++++++++++++ src/move.ts | 33 ++++++++++++++++++++++++++++- src/move/attacked.ts | 47 ++++++++++++++++++++++++++++++++++++++++- src/move/legal.ts | 13 ++++++++++-- src/move/pieces/king.ts | 2 +- src/utils/coord.ts | 12 +++++++++++ 6 files changed, 129 insertions(+), 5 deletions(-) diff --git a/src/board.ts b/src/board.ts index b3ccc64..ee09068 100644 --- a/src/board.ts +++ b/src/board.ts @@ -300,6 +300,10 @@ export class Board { return this.unsafeAt(coord) === PieceEmpty } + unsafeIsEmpty(coord: Coord): boolean { + return this.unsafeAt(coord) === PieceEmpty + } + /** * Is a square occupied? NB: returns undefined if the square is off the board. */ @@ -308,6 +312,29 @@ export class Board { return this.unsafeAt(coord) !== PieceEmpty } + unsafeIsOccupied(coord: Coord): boolean { + return this.unsafeAt(coord) !== PieceEmpty + } + + /** + * Keep going until we stumble upon a piece or fall off the board. + * + * @param start The starting square, assumed to be valid. + * @param delta The direction to move in. + */ + unsafeFindPieceInDirection( + start: Coord, + delta: { x: number; y: number } + ): { piece: Piece; coord: Coord } | null { + let coord = start + while (true) { + coord = coord.shift(delta) + if (!coord.isValid()) return null + const piece = this.unsafeAt(coord) + if (piece !== PieceEmpty) return { piece, coord } + } + } + /** * Set the board state, based on a FEN string. * diff --git a/src/move.ts b/src/move.ts index 7be3750..56f29ba 100644 --- a/src/move.ts +++ b/src/move.ts @@ -1,5 +1,5 @@ import { Board } from '@/board' -import { isKingAttackedByColor } from '@/move/attacked' +import { isKingAttackedByColor, isKingAttackedByPiece } from '@/move/attacked' import { Color, MaybePiece, @@ -7,7 +7,11 @@ import { PieceEmpty, PieceType, PieceTypeEmpty, + isBishop, isPawn, + isQueen, + isRook, + pieceColor, pieceToLetter, pieceType, pieceTypeToLetter, @@ -67,6 +71,33 @@ export function isInCheck(board: Board, color: Color): boolean { } } +/** + * Determine if the current side is in check after the opponent's (legal) move. + * + * Faster than `isInCheck` in most cases. + */ +export function isInCheckAfterOpponentsMove(board: Board, move: Move): boolean { + // En passant and castling are rare enough that we can just use normal `isInCheck` (recommended by chessprogramming) + return match(move) + .with({ kind: P.union('enPassant', 'castling') }, () => isInCheck(board, board.side)) + .with({ kind: 'normal' }, (move) => { + // The king might be attacked by the moved piece + if (isKingAttackedByPiece(board, move.to)) return true + // The king might also be attacked by a piece that was behind the moved piece. There is at most one ray that goes through the moved piece and the king, so we only need to check in one direction. + const king = board.side === Color.White ? board.kings.white : board.kings.black + const directionDelta = king.unitDeltaTowards(move.from) + if (directionDelta === null) return false + const villain = board.unsafeFindPieceInDirection(move.from, directionDelta) + if (villain === null || pieceColor(villain.piece) === board.side) return false + if (directionDelta.x === 0 || directionDelta.y === 0) { + return isRook(villain.piece) || isQueen(villain.piece) + } else { + return isBishop(villain.piece) || isQueen(villain.piece) + } + }) + .exhaustive() +} + /** * Render a move in algebraic notation. * diff --git a/src/move/attacked.ts b/src/move/attacked.ts index 81c0329..16b8785 100644 --- a/src/move/attacked.ts +++ b/src/move/attacked.ts @@ -1,7 +1,52 @@ import { Board } from '@/board' -import { MaybePiece, MaybePieceType, Color, makePiece, Piece, PieceType, PieceEmpty } from '@/piece' +import { Color, Piece, PieceEmpty, PieceType, makePiece, pieceColor, pieceType } from '@/piece' import { Coord } from '@/utils/coord' +/** + * Determine if a king is attacked by a specific piece on a specific square. + */ +export function isKingAttackedByPiece(board: Board, attacker: Coord) { + const attackerPiece = board.at(attacker) + if (attackerPiece === PieceEmpty) return false + const attackerColor = pieceColor(attackerPiece) + const kingCoord = attackerColor === Color.White ? board.kings.black : board.kings.white + switch (pieceType(attackerPiece)) { + case PieceType.Pawn: { + if (attackerColor === Color.White) { + return attacker.ne().equals(kingCoord) || attacker.nw().equals(kingCoord) + } else { + return attacker.se().equals(kingCoord) || attacker.sw().equals(kingCoord) + } + } + case PieceType.Knight: { + const deltaX = Math.abs(attacker.x - kingCoord.x) + const deltaY = Math.abs(attacker.y - kingCoord.y) + return (deltaX === 1 && deltaY === 2) || (deltaX === 2 && deltaY === 1) + } + case PieceType.Bishop: { + if (!attacker.isSameDiagonal(kingCoord)) return false + return attacker.pathTo(kingCoord, 'exclusive').every((c) => board.unsafeIsEmpty(c)) + } + case PieceType.Rook: { + if (!attacker.isSameRow(kingCoord) && !attacker.isSameColumn(kingCoord)) return false + return attacker.pathTo(kingCoord, 'exclusive').every((c) => board.unsafeIsEmpty(c)) + } + case PieceType.Queen: { + if ( + !attacker.isSameRow(kingCoord) && + !attacker.isSameColumn(kingCoord) && + !attacker.isSameDiagonal(kingCoord) + ) + return false + return attacker.pathTo(kingCoord, 'exclusive').every((c) => board.unsafeIsEmpty(c)) + } + case PieceType.King: { + // This should never happen because the last move is assumed to have been legal. + throw new Error('A king cannot attack another king.') + } + } +} + /** * Determine if a king would be attacked by pieces of a certain color, if the king was standing on that square. * diff --git a/src/move/legal.ts b/src/move/legal.ts index 6528445..36ac59b 100644 --- a/src/move/legal.ts +++ b/src/move/legal.ts @@ -1,5 +1,5 @@ import { Board } from '@/board' -import { getMoveCoords, isInCheck, Move, moveIsEqual } from '@/move' +import { getMoveCoords, isInCheck, isInCheckAfterOpponentsMove, Move, moveIsEqual } from '@/move' import { isKing } from '@/piece' import { Coord } from '@/utils/coord' import _ from 'lodash' @@ -31,7 +31,16 @@ export function isLegalMove( } // The side to move must not be in check after the move - if (isInCheck(boardAfterMove, boardBeforeMove.side)) return false + const inCheck = isInCheck(boardAfterMove, boardBeforeMove.side) + if (process.env.NODE_ENV === 'test') { + // Testing that isInCheckAfterOpponentsMove === isInCheck + const inCheck2 = isInCheckAfterOpponentsMove(boardAfterMove, move) + if (inCheck !== inCheck2) + throw new Error( + `isInCheck returned ${inCheck}, but isInCheckAfterOpponentsMove returned ${inCheck2}` + ) + } + if (inCheck) return false // If we can't rely on the move being quasi-legal (e.g. when we are checking a move that a human player wants to make), we can simply check if the move is in the list of quasi-legal moves. We don't have to worry about speed here and we already know that the move passes all legality checks *except* for actually being a quasi-legal move. if (!optAssumeQuasiLegal) { diff --git a/src/move/pieces/king.ts b/src/move/pieces/king.ts index 1d98ca0..f714676 100644 --- a/src/move/pieces/king.ts +++ b/src/move/pieces/king.ts @@ -66,7 +66,7 @@ export function kingMoves(board: Board, color: Color, coord: Coord): Move[] { const checkCastling = (move: Extract) => { const opposite = invertColor(color) return ( - move.kingFrom.pathTo(move.rookFrom, 'exclusive').every((c) => board.isEmpty(c)) && + move.kingFrom.pathTo(move.rookFrom, 'exclusive').every((c) => board.unsafeIsEmpty(c)) && move.kingFrom .pathTo(move.kingTo, 'inclusive') .every((c) => !isKingAttackedByColor(board, opposite, c)) diff --git a/src/utils/coord.ts b/src/utils/coord.ts index ac23ce6..5465e04 100644 --- a/src/utils/coord.ts +++ b/src/utils/coord.ts @@ -187,4 +187,16 @@ export class Coord { isSameColumn(to: Coord): boolean { return this.x === to.x } + + /** + * Get a unit delta that will eventually get us to the target square, if it exists. + */ + unitDeltaTowards(to: Coord): { x: 1 | 0 | -1; y: 1 | 0 | -1 } | null { + const dx = Math.sign(to.x - this.x) as -1 | 0 | 1 + const dy = Math.sign(to.y - this.y) as -1 | 0 | 1 + if (dx === 0 && dy === 0) return null + if (this.isSameColumn(to) || this.isSameRow(to) || this.isSameDiagonal(to)) + return { x: dx, y: dy } + return null + } }