Switch to a legal move generator

This commit is contained in:
Mattia Giambirtone 2023-10-23 18:02:31 +02:00
parent f9744c077b
commit b0ebdc02a6
1 changed files with 341 additions and 235 deletions

View File

@ -122,7 +122,6 @@ func emptyPiece*: Piece {.inline.} = Piece(kind: Empty, color: None)
func emptyLocation*: Location {.inline.} = (-1 , -1) func emptyLocation*: Location {.inline.} = (-1 , -1)
func opposite*(c: PieceColor): PieceColor {.inline.} = (if c == White: Black else: White) func opposite*(c: PieceColor): PieceColor {.inline.} = (if c == White: Black else: White)
proc algebraicToLocation*(s: string): Location {.inline.} proc algebraicToLocation*(s: string): Location {.inline.}
func getCapture*(self: ChessBoard, move: Move): Location
proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.discardable.} proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.discardable.}
proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.}
proc makeMove*(self: ChessBoard, startSquare, targetSquare: Location): Move {.discardable.} proc makeMove*(self: ChessBoard, startSquare, targetSquare: Location): Move {.discardable.}
@ -130,11 +129,10 @@ func emptyMove*: Move {.inline.} = Move(startSquare: emptyLocation(), targetSqua
func `+`*(a, b: Location): Location = (a.row + b.row, a.col + b.col) func `+`*(a, b: Location): Location = (a.row + b.row, a.col + b.col)
func isValid*(a: Location): bool {.inline.} = a.row in 0..7 and a.col in 0..7 func isValid*(a: Location): bool {.inline.} = a.row in 0..7 and a.col in 0..7
proc generateMoves(self: ChessBoard, location: Location): seq[Move] proc generateMoves(self: ChessBoard, location: Location): seq[Move]
proc isAttacked*(self: ChessBoard, loc: Location): bool proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): bool
proc undoMove*(self: ChessBoard, move: Move) proc undoMove*(self: ChessBoard, move: Move)
proc isLegal(self: ChessBoard, move: Move, keep: bool = false): bool proc isLegal(self: ChessBoard, move: Move): bool {.inline.}
proc doMove(self: ChessBoard, move: Move) proc doMove(self: ChessBoard, move: Move)
proc isLegalFast(self: ChessBoard, move: Move, keep: bool = false): bool
proc pretty*(self: ChessBoard): string proc pretty*(self: ChessBoard): string
proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) proc spawnPiece(self: ChessBoard, location: Location, piece: Piece)
proc updateAttackedSquares(self: ChessBoard) proc updateAttackedSquares(self: ChessBoard)
@ -535,18 +533,16 @@ func getPiece*(self: ChessBoard, square: string): Piece =
func isCapture*(self: ChessBoard, move: Move): bool {.inline.} = func isCapture*(move: Move): bool {.inline.} =
## Returns whether the given move is a capture ## Returns whether the given move is a capture
## or not ## or not
return move.flag in [Capture, EnPassant] return move.flag in [Capture, EnPassant]
func getCapture*(self: ChessBoard, move: Move): Location = func isPromotion*(move: Move): bool {.inline.} =
## Returns the location that would be captured if this ## Returns whrther the given move is a
## move were played on the board ## pawn promotion or not
if not self.isCapture(move): return move.flag in [PromoteToBishop, PromoteToKnight, PromoteToRook, PromoteToQueen]
return emptyLocation()
return move.targetSquare
proc inCheck*(self: ChessBoard, color: PieceColor = None): bool = proc inCheck*(self: ChessBoard, color: PieceColor = None): bool =
@ -559,9 +555,9 @@ proc inCheck*(self: ChessBoard, color: PieceColor = None): bool =
color = self.getActiveColor() color = self.getActiveColor()
case color: case color:
of White: of White:
return self.isAttacked(self.position.pieces.white.king) return self.isAttacked(self.position.pieces.white.king, color)
of Black: of Black:
return self.isAttacked(self.position.pieces.black.king) return self.isAttacked(self.position.pieces.black.king, color)
else: else:
# Unreachable # Unreachable
discard discard
@ -586,65 +582,67 @@ proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king:
of None: of None:
# Unreachable # Unreachable
discard discard
var if result.king or result.queen:
loc: Location var
queenSide: Location loc: Location
kingSide: Location queenSide: Location
# If the path between the king and a rook is blocked, then castling kingSide: Location
# is prohibited on that side if self.inCheck(color):
case color: # King can not castle out of check
of White: return (false, false)
loc = self.position.pieces.white.king # If the path between the king and rook on a given side is blocked or any of the
queenSide = color.leftSide() # squares where the king would travel to are attacked by the opponent,
kingSide = color.rightSide() # then castling is (temporarily) prohibited on that side
of Black: case color:
loc = self.position.pieces.black.king of White:
queenSide = color.rightSide() loc = self.position.pieces.white.king
kingSide = color.leftSide() queenSide = color.leftSide()
of None: kingSide = color.rightSide()
# Unreachable of Black:
discard loc = self.position.pieces.black.king
queenSide = color.rightSide()
kingSide = color.leftSide()
of None:
# Unreachable
discard
if result.king: if result.king:
# Short castle # Short castle
var location = loc var location = loc
while true: while true:
location = location + kingSide location = location + kingSide
if not location.isValid(): if not location.isValid():
break break
if self.grid[location.row, location.col].kind == Empty: if self.grid[location.row, location.col].color == color:
continue # Blocked by own piece
if location == color.kingSideRook() and self.grid[location.row, location.col].kind == Rook: result.king = false
break break
# Blocked by a piece # Square is attacked or blocked by enemy piece
result.king = false if self.isAttacked(location, color) or self.grid[location.row, location.col].color != None:
break result.king = false
break
if result.queen: # Square is occupied by our rook: we're done. No need to check the color or type of it (because
# Long castle # if it weren't the right color, castling rights would've already been lost and we wouldn't
var location = loc # have got this far)
while true: if location == color.kingSideRook():
location = location + queenSide break
if not location.isValid(): # Square is empty and not attacked. Keep going
break
if self.grid[location.row, location.col].kind == Empty:
continue
if location == color.queenSideRook() and self.grid[location.row, location.col].kind == Rook:
break
# Blocked by a piece
result.queen = false
break
# If the castling king would walk into, through or out of check
# while castling on a given side, then it is not possible to castle
# on that side until the threat exists
if (result.king or result.queen) and self.inCheck(color): if result.queen:
# Only check for checks if castling is still available # Long castle
# by this point (if we can avoid calls to generateMoves, var location = loc
# we should) while true:
return location = location + queenSide
# TODO: Check for attacks in the various other squares if not location.isValid():
break
if self.grid[location.row, location.col].color == color:
result.queen = false
break
if self.isAttacked(location, color) or self.grid[location.row, location.col].color != None:
result.queen = false
break
if location == color.queenSideRook():
break
proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
@ -656,48 +654,46 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
flags: seq[MoveFlag] = @[] flags: seq[MoveFlag] = @[]
doAssert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}" doAssert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}"
# Pawns can move forward one square # Pawns can move forward one square
let forwardOffset = piece.color.forward() let forward = (piece.color.forward() + location)
let forward = (forwardOffset + location)
# Only if the square is empty though # Only if the square is empty though
if forward.isValid() and self.grid[forward.row, forward.col].color == None: if forward.isValid() and self.grid[forward.row, forward.col].color == None:
locations.add(forwardOffset) locations.add(forward)
flags.add(Default) flags.add(Default)
# If the pawn is on its first rank, it can push two squares # If the pawn is on its first rank, it can push two squares
if location.row == piece.getStartRow(): if location.row == piece.getStartRow():
let doubleOffset = piece.color.doublePush() let double = location + piece.color.doublePush()
let double = location + doubleOffset # Check that both squares are empty
# Check if both squares are available
if double.isValid() and self.grid[forward.row, forward.col].color == None and self.grid[double.row, double.col].color == None: if double.isValid() and self.grid[forward.row, forward.col].color == None and self.grid[double.row, double.col].color == None:
locations.add(piece.color.doublePush()) locations.add(double)
flags.add(DoublePush) flags.add(DoublePush)
# If the target is the en passant square and the piece under en passant
# is of the opposite color of the pawn being moved, then we check if we
# can do an en passant capture
if self.position.enPassantSquare.piece.color == piece.color.opposite: if self.position.enPassantSquare.piece.color == piece.color.opposite:
if abs(self.position.enPassantSquare.targetSquare.col - location.col) == 1 and abs(self.position.enPassantSquare.targetSquare.row - location.row) == 1: if abs(self.position.enPassantSquare.targetSquare.col - location.col) == 1 and abs(self.position.enPassantSquare.targetSquare.row - location.row) == 1:
# Only viable if the piece is on the diagonal of the target # Only viable if the piece is on the front diagonal of the target
locations.add(self.position.enPassantSquare.targetSquare) locations.add(self.position.enPassantSquare.targetSquare)
flags.add(EnPassant) flags.add(EnPassant)
# They can also move on either diagonal one # They can also move on either diagonal one
# square, but only to capture # square, but only to capture
var diagonal = piece.color.topRightDiagonal() var diagonal = location + piece.color.topRightDiagonal()
if (diagonal + location).isValid() and self.isCapture(Move(piece: piece, startSquare: location, targetSquare: location + diagonal)): if diagonal.isValid() and self.grid[diagonal.row, diagonal.col].color == piece.color.opposite() and self.grid[diagonal.row, diagonal.col].kind != King:
locations.add(diagonal) locations.add(diagonal)
flags.add(Capture) flags.add(Capture)
diagonal = piece.color.topLeftDiagonal() diagonal = location + piece.color.topLeftDiagonal()
if (diagonal + location).isValid() and self.isCapture(Move(piece: piece, startSquare: location, targetSquare: location + diagonal)): if diagonal.isValid() and self.grid[diagonal.row, diagonal.col].color == piece.color.opposite() and self.grid[diagonal.row, diagonal.col].kind != King:
locations.add(diagonal) locations.add(diagonal)
flags.add(Capture) flags.add(Capture)
var var targetPiece: Piece
newLocation: Location
targetPiece: Piece
for (target, flag) in zip(locations, flags): for (target, flag) in zip(locations, flags):
newLocation = location + target targetPiece = self.grid[target.row, target.col]
targetPiece = self.grid[newLocation.row, newLocation.col] if target.row == piece.color.getLastRow():
if newLocation.row == piece.color.getLastRow():
# Pawn reached the other side of the board: generate all potential piece promotions # Pawn reached the other side of the board: generate all potential piece promotions
for promotionType in [PromoteToKnight, PromoteToBishop, PromoteToRook, PromoteToQueen]: for promotionType in [PromoteToKnight, PromoteToBishop, PromoteToRook, PromoteToQueen]:
result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece, flag: promotionType)) result.add(Move(startSquare: location, targetSquare: target, piece: piece, flag: promotionType))
continue continue
result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece, flag: flag)) result.add(Move(startSquare: location, targetSquare: target, piece: piece, flag: flag))
proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
@ -734,7 +730,9 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
if otherPiece.color == piece.color.opposite: if otherPiece.color == piece.color.opposite:
# Target square contains an enemy piece: capture # Target square contains an enemy piece: capture
# it and stop going any further # it and stop going any further
result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: Capture)) if otherPiece.kind != King:
# Can't capture the king
result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: Capture))
break break
# Target square is empty # Target square is empty
result.add(Move(startSquare: location, targetSquare: square, piece: piece)) result.add(Move(startSquare: location, targetSquare: square, piece: piece))
@ -754,30 +752,30 @@ proc generateKingMoves(self: ChessBoard, location: Location): seq[Move] =
piece.color.leftSide(), piece.color.leftSide(),
piece.color.rightSide()] piece.color.rightSide()]
# Castling # Castling
let canCastle = self.canCastle() let canCastle = self.canCastle(piece.color)
if canCastle.queen: if canCastle.queen:
directions.add(longCastleKing()) directions.add(longCastleKing())
if canCastle.king: if canCastle.king:
directions.add(shortCastleKing()) directions.add(shortCastleKing())
var flag = Default var flag = Default
for direction in directions: for direction in directions:
# Step in this direction once
let square: Location = location + direction
# End of board reached
if not square.isValid():
continue
if direction == longCastleKing(): if direction == longCastleKing():
flag = CastleLong flag = CastleLong
elif direction == shortCastleKing(): elif direction == shortCastleKing():
flag = CastleShort flag = CastleShort
else: else:
flag = Default flag = Default
# Step in this direction once
let square: Location = location + direction
# End of board reached
if not square.isValid():
continue
let otherPiece = self.grid[square.row, square.col] let otherPiece = self.grid[square.row, square.col]
if otherPiece.color == self.getActiveColor.opposite(): if otherPiece.color == self.getActiveColor.opposite():
flag = Capture flag = Capture
# A friendly piece is in the way, move onto the next # A friendly piece or the opponent king is in the way,
# direction # move onto the next direction
if otherPiece.color == piece.color: if otherPiece.color == piece.color or otherPiece.kind == King:
continue continue
# Target square is empty or contains an enemy piece: # Target square is empty or contains an enemy piece:
# All good for us! # All good for us!
@ -804,20 +802,20 @@ proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] =
if not square.isValid(): if not square.isValid():
continue continue
let otherPiece = self.grid[square.row, square.col] let otherPiece = self.grid[square.row, square.col]
# A friendly piece is in the way # A friendly piece or the opponent king is is in the way
if otherPiece.color == piece.color: if otherPiece.color == piece.color or otherPiece.kind == King:
continue continue
if otherPiece.color == piece.color.opposite: if otherPiece.color != None:
# Target square contains an enemy piece: capture # Target square contains an enemy piece: capture
# it # it
result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: Capture)) result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: Capture))
continue else:
# Target square is empty # Target square is empty
result.add(Move(startSquare: location, targetSquare: square, piece: piece)) result.add(Move(startSquare: location, targetSquare: square, piece: piece))
proc generateMoves(self: ChessBoard, location: Location): seq[Move] = proc generateMoves(self: ChessBoard, location: Location): seq[Move] =
## Returns the list of possible pseudo-legal chess moves for the ## Returns the list of possible legal chess moves for the
## piece in the given location ## piece in the given location
let piece = self.grid[location.row, location.col] let piece = self.grid[location.row, location.col]
case piece.kind: case piece.kind:
@ -834,7 +832,7 @@ proc generateMoves(self: ChessBoard, location: Location): seq[Move] =
proc generateAllMoves*(self: ChessBoard): seq[Move] = proc generateAllMoves*(self: ChessBoard): seq[Move] =
## Returns the list of all possible pseudo-legal moves ## Returns the list of all possible legal moves
## in the current position ## in the current position
for i, row in self.grid: for i, row in self.grid:
for j, piece in row: for j, piece in row:
@ -843,23 +841,36 @@ proc generateAllMoves*(self: ChessBoard): seq[Move] =
result.add(move) result.add(move)
proc getAttackers*(self: ChessBoard, square: Location): seq[Location] = proc getAttackers*(self: ChessBoard, square: Location, color = None): seq[Location] =
## Returns all the attackers of the given square ## Returns all the attackers of the given square
for attack in self.position.attacked.black: ## for the given color (a color of None means use
if attack.dest == square: ## the currently active color)
result.add(attack.source) var color = color
for attack in self.position.attacked.white: if color == None:
if attack.dest == square: color = self.getActiveColor()
result.add(attack.source) case color:
of White:
for attack in self.position.attacked.black:
if attack.dest == square:
result.add(attack.source)
of Black:
for attack in self.position.attacked.white:
if attack.dest == square:
result.add(attack.source)
else:
# Unreachable
discard
# We don't use getAttackers because this one only cares about whether # We don't use getAttackers because this one only cares about whether
# the square is attacked or not (and can therefore exit earlier than # the square is attacked or not (and can therefore exit earlier than
# getAttackers) # getAttackers)
proc isAttacked*(self: ChessBoard, loc: Location): bool = proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): bool =
## Returns whether the given location is attacked ## Returns whether the given location is attacked
## by the current opponent ## by the given opponent
let piece = self.grid[loc.row, loc.col] var color = color
case piece.color: if color == None:
color = self.getActiveColor()
case color:
of White: of White:
for attack in self.position.attacked.black: for attack in self.position.attacked.black:
if attack.dest == loc: if attack.dest == loc:
@ -869,17 +880,7 @@ proc isAttacked*(self: ChessBoard, loc: Location): bool =
if attack.dest == loc: if attack.dest == loc:
return true return true
of None: of None:
case self.getActiveColor(): discard
of White:
for attack in self.position.attacked.black:
if attack.dest == loc:
return true
of Black:
for attack in self.position.attacked.black:
if attack.dest == loc:
return true
else:
discard
proc isAttacked*(self: ChessBoard, square: string): bool = proc isAttacked*(self: ChessBoard, square: string): bool =
@ -888,20 +889,66 @@ proc isAttacked*(self: ChessBoard, square: string): bool =
return self.isAttacked(square.algebraicToLocation()) return self.isAttacked(square.algebraicToLocation())
func addAttack(self: ChessBoard, attack: tuple[source, dest: Location], color: PieceColor) {.inline.} =
if attack.source.isValid() and attack.dest.isValid():
case color:
of White:
self.position.attacked.white.add(attack)
of Black:
self.position.attacked.black.add(attack)
else:
discard
proc updatePawnAttacks(self: ChessBoard) = proc updatePawnAttacks(self: ChessBoard) =
## Internal helper of updateAttackedSquares ## Internal helper of updateAttackedSquares
for loc in self.position.pieces.white.pawns: for loc in self.position.pieces.white.pawns:
# Pawns are special in how they capture (i.e. the # Pawns are special in how they capture (i.e. the
# squares they can move to do not match the squares # squares they can move to do not match the squares
# they can capture on. Sneaky fucks) # they can capture on. Sneaky fucks)
let piece = self.grid[loc.row, loc.col] self.addAttack((loc, loc + White.topRightDiagonal()), White)
self.position.attacked.white.add((loc, loc + piece.color.topRightDiagonal())) self.addAttack((loc, loc + White.topRightDiagonal()), White)
self.position.attacked.white.add((loc, loc + piece.color.topLeftDiagonal()))
# We do the same thing for black # We do the same thing for black
for loc in self.position.pieces.black.pawns: for loc in self.position.pieces.black.pawns:
let piece = self.grid[loc.row, loc.col] self.addAttack((loc, loc + Black.topRightDiagonal()), Black)
self.position.attacked.black.add((loc, loc + piece.color.topRightDiagonal())) self.addAttack((loc, loc + Black.topRightDiagonal()), Black)
self.position.attacked.black.add((loc, loc + piece.color.topLeftDiagonal()))
proc updateKingAttacks(self: ChessBoard) =
## Internal helper of updateAttackedSquares
var king = self.position.pieces.white.king
self.addAttack((king, king + White.topRightDiagonal()), White)
self.addAttack((king, king + White.topLeftDiagonal()), White)
self.addAttack((king, king + White.bottomLeftDiagonal()), White)
self.addAttack((king, king + White.bottomRightDiagonal()), White)
king = self.position.pieces.black.king
self.addAttack((king, king + Black.topRightDiagonal()), Black)
self.addAttack((king, king + Black.topLeftDiagonal()), Black)
self.addAttack((king, king + Black.bottomLeftDiagonal()), Black)
self.addAttack((king, king + Black.bottomRightDiagonal()), Black)
proc updateKnightAttacks(self: ChessBoard) =
## Internal helper of updateAttackedSquares
for loc in self.position.pieces.white.knights:
self.addAttack((loc, loc + White.topLeftKnightMove()), White)
self.addAttack((loc, loc + White.topRightKnightMove()), White)
self.addAttack((loc, loc + White.bottomLeftKnightMove()), White)
self.addAttack((loc, loc + White.bottomRightKnightMove()), White)
self.addAttack((loc, loc + White.topLeftKnightMove(long=false)), White)
self.addAttack((loc, loc + White.topRightKnightMove(long=false)), White)
self.addAttack((loc, loc + White.bottomLeftKnightMove(long=false)), White)
self.addAttack((loc, loc + White.bottomRightKnightMove(long=false)), White)
for loc in self.position.pieces.black.knights:
self.addAttack((loc, loc + Black.topLeftKnightMove()), Black)
self.addAttack((loc, loc + Black.topRightKnightMove()), Black)
self.addAttack((loc, loc + Black.bottomLeftKnightMove()), Black)
self.addAttack((loc, loc + Black.bottomRightKnightMove()), Black)
self.addAttack((loc, loc + Black.topLeftKnightMove(long=false)), Black)
self.addAttack((loc, loc + Black.topRightKnightMove(long=false)), Black)
self.addAttack((loc, loc + Black.bottomLeftKnightMove(long=false)), Black)
self.addAttack((loc, loc + Black.bottomRightKnightMove(long=false)), Black)
proc getSlidingAttacks(self: ChessBoard, loc: Location): Attacked = proc getSlidingAttacks(self: ChessBoard, loc: Location): Attacked =
@ -947,24 +994,24 @@ proc updateSlidingAttacks(self: ChessBoard) =
# Bishops # Bishops
for loc in self.position.pieces.white.bishops: for loc in self.position.pieces.white.bishops:
for attack in self.getSlidingAttacks(loc): for attack in self.getSlidingAttacks(loc):
self.position.attacked.white.add(attack) self.addAttack(attack, White)
for loc in self.position.pieces.black.bishops: for loc in self.position.pieces.black.bishops:
for attack in self.getSlidingAttacks(loc): for attack in self.getSlidingAttacks(loc):
self.position.attacked.black.add(attack) self.addAttack(attack, Black)
# Rooks # Rooks
for loc in self.position.pieces.white.rooks: for loc in self.position.pieces.white.rooks:
for attack in self.getSlidingAttacks(loc): for attack in self.getSlidingAttacks(loc):
self.position.attacked.white.add(attack) self.addAttack(attack, White)
for loc in self.position.pieces.black.rooks: for loc in self.position.pieces.black.rooks:
for attack in self.getSlidingAttacks(loc): for attack in self.getSlidingAttacks(loc):
self.position.attacked.black.add(attack) self.addAttack(attack, Black)
# Queens # Queens
for loc in self.position.pieces.white.queens: for loc in self.position.pieces.white.queens:
for attack in self.getSlidingAttacks(loc): for attack in self.getSlidingAttacks(loc):
self.position.attacked.white.add(attack) self.addAttack(attack, White)
for loc in self.position.pieces.black.queens: for loc in self.position.pieces.black.queens:
for attack in self.getSlidingAttacks(loc): for attack in self.getSlidingAttacks(loc):
self.position.attacked.black.add(attack) self.addAttack(attack, Black)
proc updateAttackedSquares(self: ChessBoard) = proc updateAttackedSquares(self: ChessBoard) =
@ -977,28 +1024,16 @@ proc updateAttackedSquares(self: ChessBoard) =
self.updatePawnAttacks() self.updatePawnAttacks()
# Sliding pieces # Sliding pieces
self.updateSlidingAttacks() self.updateSlidingAttacks()
# Knights self.updateKnightAttacks()
for loc in self.position.pieces.white.knights: # Kings
for move in self.generateMoves(loc): self.updateKingAttacks()
self.position.attacked.white.add((move.startSquare, move.targetSquare))
# King
for move in self.generateMoves(self.position.pieces.white.king):
self.position.attacked.white.add((move.startSquare, move.targetSquare))
# Knights
for loc in self.position.pieces.black.knights:
for move in self.generateMoves(loc):
self.position.attacked.black.add((move.startSquare, move.targetSquare))
# King
for move in self.generateMoves(self.position.pieces.black.king):
self.position.attacked.black.add((move.startSquare, move.targetSquare))
proc removePiece(self: ChessBoard, location: Location, emptyGrid: bool = true) = proc removePiece(self: ChessBoard, location: Location, attack: bool = true) =
## Removes a piece from the board, updating necessary ## Removes a piece from the board, updating necessary
## metadata ## metadata
var piece = self.grid[location.row, location.col] var piece = self.grid[location.row, location.col]
if emptyGrid: self.grid[location.row, location.col] = emptyPiece()
self.grid[location.row, location.col] = emptyPiece()
case piece.color: case piece.color:
of White: of White:
case piece.kind: case piece.kind:
@ -1034,6 +1069,8 @@ proc removePiece(self: ChessBoard, location: Location, emptyGrid: bool = true) =
discard discard
else: else:
discard discard
if attack:
self.updateAttackedSquares()
proc movePiece(self: ChessBoard, move: Move, attack: bool = true) = proc movePiece(self: ChessBoard, move: Move, attack: bool = true) =
@ -1108,10 +1145,14 @@ proc movePiece(self: ChessBoard, startSquare, targetSquare: Location, attack: bo
proc updateLocations(self: ChessBoard, move: Move) = proc updateLocations(self: ChessBoard, move: Move) =
## Internal helper to update the position of ## Internal helper to update the position of
## the pieces on the board after a move ## the pieces on the board after a move
let capture = self.getCapture(move) if move.isCapture():
if capture != emptyLocation(): self.position.captured = self.grid[move.targetSquare.row, move.targetSquare.col]
self.position.captured = self.grid[capture.row, capture.col] try:
self.removePiece(capture) self.removePiece(move.targetSquare, attack=false)
except AssertionDefect:
echo move
raise
# Update the positional metadata of the moving piece # Update the positional metadata of the moving piece
self.movePiece(move) self.movePiece(move)
@ -1132,8 +1173,7 @@ proc doMove(self: ChessBoard, move: Move) =
halfMoveClock = self.position.halfMoveClock halfMoveClock = self.position.halfMoveClock
fullMoveCount = self.position.fullMoveCount fullMoveCount = self.position.fullMoveCount
castlingAvailable = self.position.castlingAvailable castlingAvailable = self.position.castlingAvailable
let capture = self.getCapture(move) if move.piece.kind == Pawn or move.isCapture():
if move.piece.kind == Pawn or self.isCapture(move):
halfMoveClock = 0 halfMoveClock = 0
else: else:
inc(halfMoveClock) inc(halfMoveClock)
@ -1161,22 +1201,22 @@ proc doMove(self: ChessBoard, move: Move) =
else: else:
discard discard
# Has a rook been captured? # Has a rook been captured?
if capture != emptyLocation(): if move.isCapture():
let piece = self.grid[capture.row, capture.col] let piece = self.grid[move.targetSquare.row, move.targetSquare.col]
if piece.kind == Rook: if piece.kind == Rook:
case piece.color: case piece.color:
of White: of White:
if capture == piece.color.queenSideRook(): if move.targetSquare == piece.color.queenSideRook():
# Queen side # Queen side
castlingAvailable.white.queen = false castlingAvailable.white.queen = false
elif capture == piece.color.kingSideRook(): elif move.targetSquare == piece.color.kingSideRook():
# King side # King side
castlingAvailable.white.king = false castlingAvailable.white.king = false
of Black: of Black:
if capture == piece.color.queenSideRook(): if move.targetSquare == piece.color.queenSideRook():
# Queen side # Queen side
castlingAvailable.black.queen = false castlingAvailable.black.queen = false
elif capture == piece.color.kingSideRook(): elif move.targetSquare == piece.color.kingSideRook():
# King side # King side
castlingAvailable.black.king = false castlingAvailable.black.king = false
else: else:
@ -1193,7 +1233,22 @@ proc doMove(self: ChessBoard, move: Move) =
castlingAvailable.black.queen = false castlingAvailable.black.queen = false
else: else:
discard discard
if move.isPromotion():
# Move is a pawn promotion: get rid of then pawn
# and spawn a new piece
self.removePiece(move.startSquare)
case move.flag:
of PromoteToBishop:
self.spawnPiece(move.targetSquare, Piece(kind: Bishop, color: move.piece.color))
of PromoteToKnight:
self.spawnPiece(move.targetSquare, Piece(kind: Knight, color: move.piece.color))
of PromoteToRook:
self.spawnPiece(move.targetSquare, Piece(kind: Rook, color: move.piece.color))
of PromoteToQueen:
self.spawnPiece(move.targetSquare, Piece(kind: Queen, color: move.piece.color))
else:
discard
let previous = self.position let previous = self.position
# Record final position for future reference # Record final position for future reference
self.positions.add(previous) self.positions.add(previous)
@ -1211,6 +1266,16 @@ proc doMove(self: ChessBoard, move: Move) =
var location: Location var location: Location
if move.flag in [CastleShort, CastleLong]: if move.flag in [CastleShort, CastleLong]:
# Revoke all castling rights for the moving king
case move.piece.color:
of White:
self.position.castlingAvailable.white.king = false
self.position.castlingAvailable.white.queen = false
of Black:
self.position.castlingAvailable.black.king = false
self.position.castlingAvailable.black.queen = false
else:
discard
# Move the rook onto the # Move the rook onto the
# correct file # correct file
var var
@ -1283,63 +1348,82 @@ proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
self.grid[location.row, location.col] = piece self.grid[location.row, location.col] = piece
proc resetBoard*(self: ChessBoard) =
## Resets the internal grid representation
## according to the positional data stored
## in the chessboard. Warning: this can be
## expensive, especially in critical paths
## or tight loops
self.grid = newMatrixFromSeq[Piece](empty, (8, 8))
for loc in self.position.pieces.white.pawns:
self.grid[loc.row, loc.col] = Piece(color: White, kind: Pawn)
for loc in self.position.pieces.black.pawns:
self.grid[loc.row, loc.col] = Piece(color: Black, kind: Pawn)
for loc in self.position.pieces.white.bishops:
self.grid[loc.row, loc.col] = Piece(color: White, kind: Bishop)
for loc in self.position.pieces.black.bishops:
self.grid[loc.row, loc.col] = Piece(color: Black, kind: Bishop)
for loc in self.position.pieces.white.knights:
self.grid[loc.row, loc.col] = Piece(color: White, kind: Knight)
for loc in self.position.pieces.black.knights:
self.grid[loc.row, loc.col] = Piece(color: Black, kind: Knight)
for loc in self.position.pieces.white.rooks:
self.grid[loc.row, loc.col] = Piece(color: White, kind: Rook)
for loc in self.position.pieces.black.rooks:
self.grid[loc.row, loc.col] = Piece(color: Black, kind: Rook)
for loc in self.position.pieces.white.queens:
self.grid[loc.row, loc.col] = Piece(color: White, kind: Queen)
for loc in self.position.pieces.black.queens:
self.grid[loc.row, loc.col] = Piece(color: Black, kind: Queen)
self.grid[self.position.pieces.white.king.row, self.position.pieces.white.king.col] = Piece(color: White, kind: King)
self.grid[self.position.pieces.black.king.row, self.position.pieces.black.king.col] = Piece(color: Black, kind: King)
proc undoMove*(self: ChessBoard, move: Move) = proc undoMove*(self: ChessBoard, move: Move) =
## Undoes the given move ## Undoes the given move
if self.positions.len() == 0: if self.positions.len() == 0:
return return
let previous = self.position
var position = self.positions[^1] var position = self.positions[^1]
while true: while position.move != move:
if position.move == move:
break
discard self.positions.pop() discard self.positions.pop()
position = self.positions[^1] position = self.positions[^1]
self.position = position
self.grid[move.startSquare.row, move.startSquare.col] = move.piece self.grid[move.startSquare.row, move.startSquare.col] = move.piece
if self.isCapture(move): if move.isCapture():
self.grid[move.targetSquare.row, move.targetSquare.col] = self.position.captured self.grid[move.targetSquare.row, move.targetSquare.col] = previous.captured
else: else:
self.grid[move.targetSquare.row, move.targetSquare.col] = emptyPiece() self.grid[move.targetSquare.row, move.targetSquare.col] = emptyPiece()
self.position = position # Reset the location of the rook in the
# grid (it's already correct in the piece
# list)
if move.flag == CastleLong:
let
rookOld = move.targetSquare + move.piece.color.rightSide()
rookNew = move.piece.color.queenSideRook()
self.grid[rookNew.row, rookNew.col] = self.grid[rookOld.row, rookOld.col]
self.grid[rookOld.row, rookOld.col] = emptyPiece()
if move.flag == CastleShort:
let
rookOld = move.targetSquare + move.piece.color.leftSide()
rookNew = move.piece.color.kingSideRook()
self.grid[rookNew.row, rookNew.col] = self.grid[rookOld.row, rookOld.col]
self.grid[rookOld.row, rookOld.col] = emptyPiece()
proc isLegal(self: ChessBoard, move: Move): bool {.inline.} =
proc isLegal(self: ChessBoard, move: Move, keep: bool = false): bool =
## Returns whether the given move is legal ## Returns whether the given move is legal
self.doMove(move) return move in self.generateMoves(move.startSquare)
if move notin self.generateMoves(move.startSquare):
# Piece cannot arrive to destination (blocked
# or otherwise invalid move)
return false
if not keep:
defer: self.undoMove(move)
# Move would reveal an attack
# on our king: not allowed
return self.inCheck(move.piece.color)
proc isLegalFast(self: ChessBoard, move: Move, keep: bool = false): bool =
## Returns whether the given move is legal
## assuming that the input move is pseudo legal
self.position.move = move
self.doMove(move)
if not keep:
defer: self.undoMove(move)
# Move would reveal an attack
# on our king: not allowed
if self.inCheck(move.piece.color):
return false
# All checks have passed: move is legal
result = true
proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} = proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} =
## Like the other makeMove(), but with a Move object ## Like the other makeMove(), but with a Move object
result = move result = move
self.position.move = move self.position.move = move
let legal = self.isLegal(move, keep=true) if not self.isLegal(move):
if not legal:
return emptyMove() return emptyMove()
self.doMove(move)
result = self.position.move result = self.position.move
@ -1409,37 +1493,59 @@ proc pretty*(self: ChessBoard): string =
result &= "\x1b[0m" result &= "\x1b[0m"
proc countLegalMoves*(self: ChessBoard, ply: int, verbose: bool = false, current: int = 0): int = proc countLegalMoves*(self: ChessBoard, ply: int, verbose: bool = false, current: int = 0): tuple[nodes: int, captures: int, castles: int, checks: int, promotions: int] =
## Counts (and debugs) the number of legal positions reached after ## Counts (and debugs) the number of legal positions reached after
## the given number of half moves ## the given number of ply
if ply == 0: if ply == 0:
result = 1 result = (1, 0, 0, 0, 0)
else: else:
var before: string var
before: string
for move in self.generateAllMoves(): for move in self.generateAllMoves():
before = self.pretty() before = self.pretty()
self.doMove(move) self.doMove(move)
if not self.inCheck(move.piece.color): case move.flag:
if verbose: of Capture:
let canCastle = self.canCastle() inc(result.captures)
echo "\x1Bc" of CastleShort, CastleLong:
echo &"Ply: {self.position.plyFromRoot} (move {current + result + 1})" inc(result.castles)
echo &"Move: {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()} (({move.startSquare.row}, {move.startSquare.col}) -> ({move.targetSquare.row}, {move.targetSquare.col}))" of PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook:
echo &"Turn: {move.piece.color}" inc(result.promotions)
echo &"Piece: {move.piece.kind}" else:
echo &"Flag: {move.flag}" discard
echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" if self.inCheck():
echo "\nBefore:" # Opponent king is in check
echo before inc(result.checks)
echo "\nNow: " if verbose:
echo self.pretty() let canCastle = self.canCastle(move.piece.color)
try: echo "\x1Bc"
discard readLine(stdin) echo &"Ply: {self.position.plyFromRoot} (move {current + result.nodes + 1})"
except IOError: echo &"Move: {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()} (({move.startSquare.row}, {move.startSquare.col}) -> ({move.targetSquare.row}, {move.targetSquare.col}))"
discard echo &"Turn: {move.piece.color}"
except EOFError: echo &"Piece: {move.piece.kind}"
discard echo &"Flag: {move.flag}"
result += self.countLegalMoves(ply - 1, verbose, result + 1) echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
echo "\nBefore:"
echo before
echo "\nNow: "
echo self.pretty()
echo &"\n\nTotal captures: {result.captures}"
echo &"Total castles: {result.castles}"
echo &"Total checks: {result.checks}"
echo &"Total promotions: {result.promotions}"
try:
discard readLine(stdin)
except IOError:
discard
except EOFError:
discard
let next = self.countLegalMoves(ply - 1, verbose, result.nodes + 1)
result.nodes += next.nodes
result.captures += next.captures
result.checks += next.checks
result.promotions += next.promotions
result.castles += next.castles
self.undoMove(move) self.undoMove(move)
@ -1501,6 +1607,6 @@ when isMainModule:
when compileOption("profiler"): when compileOption("profiler"):
import nimprof import nimprof
#b = newChessboardFromFEN("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - ")
echo b.countLegalMoves(4, verbose=false) echo b.countLegalMoves(3, verbose=false)
echo "All tests were successful" echo "All tests were successful"