Various bug fixes, improvements and optimizations

This commit is contained in:
Mattia Giambirtone 2023-10-31 23:06:27 +01:00
parent 0b9b24b8e1
commit 3dca208123
2 changed files with 155 additions and 172 deletions

View File

@ -92,16 +92,17 @@ type
# every 2 ply
fullMoveCount: int16
# En passant target square (see https://en.wikipedia.org/wiki/En_passant)
enPassantSquare*: Move
enPassantSquare*: Location
# Locations of all pieces
pieces: tuple[white: Pieces, black: Pieces]
# Potential attacking moves for black and white
# Squares attacked by both sides
attacked: tuple[white: Attacked, black: Attacked]
# Pieces pinned by both sides
pinned: tuple[white: Attacked, black: Attacked]
# The original piece captured to reach this position (may be empty)
captured: Piece
#captured: Piece
# The piece that moved to reach this position (needed to undo moves)
moved: Piece
#moved: Piece
# Active color
turn: PieceColor
@ -121,7 +122,7 @@ type
positions: seq[Position]
# Cached results of expensive
# functions in the current position
cache: Cache
#cache: Cache
# Initialized only once, copied every time
@ -144,7 +145,6 @@ proc generateMoves(self: ChessBoard, location: Location): seq[Move]
proc getAttackers*(self: ChessBoard, loc: Location, color: PieceColor): seq[Location]
proc getAttackFor*(self: ChessBoard, source, target: Location): tuple[source, target, direction: Location]
proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): bool
proc undoMove*(self: ChessBoard, move: Move)
proc isLegal(self: ChessBoard, move: Move): bool {.inline.}
proc doMove(self: ChessBoard, move: Move)
proc pretty*(self: ChessBoard): string
@ -153,7 +153,7 @@ proc updateAttackedSquares(self: ChessBoard)
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]
func invalidateCache(self: ChessBoard) {.inline.}
#[func invalidateCache(self: ChessBoard) {.inline.}]#
proc inCheck*(self: ChessBoard, color: PieceColor = None): bool
@ -243,7 +243,7 @@ func getActiveColor*(self: ChessBoard): PieceColor {.inline.} =
func getEnPassantTarget*(self: ChessBoard): Location {.inline.} =
## Returns the current en passant target square
return self.position.enPassantSquare.targetSquare
return self.position.enPassantSquare
func getMoveCount*(self: ChessBoard): int {.inline.} =
@ -296,10 +296,10 @@ proc newChessboard: ChessBoard =
new(result)
# Turns our flat sequence into an 8x8 grid
result.grid = newMatrixFromSeq[Piece](empty, (8, 8))
result.cache = Cache(canCastle: (white: CacheEntry[tuple[queen, king: bool]](), black: CacheEntry[tuple[queen, king: bool]]()),
inCheck: (white: CacheEntry[bool](), black: CacheEntry[bool]()))
#result.cache = Cache(canCastle: (white: CacheEntry[tuple[queen, king: bool]](), black: CacheEntry[tuple[queen, king: bool]]()),
# inCheck: (white: CacheEntry[bool](), black: CacheEntry[bool]()))
result.position = Position(attacked: (@[], @[]),
enPassantSquare: emptyMove(),
enPassantSquare: emptyLocation(),
move: emptyMove(),
turn: White,
fullMoveCount: 1,
@ -435,7 +435,7 @@ proc newChessboardFromFEN*(state: string): ChessBoard =
# Field is already uninitialized to the correct state
discard
else:
result.position.enPassantSquare.targetSquare = state[index..index+1].algebraicToLocation()
result.position.enPassantSquare = state[index..index+1].algebraicToLocation()
# Square metadata is 2 bytes long
inc(index)
of 4:
@ -588,17 +588,17 @@ proc inCheck*(self: ChessBoard, color: PieceColor = None): bool =
color = self.getActiveColor()
case color:
of White:
if self.cache.inCheck.white.valid:
return self.cache.inCheck.white.data
#[if self.cache.inCheck.white.valid:
return self.cache.inCheck.white.data]#
result = self.isAttacked(self.position.pieces.white.king, Black)
of Black:
if self.cache.inCheck.black.valid:
return self.cache.inCheck.black.data
#[if self.cache.inCheck.black.valid:
return self.cache.inCheck.black.data]#
result = self.isAttacked(self.position.pieces.black.king, White)
else:
# Unreachable
discard
case color:
#[case color:
of White:
self.cache.inCheck.white.valid = true
self.cache.inCheck.white.data = result
@ -607,7 +607,7 @@ proc inCheck*(self: ChessBoard, color: PieceColor = None): bool =
self.cache.inCheck.black.data = result
else:
# Unreachable
discard
discard]#
proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: bool] {.inline.} =
@ -621,26 +621,26 @@ proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king:
# rights have been lost
case color:
of White:
if self.cache.canCastle.white.valid:
return self.cache.canCastle.white.data
#[if self.cache.canCastle.white.valid:
return self.cache.canCastle.white.data]#
result.king = self.position.castlingAvailable.white.king
result.queen = self.position.castlingAvailable.white.queen
of Black:
if self.cache.canCastle.black.valid:
return self.cache.canCastle.black.data
#[if self.cache.canCastle.black.valid:
return self.cache.canCastle.black.data#]#
result.king = self.position.castlingAvailable.black.king
result.queen = self.position.castlingAvailable.black.queen
of None:
# Unreachable
discard
if self.inCheck(color):
# King can not castle out of check
return (false, false)
if result.king or result.queen:
var
loc: Location
queenSide: Location
kingSide: Location
if self.inCheck(color):
# King can not castle out of check
return (false, false)
# If the path between the king and rook on a given side is blocked or any of the
# squares where the king would travel to are attacked by the opponent,
# then castling is (temporarily) prohibited on that side
@ -662,53 +662,37 @@ proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king:
var
location = loc
otherPiece: Piece
while true:
while location != loc + shortCastleKing():
location = location + kingSide
if not location.isValid():
break
otherPiece = self.grid[location.row, location.col]
if otherPiece.color == None:
continue
if otherPiece.color == color.opposite() or otherPiece.kind != Rook or self.isAttacked(location, color.opposite()):
if otherPiece.color != None or self.isAttacked(location, color.opposite()):
result.king = false
break
if location == color.kingSideRook():
result.king = self.grid[location.row, location.col].kind == Rook
break
if result.queen:
# Long castle
var
location = loc
otherPiece: Piece
while true:
while location != loc + longCastleKing():
location = location + queenSide
if not location.isValid():
break
otherPiece = self.grid[location.row, location.col]
if otherPiece.color == None:
continue
if otherPiece.color == color.opposite() or otherPiece.kind != Rook or self.isAttacked(location, color.opposite()):
if otherPiece.color != None or self.isAttacked(location, color.opposite()):
result.queen = false
break
if location == color.queenSideRook():
result.queen = self.grid[location.row, location.col].kind == Rook
break
case color:
#[case color:
of White:
self.cache.canCastle.white.data = result
self.cache.canCastle.white.valid = true
of Black:
self.cache.canCastle.black.data = result
self.cache.canCastle.white.valid = true
self.cache.canCastle.black.valid = true
else:
discard
discard]#
proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Location] =
@ -782,11 +766,13 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
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)
let enPassantPiece = self.getEnPassantTarget() + piece.color.opposite().topSide()
let enPassantPawn = self.grid[enPassantPiece.row, enPassantPiece.col]
# They can also move on either diagonal one
# square, but only to capture or for en passant
for diagonal in [location + piece.color.topRightDiagonal(), location + piece.color.topLeftDiagonal()]:
if diagonal.isValid():
if diagonal == self.position.enPassantSquare.targetSquare:
if enPassantPawn.color == piece.color.opposite() and diagonal == self.position.enPassantSquare:
# Ensure en passant doesn't create a check
let king = self.getKing(piece.color)
var ok = true
@ -1207,9 +1193,9 @@ proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked
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)
# The enemy king is behind this enemy piece: pin it along
# this axis in both directions
result.pins.add((loc, square, direction))
result.pins.add((loc, square, -direction))
else:
break
@ -1219,7 +1205,7 @@ proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked
# valid)
let target = square + direction
if target.isValid():
result.attacks.add((square, target, direction))
result.attacks.add((loc, target, direction))
break
@ -1269,7 +1255,7 @@ proc updateAttackedSquares(self: ChessBoard) =
self.updateKingAttacks()
# Invalidate the cache whenever updates to the
# metadata are made
self.invalidateCache()
#self.invalidateCache()
proc removePiece(self: ChessBoard, location: Location, attack: bool = true) =
@ -1375,9 +1361,9 @@ proc movePiece(self: ChessBoard, move: Move, attack: bool = true) =
self.grid[move.targetSquare.row, move.targetSquare.col] = piece
if attack:
self.updateAttackedSquares()
else:
#else:
# Just to be sure
self.invalidateCache()
# self.invalidateCache()
proc movePiece(self: ChessBoard, startSquare, targetSquare: Location, attack: bool = true) =
@ -1385,23 +1371,12 @@ proc movePiece(self: ChessBoard, startSquare, targetSquare: Location, attack: bo
self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare), attack)
proc updateLocations(self: ChessBoard, move: Move) =
## Internal helper to update the position of
## the pieces on the board after a move
if move.flag == Capture:
self.position.captured = self.grid[move.targetSquare.row, move.targetSquare.col]
self.removePiece(move.targetSquare, attack=false)
# Update the positional metadata of the moving piece
self.movePiece(move)
func invalidateCache(self: ChessBoard) {.inline.} =
#[func invalidateCache(self: ChessBoard) {.inline.} =
## Invalidates the internal caches
self.cache.canCastle.white.valid = false
self.cache.canCastle.black.valid = false
self.cache.inCheck.white.valid = false
self.cache.inCheck.black.valid = false
self.cache.inCheck.black.valid = false]#
proc doMove(self: ChessBoard, move: Move) =
@ -1409,13 +1384,13 @@ proc doMove(self: ChessBoard, move: Move) =
## performing legality checks on the given move. Can
## be used in performance-critical paths where
## a move is already known to be legal
# Record final position for future reference
self.positions.add(self.position)
# Final checks
# Record the final move in the position
self.position.move = move
let piece = self.grid[move.startSquare.row, move.startSquare.col]
self.position.moved = piece
# Needed to detect draw by the 50 move rule
var
@ -1423,7 +1398,7 @@ proc doMove(self: ChessBoard, move: Move) =
fullMoveCount = self.position.fullMoveCount
castlingAvailable = self.position.castlingAvailable
if piece.kind == Pawn or move.flag == Capture:
self.position.halfMoveClock = 0
halfMoveClock = 0
else:
inc(halfMoveClock)
if piece.color == Black:
@ -1451,21 +1426,21 @@ proc doMove(self: ChessBoard, move: Move) =
discard
# Has a rook been captured?
if move.flag == Capture:
let piece = self.grid[move.targetSquare.row, move.targetSquare.col]
if piece.kind == Rook:
let captured = self.grid[move.targetSquare.row, move.targetSquare.col]
if captured.kind == Rook:
case piece.color:
of White:
if move.targetSquare == piece.color.queenSideRook():
if move.targetSquare == captured.color.queenSideRook():
# Queen side
castlingAvailable.white.queen = false
elif move.targetSquare == piece.color.kingSideRook():
elif move.targetSquare == captured.color.kingSideRook():
# King side
castlingAvailable.white.king = false
of Black:
if move.targetSquare == piece.color.queenSideRook():
if move.targetSquare == captured.color.queenSideRook():
# Queen side
castlingAvailable.black.queen = false
elif move.targetSquare == piece.color.kingSideRook():
elif move.targetSquare == captured.color.kingSideRook():
# King side
castlingAvailable.black.king = false
else:
@ -1483,23 +1458,22 @@ proc doMove(self: ChessBoard, move: Move) =
castlingAvailable.black.queen = false
else:
discard
let previous = self.position
# Record final position for future reference
self.positions.add(previous)
# Create new position
self.position = Position(plyFromRoot: self.position.plyFromRoot + 1,
halfMoveClock: halfMoveClock,
fullMoveCount: fullMoveCount,
captured: emptyPiece(),
turn: self.getActiveColor().opposite,
castlingAvailable: castlingAvailable,
# Updated at the next call to doMove()
move: emptyMove(),
pieces: previous.pieces,
halfMoveClock: halfMoveClock,
fullMoveCount: fullMoveCount,
#captured: if move.flag == Capture: self.grid[move.targetSquare.row, move.targetSquare.col] else: emptyPiece(),
turn: self.getActiveColor().opposite,
castlingAvailable: castlingAvailable,
move: move,
pieces: self.position.pieces,
enPassantSquare: if move.flag == EnPassant: move.targetSquare + piece.color.bottomSide() else: emptyLocation()
)
# Update position metadata
if move.flag in [CastleShort, CastleLong]:
# Move the rook onto the
# correct file
# correct file when castling
var
location: Location
target: Location
@ -1513,13 +1487,18 @@ proc doMove(self: ChessBoard, move: Move) =
let move = Move(startSquare: location, targetSquare: location + target, flag: move.flag)
self.movePiece(move, attack=false)
# Update position and attack metadata
self.updateLocations(move)
if move.flag == Capture:
# Get rid of captured pieces
#self.position.captured = self.grid[move.targetSquare.row, move.targetSquare.col]
self.removePiece(move.targetSquare, attack=false)
if move.flag == EnPassant:
self.removePiece(move.targetSquare + piece.color.bottomSide())
# Make the en passant pawn disappear
self.removePiece(move.targetSquare + piece.color.bottomSide(), attack=false)
elif move.isPromotion():
self.movePiece(move, attack=false)
if move.isPromotion():
# Move is a pawn promotion: get rid of the pawn
# and spawn a new piece
self.removePiece(move.targetSquare, attack=false)
@ -1534,13 +1513,9 @@ proc doMove(self: ChessBoard, move: Move) =
self.spawnPiece(move.targetSquare, Piece(kind: Queen, color: piece.color))
else:
discard
self.updateAttackedSquares()
# Check for double pawn push
if move.flag == DoublePush:
self.position.enPassantSquare = Move(startSquare: (move.startSquare.row, move.startSquare.col),
targetSquare: move.targetSquare + piece.color.bottomSide())
else:
self.position.enPassantSquare = emptyMove()
# Update attack metadata
self.updateAttackedSquares()
proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
@ -1619,42 +1594,11 @@ proc resetBoard*(self: ChessBoard) =
self.grid[self.position.pieces.black.king.row, self.position.pieces.black.king.col] = Piece(color: Black, kind: King)
proc undoMove*(self: ChessBoard, move: Move) =
## Undoes the given move
proc undoLastMove*(self: ChessBoard) =
if self.positions.len() == 0:
return
self.invalidateCache()
let previous = self.position
var position = self.positions[^1]
while position.move != move:
discard self.positions.pop()
position = self.positions[^1]
self.position = position
self.grid[move.startSquare.row, move.startSquare.col] = self.position.moved
if move.flag == Capture:
self.grid[move.targetSquare.row, move.targetSquare.col] = previous.captured
else:
self.grid[move.targetSquare.row, move.targetSquare.col] = emptyPiece()
# Reset the location of the rook in the
# grid (it's already correct in the piece
# list)
if move.flag == CastleLong:
let
rookOld = move.targetSquare + (if self.getActiveColor() == White: self.getActiveColor().rightSide() else: self.getActiveColor().leftSide())
rookNew = self.getActiveColor().queenSideRook()
self.grid[rookNew.row, rookNew.col] = self.grid[rookOld.row, rookOld.col]
self.grid[rookOld.row, rookOld.col] = emptyPiece()
if move.flag == CastleShort:
let
rookOld = move.targetSquare + (if self.getActiveColor() == White: self.getActiveColor().leftSide() else:self.getActiveColor().rightSide())
rookNew = self.getActiveColor().kingSideRook()
self.grid[rookNew.row, rookNew.col] = self.grid[rookOld.row, rookOld.col]
self.grid[rookOld.row, rookOld.col] = emptyPiece()
if move.flag == EnPassant:
let target = self.getEnPassantTarget() + self.getActiveColor().topSide()
self.grid[target.row, target.col] = Piece(kind: Pawn, color: self.getActiveColor().opposite())
self.position = self.positions.pop()
self.resetBoard()
proc isLegal(self: ChessBoard, move: Move): bool {.inline.} =
@ -1665,11 +1609,9 @@ proc isLegal(self: ChessBoard, move: Move): bool {.inline.} =
proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} =
## Makes a move on the board
result = move
self.position.move = move
if not self.isLegal(move):
return emptyMove()
self.doMove(move)
result = self.position.move
proc `$`*(self: ChessBoard): string =
@ -1778,10 +1720,28 @@ proc toFEN*(self: ChessBoard): string =
proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = false, bulk: bool = false): CountData =
## Counts (and debugs) the number of legal positions reached after
## the given number of ply
let moves = self.generateAllMoves()
if not bulk and ply == 0:
if ply == 0:
return (1, 0, 0, 0, 0, 0, 0)
if bulk and ply == 1:
let moves = self.generateAllMoves()
if ply == 1 and bulk:
if divide:
var postfix = ""
for move in moves:
case move.flag:
of PromoteToBishop:
postfix = "b"
of PromoteToKnight:
postfix = "n"
of PromoteToRook:
postfix = "r"
of PromoteToQueen:
postfix = "q"
else:
postfix = ""
echo &"{move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}{postfix}: 1"
if verbose:
echo ""
return (uint64(len(moves)), 0, 0, 0, 0, 0, 0)
if len(moves) == 0:
@ -1790,7 +1750,7 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
for move in moves:
if verbose:
let canCastle = self.canCastle(self.getActiveColor())
echo &"Ply: {self.position.plyFromRoot}"
echo &"Ply (from root): {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: {self.getActiveColor()}"
echo &"Piece: {self.grid[move.startSquare.row, move.startSquare.col].kind}"
@ -1826,7 +1786,7 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
echo &"Opponent can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
echo &"Position after move: {self.toFEN()}"
echo "\n", self.pretty()
stdout.write(">>> ")
stdout.write("nextpos>> ")
try:
discard readLine(stdin)
except IOError:
@ -1834,7 +1794,8 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
except EOFError:
discard
let next = self.perft(ply - 1, verbose, bulk=bulk)
if divide:
self.undoLastMove()
if divide and (not bulk or ply > 1):
var postfix = ""
case move.flag:
of PromoteToBishop:
@ -1857,18 +1818,23 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
result.castles += next.castles
result.enPassant += next.enPassant
result.checkmates += next.checkmates
self.undoMove(move)
proc main: int =
## Nimfish's control interface
echo "Nimfish by nocturn9x (see LICENSE)"
var board = newDefaultChessboard()
while true:
var cmd: seq[string]
var
cmd: seq[string]
cmdStr: string
try:
stdout.write(">>> ")
stdout.flushFile()
cmd = readLine(stdin).strip(leading=true, trailing=true, chars={'\t', ' '}).splitWhitespace(maxsplit=2)
cmdStr = readLine(stdin).strip(leading=true, trailing=true, chars={'\t', ' '})
if cmdStr.len() == 0:
continue
cmd = cmdStr.splitWhitespace(maxsplit=2)
case cmd[0]:
of "clear":
@ -1887,20 +1853,28 @@ proc main: int =
var
args = cmd[2].splitWhitespace()
bulk = false
verbose = false
if args.len() > 1:
case args[1]:
of "bulk-count", "bulk":
bulk = true
else:
echo &"Error: go: perft: invalid argument '{args[1]}'"
continue
var ok = true
for arg in args[1..^1]:
case arg:
of "bulk-count", "bulk":
bulk = true
of "verbose":
verbose = true
else:
echo &"Error: go: perft: invalid argument '{args[1]}'"
ok = false
break
if not ok:
continue
try:
let ply = parseInt(args[0])
if bulk:
echo &"\nNodes searched (bulk-counting enabled): {board.perft(ply, divide=true, bulk=true).nodes}\n"
echo &"\nNodes searched (bulk-counting: on): {board.perft(ply, divide=true, bulk=true, verbose=verbose).nodes}\n"
else:
let data = board.perft(ply, divide=true)
echo &"\nNodes searched: {data.nodes}"
let data = board.perft(ply, divide=true, verbose=verbose)
echo &"\nNodes searched (bulk-counting: off): {data.nodes}"
echo &" - Captures: {data.captures}"
echo &" - Checks: {data.checks}"
echo &" - E.P: {data.enPassant}"
@ -2003,5 +1977,10 @@ when isMainModule:
# Queens
testPiece(b.getPiece("d1"), Queen, White)
testPiece(b.getPiece("d8"), Queen, Black)
setControlCHook(proc () {.noconv.} = quit(0))
setControlCHook(proc () {.noconv.} = quit(0))
#b = newChessboardFromFEN("r3k2r/Pppp1ppp/1b3nbN/nP6/BBPPP3/q4N2/Pp4PP/R2Q1RK1 b kq d3 0 1")
#let m = Move(startSquare: "c7".algebraicToLocation, targetSquare: "c5".algebraicToLocation, flag: DoublePush)
#b.makeMove()
quit(main())

View File

@ -86,12 +86,14 @@ 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']} nodes in {stockfish_time:.2f} seconds")
print(f"Nimfish searched {total_nodes['nimfish']} nodes in {nimfish_time:.2f} seconds")
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")
if total_difference > 0:
print(f"Stockfish searched {total_difference} more nodes than Nimfish")
elif total_difference != 0:
print(f"Nimfish searched {-total_difference} more nodes than Stockfish")
print(f"Nimfish searched {total_difference} less node{'' if total_difference == 1 else 's'} than Stockfish")
elif total_difference < 0:
total_difference = abs(total_difference)
print(f"Nimfish searched {total_difference} more node{'' if total_difference == 1 else 's'} than Stockfish")
else:
print("Node count is identical")
pattern = re.compile(r"(?:\s\s-\sCaptures:\s(?P<captures>[0-9]+))\n"
@ -104,8 +106,9 @@ def main(args: Namespace) -> int:
extra: re.Match | None = None
if not args.bulk:
extra = pattern.search(nimfish_output)
missed_total = len(missing['stockfish']) + len(missing['nimfish'])
if missing["stockfish"] or missing["nimfish"] or mistakes:
print(f"Found {len(missing['stockfish']) + len(missing['nimfish'])} missed moves and {len(mistakes)} counting mistakes, more info below: ")
print(f"Found {missed_total} missed move{'' if missed_total == 1 else 's'} and {len(mistakes)} counting mistake{'' if len(mistakes) == 1 else 's'}, more info below: ")
if args.bulk:
print("Note: Nimfish was run in bulk-counting mode, so a detailed breakdown of each move type is not available. "
"To fix this, re-run the program without the --bulk option")
@ -122,21 +125,22 @@ def main(args: Namespace) -> int:
elif not args.bulk:
print("Unable to locate move breakdown in Nimfish output")
if missing["stockfish"] or missing["nimfish"]:
print("\n Missed moves:")
print("\n Move count breakdown:")
if missing["stockfish"]:
print(" Legal moves missed by Nimfish: ")
print(" Legal moves missed: ")
for move in missing["stockfish"]:
print(f" - {move}: {positions['stockfish'][move]}")
if missing["nimfish"]:
print(" Illegal moves missed by Nimfish: ")
print(" Illegal moves generated: ")
for move in missing["nimfish"]:
print(f" - {move}: {positions['nimfish'][move]}")
if mistakes:
print(" Mistakes:")
print(" Counting mistakes made:")
for move in mistakes:
print(f" - {move}: expected {positions['stockfish'][move]}, got {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)})")
else:
print("No mistakes were detected")
print("No discrepancies detected")