Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP idk what #24

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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.
*
Expand Down
33 changes: 32 additions & 1 deletion src/move.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { Board } from '@/board'
import { isKingAttackedByColor } from '@/move/attacked'
import { isKingAttackedByColor, isKingAttackedByPiece } from '@/move/attacked'
import {
Color,
MaybePiece,
Piece,
PieceEmpty,
PieceType,
PieceTypeEmpty,
isBishop,
isPawn,
isQueen,
isRook,
pieceColor,
pieceToLetter,
pieceType,
pieceTypeToLetter,
Expand Down Expand Up @@ -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.
*
Expand Down
47 changes: 46 additions & 1 deletion src/move/attacked.ts
Original file line number Diff line number Diff line change
@@ -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.
*
Expand Down
13 changes: 11 additions & 2 deletions src/move/legal.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/move/pieces/king.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export function kingMoves(board: Board, color: Color, coord: Coord): Move[] {
const checkCastling = (move: Extract<Move, { kind: 'castling' }>) => {
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))
Expand Down
12 changes: 12 additions & 0 deletions src/utils/coord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}