diff --git a/src/Chess/board.nim b/src/Chess/board.nim index 0210289..2996b88 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -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 [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 : 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: diff --git a/src/Chess/compare_positions.py b/src/Chess/compare_positions.py index 11ca02a..3e6c3f8 100644 --- a/src/Chess/compare_positions.py +++ b/src/Chess/compare_positions.py @@ -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())) \ No newline at end of file