Fixes to generation and added basic debugger

This commit is contained in:
Mattia Giambirtone 2023-10-20 02:23:07 +02:00
parent 60c4f28ec0
commit c6cc98a296
1 changed files with 308 additions and 224 deletions

View File

@ -17,6 +17,7 @@ export matrix
import std/strutils import std/strutils
import std/strformat import std/strformat
import std/sequtils
type type
@ -50,12 +51,16 @@ type
MoveFlag* = enum MoveFlag* = enum
## An enumeration of move flags ## An enumeration of move flags
Default = 0'i8, # Move is a regular move Default = 0'i8, # No flag
# Castling 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
# Castling metadata
CastleLong, CastleLong,
CastleShort, CastleShort,
XRay, # Move is an X-ray attack XRay, # Move is an X-ray attack
# Move is a pawn promotion # Pawn promotion metadata
PromoteToQueen, PromoteToQueen,
PromoteToRook, PromoteToRook,
PromoteToBishop, PromoteToBishop,
@ -125,9 +130,12 @@ 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 undoLastMove*(self: ChessBoard): Move {.discardable.} proc undoMove*(self: ChessBoard, move: Move): Move {.discardable.}
proc isLegal(self: ChessBoard, move: Move): 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 makeMoveFast*(self: ChessBoard, move: Move): Move {.discardable.}
proc pretty*(self: ChessBoard): string
# 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
@ -142,10 +150,10 @@ func topSide(color: PieceColor): Location {.inline.} = (if color == White: (-1,
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))
func longCastleKing(color: PieceColor): Location {.inline.} = (0, -2) func longCastleKing: Location {.inline.} = (0, -2)
func shortCastleKing(color: PieceColor): Location {.inline.} = (0, 2) func shortCastleKing: Location {.inline.} = (0, 2)
func longCastleRook(color: PieceColor): Location {.inline.} = (0, 3) func longCastleRook: Location {.inline.} = (0, 3)
func shortCastleRook(color: PieceColor): Location {.inline.} = (0, -2) func shortCastleRook: Location {.inline.} = (0, -2)
func kingSideRook(color: PieceColor): Location {.inline.} = (if color == White: (7, 7) else: (0, 7)) func kingSideRook(color: PieceColor): Location {.inline.} = (if color == White: (7, 7) else: (0, 7))
func queenSideRook(color: PieceColor): Location {.inline.} = (if color == White: (7, 0) else: (0, 0)) func queenSideRook(color: PieceColor): Location {.inline.} = (if color == White: (7, 0) else: (0, 0))
@ -155,25 +163,25 @@ func bottomLeftKnightMove(color: PieceColor, long: bool = true): Location {.inli
if long: if long:
return (2, -1) return (2, -1)
else: else:
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 (1, -2) return (2, -1)
func bottomRightKnightMove(color: PieceColor, long: bool = true): Location {.inline.} = func bottomRightKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
if color == White: if color == White:
if long: if long:
return (2, -1) return (2, 1)
else: else:
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 (1, 2) return (-1, -2)
func topLeftKnightMove(color: PieceColor, long: bool = true): Location {.inline.} = func topLeftKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
@ -199,7 +207,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.} =
@ -266,7 +274,8 @@ proc newChessboard: ChessBoard =
result.position = Position(attacked: (@[], @[]), result.position = Position(attacked: (@[], @[]),
enPassantSquare: emptyMove(), enPassantSquare: emptyMove(),
move: emptyMove(), move: emptyMove(),
turn: White) turn: White,
fullMoveCount: 1)
@ -347,8 +356,8 @@ proc newChessboardFromFEN*(state: string): ChessBoard =
column = 0 column = 0
of '0'..'9': of '0'..'9':
# Skip x columns # Skip x columns
let x = int(uint8(c) - uint8('0')) - 1 let x = int(uint8(c) - uint8('0'))
if x > 7: if x > 8:
raise newException(ValueError, "invalid skip value (> 8) in FEN string") raise newException(ValueError, "invalid skip value (> 8) in FEN string")
column += int8(x) column += int8(x)
else: else:
@ -530,7 +539,7 @@ func getCapture*(self: ChessBoard, move: Move): Location =
return return
else: else:
return ((if move.piece.color == White: move.targetSquare.row + 1 else: move.targetSquare.row - 1), move.targetSquare.col) return ((if move.piece.color == White: move.targetSquare.row + 1 else: move.targetSquare.row - 1), move.targetSquare.col)
if target.color == move.piece.color.opposite(): elif target.color == move.piece.color.opposite():
return move.targetSquare return move.targetSquare
@ -545,20 +554,17 @@ proc inCheck*(self: ChessBoard, color: PieceColor = None): bool =
## king is in check. If the color is ## king is in check. If the color is
## set to None, checks are checked ## set to None, checks are checked
## for the active color's king ## for the active color's king
var color = color
if color == None:
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)
of Black: of Black:
return self.isAttacked(self.position.pieces.black.king) return self.isAttacked(self.position.pieces.black.king)
of None: else:
case self.getActiveColor(): # Unreachable
of White: discard
return self.isAttacked(self.position.pieces.white.king)
of Black:
return self.isAttacked(self.position.pieces.black.king)
else:
# Unreachable
discard
proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: bool] {.inline.} = proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: bool] {.inline.} =
@ -647,57 +653,51 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
var var
piece = self.grid[location.row, location.col] piece = self.grid[location.row, location.col]
locations: seq[Location] = @[] locations: seq[Location] = @[]
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 forwardOffset = piece.color.forward()
let forward = (forwardOffset + location) let forward = (forwardOffset + location)
# 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(forwardOffset)
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():
locations.add(piece.color.doublePush()) let doubleOffset = piece.color.doublePush()
let double = location + doubleOffset
# 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:
locations.add(piece.color.doublePush())
flags.add(DoublePush)
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 diagonal of the target
locations.add(self.position.enPassantSquare.targetSquare) locations.add(self.position.enPassantSquare.targetSquare)
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
if location.col in 1..6: var diagonal = piece.color.topRightDiagonal()
# Top right diagonal if (diagonal + location).isValid() and self.isCapture(Move(piece: piece, startSquare: location, targetSquare: location + diagonal)):
locations.add(piece.color.topRightDiagonal()) locations.add(diagonal)
if location.row in 1..6: flags.add(Capture)
# Top left diagonal diagonal = piece.color.topLeftDiagonal()
locations.add(piece.color.topLeftDiagonal()) if (diagonal + location).isValid() and self.isCapture(Move(piece: piece, startSquare: location, targetSquare: location + diagonal)):
locations.add(diagonal)
flags.add(Capture)
# Pawn is at the right side, can only capture
# on the left one
if location.col == 7 and location.row < 7:
locations.add(piece.color.topLeftDiagonal())
# Pawn is at the left side, can only capture
# on the right one
if location.col == 0 and location.row < 7:
locations.add(piece.color.topRightDiagonal())
var var
newLocation: Location newLocation: Location
targetPiece: Piece targetPiece: Piece
for target in locations: for (target, flag) in zip(locations, flags):
newLocation = location + target newLocation = location + target
if not newLocation.isValid():
continue
targetPiece = self.grid[newLocation.row, newLocation.col] targetPiece = self.grid[newLocation.row, newLocation.col]
if targetPiece.color == piece.color:
# Can't move over a friendly piece
continue
if location.col != newLocation.col and not self.isCapture(Move(piece: piece, startSquare: location, targetSquare: newLocation)):
# Can only move diagonally when capturing
continue
if newLocation.row == piece.color.getLastRow(): if newLocation.row == piece.color.getLastRow():
# Generate all promotion moves # 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: newLocation, piece: piece, flag: promotionType))
continue continue
# Move is just a pawn push result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece, flag: flag))
result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece))
proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
@ -734,7 +734,7 @@ 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)) 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))
@ -756,21 +756,25 @@ proc generateKingMoves(self: ChessBoard, location: Location): seq[Move] =
# Castling # Castling
let canCastle = self.canCastle() let canCastle = self.canCastle()
if canCastle.queen: if canCastle.queen:
directions.add(piece.color.longCastleKing()) directions.add(longCastleKing())
if canCastle.king: if canCastle.king:
directions.add(piece.color.shortCastleKing()) directions.add(shortCastleKing())
var flag = Default var flag = Default
for direction in directions: for direction in directions:
if direction == piece.color.longCastleKing(): if direction == longCastleKing():
flag = CastleLong flag = CastleLong
elif direction == piece.color.shortCastleKing(): elif direction == shortCastleKing():
flag = CastleShort flag = CastleShort
else:
flag = Default
# Step in this direction once # Step in this direction once
let square: Location = location + direction let square: Location = location + direction
# End of board reached # End of board reached
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]
if otherPiece.color == self.getActiveColor.opposite():
flag = Capture
# A friendly piece is in the way, move onto the next # A friendly piece is in the way, move onto the next
# direction # direction
if otherPiece.color == piece.color: if otherPiece.color == piece.color:
@ -806,7 +810,7 @@ proc generateKnightMoves(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 # it
result.add(Move(startSquare: location, targetSquare: square, piece: piece)) result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: Capture))
continue continue
# 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))
@ -829,14 +833,6 @@ proc generateMoves(self: ChessBoard, location: Location): seq[Move] =
return @[] return @[]
proc generateLegalMoves*(self: ChessBoard, location: Location): seq[Move] =
## Returns the list of possible legal chess moves for the
## piece in the given location
for move in self.generateMoves(location):
if self.isLegal(move):
result.add(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 pseudo-legal moves
## in the current position ## in the current position
@ -848,18 +844,6 @@ proc generateAllMoves*(self: ChessBoard): seq[Move] =
result.add(move) result.add(move)
proc countLegalMoves*(self: ChessBoard, ply: int): int =
## Counts the number of legal positions reached after
## the given number of half moves
if ply == 0:
return 1
for move in self.generateAllMoves():
if self.isLegal(move):
#echo &"{move.piece.color} {move.piece.kind} {move.startSquare.locationToAlgebraic()} {move.targetSquare.locationtoAlgebraic()}"
result += self.countLegalMoves(ply - 1)
proc getAttackers*(self: ChessBoard, square: Location): seq[Piece] = proc getAttackers*(self: ChessBoard, square: Location): seq[Piece] =
## Returns all the attackers of the given square ## Returns all the attackers of the given square
for move in self.position.attacked.black: for move in self.position.attacked.black:
@ -910,12 +894,7 @@ 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
# We refresh the attack metadata at every move. This is an
# O(1) operation, because we're only updating the length
# field without deallocating the memory, which will promptly
# be reused by us again. Neat!
self.position.attacked.white.setLen(0)
self.position.attacked.black.setLen(0)
# Go over each piece one by one and see which squares # Go over each piece one by one and see which squares
# it currently attacks # it currently attacks
@ -988,7 +967,7 @@ proc removePiece(self: ChessBoard, location: Location) =
of Black: of Black:
case piece.kind: case piece.kind:
of Pawn: of Pawn:
self.position.pieces.black.pawns.delete(self.position.pieces.white.pawns.find(location)) self.position.pieces.black.pawns.delete(self.position.pieces.black.pawns.find(location))
of Bishop: of Bishop:
self.position.pieces.black.bishops.delete(self.position.pieces.black.bishops.find(location)) self.position.pieces.black.bishops.delete(self.position.pieces.black.bishops.find(location))
of Knight: of Knight:
@ -1005,10 +984,11 @@ proc removePiece(self: ChessBoard, location: Location) =
discard discard
proc movePiece(self: ChessBoard, move: Move) = proc movePiece(self: ChessBoard, move: Move, attack: bool = true) =
## Internal helper to move a piece. Does ## Internal helper to move a piece. If attack
## not update attacked squares, just position ## is set to false, then this function does
## metadata and the grid itself ## not update attacked squares metadata, just
## positional info and the grid itself
case move.piece.color: case move.piece.color:
of White: of White:
case move.piece.kind: case move.piece.kind:
@ -1060,17 +1040,26 @@ proc movePiece(self: ChessBoard, move: Move) =
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
if attack:
self.updateAttackedSquares()
proc updatePositions(self: ChessBoard, move: Move) = proc movePiece(self: ChessBoard, startSquare, targetSquare: Location, attack: bool = true) =
## Like the other movePiece(), but with two locations
self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare,
piece: self.grid[startSquare.row, startSquare.col],
),
attack
)
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) let capture = self.getCapture(move)
if capture != emptyLocation(): if capture != emptyLocation():
self.position.captured = self.grid[capture.row, capture.col] self.position.captured = self.grid[capture.row, capture.col]
if capture != self.getEnPassantTarget(): self.removePiece(capture)
# En passant is handled elsewhere
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)
@ -1083,7 +1072,7 @@ proc doMove(self: ChessBoard, move: Move) =
# Final checks # Final checks
# Record the move in the position # Record the final move in the position
self.position.move = move self.position.move = move
# Needed to detect draw by the 50 move rule # Needed to detect draw by the 50 move rule
@ -1091,110 +1080,115 @@ 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.piece.kind == Pawn or self.isCapture(move): if move.flag != Backwards:
halfMoveClock = 0 if move.piece.kind == Pawn or self.isCapture(move):
else: halfMoveClock = 0
inc(halfMoveClock)
if move.piece.color == Black:
inc(fullMoveCount)
# Castling check: have the rooks moved?
if move.piece.kind == Rook:
case move.piece.color:
of White:
if move.startSquare.row == move.piece.getStartRow():
if move.startSquare.col == 0:
# Queen side
castlingAvailable.white.queen = false
elif move.startSquare.col == 7:
# King side
castlingAvailable.white.king = false
of Black:
if move.startSquare.row == move.piece.getStartRow():
if move.startSquare.col == 0:
# Queen side
castlingAvailable.black.queen = false
elif move.startSquare.col == 7:
# King side
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
# Update position and attack metadata
self.updatePositions(move)
self.updateAttackedSquares()
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 = move.piece.color.shortCastleRook()
else: else:
location = move.piece.color.queenSideRook() inc(halfMoveClock)
target = move.piece.color.longCastleRook() if move.piece.color == Black:
let rook = self.grid[location.row, location.col] inc(fullMoveCount)
let move = Move(startSquare: location, targetSquare: location + target, piece: rook, flag: move.flag) # Castling check: have the rooks moved?
self.updatePositions(move) if move.piece.kind == Rook:
self.updateAttackedSquares() case move.piece.color:
of White:
if move.startSquare.row == move.piece.getStartRow():
if move.startSquare.col == 0:
# Queen side
castlingAvailable.white.queen = false
elif move.startSquare.col == 7:
# King side
castlingAvailable.white.king = false
of Black:
if move.startSquare.row == move.piece.getStartRow():
if move.startSquare.col == 0:
# Queen side
castlingAvailable.black.queen = false
elif move.startSquare.col == 7:
# King side
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:
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
if move.flag == Backwards:
self.movePiece(move.targetSquare, move.startSquare)
else:
self.movePiece(move)
# Record final position for future reference
self.positions.add(self.position)
# Create new position with
var newPos = 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(),
)
# Check for double pawn push # Check for double pawn push
if move.piece.kind == Pawn and abs(move.startSquare.row - move.targetSquare.row) == 2: if move.flag == DoublePush:
newPos.enPassantSquare = Move(piece: move.piece, self.position.enPassantSquare = Move(piece: move.piece,
startSquare: (move.startSquare.row, move.startSquare.col), startSquare: (move.startSquare.row, move.startSquare.col),
targetSquare: move.targetSquare + move.piece.color.bottomSide()) targetSquare: move.targetSquare + move.piece.color.bottomSide())
else: else:
newPos.enPassantSquare = emptyMove() self.position.enPassantSquare = emptyMove()
self.position = newPos
proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) = proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
@ -1242,41 +1236,59 @@ proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
self.grid[location.row, location.col] = piece self.grid[location.row, location.col] = piece
proc undoLastMove*(self: ChessBoard): Move {.discardable.} = proc undoMove*(self: ChessBoard, move: Move): Move {.discardable.} =
## Undoes the last move, restoring the previous position. ## Undoes the given move if possible
## If there are no positions to roll back to to, this is a
## no-op. Returns the move that was performed (may be empty)
result = emptyMove() result = emptyMove()
if self.positions.len() == 0: if self.positions.len() == 0:
return return
var var move: Move = move
previous = self.positions[^1] let previous = self.positions.pop()
oppositeMove = Move(piece: previous.move.piece, targetSquare: previous.move.startSquare, startSquare: previous.move.targetSquare) move.flag = Backwards
self.removePiece(previous.move.startSquare) self.doMove(move)
self.position = previous self.position = previous
if previous.move != emptyMove(): self.position.move = move
self.spawnPiece(previous.move.startSquare, previous.move.piece) #self.updateLocations(move)
self.updateAttackedSquares() return move
self.updatePositions(oppositeMove)
if previous.captured != emptyPiece():
self.spawnPiece(previous.move.targetSquare, previous.captured)
discard self.positions.pop()
return self.position.move
proc isLegal(self: ChessBoard, move: Move): 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 var move = move
if move.piece.kind == King and move.piece.color.longCastleKing() + move.startSquare == move.targetSquare: if move.piece.kind == King and longCastleKing() + move.startSquare == move.targetSquare:
move.flag = CastleLong move.flag = CastleLong
elif move.piece.kind == King and move.piece.color.shortCastleKing() + move.startSquare == move.targetSquare: elif move.piece.kind == King and shortCastleKing() + move.startSquare == move.targetSquare:
move.flag = CastleShort 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) self.doMove(move)
defer: self.undoLastMove() 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 isLegalFast(self: ChessBoard, move: Move, keep: bool = false): bool =
## Returns whether the given move is 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.doMove(move)
if not keep:
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):
@ -1287,11 +1299,26 @@ proc isLegal(self: ChessBoard, move: Move): bool =
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
if not self.isLegal(move): self.position.move = move
if move.flag == Backwards:
self.doMove(result)
let legal = self.isLegal(move, keep=true)
if not legal:
return emptyMove() return emptyMove()
self.doMove(result) 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.} =
@ -1360,6 +1387,63 @@ proc pretty*(self: ChessBoard): string =
result &= "\x1b[0m" result &= "\x1b[0m"
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
## the given number of half moves
if ply == 0:
result = 1
else:
var now: string
for move in self.generateAllMoves():
now = self.pretty()
if self.isLegalFast(move, keep=true):
if verbose:
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: yes"
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
result += self.countLegalMoves(ply - 1, verbose, verboseIllegal, result)
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)
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"
@ -1417,6 +1501,6 @@ when isMainModule:
when compileOption("profiler"): when compileOption("profiler"):
import nimprof import nimprof
echo b.countLegalMoves(3) b = newChessboardFromFEN("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - -")
echo b.countLegalMoves(2, verbose=true, verboseIllegal=true)
echo "All tests were successful" echo "All tests were successful"