Refactoring and bug fixes

This commit is contained in:
Mattia Giambirtone 2024-04-08 20:28:31 +02:00
parent a4954a971b
commit 77129855df
2 changed files with 286 additions and 151 deletions

View File

@ -14,7 +14,8 @@
import std/strutils import std/strutils
import std/strformat import std/strformat
import std/sequtils import std/times
import std/math
type type
@ -75,7 +76,6 @@ type
Position* = ref object Position* = ref object
## A chess position ## A chess position
move: Move
# Did the rooks on either side/the king move? # Did the rooks on either side/the king move?
castlingAvailable: tuple[white, black: tuple[queen, king: bool]] castlingAvailable: tuple[white, black: tuple[queen, king: bool]]
# Number of half-moves that were performed # Number of half-moves that were performed
@ -103,12 +103,12 @@ type
ChessBoard* = ref object ChessBoard* = ref object
## A chess board object ## A chess board object
# An 8x8 matrix we use for constant # The actual board where pieces live
# time lookup of pieces by their location # (flattened 8x8 matrix)
grid: seq[Piece] grid: seq[Piece]
# The current position # The current position
position: Position position: Position
# List of all reached positions # List of all previously reached positions
positions: seq[Position] positions: seq[Position]
@ -137,12 +137,15 @@ proc getPinnedDirections(self: ChessBoard, loc: Location): seq[Location]
proc getAttacks*(self: ChessBoard, loc: Location): Attacked proc getAttacks*(self: ChessBoard, loc: Location): Attacked
proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked, pins: Attacked] proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked, pins: Attacked]
proc inCheck*(self: ChessBoard, color: PieceColor = None): bool proc inCheck*(self: ChessBoard, color: PieceColor = None): bool
proc toFEN*(self: ChessBoard): string
proc undoLastMove*(self: ChessBoard)
proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} = proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} =
for x in other: for x in other:
self.add(x) self.add(x)
proc resetBoard*(self: ChessBoard)
# Due to our board layout, directions of movement are reversed for white and black, so # Due to our board layout, directions of movement are reversed for white and black, so
# we need these helpers to avoid going mad with integer tuples and minus signs everywhere # we need these helpers to avoid going mad with integer tuples and minus signs everywhere
@ -260,6 +263,18 @@ func getStartRow(piece: Piece): int {.inline.} =
return 0 return 0
func getKingStartingPosition(color: PieceColor): Location {.inline.} =
## Retrieves the starting location of the king
## for the given color
case color:
of White:
return (7, 4)
of Black:
return (0, 4)
else:
discard
func getLastRow(color: PieceColor): int {.inline.} = func getLastRow(color: PieceColor): int {.inline.} =
## Retrieves the location of the last ## Retrieves the location of the last
## row relative to the given color ## row relative to the given color
@ -277,9 +292,10 @@ proc newChessboard: ChessBoard =
new(result) new(result)
# Turns our flat sequence into an 8x8 grid # Turns our flat sequence into an 8x8 grid
result.grid = newSeqOfCap[Piece](64) result.grid = newSeqOfCap[Piece](64)
for _ in 0..63:
result.grid.add(emptyPiece())
result.position = Position(attacked: (@[], @[]), result.position = Position(attacked: (@[], @[]),
enPassantSquare: emptyLocation(), enPassantSquare: emptyLocation(),
move: emptyMove(),
turn: White, turn: White,
fullMoveCount: 1, fullMoveCount: 1,
pieces: (white: (king: emptyLocation(), pieces: (white: (king: emptyLocation(),
@ -331,10 +347,8 @@ proc newChessboardFromFEN*(fen: string): ChessBoard =
of 'r', 'n', 'b', 'q', 'k', 'p': of 'r', 'n', 'b', 'q', 'k', 'p':
# We know for a fact these values are in our # We know for a fact these values are in our
# enumeration, so all is good # enumeration, so all is good
{.push.}
{.warning[HoleEnumConv]:off.} {.warning[HoleEnumConv]:off.}
piece = Piece(kind: PieceKind(c.toLowerAscii()), color: if c.isUpperAscii(): White else: Black) piece = Piece(kind: PieceKind(c.toLowerAscii()), color: if c.isUpperAscii(): White else: Black)
{.pop.}
case piece.color: case piece.color:
of Black: of Black:
case piece.kind: case piece.kind:
@ -462,8 +476,8 @@ proc newDefaultChessboard*: ChessBoard {.inline.} =
proc countPieces*(self: ChessBoard, kind: PieceKind, color: PieceColor): int = proc countPieces*(self: ChessBoard, kind: PieceKind, color: PieceColor): int =
## Returns the number of pieces with ## Returns the number of pieces with
## the given color and type in the given ## the given color and type in the
## position ## current position
case color: case color:
of White: of White:
case kind: case kind:
@ -519,9 +533,9 @@ func rankToColumn(rank: int): int8 {.inline.} =
return indeces[rank - 1] return indeces[rank - 1]
func rowToRank(row: int): int {.inline.} = func rowToFile(row: int): int {.inline.} =
## Converts a row into our grid into ## Converts a row into our grid into
## a chess rank ## a chess file
const indeces = [8, 7, 6, 5, 4, 3, 2, 1] const indeces = [8, 7, 6, 5, 4, 3, 2, 1]
return indeces[row] return indeces[row]
@ -548,7 +562,7 @@ proc algebraicToLocation*(s: string): Location =
func locationToAlgebraic*(loc: Location): string {.inline.} = func locationToAlgebraic*(loc: Location): string {.inline.} =
## Converts a location from our internal row, column ## Converts a location from our internal row, column
## notation to a square in algebraic notation ## notation to a square in algebraic notation
return &"{char(uint8(loc.col) + uint8('a'))}{rowToRank(loc.row)}" return &"{char(uint8(loc.col) + uint8('a'))}{rowToFile(loc.row)}"
func getPiece*(self: ChessBoard, loc: Location): Piece {.inline.} = func getPiece*(self: ChessBoard, loc: Location): Piece {.inline.} =
@ -582,7 +596,7 @@ func getPromotionType*(move: Move): MoveFlag {.inline.} =
func isCapture*(move: Move): bool {.inline.} = func isCapture*(move: Move): bool {.inline.} =
## Returns whether the given move is a ## Returns whether the given move is a
## cature ## cature
result = (move.flags and Capture.uint16) != 0 result = (move.flags and Capture.uint16) == Capture.uint16
func isCastling*(move: Move): bool {.inline.} = func isCastling*(move: Move): bool {.inline.} =
@ -614,6 +628,30 @@ func isDoublePush*(move: Move): bool {.inline.} =
result = (move.flags and DoublePush.uint16) != 0 result = (move.flags and DoublePush.uint16) != 0
func getFlags*(move: Move): seq[MoveFlag] =
## Gets all the flags of this move
for flag in [EnPassant, Capture, DoublePush, CastleLong, CastleShort,
PromoteToBishop, PromoteToKnight, PromoteToQueen,
PromoteToRook]:
if (move.flags and flag.uint16) == flag.uint16:
result.add(flag)
if result.len() == 0:
result.add(Default)
func getKing(self: ChessBoard, color: PieceColor): Location {.inline.} =
var color = color
if color == None:
color = self.getActiveColor()
case color:
of White:
return self.position.pieces.white.king
of Black:
return self.position.pieces.black.king
else:
discard
proc inCheck*(self: ChessBoard, color: PieceColor = None): bool = proc inCheck*(self: ChessBoard, color: PieceColor = None): bool =
## Returns whether the given color's ## Returns whether the given color's
## king is in check. If the color is ## king is in check. If the color is
@ -650,6 +688,12 @@ proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king:
of None: of None:
# Unreachable # Unreachable
discard discard
# Some of these checks may seem redundant, but we
# perform them because they're less expensive
# King is not on its starting square
if self.getKing(color) != getKingStartingPosition(color):
return (false, false)
if self.inCheck(color): if self.inCheck(color):
# King can not castle out of check # King can not castle out of check
return (false, false) return (false, false)
@ -752,6 +796,7 @@ proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Location] =
# in which case either the king has to move or # in which case either the king has to move or
# that piece has to be captured, but this is # that piece has to be captured, but this is
# already implicitly handled by the loop below) # already implicitly handled by the loop below)
var location = attacker var location = attacker
while location != king: while location != king:
location = location + attack.direction location = location + attack.direction
@ -760,40 +805,24 @@ proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Location] =
result.add(location) result.add(location)
func getKing(self: ChessBoard, color: PieceColor): Location {.inline.} =
var color = color
if color == None:
color = self.getActiveColor()
case color:
of White:
return self.position.pieces.white.king
of Black:
return self.position.pieces.black.king
else:
discard
proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
## Generates the possible moves for the pawn in the given ## Generates the possible moves for the pawn in the given
## location ## location
var var
piece = self.grid[location.row, location.col] piece = self.grid[location.row, location.col]
locations: seq[Location] = @[] targets: 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 forward = piece.color.topSide() + location let forward = piece.color.topSide() + location
# Only if the square is empty though # Only if the square is empty though
if forward.isValid() and self.grid[forward.row, forward.col].color == None: if forward.isValid() and self.grid[forward.row, forward.col].color == None:
locations.add(forward) targets.add(forward)
flags.add(Default)
# If the pawn is on its first rank, it can push two squares # If the pawn is on its first rank, it can push two squares
if location.row == piece.getStartRow(): if location.row == piece.getStartRow():
let double = location + piece.color.doublePush() let double = location + piece.color.doublePush()
# Check that both squares are empty # Check that both squares are empty
if double.isValid() and self.grid[forward.row, forward.col].color == None and self.grid[double.row, double.col].color == None: if double.isValid() and self.grid[forward.row, forward.col].color == None and self.grid[double.row, double.col].color == None:
locations.add(double) targets.add(double)
flags.add(DoublePush)
let enPassantPawn = self.getEnPassantTarget() + piece.color.opposite().topSide() let enPassantPawn = self.getEnPassantTarget() + piece.color.opposite().topSide()
# They can also move on either diagonal one # They can also move on either diagonal one
# square, but only to capture or for en passant # square, but only to capture or for en passant
@ -819,40 +848,37 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
if p.color == piece.color.opposite() and p.kind in [Queen, Rook]: if p.color == piece.color.opposite() and p.kind in [Queen, Rook]:
ok = false ok = false
if ok: if ok:
locations.add(diagonal) targets.add(diagonal)
flags.add(EnPassant)
elif otherPiece.color == piece.color.opposite() and otherPiece.kind != King: elif otherPiece.color == piece.color.opposite() and otherPiece.kind != King:
locations.add(diagonal) targets.add(diagonal)
flags.add(Capture)
var
newLocation: Location
newFlags: seq[MoveFlag]
newLocations: seq[Location]
# Check for pins # Check for pins
let pins = self.getPinnedDirections(location) let pinned = self.getPinnedDirections(location)
for pin in pins: if pinned.len() > 0:
newLocation = location + pin var newTargets: seq[Location] = @[]
let loc = locations.find(newLocation) for target in targets:
if loc != -1: if target in pinned:
# Pin direction is legal for this piece newTargets.add(target)
newLocations.add(newLocation) targets = newTargets
newFlags.add(flags[loc])
if pins.len() > 0:
locations = newLocations
flags = newFlags
let checked = self.inCheck() let checked = self.inCheck()
let resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color) let resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color)
var targetPiece: Piece var targetPiece: Piece
for (target, flag) in zip(locations, flags): for target in targets:
if checked and target notin resolutions: if checked and target notin resolutions:
continue continue
targetPiece = self.grid[target.row, target.col] targetPiece = self.grid[target.row, target.col]
var flags: uint16 = Default.uint16
if targetPiece.color != None:
flags = flags or Capture.uint16
elif abs(location.row - target.row) == 2:
flags = flags or DoublePush.uint16
elif target == self.getEnPassantTarget():
flags = flags or EnPassant.uint16
if target.row == piece.color.getLastRow(): if target.row == piece.color.getLastRow():
# Pawn reached the other side of the board: generate all potential piece promotions # Pawn reached the other side of the board: generate all potential piece promotions
for promotionType in [PromoteToKnight, PromoteToBishop, PromoteToRook, PromoteToQueen]: for promotionType in [PromoteToKnight, PromoteToBishop, PromoteToRook, PromoteToQueen]:
result.add(Move(startSquare: location, targetSquare: target, flags: promotionType.uint16 or flag.uint16)) result.add(Move(startSquare: location, targetSquare: target, flags: promotionType.uint16 or flags))
continue continue
result.add(Move(startSquare: location, targetSquare: target, flags: flag.uint16)) result.add(Move(startSquare: location, targetSquare: target, flags: flags))
proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
@ -874,7 +900,11 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
directions.add(piece.color.leftSide()) directions.add(piece.color.leftSide())
let pinned = self.getPinnedDirections(location) let pinned = self.getPinnedDirections(location)
if pinned.len() > 0: if pinned.len() > 0:
directions = pinned var newDirections: seq[Location] = @[]
for direction in directions:
if direction in pinned:
newDirections.add(direction)
directions = newDirections
let checked = self.inCheck() let checked = self.inCheck()
let resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color) let resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color)
for direction in directions: for direction in directions:
@ -892,7 +922,9 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
if otherPiece.color == piece.color: if otherPiece.color == piece.color:
break break
if checked and square notin resolutions: if checked and square notin resolutions:
break # We don't break out of the loop because
# we might resolve the check later
continue
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
@ -978,7 +1010,6 @@ proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] =
# A friendly piece or the opponent king is is in the way # A friendly piece or the opponent king is is in the way
if otherPiece.color == piece.color or otherPiece.kind == King: if otherPiece.color == piece.color or otherPiece.kind == King:
continue continue
if checked and square notin resolutions: if checked and square notin resolutions:
continue continue
if otherPiece.color != None: if otherPiece.color != None:
@ -1070,7 +1101,7 @@ proc getAttacks*(self: ChessBoard, loc: Location): Attacked =
proc getAttackFor*(self: ChessBoard, source, target: Location): tuple[source, target, direction: Location] = proc getAttackFor*(self: ChessBoard, source, target: Location): tuple[source, target, direction: Location] =
## Returns the first attacks of the piece in the given ## Returns the first attack of the piece in the given
## source location that also attacks the target location ## source location that also attacks the target location
let piece = self.grid[source.row, source.col] let piece = self.grid[source.row, source.col]
case piece.color: case piece.color:
@ -1283,11 +1314,12 @@ proc updateAttackedSquares(self: ChessBoard) =
self.updateKingAttacks() self.updateKingAttacks()
proc removePiece(self: ChessBoard, location: Location, attack: bool = true) = proc removePiece(self: ChessBoard, location: Location, attack: bool = true, empty: 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 empty:
self.grid[location.row, location.col] = emptyPiece()
case piece.color: case piece.color:
of White: of White:
case piece.kind: case piece.kind:
@ -1388,11 +1420,11 @@ proc movePiece(self: ChessBoard, move: Move, attack: bool = true) =
self.updateAttackedSquares() self.updateAttackedSquares()
proc movePiece(self: ChessBoard, startSquare, targetSquare: Location, attack: bool = true) = proc movePiece(self: ChessBoard, startSquare, targetSquare: Location, attack: bool = true) =
## Like the other movePiece(), but with two locations ## Like the other movePiece(), but with two locations
self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare), attack) self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare), attack)
proc doMove(self: ChessBoard, move: Move) = proc doMove(self: ChessBoard, move: Move) =
## Internal function called by makeMove after ## Internal function called by makeMove after
## performing legality checks. Can be used in ## performing legality checks. Can be used in
@ -1482,13 +1514,13 @@ proc doMove(self: ChessBoard, move: Move) =
castlingAvailable.black.queen = false castlingAvailable.black.queen = false
else: else:
discard discard
# Create new position # Create new position
self.position = Position(plyFromRoot: self.position.plyFromRoot + 1, self.position = Position(plyFromRoot: self.position.plyFromRoot + 1,
halfMoveClock: halfMoveClock, halfMoveClock: halfMoveClock,
fullMoveCount: fullMoveCount, fullMoveCount: fullMoveCount,
turn: self.getActiveColor().opposite, turn: self.getActiveColor().opposite,
castlingAvailable: castlingAvailable, castlingAvailable: castlingAvailable,
move: move,
pieces: self.position.pieces, pieces: self.position.pieces,
enPassantSquare: enPassantTarget enPassantSquare: enPassantTarget
) )
@ -1500,26 +1532,23 @@ proc doMove(self: ChessBoard, move: Move) =
var var
location: Location location: Location
target: Location target: Location
flags: uint16
if move.getCastlingType() == CastleShort: if move.getCastlingType() == CastleShort:
location = piece.color.kingSideRook() location = piece.color.kingSideRook()
target = shortCastleRook() target = shortCastleRook()
flags = flags or CastleShort.uint16
else: else:
location = piece.color.queenSideRook() location = piece.color.queenSideRook()
target = longCastleRook() target = longCastleRook()
flags = flags or CastleLong.uint16
let rook = self.grid[location.row, location.col] let rook = self.grid[location.row, location.col]
let move = Move(startSquare: location, targetSquare: location + target, flags: move.flags) let move = Move(startSquare: location, targetSquare: location + target, flags: flags)
self.movePiece(move, attack=false) self.movePiece(move, attack=false)
if move.isCapture():
# Get rid of captured pieces
self.removePiece(move.targetSquare, attack=false)
if move.isEnPassant(): if move.isEnPassant():
# Make the en passant pawn disappear # Make the en passant pawn disappear
self.removePiece(move.targetSquare + piece.color.bottomSide(), attack=false) self.removePiece(move.targetSquare + piece.color.bottomSide(), attack=false)
self.movePiece(move, attack=false)
if move.isPromotion(): if move.isPromotion():
# Move is a pawn promotion: get rid of the pawn # Move is a pawn promotion: get rid of the pawn
# and spawn a new piece # and spawn a new piece
@ -1535,9 +1564,15 @@ proc doMove(self: ChessBoard, move: Move) =
self.spawnPiece(move.targetSquare, Piece(kind: Queen, color: piece.color)) self.spawnPiece(move.targetSquare, Piece(kind: Queen, color: piece.color))
else: else:
discard discard
# Update attack metadata
self.updateAttackedSquares()
if move.isCapture():
# Get rid of captured pieces
self.removePiece(move.targetSquare, attack=false, empty=false)
# Move the piece to its target square and update attack metadata
self.movePiece(move)
# TODO: Remove this, once I figure out what the heck is wrong
# with updating the board representation
self.resetBoard()
proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) = proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
@ -1588,9 +1623,7 @@ proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
proc resetBoard*(self: ChessBoard) = proc resetBoard*(self: ChessBoard) =
## Resets the internal grid representation ## Resets the internal grid representation
## according to the positional data stored ## according to the positional data stored
## in the chessboard. Warning: this can be ## in the chessboard
## expensive, especially in critical paths
## or tight loops
for i in 0..63: for i in 0..63:
self.grid[i] = emptyPiece() self.grid[i] = emptyPiece()
for loc in self.position.pieces.white.pawns: for loc in self.position.pieces.white.pawns:
@ -1618,9 +1651,8 @@ proc resetBoard*(self: ChessBoard) =
proc undoLastMove*(self: ChessBoard) = proc undoLastMove*(self: ChessBoard) =
if self.positions.len() == 0: if self.positions.len() > 0:
return self.position = self.positions.pop()
self.position = self.positions.pop()
self.resetBoard() self.resetBoard()
@ -1778,12 +1810,13 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
## the given number of ply ## the given number of ply
let moves = self.generateAllMoves() let moves = self.generateAllMoves()
if len(moves) == 0: if not bulk:
result.checkmates = 1 if len(moves) == 0 and self.inCheck():
if ply == 0: result.checkmates = 1
result.nodes = 1 if ply == 0:
return result.nodes = 1
if ply == 1 and bulk: return
elif ply == 1 and bulk:
if divide: if divide:
var postfix = "" var postfix = ""
for move in moves: for move in moves:
@ -1810,7 +1843,7 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
echo &"Move: {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}, from ({move.startSquare.row}, {move.startSquare.col}) to ({move.targetSquare.row}, {move.targetSquare.col})" echo &"Move: {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}, from ({move.startSquare.row}, {move.startSquare.col}) to ({move.targetSquare.row}, {move.targetSquare.col})"
echo &"Turn: {self.getActiveColor()}" echo &"Turn: {self.getActiveColor()}"
echo &"Piece: {self.grid[move.startSquare.row, move.startSquare.col].kind}" echo &"Piece: {self.grid[move.startSquare.row, move.startSquare.col].kind}"
echo &"Flag: {move.flags}" echo &"Flags: {move.getFlags()}"
echo &"In check: {(if self.inCheck(): \"yes\" else: \"no\")}" 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 &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
echo &"Position before move: {self.toFEN()}" echo &"Position before move: {self.toFEN()}"
@ -1821,14 +1854,15 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
echo "None" echo "None"
echo "\n", self.pretty() echo "\n", self.pretty()
self.doMove(move) self.doMove(move)
if move.isCapture(): if ply == 1:
inc(result.captures) if move.isCapture():
if move.isCastling(): inc(result.captures)
inc(result.castles) if move.isCastling():
if move.isPromotion(): inc(result.castles)
inc(result.promotions) if move.isPromotion():
if move.isEnPassant(): inc(result.promotions)
inc(result.enPassant) if move.isEnPassant():
inc(result.enPassant)
if self.inCheck(): if self.inCheck():
# Opponent king is in check # Opponent king is in check
inc(result.checks) inc(result.checks)
@ -1891,7 +1925,7 @@ proc handleGoCommand(board: ChessBoard, command: seq[string]) =
var ok = true var ok = true
for arg in args[1..^1]: for arg in args[1..^1]:
case arg: case arg:
of "bulk-count", "bulk": of "bulk":
bulk = true bulk = true
of "verbose": of "verbose":
verbose = true verbose = true
@ -1904,8 +1938,12 @@ proc handleGoCommand(board: ChessBoard, command: seq[string]) =
try: try:
let ply = parseInt(args[0]) let ply = parseInt(args[0])
if bulk: if bulk:
echo &"\nNodes searched (bulk-counting: on): {board.perft(ply, divide=true, bulk=true, verbose=verbose).nodes}\n" let t = cpuTime()
let nodes = board.perft(ply, divide=true, bulk=true, verbose=verbose).nodes
echo &"\nNodes searched (bulk-counting: on): {nodes}"
echo &"Time taken: {round(cpuTime() - t, 3)} seconds\n"
else: else:
let t = cpuTime()
let data = board.perft(ply, divide=true, verbose=verbose) let data = board.perft(ply, divide=true, verbose=verbose)
echo &"\nNodes searched (bulk-counting: off): {data.nodes}" echo &"\nNodes searched (bulk-counting: off): {data.nodes}"
echo &" - Captures: {data.captures}" echo &" - Captures: {data.captures}"
@ -1915,40 +1953,14 @@ proc handleGoCommand(board: ChessBoard, command: seq[string]) =
echo &" - Castles: {data.castles}" echo &" - Castles: {data.castles}"
echo &" - Promotions: {data.promotions}" echo &" - Promotions: {data.promotions}"
echo "" echo ""
echo &"Time taken: {round(cpuTime() - t, 3)} seconds"
except ValueError: except ValueError:
echo "Error: go: perft: invalid depth" echo "Error: go: perft: invalid depth"
else: else:
echo &"Error: go: unknown subcommand '{command[1]}'" echo &"Error: go: unknown subcommand '{command[1]}'"
proc handlePositionCommand(board: var ChessBoard, command: seq[string]) = proc handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discardable.} =
case len(command):
of 2:
case command[1]:
of "startpos":
board = newDefaultChessboard()
of "current", "cur":
echo &"Current position: {board.toFEN()}"
of "pretty":
echo board.pretty()
of "print", "show":
echo board
else:
echo &"Error: position: invalid argument '{command[1]}'"
of 3:
case command[1]:
of "fen":
try:
board = newChessboardFromFEN(command[2])
except ValueError:
echo &"Error: position: invalid FEN string '{command[2]}': {getCurrentExceptionMsg()}"
else:
echo &"Error: position: unknown subcommand '{command[1]}'"
else:
echo &"Error: position: invalid number of arguments"
proc handleMoveCommand(board: ChessBoard, command: seq[string]) =
if len(command) != 2: if len(command) != 2:
echo &"Error: move: invalid number of arguments" echo &"Error: move: invalid number of arguments"
return return
@ -1964,18 +1976,22 @@ proc handleMoveCommand(board: ChessBoard, command: seq[string]) =
try: try:
startSquare = moveString[0..1].algebraicToLocation() startSquare = moveString[0..1].algebraicToLocation()
except ValueError: except ValueError:
echo &"Error: move: invalid start square" echo &"Error: move: invalid start square ({moveString[0..1]})"
return return
try: try:
targetSquare = moveString[2..3].algebraicToLocation() targetSquare = moveString[2..3].algebraicToLocation()
except ValueError: except ValueError:
echo &"Error: move: invalid target square" echo &"Error: move: invalid target square ({moveString[2..3]})"
return return
# Since the user tells us just the source and target square of the move,
# we have to figure out all the flags by ourselves (whether it's a double
# push, a capture, a promotion, castling, etc.)
if board.grid[targetSquare.row, targetSquare.col].kind != Empty: if board.grid[targetSquare.row, targetSquare.col].kind != Empty:
flags = flags or Capture.uint16 flags = flags or Capture.uint16
if board.grid[startSquare.row, startSquare.col].kind == Pawn and abs(startSquare.row - targetSquare.row) == 2: elif board.grid[startSquare.row, startSquare.col].kind == Pawn and abs(startSquare.row - targetSquare.row) == 2:
flags = flags or DoublePush.uint16 flags = flags or DoublePush.uint16
if len(moveString) == 5: if len(moveString) == 5:
@ -1993,10 +2009,115 @@ proc handleMoveCommand(board: ChessBoard, command: seq[string]) =
echo &"Error: move: invalid promotion type" echo &"Error: move: invalid promotion type"
return return
let move = Move(startSquare: startSquare, targetSquare: targetSquare, flags: flags)
if board.makeMove(move) == emptyMove():
echo "Error: move: illegal move"
var move = Move(startSquare: startSquare, targetSquare: targetSquare, flags: flags)
if board.getPiece(move.startSquare).kind == King and move.startSquare == board.getActiveColor().getKingStartingPosition():
if move.targetSquare == move.startSquare + longCastleKing():
move.flags = move.flags or CastleLong.uint16
elif move.targetSquare == move.startSquare + shortCastleKing():
move.flags = move.flags or CastleShort.uint16
result = board.makeMove(move)
if result == emptyMove():
echo &"Error: move: {moveString} is illegal"
proc handlePositionCommand(board: var ChessBoard, command: seq[string]) =
if len(command) < 2:
echo "Error: position: invalid number of arguments"
return
# Makes sure we don't leave the board in an invalid state if
# some error occurs
var tempBoard = newChessboard()
case command[1]:
of "startpos":
tempBoard = newDefaultChessboard()
if command.len() > 2:
let args = command[2].splitWhitespace()
if args.len() > 0:
var i = 0
while i < args.len():
case args[i]:
of "moves":
var j = i + 1
while j < args.len():
if handleMoveCommand(tempBoard, @["move", args[j]]) == emptyMove():
return
inc(j)
inc(i)
board = tempBoard
of "fen":
if len(command) == 2:
echo &"Current position: {board.toFEN()}"
return
var
args = command[2].splitWhitespace()
fenString = ""
stop = 0
for i, arg in args:
if arg in ["moves", ]:
break
if i > 0:
fenString &= " "
fenString &= arg
inc(stop)
args = args[stop..^1]
try:
tempBoard = newChessboardFromFEN(fenString)
except ValueError:
echo &"Error: position: {getCurrentExceptionMsg()}"
return
if args.len() > 0:
var i = 0
while i < args.len():
case args[i]:
of "moves":
var j = i + 1
while j < args.len():
if handleMoveCommand(tempBoard, @["move", args[j]]) == emptyMove():
return
inc(j)
inc(i)
board = tempBoard
of "print":
echo board
of "pretty":
echo board.pretty()
const HELP_TEXT = """Nimfish help menu:
- go: Begin a search
Subcommands:
- perft <depth> [options]: Run the performance test at the given depth (in ply) and
print the results
Options:
- bulk: Enable bulk-counting (significantly faster, gives less statistics)
- verbose: Enable move debugging (for each and every move, not recommended on large searches)
Example: go perft 5 bulk
- position: Get/set board position
Subcommands:
- fen [string]: Set the board to the given fen string if one is provided, or print
the current position as a FEN string if no arguments are given
- startpos: Set the board to the starting position
- pretty: Pretty-print the current position
- print: Print the current position using ASCII characters only
Options:
- moves {moveList}: Perform the given moves (space-separated, all-lowercase)
in algebraic notation after the position is loaded. This option only applies
to the "startpos" and "fen" subcommands: it is ignored otherwise
Examples:
- position startpos
- position fen "..." moves a2a3 a7a6
- clear: Clear the screen
- move <move>: Perform the given move in algebraic notation
- castle: Print castling rights for each side
- check: Print if the current side to move is in check
- undo: Undoes the last move that was performed. Can be used in succession
- turn: Print which side is to move
- ep: Print the current en passant target
- pretty: Shorthand for "position pretty"
- print: Shorthand for "position print"
"""
proc main: int = proc main: int =
## Nimfish's control interface ## Nimfish's control interface
@ -2018,17 +2139,30 @@ proc main: int =
of "clear": of "clear":
echo "\x1Bc" echo "\x1Bc"
of "help": of "help":
echo "TODO" echo HELP_TEXT
of "go": of "go":
handleGoCommand(board, cmd) handleGoCommand(board, cmd)
of "position": of "position":
handlePositionCommand(board, cmd) handlePositionCommand(board, cmd)
of "move": of "move":
handleMoveCommand(board, cmd) handleMoveCommand(board, cmd)
of "pretty": of "pretty", "print":
echo board.pretty() handlePositionCommand(board, @["position", cmd[0]])
of "undo": of "undo":
board.undoLastMove() board.undoLastMove()
of "turn":
echo &"Active color: {board.getActiveColor()}"
of "ep":
let target = board.getEnPassantTarget()
if target != emptyLocation():
echo &"En passant target: {target.locationToAlgebraic()}"
else:
echo "En passant target: None"
of "castle":
let canCastle = board.canCastle()
echo &"Castling rights for {($board.getActiveColor()).toLowerAscii()}:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
of "check":
echo &"{board.getActiveColor()} king in check: {(if board.inCheck(): \"yes\" else: \"no\")}"
else: else:
echo &"Unknown command '{cmd[0]}'. Type 'help' for more information." echo &"Unknown command '{cmd[0]}'. Type 'help' for more information."
except IOError: except IOError:

View File

@ -1,4 +1,5 @@
import re import re
import os
import sys import sys
import time import time
import subprocess import subprocess
@ -25,14 +26,19 @@ def main(args: Namespace) -> int:
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
encoding="u8" encoding="u8",
text=True,
bufsize=1
) )
print(f"Starting Nimfish engine at {NIMFISH.as_posix()!r}") print(f"Starting Nimfish engine at {NIMFISH.as_posix()!r}")
nimfish_process = subprocess.Popen(NIMFISH, nimfish_process = subprocess.Popen(NIMFISH,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
encoding="u8") encoding="u8",
text=True,
bufsize=1
)
print(f"Setting position to {(args.fen if args.fen else 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1')!r}") print(f"Setting position to {(args.fen if args.fen else 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1')!r}")
if args.fen: if args.fen:
nimfish_process.stdin.write(f"position fen {args.fen}\n") nimfish_process.stdin.write(f"position fen {args.fen}\n")
@ -43,13 +49,8 @@ def main(args: Namespace) -> int:
print(f"Engines started, beginning search to depth {args.ply}") print(f"Engines started, beginning search to depth {args.ply}")
nimfish_process.stdin.write(f"go perft {args.ply} {'bulk' if args.bulk else ''}\n") nimfish_process.stdin.write(f"go perft {args.ply} {'bulk' if args.bulk else ''}\n")
stockfish_process.stdin.write(f"go perft {args.ply}\n") stockfish_process.stdin.write(f"go perft {args.ply}\n")
print("Search started, waiting for engine completion")
start_time = time.time()
stockfish_output = stockfish_process.communicate()[0] stockfish_output = stockfish_process.communicate()[0]
stockfish_time = time.time() - start_time
start_time = time.time()
nimfish_output = nimfish_process.communicate()[0] nimfish_output = nimfish_process.communicate()[0]
nimfish_time = time.time() - start_time
positions = { positions = {
"all": {}, "all": {},
"stockfish": {}, "stockfish": {},
@ -89,8 +90,8 @@ def main(args: Namespace) -> int:
total_nodes = {"stockfish": sum(positions["stockfish"][move] for move in positions["stockfish"]), total_nodes = {"stockfish": sum(positions["stockfish"][move] for move in positions["stockfish"]),
"nimfish": sum(positions["nimfish"][move] for move in positions["nimfish"])} "nimfish": sum(positions["nimfish"][move] for move in positions["nimfish"])}
total_difference = total_nodes["stockfish"] - total_nodes["nimfish"] total_difference = total_nodes["stockfish"] - total_nodes["nimfish"]
print(f"Stockfish searched {total_nodes['stockfish']} node{'' if total_nodes['stockfish'] == 1 else 's'} in {stockfish_time:.2f} seconds") print(f"Stockfish searched {total_nodes['stockfish']} node{'' if total_nodes['stockfish'] == 1 else 's'}")
print(f"Nimfish searched {total_nodes['nimfish']} node{'' if total_nodes['nimfish'] == 1 else 's'} in {nimfish_time:.2f} seconds") print(f"Nimfish searched {total_nodes['nimfish']} node{'' if total_nodes['nimfish'] == 1 else 's'}")
if total_difference > 0: if total_difference > 0:
print(f"Nimfish searched {total_difference} less node{'' if total_difference == 1 else 's'} than Stockfish") print(f"Nimfish searched {total_difference} less node{'' if total_difference == 1 else 's'} than Stockfish")
@ -117,13 +118,12 @@ def main(args: Namespace) -> int:
"To fix this, re-run the program without the --bulk option") "To fix this, re-run the program without the --bulk option")
if extra: if extra:
print(f" Breakdown by move type:") print(f" Breakdown by move type:")
print(f" - Captures: {extra.group('captures')}") print(f" - Captures: {extra.group('captures')}")
print(f" - Checks: {extra.group('checks')}") print(f" - Checks: {extra.group('checks')}")
print(f" - En Passant: {extra.group('enPassant')}") print(f" - En Passant: {extra.group('enPassant')}")
print(f" - Checkmates: {extra.group('checkmates')}") print(f" - Checkmates: {extra.group('checkmates')}")
print(f" - Castles: {extra.group('castles')}") print(f" - Castles: {extra.group('castles')}")
print(f" - Promotions: {extra.group('promotions')}") print(f" - Promotions: {extra.group('promotions')}")
print(f" - Total: {total_nodes['nimfish']}")
elif not args.bulk: elif not args.bulk:
print("Unable to locate move breakdown in Nimfish output") print("Unable to locate move breakdown in Nimfish output")
@ -134,11 +134,11 @@ def main(args: Namespace) -> int:
for move in missing["stockfish"]: for move in missing["stockfish"]:
print(f" - {move}: {positions['stockfish'][move]}") print(f" - {move}: {positions['stockfish'][move]}")
if missing["nimfish"]: if missing["nimfish"]:
print(" Illegal moves generated: ") print("\n Illegal moves generated: ")
for move in missing["nimfish"]: for move in missing["nimfish"]:
print(f" - {move}: {positions['nimfish'][move]}") print(f" - {move}: {positions['nimfish'][move]}")
if mistakes: if mistakes:
print(" Counting mistakes made:") print("\n Counting mistakes made:")
for move in mistakes: for move in mistakes:
missed = positions["stockfish"][move] - positions["nimfish"][move] missed = positions["stockfish"][move] - positions["nimfish"][move]
print(f" - {move}: expected {positions['stockfish'][move]}, got {positions['nimfish'][move]} ({'-' if missed > 0 else '+'}{abs(missed)})") print(f" - {move}: expected {positions['stockfish'][move]}, got {positions['nimfish'][move]} ({'-' if missed > 0 else '+'}{abs(missed)})")
@ -155,4 +155,5 @@ if __name__ == "__main__":
parser.add_argument("--bulk", action="store_true", help="Enable bulk-counting for Nimfish (faster, less debuggable)", default=False) parser.add_argument("--bulk", action="store_true", help="Enable bulk-counting for Nimfish (faster, less debuggable)", default=False)
parser.add_argument("--stockfish", type=Path, help="Path to the stockfish executable. Defaults to '' (detected automatically)", default=None) parser.add_argument("--stockfish", type=Path, help="Path to the stockfish executable. Defaults to '' (detected automatically)", default=None)
parser.add_argument("--nimfish", type=Path, help="Path to the nimfish executable. Defaults to '' (detected automatically)", default=None) parser.add_argument("--nimfish", type=Path, help="Path to the nimfish executable. Defaults to '' (detected automatically)", default=None)
parser.add_argument("--auto-mode", action="store_true", help="Automatically attempt to detect which moves Nimfish got wrong")
sys.exit(main(parser.parse_args())) sys.exit(main(parser.parse_args()))