From 9047e3a53d0720d6757004c2a57638b7e1a7e1ce Mon Sep 17 00:00:00 2001 From: nocturn9x Date: Mon, 13 Nov 2023 09:52:37 +0100 Subject: [PATCH] Switch to bitwise flags for moves and fix perft counting mistakes --- src/Chess/board.nim | 499 +++++++++++++++++++++------------ src/Chess/compare_positions.py | 4 +- 2 files changed, 317 insertions(+), 186 deletions(-) diff --git a/src/Chess/board.nim b/src/Chess/board.nim index 0dbc326..89ec542 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -11,9 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import ../util/matrix - -export matrix import std/strutils import std/strformat @@ -45,18 +42,18 @@ type MoveFlag* = enum ## An enumeration of move flags - Default = 0'i8, # No flag - EnPassant, # Move is a capture with en passant - Capture, # Move is a capture - DoublePush, # Move is a double pawn push + Default = 0'u16, # No flag + EnPassant = 1, # Move is a capture with en passant + Capture = 2, # Move is a capture + DoublePush = 4, # Move is a double pawn push # Castling metadata - CastleLong, - CastleShort, + CastleLong = 8, + CastleShort = 16, # Pawn promotion metadata - PromoteToQueen, - PromoteToRook, - PromoteToBishop, - PromoteToKnight + PromoteToQueen = 32, + PromoteToRook = 64, + PromoteToBishop = 128, + PromoteToKnight = 256 # Useful type aliases Location* = tuple[row, col: int8] @@ -73,7 +70,8 @@ type ## A chess move startSquare*: Location targetSquare*: Location - flag*: MoveFlag + flag*: uint16 + Position* = ref object ## A chess position @@ -107,19 +105,13 @@ type # An 8x8 matrix we use for constant # time lookup of pieces by their location - grid: Matrix[Piece] + grid: seq[Piece] # The current position position: Position # List of all reached positions positions: seq[Position] -# Initialized only once, copied every time -var empty: seq[Piece] = @[] -for _ in countup(0, 63): - empty.add(Piece(kind: Empty, color: None)) - - # A bunch of simple utility functions func emptyPiece*: Piece {.inline.} = Piece(kind: Empty, color: None) @@ -284,7 +276,7 @@ proc newChessboard: ChessBoard = ## Returns a new, empty chessboard new(result) # Turns our flat sequence into an 8x8 grid - result.grid = newMatrixFromSeq[Piece](empty, (8, 8)) + result.grid = newSeqOfCap[Piece](64) result.position = Position(attacked: (@[], @[]), enPassantSquare: emptyLocation(), move: emptyMove(), @@ -304,6 +296,10 @@ proc newChessboard: ChessBoard = pawns: @[]))) +func coordToIndex(row, col: int): int {.inline.} = (row * 8) + col +func `[]`(self: seq[Piece], row, column: Natural): Piece {.inline.} = self[coordToIndex(row, column)] +proc `[]=`(self: var seq[Piece], row, column: Natural, piece: Piece) {.inline.} = self[coordToIndex(row, column)] = piece + proc newChessboardFromFEN*(fen: string): ChessBoard = ## Initializes a chessboard with the @@ -567,9 +563,55 @@ func getPiece*(self: ChessBoard, square: string): Piece {.inline.} = func isPromotion*(move: Move): bool {.inline.} = - ## Returns whrther the given move is a - ## pawn promotion or not - return move.flag in [PromoteToBishop, PromoteToKnight, PromoteToRook, PromoteToQueen] + ## Returns whether the given move is a + ## pawn promotion + for promotion in [PromoteToBishop, PromoteToKnight, PromoteToRook, PromoteToQueen]: + if (move.flag and promotion.uint16) != 0: + return true + + +func getPromotionType*(move: Move): MoveFlag {.inline.} = + ## Returns the promotion type of the given move. + ## The return value of this function is only valid + ## if isPromotion() returns true + for promotion in [PromoteToBishop, PromoteToKnight, PromoteToRook, PromoteToQueen]: + if (move.flag and promotion.uint16) != 0: + return promotion + + +func isCapture*(move: Move): bool {.inline.} = + ## Returns whether the given move is a + ## cature + result = (move.flag and Capture.uint16) != 0 + + +func isCastling*(move: Move): bool {.inline.} = + ## Returns whether the given move is a + ## castle + for flag in [CastleLong, CastleShort]: + if (move.flag and flag.uint16) != 0: + return true + + +func getCastlingType*(move: Move): MoveFlag {.inline.} = + ## Returns the castling type of the given move. + ## The return value of this function is only valid + ## if isCastling() returns true + for flag in [CastleLong, CastleShort]: + if (move.flag and flag.uint16) != 0: + return flag + + +func isEnPassant*(move: Move): bool {.inline.} = + ## Returns whether the given move is an + ## en passant capture + result = (move.flag and EnPassant.uint16) != 0 + + +func isDoublePush*(move: Move): bool {.inline.} = + ## Returns whether the given move is a + ## double pawn push + result = (move.flag and DoublePush.uint16) != 0 proc inCheck*(self: ChessBoard, color: PieceColor = None): bool = @@ -772,7 +814,7 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = break if p.color == None: continue - # Bishops can't create checks through en passant (I'm pretty sure at least) + # Bishops can't create checks through en passant if p.color == piece.color.opposite() and p.kind in [Queen, Rook]: ok = false if ok: @@ -807,9 +849,9 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = 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, flag: promotionType)) + result.add(Move(startSquare: location, targetSquare: target, flag: promotionType.uint16 and flag.uint16)) continue - result.add(Move(startSquare: location, targetSquare: target, flag: flag)) + result.add(Move(startSquare: location, targetSquare: target, flag: flag.uint16)) proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = @@ -855,7 +897,7 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = # it and stop going any further if otherPiece.kind != King: # Can't capture the king - result.add(Move(startSquare: location, targetSquare: square, flag: Capture)) + result.add(Move(startSquare: location, targetSquare: square, flag: Capture.uint16)) break # Target square is empty result.add(Move(startSquare: location, targetSquare: square)) @@ -903,7 +945,7 @@ proc generateKingMoves(self: ChessBoard, location: Location): seq[Move] = continue # Target square is empty or contains an enemy piece: # All good for us! - result.add(Move(startSquare: location, targetSquare: square, flag: flag)) + result.add(Move(startSquare: location, targetSquare: square, flag: flag.uint16)) proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] = @@ -941,7 +983,7 @@ proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] = if otherPiece.color != None: # Target square contains an enemy piece: capture # it - result.add(Move(startSquare: location, targetSquare: square, flag: Capture)) + result.add(Move(startSquare: location, targetSquare: square, flag: Capture.uint16)) else: # Target square is empty result.add(Move(startSquare: location, targetSquare: square)) @@ -967,8 +1009,8 @@ proc generateMoves(self: ChessBoard, location: Location): seq[Move] = proc generateAllMoves*(self: ChessBoard): seq[Move] = ## Returns the list of all possible legal moves ## in the current position - for i, row in self.grid: - for j, piece in row: + for i in 0..7: + for j in 0..7: if self.grid[i, j].color == self.getActiveColor(): for move in self.generateMoves((int8(i), int8(j))): result.add(move) @@ -1350,9 +1392,6 @@ proc movePiece(self: ChessBoard, startSquare, targetSquare: Location, attack: bo ## 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 @@ -1371,7 +1410,7 @@ proc doMove(self: ChessBoard, move: Move) = castlingAvailable = self.position.castlingAvailable enPassantTarget = self.getEnPassantTarget() # Needed to detect draw by the 50 move rule - if piece.kind == Pawn or move.flag == Capture: + if piece.kind == Pawn or move.isCapture(): halfMoveClock = 0 else: inc(halfMoveClock) @@ -1384,7 +1423,7 @@ proc doMove(self: ChessBoard, move: Move) = if self.grid[enPassantPawn.row, enPassantPawn.col].color == piece.color.opposite(): enPassantTarget = emptyLocation() - if move.flag == DoublePush: + if move.isDoublePush(): enPassantTarget = move.targetSquare + piece.color.bottomSide() # Castling check: have the rooks moved? @@ -1409,7 +1448,7 @@ proc doMove(self: ChessBoard, move: Move) = else: discard # Has a rook been captured? - if move.flag == Capture: + if move.isCapture(): let captured = self.grid[move.targetSquare.row, move.targetSquare.col] if captured.kind == Rook: case piece.color: @@ -1431,7 +1470,7 @@ proc doMove(self: ChessBoard, move: Move) = # Unreachable discard # Has the king moved? - if piece.kind == King or move.flag in [CastleLong, CastleShort]: + if piece.kind == King or move.isCastling(): # Revoke all castling rights for the moving king case piece.color: of White: @@ -1454,13 +1493,13 @@ proc doMove(self: ChessBoard, move: Move) = ) # Update position metadata - if move.flag in [CastleShort, CastleLong]: + if move.isCastling(): # Move the rook onto the # correct file when castling var location: Location target: Location - if move.flag == CastleShort: + if move.getCastlingType() == CastleShort: location = piece.color.kingSideRook() target = shortCastleRook() else: @@ -1470,11 +1509,11 @@ proc doMove(self: ChessBoard, move: Move) = let move = Move(startSquare: location, targetSquare: location + target, flag: move.flag) self.movePiece(move, attack=false) - if move.flag == Capture: + if move.isCapture(): # Get rid of captured pieces self.removePiece(move.targetSquare, attack=false) - if move.flag == EnPassant: + if move.isEnPassant(): # Make the en passant pawn disappear self.removePiece(move.targetSquare + piece.color.bottomSide(), attack=false) @@ -1484,7 +1523,7 @@ proc doMove(self: ChessBoard, move: Move) = # Move is a pawn promotion: get rid of the pawn # and spawn a new piece self.removePiece(move.targetSquare, attack=false) - case move.flag: + case move.getPromotionType(): of PromoteToBishop: self.spawnPiece(move.targetSquare, Piece(kind: Bishop, color: piece.color)) of PromoteToKnight: @@ -1551,7 +1590,8 @@ proc resetBoard*(self: ChessBoard) = ## in the chessboard. Warning: this can be ## expensive, especially in critical paths ## or tight loops - self.grid = newMatrixFromSeq[Piece](empty, (8, 8)) + for i in 0..63: + self.grid[i] = emptyPiece() for loc in self.position.pieces.white.pawns: self.grid[loc.row, loc.col] = Piece(color: White, kind: Pawn) for loc in self.position.pieces.black.pawns: @@ -1585,6 +1625,8 @@ proc undoLastMove*(self: ChessBoard) = proc isLegal(self: ChessBoard, move: Move): bool {.inline.} = ## Returns whether the given move is legal + if self.grid[move.startSquare.row, move.startSquare.col].color != self.getActiveColor(): + return false return move in self.generateMoves(move.startSquare) @@ -1596,53 +1638,83 @@ proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} = self.doMove(move) -proc `$`*(self: ChessBoard): string = - result &= "- - - - - - - -" - for i, row in self.grid: - result &= "\n" - for piece in row: - if piece.kind == Empty: - result &= "x " - continue - if piece.color == White: - result &= &"{char(piece.kind).toUpperAscii()} " - else: - result &= &"{char(piece.kind)} " - result &= &"{rankToColumn(i + 1) + 1}" - result &= "\n- - - - - - - -" - result &= "\na b c d e f g h" - - proc toChar*(piece: Piece): char = if piece.color == White: return char(piece.kind).toUpperAscii() return char(piece.kind) +proc `$`*(self: ChessBoard): string = + result &= "- - - - - - - -" + for i in 0..7: + result &= "\n" + for j in 0..7: + let piece = self.grid[i, j] + if piece.kind == Empty: + result &= "x " + continue + result &= &"{piece.toChar()} " + result &= &"{rankToColumn(i + 1) + 1}" + result &= "\n- - - - - - - -" + result &= "\na b c d e f g h" + + +proc toPretty*(piece: Piece): string = + case piece.color: + of Black: + case piece.kind: + of King: + return "\U2654" + of Queen: + return "\U2655" + of Rook: + return "\U2656" + of Bishop: + return "\U2657" + of Knight: + return "\U2658" + of Pawn: + return "\U2659" + else: + discard + of White: + case piece.kind: + of King: + return "\U265A" + of Queen: + return "\U265B" + of Rook: + return "\U265C" + of Bishop: + return "\U265D" + of Knight: + return "\U265E" + of Pawn: + return "\U265F" + else: + discard + else: + discard + + proc pretty*(self: ChessBoard): string = ## Returns a colorized version of the ## board for easier visualization - result &= "- - - - - - - -" - - for i, row in self.grid: - result &= "\n" - for j, piece in row: - if piece.kind == Empty: - result &= "\x1b[36;1mx" - # Avoids the color overflowing - # onto the numbers - if j < 7: - result &= " \x1b[0m" - else: - result &= "\x1b[0m " - continue - if piece.color == White: - result &= &"\x1b[37;1m{char(piece.kind).toUpperAscii()}\x1b[0m " + for i in 0..7: + if i > 0: + result &= "\n" + for j in 0..7: + if ((i + j) mod 2) == 0: + result &= "\x1b[39;44;1m" else: - result &= &"\x1b[30;1m{char(piece.kind)} " - result &= &"\x1b[33;1m{rankToColumn(i + 1) + 1}\x1b[0m" + result &= "\x1b[39;40;1m" + let piece = self.grid[i, j] + if piece.kind == Empty: + result &= " \x1b[0m" + else: + result &= &"{piece.toPretty()} \x1b[0m" + result &= &" \x1b[33;1m{rankToColumn(i + 1) + 1}\x1b[0m" - result &= "\n- - - - - - - -" result &= "\n\x1b[31;1ma b c d e f g h" result &= "\x1b[0m" @@ -1653,9 +1725,10 @@ proc toFEN*(self: ChessBoard): string = ## position in the chessboard var skip: int # Piece placement data - for i, row in self.grid: + for i in 0..7: skip = 0 - for j, piece in row: + for j in 0..7: + let piece = self.grid[i, j] if piece.kind == Empty: inc(skip) elif skip > 0: @@ -1702,15 +1775,18 @@ 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 - if ply == 0: - return (1, 0, 0, 0, 0, 0, 0) let moves = self.generateAllMoves() + if len(moves) == 0: + result.checkmates = 1 + if ply == 0: + result.nodes = 1 + return if ply == 1 and bulk: if divide: var postfix = "" for move in moves: - case move.flag: + case move.getPromotionType(): of PromoteToBishop: postfix = "b" of PromoteToKnight: @@ -1726,9 +1802,6 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa echo "" return (uint64(len(moves)), 0, 0, 0, 0, 0, 0) - if len(moves) == 0: - inc(result.checkmates) - for move in moves: if verbose: let canCastle = self.canCastle(self.getActiveColor()) @@ -1747,17 +1820,14 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa echo "None" echo "\n", self.pretty() self.doMove(move) - case move.flag: - of Capture: - inc(result.captures) - of CastleShort, CastleLong: - inc(result.castles) - of PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook: - inc(result.promotions) - of EnPassant: - inc(result.enPassant) - else: - discard + 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) @@ -1779,17 +1849,18 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa self.undoLastMove() if divide and (not bulk or ply > 1): var postfix = "" - case move.flag: - of PromoteToBishop: - postfix = "b" - of PromoteToKnight: - postfix = "n" - of PromoteToRook: - postfix = "r" - of PromoteToQueen: - postfix = "q" - else: - discard + if move.isPromotion(): + case move.getPromotionType(): + of PromoteToBishop: + postfix = "b" + of PromoteToKnight: + postfix = "n" + of PromoteToRook: + postfix = "r" + of PromoteToQueen: + postfix = "q" + else: + discard echo &"{move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}{postfix}: {next.nodes}" if verbose: echo "" @@ -1802,6 +1873,130 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa result.checkmates += next.checkmates +proc handleGoCommand(board: ChessBoard, command: seq[string]) = + if len(command) < 2: + echo &"Error: go: invalid number of arguments" + return + case command[1]: + of "perft": + if len(command) == 2: + echo &"Error: go: perft: invalid number of arguments" + return + var + args = command[2].splitWhitespace() + bulk = false + verbose = false + if args.len() > 1: + 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: + return + 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" + else: + 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}" + echo &" - Checkmates: {data.checkmates}" + echo &" - Castles: {data.castles}" + echo &" - Promotions: {data.promotions}" + echo "" + 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]) = + if len(command) != 2: + echo &"Error: move: invalid number of arguments" + return + let moveString = command[1] + if len(moveString) notin 4..5: + echo &"Error: move: invalid move syntax" + return + var + startSquare: Location + targetSquare: Location + flag: uint16 + + try: + startSquare = moveString[0..1].algebraicToLocation() + except ValueError: + echo &"Error: move: invalid start square" + return + try: + targetSquare = moveString[2..3].algebraicToLocation() + except ValueError: + echo &"Error: move: invalid target square" + return + + if board.grid[targetSquare.row, targetSquare.col].kind != Empty: + flag = flag or Capture.uint16 + + if board.grid[startSquare.row, startSquare.col].kind == Pawn and abs(startSquare.row - targetSquare.row) == 2: + flag = flag or DoublePush.uint16 + + if len(moveString) == 5: + # Promotion + case moveString[4]: + of 'b': + flag = flag and PromoteToBishop.uint16 + of 'n': + flag = flag and PromoteToKnight.uint16 + of 'q': + flag = flag and PromoteToQueen.uint16 + of 'r': + flag = flag and PromoteToRook.uint16 + else: + echo &"Error: move: invalid promotion type" + return + + let move = Move(startSquare: startSquare, targetSquare: targetSquare, flag: flag) + if board.makeMove(move) == emptyMove(): + echo "Error: move: illegal move" + + proc main: int = ## Nimfish's control interface echo "Nimfish by nocturn9x (see LICENSE)" @@ -1824,79 +2019,15 @@ proc main: int = of "help": echo "TODO" of "go": - if len(cmd) < 2: - echo &"Error: go: invalid number of arguments" - continue - case cmd[1]: - of "perft": - if len(cmd) == 2: - echo &"Error: go: perft: invalid number of arguments" - continue - var - args = cmd[2].splitWhitespace() - bulk = false - verbose = false - if args.len() > 1: - 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: on): {board.perft(ply, divide=true, bulk=true, verbose=verbose).nodes}\n" - else: - 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}" - echo &" - Checkmates: {data.checkmates}" - echo &" - Castles: {data.castles}" - echo &" - Promotions: {data.promotions}" - echo "" - except ValueError: - echo "Error: go: perft: invalid depth" - continue - else: - echo &"Error: go: unknown subcommand '{cmd[1]}'" - continue + handleGoCommand(board, cmd) of "position": - case len(cmd): - of 2: - case cmd[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 '{cmd[1]}'" - continue - of 3: - case cmd[1]: - of "fen": - try: - board = newChessboardFromFEN(cmd[2]) - except ValueError: - echo &"Error: position: invalid FEN string '{cmd[2]}': {getCurrentExceptionMsg()}" - else: - echo &"Error: position: unknown subcommand '{cmd[1]}'" - else: - echo &"Error: position: invalid number of arguments" - continue + handlePositionCommand(board, cmd) + of "move": + handleMoveCommand(board, cmd) + of "pretty": + echo board.pretty() + of "undo": + board.undoLastMove() else: echo &"Unknown command '{cmd[0]}'. Type 'help' for more information." except IOError: diff --git a/src/Chess/compare_positions.py b/src/Chess/compare_positions.py index 479d480..11ca02a 100644 --- a/src/Chess/compare_positions.py +++ b/src/Chess/compare_positions.py @@ -16,7 +16,7 @@ def main(args: Namespace) -> int: print(f"Could not locate stockfish executable -> {type(e).__name__}: {e}") return -1 try: - NIMFISH = (args.nimfish or (Path.cwd() / "nimfish")).resolve(strict=True) + NIMFISH = (args.nimfish or (Path.cwd() / "bin" / "nimfish")).resolve(strict=True) except Exception as e: print(f"Could not locate nimfish executable -> {type(e).__name__}: {e}") return -1 @@ -154,5 +154,5 @@ if __name__ == "__main__": parser.add_argument("--ply", "-d", type=int, required=True, help="The depth to stop at, expressed in plys (half-moves)") 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 'nimfish'", default=Path("nimfish")) + parser.add_argument("--nimfish", type=Path, help="Path to the nimfish executable. Defaults to '' (detected automatically)", default=None) sys.exit(main(parser.parse_args())) \ No newline at end of file