Initial work on checkmate detection and pins

This commit is contained in:
Mattia Giambirtone 2023-10-17 10:31:38 +02:00
parent 942f195ddc
commit 17f15e682c
1 changed files with 115 additions and 65 deletions

View File

@ -100,6 +100,8 @@ func emptyLocation*: Location {.inline.} = (-1 , -1)
func opposite*(c: PieceColor): PieceColor {.inline.} = (if c == White: Black else: White)
proc algebraicToPosition*(s: string): Location {.inline.}
proc getCapture*(self: ChessBoard, move: Move): Location
proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move
proc makeMove*(self: ChessBoard, move: Move): Move
func emptyMove*: Move {.inline.} = Move(startSquare: emptyLocation(), targetSquare: emptyLocation(), piece: emptyPiece())
func `+`*(a, b: Location): Location = (a.row + b.row, a.col + b.col)
func isValid*(a: Location): bool = a.row in 0..7 and a.col in 0..7
@ -148,6 +150,13 @@ func getStartRow(piece: Piece): int {.inline.} =
func getLastRow(color: PieceColor): int {.inline.} =
## Retrieves the location of the last
## row relative to the given color
case color:
of White:
return 0
of Black:
return 7
else:
return -1
proc newChessboard: ChessBoard =
@ -509,7 +518,10 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
# A friendly piece is in the way
if otherPiece.color == piece.color:
break
# Target square is empty or occupied by the enemy
if otherPiece.color == piece.color.opposite:
result.add(Move(startSquare: location, targetSquare: square, piece: piece))
break
# Target square is empty
result.add(Move(startSquare: location, targetSquare: square, piece: piece))
@ -551,6 +563,53 @@ proc testMoveOffsets(self: ChessBoard, move: Move): bool =
return false
proc getAttackers*(self: ChessBoard, square: string): seq[Piece] =
## Returns all the attackers of the given square
let loc = square.algebraicToPosition()
for move in self.attacked.black:
if move.targetSquare == loc:
result.add(move.piece)
for move in self.attacked.white:
if move.targetSquare == loc:
result.add(move.piece)
# We don't use getAttackers because this one only cares about whether
# the square is attacked or not (and can therefore exit earlier than
# getAttackers)
proc isAttacked*(self: ChessBoard, loc: Location): bool =
## Returns whether the given location is attacked
## by the current opponent
let piece = self.grid[loc.row, loc.col]
case piece.color:
of White:
for move in self.attacked.black:
if move.targetSquare == loc:
return true
of Black:
for move in self.attacked.white:
if move.targetSquare == loc:
return true
of None:
case self.turn:
of White:
for move in self.attacked.black:
if move.targetSquare == loc:
return true
of Black:
for move in self.attacked.white:
if move.targetSquare == loc:
return true
else:
discard
proc isAttacked*(self: ChessBoard, square: string): bool =
## Returns whether the given square is attacked
## by the current inactive color
return self.isAttacked(square.algebraicToPosition())
proc updateAttackedSquares(self: ChessBoard) =
## Updates internal metadata about which squares
## are attacked. Called internally by doMove
@ -580,7 +639,6 @@ proc updateAttackedSquares(self: ChessBoard) =
for loc in self.pieces.black.bishops:
for move in self.generateMoves(loc):
self.attacked.black.add(move)
# White rooks
for loc in self.pieces.white.rooks:
for move in self.generateMoves(loc):
@ -599,50 +657,6 @@ proc updateAttackedSquares(self: ChessBoard) =
self.attacked.black.add(move)
proc getAttackers*(self: ChessBoard, square: string): seq[Piece] =
## Returns the attackers of the given square.
## If the square has no attackers, an empty
## seq is returned
let loc = square.algebraicToPosition()
case self.turn:
of White:
for move in self.attacked.black:
if move.targetSquare == loc:
result.add(move.piece)
of Black:
for move in self.attacked.white:
if move.targetSquare == loc:
result.add(move.piece)
else:
return @[]
# We don't use getAttackers because this one only cares about whether
# the square is attacked or not (and can therefore exit earlier than
# getAttackers)
proc isAttacked*(self: ChessBoard, loc: Location): bool =
## Returns whether the given location is attacked
## by the current inactive color
case self.turn:
of White:
for move in self.attacked.black:
if move.targetSquare == loc:
return true
return false
of Black:
for move in self.attacked.white:
if move.targetSquare == loc:
return true
return false
else:
discard
proc isAttacked*(self: ChessBoard, square: string): bool =
## Returns whether the given square is attacked
## by the current inactive color
return self.isAttacked(square.algebraicToPosition())
proc removePiece(self: ChessBoard, location: Location) =
## Removes a piece from the board, updating necessary
## metadata
@ -749,6 +763,19 @@ proc updatePositions(self: ChessBoard, move: Move) =
self.grid[move.targetSquare.row, move.targetSquare.col] = move.piece
proc inCheck(self: ChessBoard): bool =
## Returns whether the active color's
## king is in check
case self.turn:
of White:
return self.isAttacked(self.pieces.white.king)
of Black:
return self.isAttacked(self.pieces.black.king)
else:
# Unreachable
discard
proc doMove(self: ChessBoard, move: Move) =
## Internal function called by makeMove after
## performing legality checks on the given move. Can
@ -760,34 +787,57 @@ proc doMove(self: ChessBoard, move: Move) =
# pawn has moved
if self.enPassantSquare != emptyMove() and self.enPassantSquare.piece.color == self.turn.opposite():
self.enPassantSquare = emptyMove()
self.turn = self.turn.opposite()
proc undoMove(self: ChessBoard, move: Move): Move {.discardable.} =
## Undoes the given move if possible
result = Move(piece: move.piece, startSquare: move.targetSquare, targetSquare: move.startSquare)
let castling = self.castling
let enPassant = self.enPassantSquare
# Swap start and target square and do the move in reverse
self.doMove(result)
# We need to reset the entire position when the move is undone!
self.enPassantSquare = enPassant
#self.turn = self.turn.opposite()
self.castling = castling
proc checkMove(self: ChessBoard, startSquare, targetSquare: string): Move =
proc checkMove(self: ChessBoard, move: Move): bool =
## Internal function called by makeMove to check a move for legality
var pieceToMove = self.getPiece(startSquare)
# Start square doesn't contain a piece (and it isn't the en passant square)
# or it is of the wrong color for which turn it is to move
if pieceToMove.kind == Empty or pieceToMove.color != self.turn:
return emptyMove()
var destination = self.getPiece(targetSquare)
if move.piece.kind == Empty or move.piece.color != self.turn:
return false
var destination = self.grid[move.targetSquare.row, move.targetSquare.col]
# Destination square is occupied by a piece of the same color as the piece
# being moved: illegal!
if destination.kind != Empty and destination.color == self.turn:
return emptyMove()
var
startLocation = startSquare.algebraicToPosition()
targetLocation = targetSquare.algebraicToPosition()
result = Move(startSquare: startLocation, targetSquare: targetLocation, piece: pieceToMove)
if not self.testMoveOffsets(result):
return false
if not self.testMoveOffsets(move):
# Piece cannot arrive to destination (either
# because it is blocked or because the moving
# pattern is incorrect)
return emptyMove()
# TODO: Check for checks and pins (moves are currently pseudo-legal)
return false
echo move
self.doMove(move)
defer: self.undoMove(move)
# Move would reveal an attack
# on our king: not allowed
if self.inCheck():
echo "in check"
return false
# All checks have passed: move is legal
result = true
proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} =
## Like the other makeMove(), but with a Move object
result = move
if not self.checkMove(move):
return emptyMove()
self.doMove(result)
self.turn = self.turn.opposite()
proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.discardable.} =
## Makes a move on the board from the chosen start square to
@ -797,11 +847,11 @@ proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.disc
## For efficiency purposes, no exceptions are raised if the move is
## illegal, but the move's piece kind will be Empty (its color will be None
## too) and the locations will both be set to the tuple (-1, -1)
result = self.checkMove(startSquare, targetSquare)
if result == emptyMove():
return
self.doMove(result)
var
startLocation = startSquare.algebraicToPosition()
targetLocation = targetSquare.algebraicToPosition()
result = Move(startSquare: startLocation, targetSquare: targetLocation, piece: self.grid[startLocation.row, startLocation.col])
return self.makeMove(result)