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/strformat
import std/sequtils
import std/times
import std/math
type
@ -75,7 +76,6 @@ type
Position* = ref object
## A chess position
move: Move
# Did the rooks on either side/the king move?
castlingAvailable: tuple[white, black: tuple[queen, king: bool]]
# Number of half-moves that were performed
@ -103,12 +103,12 @@ type
ChessBoard* = ref object
## A chess board object
# An 8x8 matrix we use for constant
# time lookup of pieces by their location
# The actual board where pieces live
# (flattened 8x8 matrix)
grid: seq[Piece]
# The current position
position: Position
# List of all reached positions
# List of all previously reached positions
positions: seq[Position]
@ -137,12 +137,15 @@ proc getPinnedDirections(self: ChessBoard, loc: Location): seq[Location]
proc getAttacks*(self: ChessBoard, loc: Location): Attacked
proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked, pins: Attacked]
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.} =
for x in other:
self.add(x)
proc resetBoard*(self: ChessBoard)
# 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
@ -260,6 +263,18 @@ func getStartRow(piece: Piece): int {.inline.} =
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.} =
## Retrieves the location of the last
## row relative to the given color
@ -277,9 +292,10 @@ proc newChessboard: ChessBoard =
new(result)
# Turns our flat sequence into an 8x8 grid
result.grid = newSeqOfCap[Piece](64)
for _ in 0..63:
result.grid.add(emptyPiece())
result.position = Position(attacked: (@[], @[]),
enPassantSquare: emptyLocation(),
move: emptyMove(),
turn: White,
fullMoveCount: 1,
pieces: (white: (king: emptyLocation(),
@ -331,10 +347,8 @@ proc newChessboardFromFEN*(fen: string): ChessBoard =
of 'r', 'n', 'b', 'q', 'k', 'p':
# We know for a fact these values are in our
# enumeration, so all is good
{.push.}
{.warning[HoleEnumConv]:off.}
piece = Piece(kind: PieceKind(c.toLowerAscii()), color: if c.isUpperAscii(): White else: Black)
{.pop.}
case piece.color:
of Black:
case piece.kind:
@ -462,8 +476,8 @@ proc newDefaultChessboard*: ChessBoard {.inline.} =
proc countPieces*(self: ChessBoard, kind: PieceKind, color: PieceColor): int =
## Returns the number of pieces with
## the given color and type in the given
## position
## the given color and type in the
## current position
case color:
of White:
case kind:
@ -519,9 +533,9 @@ func rankToColumn(rank: int): int8 {.inline.} =
return indeces[rank - 1]
func rowToRank(row: int): int {.inline.} =
func rowToFile(row: int): int {.inline.} =
## Converts a row into our grid into
## a chess rank
## a chess file
const indeces = [8, 7, 6, 5, 4, 3, 2, 1]
return indeces[row]
@ -548,7 +562,7 @@ proc algebraicToLocation*(s: string): Location =
func locationToAlgebraic*(loc: Location): string {.inline.} =
## Converts a location from our internal row, column
## 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.} =
@ -582,7 +596,7 @@ func getPromotionType*(move: Move): MoveFlag {.inline.} =
func isCapture*(move: Move): bool {.inline.} =
## Returns whether the given move is a
## cature
result = (move.flags and Capture.uint16) != 0
result = (move.flags and Capture.uint16) == Capture.uint16
func isCastling*(move: Move): bool {.inline.} =
@ -614,6 +628,30 @@ func isDoublePush*(move: Move): bool {.inline.} =
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 =
## Returns whether the given color's
## king is in check. If the color is
@ -650,6 +688,12 @@ proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king:
of None:
# Unreachable
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):
# King can not castle out of check
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
# that piece has to be captured, but this is
# already implicitly handled by the loop below)
var location = attacker
while location != king:
location = location + attack.direction
@ -760,40 +805,24 @@ proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[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] =
## Generates the possible moves for the pawn in the given
## location
var
piece = self.grid[location.row, location.col]
locations: seq[Location] = @[]
flags: seq[MoveFlag] = @[]
targets: seq[Location] = @[]
doAssert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}"
# Pawns can move forward one square
let forward = piece.color.topSide() + location
# Only if the square is empty though
if forward.isValid() and self.grid[forward.row, forward.col].color == None:
locations.add(forward)
flags.add(Default)
targets.add(forward)
# If the pawn is on its first rank, it can push two squares
if location.row == piece.getStartRow():
let double = location + piece.color.doublePush()
# 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:
locations.add(double)
flags.add(DoublePush)
targets.add(double)
let enPassantPawn = self.getEnPassantTarget() + piece.color.opposite().topSide()
# They can also move on either diagonal one
# 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]:
ok = false
if ok:
locations.add(diagonal)
flags.add(EnPassant)
targets.add(diagonal)
elif otherPiece.color == piece.color.opposite() and otherPiece.kind != King:
locations.add(diagonal)
flags.add(Capture)
var
newLocation: Location
newFlags: seq[MoveFlag]
newLocations: seq[Location]
targets.add(diagonal)
# Check for pins
let pins = self.getPinnedDirections(location)
for pin in pins:
newLocation = location + pin
let loc = locations.find(newLocation)
if loc != -1:
# Pin direction is legal for this piece
newLocations.add(newLocation)
newFlags.add(flags[loc])
if pins.len() > 0:
locations = newLocations
flags = newFlags
let pinned = self.getPinnedDirections(location)
if pinned.len() > 0:
var newTargets: seq[Location] = @[]
for target in targets:
if target in pinned:
newTargets.add(target)
targets = newTargets
let checked = self.inCheck()
let resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color)
var targetPiece: Piece
for (target, flag) in zip(locations, flags):
for target in targets:
if checked and target notin resolutions:
continue
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():
# 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: target, flags: promotionType.uint16 or flag.uint16))
result.add(Move(startSquare: location, targetSquare: target, flags: promotionType.uint16 or flags))
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] =
@ -874,7 +900,11 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
directions.add(piece.color.leftSide())
let pinned = self.getPinnedDirections(location)
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 resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color)
for direction in directions:
@ -892,7 +922,9 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
if otherPiece.color == piece.color:
break
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:
# Target square contains an enemy piece: capture
# 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
if otherPiece.color == piece.color or otherPiece.kind == King:
continue
if checked and square notin resolutions:
continue
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] =
## 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
let piece = self.grid[source.row, source.col]
case piece.color:
@ -1283,11 +1314,12 @@ proc updateAttackedSquares(self: ChessBoard) =
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
## metadata
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:
of White:
case piece.kind:
@ -1388,11 +1420,11 @@ proc movePiece(self: ChessBoard, move: Move, attack: bool = true) =
self.updateAttackedSquares()
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), attack)
proc doMove(self: ChessBoard, move: Move) =
## Internal function called by makeMove after
## performing legality checks. Can be used in
@ -1482,13 +1514,13 @@ proc doMove(self: ChessBoard, move: Move) =
castlingAvailable.black.queen = false
else:
discard
# Create new position
self.position = Position(plyFromRoot: self.position.plyFromRoot + 1,
halfMoveClock: halfMoveClock,
fullMoveCount: fullMoveCount,
turn: self.getActiveColor().opposite,
castlingAvailable: castlingAvailable,
move: move,
pieces: self.position.pieces,
enPassantSquare: enPassantTarget
)
@ -1500,26 +1532,23 @@ proc doMove(self: ChessBoard, move: Move) =
var
location: Location
target: Location
flags: uint16
if move.getCastlingType() == CastleShort:
location = piece.color.kingSideRook()
target = shortCastleRook()
flags = flags or CastleShort.uint16
else:
location = piece.color.queenSideRook()
target = longCastleRook()
flags = flags or CastleLong.uint16
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)
if move.isCapture():
# Get rid of captured pieces
self.removePiece(move.targetSquare, attack=false)
if move.isEnPassant():
# Make the en passant pawn disappear
self.removePiece(move.targetSquare + piece.color.bottomSide(), attack=false)
self.movePiece(move, attack=false)
if move.isPromotion():
# Move is a pawn promotion: get rid of the pawn
# 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))
else:
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) =
@ -1588,9 +1623,7 @@ proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
proc resetBoard*(self: ChessBoard) =
## Resets the internal grid representation
## according to the positional data stored
## in the chessboard. Warning: this can be
## expensive, especially in critical paths
## or tight loops
## in the chessboard
for i in 0..63:
self.grid[i] = emptyPiece()
for loc in self.position.pieces.white.pawns:
@ -1618,9 +1651,8 @@ proc resetBoard*(self: ChessBoard) =
proc undoLastMove*(self: ChessBoard) =
if self.positions.len() == 0:
return
self.position = self.positions.pop()
if self.positions.len() > 0:
self.position = self.positions.pop()
self.resetBoard()
@ -1778,12 +1810,13 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
## the given number of ply
let moves = self.generateAllMoves()
if len(moves) == 0:
result.checkmates = 1
if ply == 0:
result.nodes = 1
return
if ply == 1 and bulk:
if not bulk:
if len(moves) == 0 and self.inCheck():
result.checkmates = 1
if ply == 0:
result.nodes = 1
return
elif ply == 1 and bulk:
if divide:
var postfix = ""
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 &"Turn: {self.getActiveColor()}"
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 &"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()}"
@ -1821,14 +1854,15 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
echo "None"
echo "\n", self.pretty()
self.doMove(move)
if move.isCapture():
inc(result.captures)
if move.isCastling():
inc(result.castles)
if move.isPromotion():
inc(result.promotions)
if move.isEnPassant():
inc(result.enPassant)
if ply == 1:
if move.isCapture():
inc(result.captures)
if move.isCastling():
inc(result.castles)
if move.isPromotion():
inc(result.promotions)
if move.isEnPassant():
inc(result.enPassant)
if self.inCheck():
# Opponent king is in check
inc(result.checks)
@ -1891,7 +1925,7 @@ proc handleGoCommand(board: ChessBoard, command: seq[string]) =
var ok = true
for arg in args[1..^1]:
case arg:
of "bulk-count", "bulk":
of "bulk":
bulk = true
of "verbose":
verbose = true
@ -1904,8 +1938,12 @@ proc handleGoCommand(board: ChessBoard, command: seq[string]) =
try:
let ply = parseInt(args[0])
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:
let t = cpuTime()
let data = board.perft(ply, divide=true, verbose=verbose)
echo &"\nNodes searched (bulk-counting: off): {data.nodes}"
echo &" - Captures: {data.captures}"
@ -1915,40 +1953,14 @@ proc handleGoCommand(board: ChessBoard, command: seq[string]) =
echo &" - Castles: {data.castles}"
echo &" - Promotions: {data.promotions}"
echo ""
echo &"Time taken: {round(cpuTime() - t, 3)} seconds"
except ValueError:
echo "Error: go: perft: invalid depth"
else:
echo &"Error: go: unknown subcommand '{command[1]}'"
proc handlePositionCommand(board: var ChessBoard, command: seq[string]) =
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]) =
proc handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discardable.} =
if len(command) != 2:
echo &"Error: move: invalid number of arguments"
return
@ -1964,18 +1976,22 @@ proc handleMoveCommand(board: ChessBoard, command: seq[string]) =
try:
startSquare = moveString[0..1].algebraicToLocation()
except ValueError:
echo &"Error: move: invalid start square"
echo &"Error: move: invalid start square ({moveString[0..1]})"
return
try:
targetSquare = moveString[2..3].algebraicToLocation()
except ValueError:
echo &"Error: move: invalid target square"
echo &"Error: move: invalid target square ({moveString[2..3]})"
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:
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
if len(moveString) == 5:
@ -1993,10 +2009,115 @@ proc handleMoveCommand(board: ChessBoard, command: seq[string]) =
echo &"Error: move: invalid promotion type"
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 =
## Nimfish's control interface
@ -2018,17 +2139,30 @@ proc main: int =
of "clear":
echo "\x1Bc"
of "help":
echo "TODO"
echo HELP_TEXT
of "go":
handleGoCommand(board, cmd)
of "position":
handlePositionCommand(board, cmd)
of "move":
handleMoveCommand(board, cmd)
of "pretty":
echo board.pretty()
of "pretty", "print":
handlePositionCommand(board, @["position", cmd[0]])
of "undo":
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:
echo &"Unknown command '{cmd[0]}'. Type 'help' for more information."
except IOError:

View File

@ -1,4 +1,5 @@
import re
import os
import sys
import time
import subprocess
@ -25,14 +26,19 @@ def main(args: Namespace) -> int:
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
stdin=subprocess.PIPE,
encoding="u8"
encoding="u8",
text=True,
bufsize=1
)
print(f"Starting Nimfish engine at {NIMFISH.as_posix()!r}")
nimfish_process = subprocess.Popen(NIMFISH,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
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}")
if args.fen:
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}")
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")
print("Search started, waiting for engine completion")
start_time = time.time()
stockfish_output = stockfish_process.communicate()[0]
stockfish_time = time.time() - start_time
start_time = time.time()
nimfish_output = nimfish_process.communicate()[0]
nimfish_time = time.time() - start_time
positions = {
"all": {},
"stockfish": {},
@ -89,8 +90,8 @@ def main(args: Namespace) -> int:
total_nodes = {"stockfish": sum(positions["stockfish"][move] for move in positions["stockfish"]),
"nimfish": sum(positions["nimfish"][move] for move in positions["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"Nimfish searched {total_nodes['nimfish']} node{'' if total_nodes['nimfish'] == 1 else 's'} in {nimfish_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'}")
if total_difference > 0:
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")
if extra:
print(f" Breakdown by move type:")
print(f" - Captures: {extra.group('captures')}")
print(f" - Checks: {extra.group('checks')}")
print(f" - En Passant: {extra.group('enPassant')}")
print(f" - Checkmates: {extra.group('checkmates')}")
print(f" - Castles: {extra.group('castles')}")
print(f" - Promotions: {extra.group('promotions')}")
print(f" - Total: {total_nodes['nimfish']}")
print(f" - Captures: {extra.group('captures')}")
print(f" - Checks: {extra.group('checks')}")
print(f" - En Passant: {extra.group('enPassant')}")
print(f" - Checkmates: {extra.group('checkmates')}")
print(f" - Castles: {extra.group('castles')}")
print(f" - Promotions: {extra.group('promotions')}")
elif not args.bulk:
print("Unable to locate move breakdown in Nimfish output")
@ -134,11 +134,11 @@ def main(args: Namespace) -> int:
for move in missing["stockfish"]:
print(f" - {move}: {positions['stockfish'][move]}")
if missing["nimfish"]:
print(" Illegal moves generated: ")
print("\n Illegal moves generated: ")
for move in missing["nimfish"]:
print(f" - {move}: {positions['nimfish'][move]}")
if mistakes:
print(" Counting mistakes made:")
print("\n Counting mistakes made:")
for move in mistakes:
missed = positions["stockfish"][move] - positions["nimfish"][move]
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("--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("--auto-mode", action="store_true", help="Automatically attempt to detect which moves Nimfish got wrong")
sys.exit(main(parser.parse_args()))