Switch to better mechanism to keep track of pins

This commit is contained in:
Mattia Giambirtone 2023-10-25 22:41:04 +02:00
parent b0ebdc02a6
commit 1a89b437fa
1 changed files with 207 additions and 117 deletions

View File

@ -19,16 +19,7 @@ import std/strutils
import std/strformat import std/strformat
import std/sequtils import std/sequtils
type type
# Useful type aliases
Location* = tuple[row, col: int8]
Attacked = seq[tuple[source, dest: Location]]
Pieces = tuple[king: Location, queens: seq[Location], rooks: seq[Location],
bishops: seq[Location], knights: seq[Location],
pawns: seq[Location]]
PieceColor* = enum PieceColor* = enum
## A piece color enumeration ## A piece color enumeration
@ -38,7 +29,7 @@ type
PieceKind* = enum PieceKind* = enum
## A chess piece enumeration ## A chess piece enumeration
Empty = '\0', # No piece Empty = 0'i8, # No piece
Bishop = 'b', Bishop = 'b',
King = 'k' King = 'k'
Knight = 'n', Knight = 'n',
@ -60,13 +51,24 @@ type
# Castling metadata # Castling metadata
CastleLong, CastleLong,
CastleShort, CastleShort,
XRay, # Move is an X-ray attack
# Pawn promotion metadata # Pawn promotion metadata
PromoteToQueen, PromoteToQueen,
PromoteToRook, PromoteToRook,
PromoteToBishop, PromoteToBishop,
PromoteToKnight PromoteToKnight
# Useful type aliases
Location* = tuple[row, col: int8]
Attacked = seq[tuple[source, target: Location]]
Pinned = seq[tuple[source, target, direction: Location]]
Pieces = tuple[king: Location, queens: seq[Location], rooks: seq[Location],
bishops: seq[Location], knights: seq[Location],
pawns: seq[Location]]
CountData = tuple[nodes: uint64, captures: uint64, castles: uint64, checks: uint64, promotions: uint64, enPassant: uint64, checkmates: uint64]
Move* = object Move* = object
## A chess move ## A chess move
piece*: Piece piece*: Piece
@ -91,13 +93,12 @@ type
# every 2 ply # every 2 ply
fullMoveCount: int16 fullMoveCount: int16
# En passant target square (see https://en.wikipedia.org/wiki/En_passant) # En passant target square (see https://en.wikipedia.org/wiki/En_passant)
# If en passant is not possible, both the row and
# column of the position will be set to -1
enPassantSquare*: Move enPassantSquare*: Move
# 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: Attacked, black: Attacked] attacked: tuple[white: Attacked, black: Attacked]
pinned: tuple[white: Pinned, black: Pinned]
# 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
@ -127,6 +128,8 @@ proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.}
proc makeMove*(self: ChessBoard, startSquare, targetSquare: Location): Move {.discardable.} proc makeMove*(self: ChessBoard, startSquare, targetSquare: Location): Move {.discardable.}
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 `-`*(a: Location): Location = (-a.row, -a.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, color: PieceColor = None): bool proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): bool
@ -136,6 +139,13 @@ proc doMove(self: ChessBoard, move: Move)
proc pretty*(self: ChessBoard): string proc pretty*(self: ChessBoard): string
proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) proc spawnPiece(self: ChessBoard, location: Location, piece: Piece)
proc updateAttackedSquares(self: ChessBoard) proc updateAttackedSquares(self: ChessBoard)
proc getPinnedDirections(self: ChessBoard, loc: Location): seq[Location]
proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} =
for x in other:
self.add(x)
# 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 +156,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.} = (-1, 0) func topSide(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 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))
@ -275,7 +285,19 @@ proc newChessboard: ChessBoard =
enPassantSquare: emptyMove(), enPassantSquare: emptyMove(),
move: emptyMove(), move: emptyMove(),
turn: White, turn: White,
fullMoveCount: 1) fullMoveCount: 1,
pieces: (white: (king: emptyLocation(),
queens: @[],
rooks: @[],
bishops: @[],
knights: @[],
pawns: @[]),
black: (king: emptyLocation(),
queens: @[],
rooks: @[],
bishops: @[],
knights: @[],
pawns: @[])))
@ -532,7 +554,6 @@ func getPiece*(self: ChessBoard, square: string): Piece =
return self.getPiece(square.algebraicToLocation()) return self.getPiece(square.algebraicToLocation())
func isCapture*(move: Move): bool {.inline.} = func isCapture*(move: Move): bool {.inline.} =
## Returns whether the given move is a capture ## Returns whether the given move is a capture
## or not ## or not
@ -555,9 +576,9 @@ proc inCheck*(self: ChessBoard, color: PieceColor = None): bool =
color = self.getActiveColor() color = self.getActiveColor()
case color: case color:
of White: of White:
return self.isAttacked(self.position.pieces.white.king, color) return self.isAttacked(self.position.pieces.white.king, Black)
of Black: of Black:
return self.isAttacked(self.position.pieces.black.king, color) return self.isAttacked(self.position.pieces.black.king, White)
else: else:
# Unreachable # Unreachable
discard discard
@ -608,38 +629,40 @@ proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king:
if result.king: if result.king:
# Short castle # Short castle
var location = loc var
location = loc
otherPiece: Piece
while true: while true:
location = location + kingSide location = location + kingSide
if not location.isValid(): if not location.isValid():
break break
if self.grid[location.row, location.col].color == color: otherPiece = self.grid[location.row, location.col]
# Blocked by own piece
if otherPiece.color == None:
continue
if otherPiece.color == color.opposite() or otherPiece.kind != Rook or self.isAttacked(location, color.opposite()):
result.king = false result.king = false
break break
# Square is attacked or blocked by enemy piece
if self.isAttacked(location, color) or self.grid[location.row, location.col].color != None:
result.king = false
break
# Square is occupied by our rook: we're done. No need to check the color or type of it (because
# if it weren't the right color, castling rights would've already been lost and we wouldn't
# have got this far)
if location == color.kingSideRook(): if location == color.kingSideRook():
break break
# Square is empty and not attacked. Keep going
if result.queen: if result.queen:
# Long castle # Long castle
var location = loc var
location = loc
otherPiece: Piece
while true: while true:
location = location + queenSide location = location + queenSide
if not location.isValid(): if not location.isValid():
break break
if self.grid[location.row, location.col].color == color: otherPiece = self.grid[location.row, location.col]
result.queen = false
break if otherPiece.color == None:
if self.isAttacked(location, color) or self.grid[location.row, location.col].color != None: continue
result.queen = false
if otherPiece.color == color.opposite() or otherPiece.kind != Rook or self.isAttacked(location, color.opposite()):
result.king = false
break break
if location == color.queenSideRook(): if location == color.queenSideRook():
break break
@ -684,6 +707,17 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
if diagonal.isValid() and self.grid[diagonal.row, diagonal.col].color == piece.color.opposite() and self.grid[diagonal.row, diagonal.col].kind != King: if diagonal.isValid() and self.grid[diagonal.row, diagonal.col].color == piece.color.opposite() and self.grid[diagonal.row, diagonal.col].kind != King:
locations.add(diagonal) locations.add(diagonal)
flags.add(Capture) flags.add(Capture)
var
newLocation: Location
newLocations: seq[Location]
let pins = self.getPinnedDirections(location)
for pin in pins:
newLocation = location + pin
# Pin direction is legal
if newLocation in locations:
newLocations.add(newLocation)
if pins.len() > 0:
locations = newLocations
var targetPiece: Piece var targetPiece: Piece
for (target, flag) in zip(locations, flags): for (target, flag) in zip(locations, flags):
@ -698,10 +732,10 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
## Generates moves for the sliding piece in the given location ## Generates moves for the sliding piece in the given location
var let piece = self.grid[location.row, location.col]
piece = self.grid[location.row, location.col]
doAssert piece.kind in [Bishop, Rook, Queen], &"generateSlidingMoves called on a {piece.kind}" doAssert piece.kind in [Bishop, Rook, Queen], &"generateSlidingMoves called on a {piece.kind}"
var directions: seq[Location] = @[] var directions: seq[Location] = @[]
# Only check in the right directions for the chosen piece # Only check in the right directions for the chosen piece
if piece.kind in [Bishop, Queen]: if piece.kind in [Bishop, Queen]:
directions.add(piece.color.topLeftDiagonal()) directions.add(piece.color.topLeftDiagonal())
@ -713,6 +747,11 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
directions.add(piece.color.bottomSide()) directions.add(piece.color.bottomSide())
directions.add(piece.color.rightSide()) directions.add(piece.color.rightSide())
directions.add(piece.color.leftSide()) directions.add(piece.color.leftSide())
let pinned = self.getPinnedDirections(location)
if pinned.len() > 0:
# If a sliding piece is pinned then it can only
# move along the pinning direction(s)
directions = pinned
for direction in directions: for direction in directions:
# Slide in this direction as long as it's possible # Slide in this direction as long as it's possible
var var
@ -795,6 +834,11 @@ proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] =
piece.color.bottomRightKnightMove(long=false), piece.color.bottomRightKnightMove(long=false),
piece.color.topLeftKnightMove(long=false), piece.color.topLeftKnightMove(long=false),
piece.color.topRightKnightMove(long=false)] piece.color.topRightKnightMove(long=false)]
let pinned = self.getPinnedDirections(location)
if pinned.len() > 0:
# Knight is pinned: can't move!
return @[]
for direction in directions: for direction in directions:
# Jump to this square # Jump to this square
let square: Location = location + direction let square: Location = location + direction
@ -851,11 +895,11 @@ proc getAttackers*(self: ChessBoard, square: Location, color = None): seq[Locati
case color: case color:
of White: of White:
for attack in self.position.attacked.black: for attack in self.position.attacked.black:
if attack.dest == square: if attack.target == square:
result.add(attack.source) result.add(attack.source)
of Black: of Black:
for attack in self.position.attacked.white: for attack in self.position.attacked.white:
if attack.dest == square: if attack.target == square:
result.add(attack.source) result.add(attack.source)
else: else:
# Unreachable # Unreachable
@ -866,18 +910,18 @@ proc getAttackers*(self: ChessBoard, square: Location, color = None): seq[Locati
# getAttackers) # getAttackers)
proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): bool = proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): bool =
## Returns whether the given location is attacked ## Returns whether the given location is attacked
## by the given opponent ## by the given color
var color = color var color = color
if color == None: if color == None:
color = self.getActiveColor() color = self.getActiveColor().opposite()
case color: case color:
of White:
for attack in self.position.attacked.black:
if attack.dest == loc:
return true
of Black: of Black:
for attack in self.position.attacked.black: for attack in self.position.attacked.black:
if attack.dest == loc: if attack.target == loc:
return true
of White:
for attack in self.position.attacked.white:
if attack.target == loc:
return true return true
of None: of None:
discard discard
@ -885,12 +929,12 @@ proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): boo
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 the current
return self.isAttacked(square.algebraicToLocation()) return self.isAttacked(square.algebraicToLocation())
func addAttack(self: ChessBoard, attack: tuple[source, dest: Location], color: PieceColor) {.inline.} = func addAttack(self: ChessBoard, attack: tuple[source, target: Location], color: PieceColor) {.inline.} =
if attack.source.isValid() and attack.dest.isValid(): if attack.source.isValid() and attack.target.isValid():
case color: case color:
of White: of White:
self.position.attacked.white.add(attack) self.position.attacked.white.add(attack)
@ -900,18 +944,33 @@ func addAttack(self: ChessBoard, attack: tuple[source, dest: Location], color: P
discard discard
proc updatePawnAttacks(self: ChessBoard) = proc getPinnedDirections(self: ChessBoard, loc: Location): seq[Location] =
let piece = self.grid[loc.row, loc.col]
case piece.color:
of None:
discard
of White:
for pin in self.position.pinned.black:
if pin.target == loc:
result.add(pin.direction)
of Black:
for pin in self.position.pinned.white:
if pin.target == loc:
result.add(pin.direction)
proc updatePawnAttacks(self: ChessBoard) {.thread.} =
## Internal helper of updateAttackedSquares ## Internal helper of updateAttackedSquares
for loc in self.position.pieces.white.pawns: for loc in self.position.pieces.white.pawns:
# Pawns are special in how they capture (i.e. the # Pawns are special in how they capture (i.e. the
# squares they can move to do not match the squares # squares they can move to do not match the squares
# they can capture on. Sneaky fucks) # they can capture on. Sneaky fucks)
self.addAttack((loc, loc + White.topRightDiagonal()), White) self.addAttack((loc, loc + White.topRightDiagonal()), White)
self.addAttack((loc, loc + White.topRightDiagonal()), White) self.addAttack((loc, loc + White.topLeftDiagonal()), White)
# We do the same thing for black # We do the same thing for black
for loc in self.position.pieces.black.pawns: for loc in self.position.pieces.black.pawns:
self.addAttack((loc, loc + Black.topRightDiagonal()), Black) self.addAttack((loc, loc + Black.topRightDiagonal()), Black)
self.addAttack((loc, loc + Black.topRightDiagonal()), Black) self.addAttack((loc, loc + Black.topLeftDiagonal()), Black)
proc updateKingAttacks(self: ChessBoard) = proc updateKingAttacks(self: ChessBoard) =
@ -951,25 +1010,27 @@ proc updateKnightAttacks(self: ChessBoard) =
self.addAttack((loc, loc + Black.bottomRightKnightMove(long=false)), Black) self.addAttack((loc, loc + Black.bottomRightKnightMove(long=false)), Black)
proc getSlidingAttacks(self: ChessBoard, loc: Location): Attacked = proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked, pins: Pinned] =
## Internal helper of updateSlidingAttacks ## Internal helper of updateSlidingAttacks
var var
directions: seq[Location] = @[] directions: seq[Location] = @[]
square: Location = loc
otherPiece: Piece
let piece = self.grid[loc.row, loc.col] let piece = self.grid[loc.row, loc.col]
if piece.kind in [Bishop, Queen]: if piece.kind in [Bishop, Queen]:
directions.add(piece.color.topLeftDiagonal()) directions.add(piece.color.topLeftDiagonal())
directions.add(piece.color.topRightDiagonal()) directions.add(piece.color.topRightDiagonal())
directions.add(piece.color.bottomLeftDiagonal()) directions.add(piece.color.bottomLeftDiagonal())
directions.add(piece.color.bottomRightDiagonal()) directions.add(piece.color.bottomRightDiagonal())
if piece.kind in [Queen, Rook]: if piece.kind in [Queen, Rook]:
directions.add(piece.color.topSide()) directions.add(piece.color.topSide())
directions.add(piece.color.bottomSide()) directions.add(piece.color.bottomSide())
directions.add(piece.color.rightSide()) directions.add(piece.color.rightSide())
directions.add(piece.color.leftSide()) directions.add(piece.color.leftSide())
for direction in directions: for direction in directions:
var
square = loc square = loc
otherPiece: Piece
# Slide in this direction as long as it's possible # Slide in this direction as long as it's possible
while true: while true:
square = square + direction square = square + direction
@ -977,46 +1038,71 @@ proc getSlidingAttacks(self: ChessBoard, loc: Location): Attacked =
if not square.isValid(): if not square.isValid():
break break
otherPiece = self.grid[square.row, square.col] otherPiece = self.grid[square.row, square.col]
# A piece is in the way: we cannot proceed # Target square is attacked (even if a friendly piece
# any further # is present, because in this case we're defending
if otherPiece.color notin [piece.color.opposite(), None]: # it)
result.attacks.add((loc, square))
# Empty square, keep going
if otherPiece.color == None:
continue
if otherPiece.color == piece.color.opposite and otherPiece.kind != King:
# We found an enemy piece that is not
# the enemy king. We don't break out
# immediately because we first want
# to check if we've pinned a piece
var
otherSquare: Location = square
behindPiece: Piece
while true:
otherSquare = otherSquare + direction
if not otherSquare.isValid():
break
behindPiece = self.grid[otherSquare.row, otherSquare.col]
if behindPiece.color == None:
continue
if behindPiece.color == piece.color.opposite and behindPiece.kind == King:
# The enemy king is behind this enemy piece: pin it in
# this direction relative to them (that's why we have the
# minus sign: up for us is down for them and vice versa)
result.pins.add((loc, square, -direction))
else:
break
break break
# Target square is attacked
result.add((loc, square))
proc updateSlidingAttacks(self: ChessBoard) = proc updateSlidingAttacks(self: ChessBoard) =
## Internal helper of updateAttackedSquares ## Internal helper of updateAttackedSquares
var var data: tuple[attacks: Attacked, pins: Pinned]
directions: seq[Location]
piece: Piece
# Bishops
for loc in self.position.pieces.white.bishops: for loc in self.position.pieces.white.bishops:
for attack in self.getSlidingAttacks(loc): data = self.getSlidingAttacks(loc)
self.addAttack(attack, White) self.position.attacked.white.extend(data.attacks)
for loc in self.position.pieces.black.bishops: self.position.pinned.white.extend(data.pins)
for attack in self.getSlidingAttacks(loc):
self.addAttack(attack, Black)
# Rooks
for loc in self.position.pieces.white.rooks: for loc in self.position.pieces.white.rooks:
for attack in self.getSlidingAttacks(loc): data = self.getSlidingAttacks(loc)
self.addAttack(attack, White) self.position.attacked.white.extend(data.attacks)
for loc in self.position.pieces.black.rooks: self.position.pinned.white.extend(data.pins)
for attack in self.getSlidingAttacks(loc):
self.addAttack(attack, Black)
# Queens
for loc in self.position.pieces.white.queens: for loc in self.position.pieces.white.queens:
for attack in self.getSlidingAttacks(loc): data = self.getSlidingAttacks(loc)
self.addAttack(attack, White) self.position.attacked.white.extend(data.attacks)
self.position.pinned.white.extend(data.pins)
for loc in self.position.pieces.black.bishops:
data = self.getSlidingAttacks(loc)
self.position.attacked.black.extend(data.attacks)
self.position.pinned.black.extend(data.pins)
for loc in self.position.pieces.black.rooks:
data = self.getSlidingAttacks(loc)
self.position.attacked.black.extend(data.attacks)
self.position.pinned.black.extend(data.pins)
for loc in self.position.pieces.black.queens: for loc in self.position.pieces.black.queens:
for attack in self.getSlidingAttacks(loc): data = self.getSlidingAttacks(loc)
self.addAttack(attack, Black) self.position.attacked.black.extend(data.attacks)
self.position.pinned.black.extend(data.pins)
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
self.position.attacked.white.setLen(0) self.position.attacked.white.setLen(0)
self.position.attacked.black.setLen(0) self.position.attacked.black.setLen(0)
@ -1024,6 +1110,7 @@ proc updateAttackedSquares(self: ChessBoard) =
self.updatePawnAttacks() self.updatePawnAttacks()
# Sliding pieces # Sliding pieces
self.updateSlidingAttacks() self.updateSlidingAttacks()
# Knights
self.updateKnightAttacks() self.updateKnightAttacks()
# Kings # Kings
self.updateKingAttacks() self.updateKingAttacks()
@ -1147,11 +1234,7 @@ proc updateLocations(self: ChessBoard, move: Move) =
## the pieces on the board after a move ## the pieces on the board after a move
if move.isCapture(): if move.isCapture():
self.position.captured = self.grid[move.targetSquare.row, move.targetSquare.col] self.position.captured = self.grid[move.targetSquare.row, move.targetSquare.col]
try:
self.removePiece(move.targetSquare, attack=false) self.removePiece(move.targetSquare, attack=false)
except AssertionDefect:
echo move
raise
# Update the positional metadata of the moving piece # Update the positional metadata of the moving piece
self.movePiece(move) self.movePiece(move)
@ -1246,7 +1329,6 @@ proc doMove(self: ChessBoard, move: Move) =
self.spawnPiece(move.targetSquare, Piece(kind: Rook, color: move.piece.color)) self.spawnPiece(move.targetSquare, Piece(kind: Rook, color: move.piece.color))
of PromoteToQueen: of PromoteToQueen:
self.spawnPiece(move.targetSquare, Piece(kind: Queen, color: move.piece.color)) self.spawnPiece(move.targetSquare, Piece(kind: Queen, color: move.piece.color))
else: else:
discard discard
let previous = self.position let previous = self.position
@ -1493,16 +1575,35 @@ proc pretty*(self: ChessBoard): string =
result &= "\x1b[0m" result &= "\x1b[0m"
proc countLegalMoves*(self: ChessBoard, ply: int, verbose: bool = false, current: int = 0): tuple[nodes: int, captures: int, castles: int, checks: int, promotions: int] = proc perft*(self: ChessBoard, ply: int, verbose: bool = false): CountData =
## Counts (and debugs) the number of legal positions reached after ## Counts (and debugs) the number of legal positions reached after
## the given number of ply ## the given number of ply
var verbose = verbose
if ply == 0: if ply == 0:
result = (1, 0, 0, 0, 0) result = (1, 0, 0, 0, 0, 0, 0)
else: else:
var let moves = self.generateAllMoves()
before: string if len(moves) == 0:
for move in self.generateAllMoves(): inc(result.checkmates)
before = self.pretty() for move in moves:
if verbose:
let canCastle = self.canCastle(move.piece.color)
#echo "\x1Bc"
echo &"Ply: {self.position.plyFromRoot}"
echo &"Move: {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}, from ({move.startSquare.row}, {move.startSquare.col}) to ({move.targetSquare.row}, {move.targetSquare.col})"
echo &"Turn: {move.piece.color}"
echo &"Piece: {move.piece.kind}"
echo &"Flag: {move.flag}"
echo &"In check: {(if self.inCheck(): \"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 "Before:\n"
echo self.pretty()
try:
discard readLine(stdin)
except IOError:
discard
except EOFError:
discard
self.doMove(move) self.doMove(move)
case move.flag: case move.flag:
of Capture: of Capture:
@ -1511,41 +1612,30 @@ proc countLegalMoves*(self: ChessBoard, ply: int, verbose: bool = false, current
inc(result.castles) inc(result.castles)
of PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook: of PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook:
inc(result.promotions) inc(result.promotions)
of EnPassant:
inc(result.enPassant)
else: else:
discard discard
if self.inCheck(): if self.inCheck():
# Opponent king is in check # Opponent king is in check
inc(result.checks) inc(result.checks)
if verbose: if verbose:
let canCastle = self.canCastle(move.piece.color) echo "Now:\n"
echo "\x1Bc"
echo &"Ply: {self.position.plyFromRoot} (move {current + result.nodes + 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 &"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 before
echo "\nNow: "
echo self.pretty() echo self.pretty()
echo &"\n\nTotal captures: {result.captures}"
echo &"Total castles: {result.castles}"
echo &"Total checks: {result.checks}"
echo &"Total promotions: {result.promotions}"
try: try:
discard readLine(stdin) discard readLine(stdin)
except IOError: except IOError:
discard discard
except EOFError: except EOFError:
discard discard
let next = self.countLegalMoves(ply - 1, verbose, result.nodes + 1) let next = self.perft(ply - 1, verbose)
result.nodes += next.nodes result.nodes += next.nodes
result.captures += next.captures result.captures += next.captures
result.checks += next.checks result.checks += next.checks
result.promotions += next.promotions result.promotions += next.promotions
result.castles += next.castles result.castles += next.castles
result.enPassant += next.enPassant
result.checkmates += next.checkmates
self.undoMove(move) self.undoMove(move)
@ -1607,6 +1697,6 @@ when isMainModule:
when compileOption("profiler"): when compileOption("profiler"):
import nimprof import nimprof
#b = newChessboardFromFEN("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - ") # b = newChessboardFromFEN("fen")
echo b.countLegalMoves(3, verbose=false) echo b.perft(4, verbose=true)
echo "All tests were successful" echo "All tests were successful"