Fixes to performance & improve legality checks

This commit is contained in:
Mattia Giambirtone 2023-10-21 18:19:41 +02:00
parent c6cc98a296
commit b9dcde1563
1 changed files with 269 additions and 269 deletions

View File

@ -23,6 +23,8 @@ import std/sequtils
type type
# Useful type aliases # Useful type aliases
Location* = tuple[row, col: int8] Location* = tuple[row, col: int8]
Attacked = seq[tuple[source, dest: Location]]
Pieces = tuple[king: Location, queens: seq[Location], rooks: seq[Location], Pieces = tuple[king: Location, queens: seq[Location], rooks: seq[Location],
bishops: seq[Location], knights: seq[Location], bishops: seq[Location], knights: seq[Location],
@ -52,9 +54,8 @@ type
MoveFlag* = enum MoveFlag* = enum
## An enumeration of move flags ## An enumeration of move flags
Default = 0'i8, # No flag Default = 0'i8, # No flag
EnPassant, # Move is a capture with en passant
Capture, # Move is a capture Capture, # Move is a capture
EnPassant, # Move is an en passant
Backwards, # Move is a backward move generated by undoMove
DoublePush, # Move is a double pawn push DoublePush, # Move is a double pawn push
# Castling metadata # Castling metadata
CastleLong, CastleLong,
@ -96,7 +97,7 @@ type
# Locations of all pieces # Locations of all pieces
pieces: tuple[white: Pieces, black: Pieces] pieces: tuple[white: Pieces, black: Pieces]
# Potential attacking moves for black and white # Potential attacking moves for black and white
attacked: tuple[white: seq[Move], black: seq[Move]] attacked: tuple[white: Attacked, black: Attacked]
# Has any piece been captured to reach this position? # Has any piece been captured to reach this position?
captured: Piece captured: Piece
# Active color # Active color
@ -122,20 +123,21 @@ 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 func getCapture*(self: ChessBoard, move: Move): Location
proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.discardable.}
proc makeMove*(self: ChessBoard, move: Move): Move proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.}
proc makeMove*(self: ChessBoard, startSquare, targetSquare: Location): Move proc makeMove*(self: ChessBoard, startSquare, targetSquare: Location): Move {.discardable.}
func emptyMove*: Move {.inline.} = Move(startSquare: emptyLocation(), targetSquare: emptyLocation(), piece: emptyPiece()) 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 `+`*(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): bool
proc undoMove*(self: ChessBoard, move: Move): Move {.discardable.} proc undoMove*(self: ChessBoard, move: Move)
proc isLegal(self: ChessBoard, move: Move, keep: bool = false): bool proc isLegal(self: ChessBoard, move: Move, keep: bool = false): bool
proc doMove(self: ChessBoard, move: Move) proc doMove(self: ChessBoard, move: Move)
proc isLegalFast(self: ChessBoard, move: Move, keep: bool = false): bool proc isLegalFast(self: ChessBoard, move: Move, keep: bool = false): bool
proc makeMoveFast*(self: ChessBoard, move: Move): Move {.discardable.}
proc pretty*(self: ChessBoard): string proc pretty*(self: ChessBoard): string
proc spawnPiece(self: ChessBoard, location: Location, piece: Piece)
proc updateAttackedSquares(self: ChessBoard)
# Due to our board layout, directions of movement are reversed for white/black so # Due to our board layout, directions of movement are reversed for white/black so
# we need these helpers to avoid going mad with integer tuples and minus signs # we need these helpers to avoid going mad with integer tuples and minus signs
@ -146,7 +148,7 @@ func bottomLeftDiagonal(color: PieceColor): Location {.inline.} = (if color == W
func bottomRightDiagonal(color: PieceColor): Location {.inline.} = (if color == White: (1, 1) else: (-1, -1)) func bottomRightDiagonal(color: PieceColor): Location {.inline.} = (if color == White: (1, 1) else: (-1, -1))
func leftSide(color: PieceColor): Location {.inline.} = (if color == White: (0, -1) else: (0, 1)) func leftSide(color: PieceColor): Location {.inline.} = (if color == White: (0, -1) else: (0, 1))
func rightSide(color: PieceColor): Location {.inline.} = (if color == White: (0, 1) else: (0, -1)) func rightSide(color: PieceColor): Location {.inline.} = (if color == White: (0, 1) else: (0, -1))
func topSide(color: PieceColor): Location {.inline.} = (if color == White: (-1, 0) else: (1, 0)) func topSide(color: PieceColor): Location {.inline.} = (-1, 0)
func bottomSide(color: PieceColor): Location {.inline.} = (if color == White: (1, 0) else: (-1, 0)) func bottomSide(color: PieceColor): Location {.inline.} = (if color == White: (1, 0) else: (-1, 0))
func forward(color: PieceColor): Location {.inline.} = (if color == White: (-1, 0) else: (1, 0)) func forward(color: PieceColor): Location {.inline.} = (if color == White: (-1, 0) else: (1, 0))
func doublePush(color: PieceColor): Location {.inline.} = (if color == White: (-2, 0) else: (2, 0)) func doublePush(color: PieceColor): Location {.inline.} = (if color == White: (-2, 0) else: (2, 0))
@ -166,9 +168,9 @@ func bottomLeftKnightMove(color: PieceColor, long: bool = true): Location {.inli
return (1, -2) return (1, -2)
elif color == Black: elif color == Black:
if long: if long:
return (2, 1) return (-2, 1)
else: else:
return (2, -1) return (1, -2)
func bottomRightKnightMove(color: PieceColor, long: bool = true): Location {.inline.} = func bottomRightKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
@ -207,7 +209,7 @@ func topRightKnightMove(color: PieceColor, long: bool = true): Location {.inline
if long: if long:
return (2, -1) return (2, -1)
else: else:
return (1, -2) return (-1, 2)
func getActiveColor*(self: ChessBoard): PieceColor {.inline.} = func getActiveColor*(self: ChessBoard): PieceColor {.inline.} =
@ -423,6 +425,8 @@ proc newChessboardFromFEN*(state: string): ChessBoard =
else: else:
raise newException(ValueError, "too many fields in FEN string") raise newException(ValueError, "too many fields in FEN string")
inc(index) inc(index)
result.updateAttackedSquares()
proc newDefaultChessboard*: ChessBoard = proc newDefaultChessboard*: ChessBoard =
@ -519,34 +523,30 @@ func locationToAlgebraic*(loc: Location): string {.inline.} =
return &"{char(uint8(loc.col) + uint8('a'))}{rowToRank(loc.row)}" return &"{char(uint8(loc.col) + uint8('a'))}{rowToRank(loc.row)}"
func getPiece*(self: ChessBoard, square: string): Piece = func getPiece*(self: ChessBoard, loc: Location): Piece =
## Gets the piece on the given square ## Gets the piece at the given location
## in algebraic notation
let loc = square.algebraicToLocation()
return self.grid[loc.row, loc.col] return self.grid[loc.row, loc.col]
func getCapture*(self: ChessBoard, move: Move): Location = func getPiece*(self: ChessBoard, square: string): Piece =
## Returns the location that would be captured if this ## Gets the piece on the given square
## move were played on the board, taking en passant and ## in algebraic notation
## other things into account (the move is assumed to be return self.getPiece(square.algebraicToLocation())
## already valid). An empty location is returned if no
## piece is captured by the given move
result = emptyLocation()
let target = self.grid[move.targetSquare.row, move.targetSquare.col]
if target.color == None:
if move.targetSquare != self.position.enPassantSquare.targetSquare:
return
else:
return ((if move.piece.color == White: move.targetSquare.row + 1 else: move.targetSquare.row - 1), move.targetSquare.col)
elif target.color == move.piece.color.opposite():
return move.targetSquare
func isCapture*(self: ChessBoard, move: Move): bool {.inline.} = func isCapture*(self: ChessBoard, 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 self.getCapture(move) != emptyLocation() return move.flag in [Capture, EnPassant]
func getCapture*(self: ChessBoard, move: Move): Location =
## Returns the location that would be captured if this
## move were played on the board
if not self.isCapture(move):
return emptyLocation()
return move.targetSquare
proc inCheck*(self: ChessBoard, color: PieceColor = None): bool = proc inCheck*(self: ChessBoard, color: PieceColor = None): bool =
@ -838,20 +838,19 @@ proc generateAllMoves*(self: ChessBoard): seq[Move] =
## 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:
if self.grid[i, j].color != self.getActiveColor(): if self.grid[i, j].color == self.getActiveColor():
continue for move in self.generateMoves((int8(i), int8(j))):
for move in self.generateMoves((int8(i), int8(j))): result.add(move)
result.add(move)
proc getAttackers*(self: ChessBoard, square: Location): seq[Piece] = proc getAttackers*(self: ChessBoard, square: Location): seq[Location] =
## Returns all the attackers of the given square ## Returns all the attackers of the given square
for move in self.position.attacked.black: for attack in self.position.attacked.black:
if move.targetSquare == square: if attack.dest == square:
result.add(move.piece) result.add(attack.source)
for move in self.position.attacked.white: for attack in self.position.attacked.white:
if move.targetSquare == square: if attack.dest == square:
result.add(move.piece) result.add(attack.source)
# 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
@ -862,91 +861,144 @@ proc isAttacked*(self: ChessBoard, loc: Location): bool =
let piece = self.grid[loc.row, loc.col] let piece = self.grid[loc.row, loc.col]
case piece.color: case piece.color:
of White: of White:
for move in self.position.attacked.black: for attack in self.position.attacked.black:
if move.targetSquare == loc: if attack.dest == loc:
return true return true
of Black: of Black:
for move in self.position.attacked.white: for attack in self.position.attacked.black:
if move.targetSquare == loc: if attack.dest == loc:
return true return true
of None: of None:
case self.getActiveColor(): case self.getActiveColor():
of White: of White:
for move in self.position.attacked.black: for attack in self.position.attacked.black:
if move.targetSquare == loc: if attack.dest == loc:
return true return true
of Black: of Black:
for move in self.position.attacked.white: for attack in self.position.attacked.black:
if move.targetSquare == loc: if attack.dest == loc:
return true return true
else: else:
discard discard
proc isAttacked*(self: ChessBoard, square: string): bool = proc isAttacked*(self: ChessBoard, square: string): bool =
## Returns whether the given square is attacked ## Returns whether the given square is attacked
## by its opponent ## by its opponent
return self.isAttacked(square.algebraicToLocation()) return self.isAttacked(square.algebraicToLocation())
proc updatePawnAttacks(self: ChessBoard) =
## Internal helper of updateAttackedSquares
for loc in self.position.pieces.white.pawns:
# Pawns are special in how they capture (i.e. the
# squares they can move to do not match the squares
# they can capture on. Sneaky fucks)
let piece = self.grid[loc.row, loc.col]
self.position.attacked.white.add((loc, loc + piece.color.topRightDiagonal()))
self.position.attacked.white.add((loc, loc + piece.color.topLeftDiagonal()))
# We do the same thing for black
for loc in self.position.pieces.black.pawns:
let piece = self.grid[loc.row, loc.col]
self.position.attacked.black.add((loc, loc + piece.color.topRightDiagonal()))
self.position.attacked.black.add((loc, loc + piece.color.topLeftDiagonal()))
proc getSlidingAttacks(self: ChessBoard, loc: Location): Attacked =
## Internal helper of updateSlidingAttacks
var
directions: seq[Location] = @[]
square: Location = loc
otherPiece: Piece
let piece = self.grid[loc.row, loc.col]
if piece.kind in [Bishop, Queen]:
directions.add(piece.color.topLeftDiagonal())
directions.add(piece.color.topRightDiagonal())
directions.add(piece.color.bottomLeftDiagonal())
directions.add(piece.color.bottomRightDiagonal())
if piece.kind in [Queen, Rook]:
directions.add(piece.color.topSide())
directions.add(piece.color.bottomSide())
directions.add(piece.color.rightSide())
directions.add(piece.color.leftSide())
for direction in directions:
square = loc
# Slide in this direction as long as it's possible
while true:
square = square + direction
# End of board reached
if not square.isValid():
break
otherPiece = self.grid[square.row, square.col]
# A piece is in the way: we cannot proceed
# any further
if otherPiece.color notin [piece.color.opposite(), None]:
break
# Target square is attacked
result.add((loc, square))
proc updateSlidingAttacks(self: ChessBoard) =
## Internal helper of updateAttackedSquares
var
directions: seq[Location]
piece: Piece
# Bishops
for loc in self.position.pieces.white.bishops:
for attack in self.getSlidingAttacks(loc):
self.position.attacked.white.add(attack)
for loc in self.position.pieces.black.bishops:
for attack in self.getSlidingAttacks(loc):
self.position.attacked.black.add(attack)
# Rooks
for loc in self.position.pieces.white.rooks:
for attack in self.getSlidingAttacks(loc):
self.position.attacked.white.add(attack)
for loc in self.position.pieces.black.rooks:
for attack in self.getSlidingAttacks(loc):
self.position.attacked.black.add(attack)
# Queens
for loc in self.position.pieces.white.queens:
for attack in self.getSlidingAttacks(loc):
self.position.attacked.white.add(attack)
for loc in self.position.pieces.black.queens:
for attack in self.getSlidingAttacks(loc):
self.position.attacked.black.add(attack)
proc updateAttackedSquares(self: ChessBoard) = proc updateAttackedSquares(self: ChessBoard) =
## Updates internal metadata about which squares ## Updates internal metadata about which squares
## are attacked. Called internally by doMove ## are attacked. Called internally by doMove
self.position.attacked.white.setLen(0)
# Go over each piece one by one and see which squares self.position.attacked.black.setLen(0)
# it currently attacks
# Pawns # Pawns
for loc in self.position.pieces.white.pawns: self.updatePawnAttacks()
for move in self.generateMoves(loc): # Sliding pieces
self.position.attacked.white.add(move) self.updateSlidingAttacks()
# Bishops
for loc in self.position.pieces.white.bishops:
for move in self.generateMoves(loc):
self.position.attacked.white.add(move)
# Knights # Knights
for loc in self.position.pieces.white.knights: for loc in self.position.pieces.white.knights:
for move in self.generateMoves(loc): for move in self.generateMoves(loc):
self.position.attacked.white.add(move) self.position.attacked.white.add((move.startSquare, move.targetSquare))
# Rooks
for loc in self.position.pieces.white.rooks:
for move in self.generateMoves(loc):
self.position.attacked.white.add(move)
# Queens
for loc in self.position.pieces.white.queens:
for move in self.generateMoves(loc):
self.position.attacked.white.add(move)
# King # King
for move in self.generateMoves(self.position.pieces.white.king): for move in self.generateMoves(self.position.pieces.white.king):
self.position.attacked.white.add(move) self.position.attacked.white.add((move.startSquare, move.targetSquare))
# Knights
# Same for black
for loc in self.position.pieces.black.pawns:
for move in self.generateMoves(loc):
self.position.attacked.black.add(move)
for loc in self.position.pieces.black.bishops:
for move in self.generateMoves(loc):
self.position.attacked.black.add(move)
for loc in self.position.pieces.black.knights: for loc in self.position.pieces.black.knights:
for move in self.generateMoves(loc): for move in self.generateMoves(loc):
self.position.attacked.white.add(move) self.position.attacked.black.add((move.startSquare, move.targetSquare))
for loc in self.position.pieces.black.rooks: # King
for move in self.generateMoves(loc):
self.position.attacked.black.add(move)
for loc in self.position.pieces.black.queens:
for move in self.generateMoves(loc):
self.position.attacked.black.add(move)
for move in self.generateMoves(self.position.pieces.black.king): for move in self.generateMoves(self.position.pieces.black.king):
self.position.attacked.black.add(move) self.position.attacked.black.add((move.startSquare, move.targetSquare))
proc removePiece(self: ChessBoard, location: Location) = proc removePiece(self: ChessBoard, location: Location, emptyGrid: 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]
self.grid[location.row, location.col] = emptyPiece() if emptyGrid:
self.grid[location.row, location.col] = emptyPiece()
case piece.color: case piece.color:
of White: of White:
case piece.kind: case piece.kind:
@ -1036,7 +1088,7 @@ proc movePiece(self: ChessBoard, move: Move, attack: bool = true) =
discard discard
else: else:
discard discard
# Empty out the starting square # Empty out the starting square
self.grid[move.startSquare.row, move.startSquare.col] = emptyPiece() self.grid[move.startSquare.row, move.startSquare.col] = emptyPiece()
# Actually move the piece # Actually move the piece
self.grid[move.targetSquare.row, move.targetSquare.col] = move.piece self.grid[move.targetSquare.row, move.targetSquare.col] = move.piece
@ -1062,7 +1114,7 @@ proc updateLocations(self: ChessBoard, move: Move) =
self.removePiece(capture) self.removePiece(capture)
# Update the positional metadata of the moving piece # Update the positional metadata of the moving piece
self.movePiece(move) self.movePiece(move)
proc doMove(self: ChessBoard, move: Move) = proc doMove(self: ChessBoard, move: Move) =
## Internal function called by makeMove after ## Internal function called by makeMove after
@ -1080,108 +1132,103 @@ 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
if move.flag != Backwards: let capture = self.getCapture(move)
if move.piece.kind == Pawn or self.isCapture(move): if move.piece.kind == Pawn or self.isCapture(move):
halfMoveClock = 0 halfMoveClock = 0
else: else:
inc(halfMoveClock) inc(halfMoveClock)
if move.piece.color == Black: if move.piece.color == Black:
inc(fullMoveCount) inc(fullMoveCount)
# Castling check: have the rooks moved? # Castling check: have the rooks moved?
if move.piece.kind == Rook: if move.piece.kind == Rook:
case move.piece.color: case move.piece.color:
of White: of White:
if move.startSquare.row == move.piece.getStartRow(): if move.startSquare.row == move.piece.getStartRow():
if move.startSquare.col == 0: if move.startSquare.col == 0:
# Queen side # Queen side
castlingAvailable.white.queen = false castlingAvailable.white.queen = false
elif move.startSquare.col == 7: elif move.startSquare.col == 7:
# King side # King side
castlingAvailable.white.king = false castlingAvailable.white.king = false
of Black: of Black:
if move.startSquare.row == move.piece.getStartRow(): if move.startSquare.row == move.piece.getStartRow():
if move.startSquare.col == 0: if move.startSquare.col == 0:
# Queen side # Queen side
castlingAvailable.black.queen = false castlingAvailable.black.queen = false
elif move.startSquare.col == 7: elif move.startSquare.col == 7:
# King side # King side
castlingAvailable.black.king = false castlingAvailable.black.king = false
else:
discard
# Has a rook been captured?
let capture = self.getCapture(move)
if capture != emptyLocation():
let piece = self.grid[capture.row, capture.col]
if piece.kind == Rook:
case piece.color:
of White:
if capture == piece.color.queenSideRook():
# Queen side
castlingAvailable.white.queen = false
elif capture == piece.color.kingSideRook():
# King side
castlingAvailable.white.king = false
of Black:
if capture == piece.color.queenSideRook():
# Queen side
castlingAvailable.black.queen = false
elif capture == piece.color.kingSideRook():
# King side
castlingAvailable.black.king = false
else:
# Unreachable
discard
# Has the king moved?
if move.piece.kind == King:
case move.piece.color:
of White:
castlingAvailable.white.king = false
castlingAvailable.white.queen = false
of Black:
castlingAvailable.black.king = false
castlingAvailable.black.queen = false
else:
discard
let previous = self.position
if move.flag != Backwards:
# Record final position for future reference
self.positions.add(previous)
# Create new position
self.position = Position(plyFromRoot: self.position.plyFromRoot + 1,
halfMoveClock: halfMoveClock,
fullMoveCount: fullMoveCount,
captured: emptyPiece(),
turn: self.getActiveColor().opposite,
castlingAvailable: castlingAvailable,
# Updated at the next call to doMove()
move: emptyMove(),
pieces: previous.pieces,
)
var location: Location
if move.flag in [CastleShort, CastleLong]:
# Move the rook onto the
# correct file
var
location: Location
target: Location
if move.flag == CastleShort:
location = move.piece.color.kingSideRook()
target = shortCastleRook()
else: else:
location = move.piece.color.queenSideRook() discard
target = longCastleRook() # Has a rook been captured?
let rook = self.grid[location.row, location.col] if capture != emptyLocation():
let move = Move(startSquare: location, targetSquare: location + target, piece: rook, flag: move.flag) let piece = self.grid[capture.row, capture.col]
self.movePiece(move, attack=false) if piece.kind == Rook:
case piece.color:
of White:
if capture == piece.color.queenSideRook():
# Queen side
castlingAvailable.white.queen = false
elif capture == piece.color.kingSideRook():
# King side
castlingAvailable.white.king = false
of Black:
if capture == piece.color.queenSideRook():
# Queen side
castlingAvailable.black.queen = false
elif capture == piece.color.kingSideRook():
# King side
castlingAvailable.black.king = false
else:
# Unreachable
discard
# Has the king moved?
if move.piece.kind == King:
case move.piece.color:
of White:
castlingAvailable.white.king = false
castlingAvailable.white.queen = false
of Black:
castlingAvailable.black.king = false
castlingAvailable.black.queen = false
else:
discard
let previous = self.position
# Record final position for future reference
self.positions.add(previous)
# Create new position
self.position = Position(plyFromRoot: self.position.plyFromRoot + 1,
halfMoveClock: halfMoveClock,
fullMoveCount: fullMoveCount,
captured: emptyPiece(),
turn: self.getActiveColor().opposite,
castlingAvailable: castlingAvailable,
# Updated at the next call to doMove()
move: emptyMove(),
pieces: previous.pieces,
)
var location: Location
if move.flag in [CastleShort, CastleLong]:
# Move the rook onto the
# correct file
var
location: Location
target: Location
if move.flag == CastleShort:
location = move.piece.color.kingSideRook()
target = shortCastleRook()
else:
location = move.piece.color.queenSideRook()
target = longCastleRook()
let rook = self.grid[location.row, location.col]
let move = Move(startSquare: location, targetSquare: location + target, piece: rook, flag: move.flag)
self.movePiece(move, attack=false)
# Update position and attack metadata # Update position and attack metadata
if move.flag == Backwards: self.updateLocations(move)
self.movePiece(move.targetSquare, move.startSquare)
else:
self.movePiece(move)
# Check for double pawn push # Check for double pawn push
if move.flag == DoublePush: if move.flag == DoublePush:
self.position.enPassantSquare = Move(piece: move.piece, self.position.enPassantSquare = Move(piece: move.piece,
@ -1236,59 +1283,48 @@ proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
self.grid[location.row, location.col] = piece self.grid[location.row, location.col] = piece
proc undoMove*(self: ChessBoard, move: Move): Move {.discardable.} = proc undoMove*(self: ChessBoard, move: Move) =
## Undoes the given move if possible ## Undoes the given move
result = emptyMove()
if self.positions.len() == 0: if self.positions.len() == 0:
return return
var move: Move = move var position = self.positions[^1]
let previous = self.positions.pop() while true:
move.flag = Backwards if position.move == move:
self.doMove(move) break
self.position = previous discard self.positions.pop()
self.position.move = move position = self.positions[^1]
#self.updateLocations(move)
return move
self.grid[move.startSquare.row, move.startSquare.col] = move.piece
if self.isCapture(move):
self.grid[move.targetSquare.row, move.targetSquare.col] = self.position.captured
else:
self.grid[move.targetSquare.row, move.targetSquare.col] = emptyPiece()
self.position = position
proc isLegal(self: ChessBoard, move: Move, keep: bool = false): bool = proc isLegal(self: ChessBoard, move: Move, keep: bool = false): bool =
## Returns whether the given move is legal ## Returns whether the given move is legal
var move = move self.doMove(move)
if move.piece.kind == King and longCastleKing() + move.startSquare == move.targetSquare:
move.flag = CastleLong
elif move.piece.kind == King and shortCastleKing() + move.startSquare == move.targetSquare:
move.flag = CastleShort
if move.piece.kind == Pawn and move.piece.color.doublePush() + move.startSquare == move.targetSquare:
move.flag = DoublePush
if move notin self.generateMoves(move.startSquare): if move notin self.generateMoves(move.startSquare):
# Piece cannot arrive to destination (blocked # Piece cannot arrive to destination (blocked
# or otherwise invalid move) # or otherwise invalid move)
return false return false
self.doMove(move)
if not keep: if not keep:
defer: self.undoMove(move) defer: self.undoMove(move)
# Move would reveal an attack # Move would reveal an attack
# on our king: not allowed # on our king: not allowed
if self.inCheck(move.piece.color): return self.inCheck(move.piece.color)
return false
# All checks have passed: move is legal
result = true
proc isLegalFast(self: ChessBoard, move: Move, keep: bool = false): bool = proc isLegalFast(self: ChessBoard, move: Move, keep: bool = false): bool =
## Returns whether the given move is legal ## Returns whether the given move is legal
## assuming that the input move is pseudo legal ## assuming that the input move is pseudo legal
var move = move
if move.piece.kind == King and longCastleKing() + move.startSquare == move.targetSquare:
move.flag = CastleLong
elif move.piece.kind == King and shortCastleKing() + move.startSquare == move.targetSquare:
move.flag = CastleShort
if move.piece.kind == Pawn and move.piece.color.doublePush() + move.startSquare == move.targetSquare:
move.flag = DoublePush
self.position.move = move self.position.move = move
self.doMove(move) self.doMove(move)
if not keep: if not keep:
defer: self.undoMove(move) defer: self.undoMove(move)
# Move would reveal an attack # Move would reveal an attack
# on our king: not allowed # on our king: not allowed
if self.inCheck(move.piece.color): if self.inCheck(move.piece.color):
@ -1301,26 +1337,12 @@ 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
if move.flag == Backwards:
self.doMove(result)
let legal = self.isLegal(move, keep=true) let legal = self.isLegal(move, keep=true)
if not legal: if not legal:
return emptyMove() return emptyMove()
result = self.position.move result = self.position.move
proc makeMoveFast*(self: ChessBoard, move: Move): Move {.discardable.} =
## Like the other makeMove(), but uses isLegalFast
result = move
self.position.move = move
if move.flag == Backwards:
self.doMove(result)
let legal = self.isLegalFast(move, keep=true)
if not legal:
return emptyMove()
result = self.position.move
proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.discardable.} = proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.discardable.} =
## Makes a move on the board from the chosen start square to ## Makes a move on the board from the chosen start square to
## the chosen target square, ensuring it is legal (turns are ## the chosen target square, ensuring it is legal (turns are
@ -1387,18 +1409,17 @@ 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, verboseIllegal: bool = false, current: int = 0): 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 half moves
if ply == 0: if ply == 0:
result = 1 result = 1
else: else:
var now: string var before: string
for move in self.generateAllMoves(): for move in self.generateAllMoves():
now = self.pretty() before = self.pretty()
if self.isLegalFast(move, keep=true): self.doMove(move)
if not self.inCheck(move.piece.color):
if verbose: if verbose:
let canCastle = self.canCastle() let canCastle = self.canCastle()
echo "\x1Bc" echo "\x1Bc"
@ -1406,12 +1427,10 @@ proc countLegalMoves*(self: ChessBoard, ply: int, verbose: bool = false, verbose
echo &"Move: {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()} (({move.startSquare.row}, {move.startSquare.col}) -> ({move.targetSquare.row}, {move.targetSquare.col}))" echo &"Move: {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()} (({move.startSquare.row}, {move.startSquare.col}) -> ({move.targetSquare.row}, {move.targetSquare.col}))"
echo &"Turn: {move.piece.color}" echo &"Turn: {move.piece.color}"
echo &"Piece: {move.piece.kind}" echo &"Piece: {move.piece.kind}"
echo &"In check: {(if self.inCheck(move.piece.color): \"yes\" else: \"no\")}"
echo "Legal: yes"
echo &"Flag: {move.flag}" echo &"Flag: {move.flag}"
echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
echo "\nBefore:" echo "\nBefore:"
echo now echo before
echo "\nNow: " echo "\nNow: "
echo self.pretty() echo self.pretty()
try: try:
@ -1420,30 +1439,11 @@ proc countLegalMoves*(self: ChessBoard, ply: int, verbose: bool = false, verbose
discard discard
except EOFError: except EOFError:
discard discard
result += self.countLegalMoves(ply - 1, verbose, verboseIllegal, result) result += self.countLegalMoves(ply - 1, verbose, result + 1)
elif verboseIllegal:
let canCastle = self.canCastle()
echo "\x1Bc"
echo &"Ply: {self.position.plyFromRoot} (move {current + result + 1})"
echo &"Move: {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()} (({move.startSquare.row}, {move.startSquare.col}) -> ({move.targetSquare.row}, {move.targetSquare.col}))"
echo &"Turn: {move.piece.color}"
echo &"Piece: {move.piece.kind}"
echo &"In check: {(if self.inCheck(move.piece.color): \"yes\" else: \"no\")}"
echo "Legal: no"
echo &"Flag: {move.flag}"
echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
echo "\nBefore:"
echo now
echo "\nNow: "
echo self.pretty()
try:
discard readLine(stdin)
except IOError:
discard
except EOFError:
discard
self.undoMove(move) self.undoMove(move)
when isMainModule: when isMainModule:
proc testPiece(piece: Piece, kind: PieceKind, color: PieceColor) = proc testPiece(piece: Piece, kind: PieceKind, color: PieceColor) =
doAssert piece.kind == kind and piece.color == color, &"expected piece of kind {kind} and color {color}, got {piece.kind} / {piece.color} instead" doAssert piece.kind == kind and piece.color == color, &"expected piece of kind {kind} and color {color}, got {piece.kind} / {piece.color} instead"
@ -1501,6 +1501,6 @@ when isMainModule:
when compileOption("profiler"): when compileOption("profiler"):
import nimprof import nimprof
b = newChessboardFromFEN("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - -")
echo b.countLegalMoves(2, verbose=true, verboseIllegal=true) echo b.countLegalMoves(4, verbose=false)
echo "All tests were successful" echo "All tests were successful"