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/strformat
import std/sequtils
type
@ -50,12 +51,16 @@ type
MoveFlag* = enum
## An enumeration of move flags
Default = 0'i8, # Move is a regular move
# Castling
Default = 0'i8, # No flag
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,
CastleShort,
XRay, # Move is an X-ray attack
# Move is a pawn promotion
# Pawn promotion metadata
PromoteToQueen,
PromoteToRook,
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
proc generateMoves(self: ChessBoard, location: Location): seq[Move]
proc isAttacked*(self: ChessBoard, loc: Location): bool
proc undoLastMove*(self: ChessBoard): Move {.discardable.}
proc isLegal(self: ChessBoard, move: Move): bool
proc undoMove*(self: ChessBoard, move: Move): Move {.discardable.}
proc isLegal(self: ChessBoard, move: Move, keep: bool = false): bool
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
# 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 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 longCastleKing(color: PieceColor): Location {.inline.} = (0, -2)
func shortCastleKing(color: PieceColor): Location {.inline.} = (0, 2)
func longCastleRook(color: PieceColor): Location {.inline.} = (0, 3)
func shortCastleRook(color: PieceColor): Location {.inline.} = (0, -2)
func longCastleKing: Location {.inline.} = (0, -2)
func shortCastleKing: Location {.inline.} = (0, 2)
func longCastleRook: Location {.inline.} = (0, 3)
func shortCastleRook: Location {.inline.} = (0, -2)
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))
@ -155,25 +163,25 @@ func bottomLeftKnightMove(color: PieceColor, long: bool = true): Location {.inli
if long:
return (2, -1)
else:
return (-1, -2)
return (1, -2)
elif color == Black:
if long:
return (-2, 1)
return (2, 1)
else:
return (1, -2)
return (2, -1)
func bottomRightKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
if color == White:
if long:
return (2, -1)
return (2, 1)
else:
return (1, 2)
elif color == Black:
if long:
return (2, 1)
return (-2, -1)
else:
return (1, 2)
return (-1, -2)
func topLeftKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
@ -199,7 +207,7 @@ func topRightKnightMove(color: PieceColor, long: bool = true): Location {.inline
if long:
return (2, -1)
else:
return (-1, 2)
return (1, -2)
func getActiveColor*(self: ChessBoard): PieceColor {.inline.} =
@ -266,7 +274,8 @@ proc newChessboard: ChessBoard =
result.position = Position(attacked: (@[], @[]),
enPassantSquare: emptyMove(),
move: emptyMove(),
turn: White)
turn: White,
fullMoveCount: 1)
@ -347,8 +356,8 @@ proc newChessboardFromFEN*(state: string): ChessBoard =
column = 0
of '0'..'9':
# Skip x columns
let x = int(uint8(c) - uint8('0')) - 1
if x > 7:
let x = int(uint8(c) - uint8('0'))
if x > 8:
raise newException(ValueError, "invalid skip value (> 8) in FEN string")
column += int8(x)
else:
@ -530,7 +539,7 @@ func getCapture*(self: ChessBoard, move: Move): Location =
return
else:
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
@ -545,20 +554,17 @@ proc inCheck*(self: ChessBoard, color: PieceColor = None): bool =
## king is in check. If the color is
## set to None, checks are checked
## for the active color's king
var color = color
if color == None:
color = self.getActiveColor()
case color:
of White:
return self.isAttacked(self.position.pieces.white.king)
of Black:
return self.isAttacked(self.position.pieces.black.king)
of None:
case self.getActiveColor():
of White:
return self.isAttacked(self.position.pieces.white.king)
of Black:
return self.isAttacked(self.position.pieces.black.king)
else:
# Unreachable
discard
else:
# Unreachable
discard
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
piece = self.grid[location.row, location.col]
locations: seq[Location] = @[]
flags: seq[MoveFlag] = @[]
doAssert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}"
# Pawns can move forward one square
let forwardOffset = piece.color.forward()
let forward = (forwardOffset + location)
# Only if the square is empty though
if forward.isValid() and self.grid[forward.row, forward.col].color == None:
locations.add(forwardOffset)
flags.add(Default)
# If the pawn is on its first rank, it can push two squares
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 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
locations.add(self.position.enPassantSquare.targetSquare)
flags.add(EnPassant)
# They can also move on either diagonal one
# square, but only to capture
if location.col in 1..6:
# Top right diagonal
locations.add(piece.color.topRightDiagonal())
if location.row in 1..6:
# Top left diagonal
locations.add(piece.color.topLeftDiagonal())
# 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 diagonal = piece.color.topRightDiagonal()
if (diagonal + location).isValid() and self.isCapture(Move(piece: piece, startSquare: location, targetSquare: location + diagonal)):
locations.add(diagonal)
flags.add(Capture)
diagonal = piece.color.topLeftDiagonal()
if (diagonal + location).isValid() and self.isCapture(Move(piece: piece, startSquare: location, targetSquare: location + diagonal)):
locations.add(diagonal)
flags.add(Capture)
var
newLocation: Location
targetPiece: Piece
for target in locations:
for (target, flag) in zip(locations, flags):
newLocation = location + target
if not newLocation.isValid():
continue
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():
# Generate all promotion moves
# Pawn reached the other side of the board: generate all potential piece promotions
for promotionType in [PromoteToKnight, PromoteToBishop, PromoteToRook, PromoteToQueen]:
result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece, flag: promotionType))
continue
# Move is just a pawn push
result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece))
result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece, flag: flag))
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:
# Target square contains an enemy piece: capture
# 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
# Target square is empty
result.add(Move(startSquare: location, targetSquare: square, piece: piece))
@ -756,21 +756,25 @@ proc generateKingMoves(self: ChessBoard, location: Location): seq[Move] =
# Castling
let canCastle = self.canCastle()
if canCastle.queen:
directions.add(piece.color.longCastleKing())
directions.add(longCastleKing())
if canCastle.king:
directions.add(piece.color.shortCastleKing())
directions.add(shortCastleKing())
var flag = Default
for direction in directions:
if direction == piece.color.longCastleKing():
if direction == longCastleKing():
flag = CastleLong
elif direction == piece.color.shortCastleKing():
elif direction == shortCastleKing():
flag = CastleShort
else:
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]
if otherPiece.color == self.getActiveColor.opposite():
flag = Capture
# A friendly piece is in the way, move onto the next
# direction
if otherPiece.color == piece.color:
@ -806,7 +810,7 @@ proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] =
if otherPiece.color == piece.color.opposite:
# Target square contains an enemy piece: capture
# it
result.add(Move(startSquare: location, targetSquare: square, piece: piece))
result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: Capture))
continue
# Target square is empty
result.add(Move(startSquare: location, targetSquare: square, piece: piece))
@ -829,14 +833,6 @@ proc generateMoves(self: ChessBoard, location: Location): seq[Move] =
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] =
## Returns the list of all possible pseudo-legal moves
## in the current position
@ -848,18 +844,6 @@ proc generateAllMoves*(self: ChessBoard): seq[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] =
## Returns all the attackers of the given square
for move in self.position.attacked.black:
@ -910,12 +894,7 @@ proc updateAttackedSquares(self: ChessBoard) =
## Updates internal metadata about which squares
## 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
# it currently attacks
@ -988,7 +967,7 @@ proc removePiece(self: ChessBoard, location: Location) =
of Black:
case piece.kind:
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:
self.position.pieces.black.bishops.delete(self.position.pieces.black.bishops.find(location))
of Knight:
@ -1005,10 +984,11 @@ proc removePiece(self: ChessBoard, location: Location) =
discard
proc movePiece(self: ChessBoard, move: Move) =
## Internal helper to move a piece. Does
## not update attacked squares, just position
## metadata and the grid itself
proc movePiece(self: ChessBoard, move: Move, attack: bool = true) =
## Internal helper to move a piece. If attack
## is set to false, then this function does
## not update attacked squares metadata, just
## positional info and the grid itself
case move.piece.color:
of White:
case move.piece.kind:
@ -1059,21 +1039,30 @@ proc movePiece(self: ChessBoard, move: Move) =
# Empty out the starting square
self.grid[move.startSquare.row, move.startSquare.col] = emptyPiece()
# 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
## the pieces on the board after a move
let capture = self.getCapture(move)
if capture != emptyLocation():
self.position.captured = self.grid[capture.row, capture.col]
if capture != self.getEnPassantTarget():
# En passant is handled elsewhere
self.removePiece(capture)
self.removePiece(capture)
# Update the positional metadata of the moving piece
self.movePiece(move)
proc doMove(self: ChessBoard, move: Move) =
## Internal function called by makeMove after
@ -1083,7 +1072,7 @@ proc doMove(self: ChessBoard, move: Move) =
# Final checks
# Record the move in the position
# Record the final move in the position
self.position.move = move
# Needed to detect draw by the 50 move rule
@ -1091,110 +1080,115 @@ proc doMove(self: ChessBoard, move: Move) =
halfMoveClock = self.position.halfMoveClock
fullMoveCount = self.position.fullMoveCount
castlingAvailable = self.position.castlingAvailable
if move.piece.kind == Pawn or self.isCapture(move):
halfMoveClock = 0
else:
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()
if move.flag != Backwards:
if move.piece.kind == Pawn or self.isCapture(move):
halfMoveClock = 0
else:
location = move.piece.color.queenSideRook()
target = move.piece.color.longCastleRook()
let rook = self.grid[location.row, location.col]
let move = Move(startSquare: location, targetSquare: location + target, piece: rook, flag: move.flag)
self.updatePositions(move)
self.updateAttackedSquares()
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
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
if move.piece.kind == Pawn and abs(move.startSquare.row - move.targetSquare.row) == 2:
newPos.enPassantSquare = Move(piece: move.piece,
if move.flag == DoublePush:
self.position.enPassantSquare = Move(piece: move.piece,
startSquare: (move.startSquare.row, move.startSquare.col),
targetSquare: move.targetSquare + move.piece.color.bottomSide())
else:
newPos.enPassantSquare = emptyMove()
self.position = newPos
self.position.enPassantSquare = emptyMove()
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
proc undoLastMove*(self: ChessBoard): Move {.discardable.} =
## Undoes the last move, restoring the previous position.
## If there are no positions to roll back to to, this is a
## no-op. Returns the move that was performed (may be empty)
proc undoMove*(self: ChessBoard, move: Move): Move {.discardable.} =
## Undoes the given move if possible
result = emptyMove()
if self.positions.len() == 0:
return
var
previous = self.positions[^1]
oppositeMove = Move(piece: previous.move.piece, targetSquare: previous.move.startSquare, startSquare: previous.move.targetSquare)
self.removePiece(previous.move.startSquare)
var move: Move = move
let previous = self.positions.pop()
move.flag = Backwards
self.doMove(move)
self.position = previous
if previous.move != emptyMove():
self.spawnPiece(previous.move.startSquare, previous.move.piece)
self.updateAttackedSquares()
self.updatePositions(oppositeMove)
if previous.captured != emptyPiece():
self.spawnPiece(previous.move.targetSquare, previous.captured)
discard self.positions.pop()
return self.position.move
self.position.move = move
#self.updateLocations(move)
return 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
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
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
if move.piece.kind == Pawn and move.piece.color.doublePush() + move.startSquare == move.targetSquare:
move.flag = DoublePush
if move notin self.generateMoves(move.startSquare):
# Piece cannot arrive to destination (blocked
# or otherwise invalid move)
return false
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
# on our king: not allowed
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.} =
## Like the other makeMove(), but with a Move object
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()
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.} =
@ -1360,6 +1387,63 @@ proc pretty*(self: ChessBoard): string =
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:
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"
@ -1417,6 +1501,6 @@ when isMainModule:
when compileOption("profiler"):
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"