From 64c30b8a90a80d91c00e3c5fb20f5bd72d515149 Mon Sep 17 00:00:00 2001 From: nocturn9x Date: Fri, 19 Apr 2024 21:00:40 +0200 Subject: [PATCH] More bug fixes. Still borked. Improve modularity --- Chess/nimfish/nimfish.nim | 613 ++++++-------------------------------- Chess/nimfish/tui.nim | 427 ++++++++++++++++++++++++++ 2 files changed, 510 insertions(+), 530 deletions(-) create mode 100644 Chess/nimfish/tui.nim diff --git a/Chess/nimfish/nimfish.nim b/Chess/nimfish/nimfish.nim index 1a76072..16e9be4 100644 --- a/Chess/nimfish/nimfish.nim +++ b/Chess/nimfish/nimfish.nim @@ -50,8 +50,8 @@ type # En passant target square (see https://en.wikipedia.org/wiki/En_passant) enPassantSquare*: Square - # Active color - turn: PieceColor + # The side to move + sideToMove: PieceColor # Positional bitboards for all pieces pieces: tuple[white, black: tuple[king, queens, rooks, bishops, knights, pawns: Bitboard]] # Pinned pieces for each side @@ -69,14 +69,12 @@ type position: Position # List of all previously reached positions positions: seq[Position] - # Index of the current position - currPos: int # A bunch of simple utility functions and forward declarations proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} proc isLegal(self: ChessBoard, move: Move): bool {.inline.} -proc doMove(self: ChessBoard, move: Move) +proc doMove*(self: ChessBoard, move: Move) proc pretty*(self: ChessBoard): string proc spawnPiece(self: ChessBoard, square: Square, piece: Piece) proc toFEN*(self: ChessBoard): string @@ -89,13 +87,16 @@ proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} = for x in other: self.add(x) -proc updateBoard*(self: ChessBoard) +proc update*(self: ChessBoard) +func setSideToMove*(self: ChessBoard, side: PieceColor) {.inline.} = + self.position.sideToMove = side + # A bunch of getters func getSideToMove*(self: ChessBoard): PieceColor {.inline.} = ## Returns the currently side to move - return self.position.turn + return self.position.sideToMove func getEnPassantTarget*(self: ChessBoard): Square {.inline.} = @@ -103,6 +104,11 @@ func getEnPassantTarget*(self: ChessBoard): Square {.inline.} = return self.position.enPassantSquare +func getPlyFromRoot*(self: ChessBoard): int8 {.inline.} = + ## Returns the current distance from the root in plys + return self.position.plyFromRoot + + func getMoveCount*(self: ChessBoard): int {.inline.} = ## Returns the number of full moves that ## have been played @@ -136,7 +142,7 @@ func getStartRank(piece: Piece): int {.inline.} = return 0 -func getKingStartingSquare(color: PieceColor): Square {.inline.} = +func getKingStartingSquare*(color: PieceColor): Square {.inline.} = ## Retrieves the starting square of the king ## for the given color case color: @@ -148,14 +154,16 @@ func getKingStartingSquare(color: PieceColor): Square {.inline.} = discard -func kingSideRook(color: PieceColor): Square {.inline.} = (if color == White: "h1".toSquare() else: "h8".toSquare()) -func queenSideRook(color: PieceColor): Square {.inline.} = (if color == White: "a8".toSquare() else: "a1".toSquare()) -func longCastleKing(color: PieceColor): Square {.inline.} = (if color == White: "c1".toSquare() else: "c8".toSquare()) -func shortCastleKing(color: PieceColor): Square {.inline.} = (if color == White: "g1".toSquare() else: "g8".toSquare()) -func longCastleRook(color: PieceColor): Square {.inline.} = (if color == White: makeSquare(7, 3) else: makeSquare(7, 5)) -func shortCastleRook(color: PieceColor): Square {.inline.} = (if color == White: makeSquare(0, 0) else: makeSquare(0, 2)) +# FIXME: Check this shit. +func kingSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "h1".toSquare() else: "h8".toSquare()) +func queenSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "a8".toSquare() else: "a1".toSquare()) +func longCastleKing*(color: PieceColor): Square {.inline.} = (if color == White: "c1".toSquare() else: "c8".toSquare()) +func shortCastleKing*(color: PieceColor): Square {.inline.} = (if color == White: "g1".toSquare() else: "g8".toSquare()) +func longCastleRook*(color: PieceColor): Square {.inline.} = (if color == White: "d1".toSquare() else: "d8".toSquare()) +func shortCastleRook*(color: PieceColor): Square {.inline.} = (if color == White: "f1".toSquare() else: "f8".toSquare()) -proc inCheck(self: ChessBoard, side: PieceColor): bool + +proc inCheck*(self: ChessBoard, side: PieceColor): bool proc newChessboard: ChessBoard = @@ -163,8 +171,7 @@ proc newChessboard: ChessBoard = new(result) for i in 0..63: result.grid[i] = nullPiece() - result.currPos = -1 - result.position = Position(enPassantSquare: nullSquare(), turn: White) + result.position = Position(enPassantSquare: nullSquare(), sideToMove: White) # Indexing operations func `[]`(self: array[64, Piece], square: Square): Piece {.inline.} = self[square.int8] @@ -314,9 +321,9 @@ proc newChessboardFromFEN*(fen: string): ChessBoard = # Active color case c: of 'w': - result.position.turn = White + result.position.sideToMove = White of 'b': - result.position.turn = Black + result.position.sideToMove = Black else: raise newException(ValueError, &"invalid FEN: invalid active color identifier '{c}'") of 2: @@ -504,12 +511,9 @@ func getFlags*(move: Move): seq[MoveFlag] = result.add(Default) -func getKingSquare(self: ChessBoard, color: PieceColor = None): Square {.inline.} = +func getKingSquare*(self: ChessBoard, color: PieceColor): Square {.inline.} = ## Returns the square of the king for the given - ## side (if it is None, the side to move is used) - var color = color - if color == None: - color = self.getSideToMove() + ## side case color: of White: return self.position.pieces.white.king.toSquare() @@ -539,7 +543,6 @@ proc getOccupancy(self: ChessBoard): Bitboard = result = self.getOccupancyFor(Black) or self.getOccupancyFor(White) - proc getPawnAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard = ## Returns the attack bitboard for the given square from ## the pawns of the given side @@ -601,7 +604,7 @@ proc getSlidingAttacks(self: ChessBoard, square: Square, attacker: PieceColor): result = result or queen.toBitboard() -proc getAttacksTo(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard = +proc getAttacksTo*(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard = ## Computes the attack bitboard for the given square from ## the given side result = Bitboard(0) @@ -613,12 +616,30 @@ proc getAttacksTo(self: ChessBoard, square: Square, attacker: PieceColor): Bitbo result = result or self.getSlidingAttacks(square, attacker) +proc updateCheckers(self: ChessBoard) = + let side = self.getSideToMove() + let checkers = self.getAttacksTo(self.getKingSquare(side), side.opposite()) + case side: + of White: + self.position.checkers.white = checkers + of Black: + self.position.checkers.black = checkers + else: + discard + + proc inCheck(self: ChessBoard, side: PieceColor): bool = ## Returns if the current side to move is in check - return self.getAttacksTo(self.getKingSquare(side), side.opposite()) != 0 + case self.getSideToMove(): + of White: + return self.position.checkers.white != 0 + of Black: + return self.position.checkers.black != 0 + else: + discard -proc canCastle(self: ChessBoard, side: PieceColor): tuple[king, queen: bool] = +proc canCastle*(self: ChessBoard, side: PieceColor): tuple[king, queen: bool] = ## Returns if the current side to move can castle return (false, false) # TODO @@ -659,10 +680,12 @@ proc generatePawnCaptures(self: ChessBoard, moves: var MoveList) = pawns = self.getBitboard(Pawn, sideToMove) # We can only capture enemy pieces (except the king) enemyPieces = self.getCapturablePieces(nonSideToMove) + enemyPawns = self.getBitboard(Pawn, nonSideToMove) rightMovement = pawns.forwardRightRelativeTo(sideToMove) leftMovement = pawns.forwardLeftRelativeTo(sideToMove) epTarget = self.getEnPassantTarget() - let epBitboard = if (epTarget != nullSquare()): epTarget.toBitboard() else: Bitboard(0) + var epBitboard = if (epTarget != nullSquare()): epTarget.toBitboard() else: Bitboard(0) + epBitboard = epBitboard and enemyPawns # Top right attacks for square in rightMovement and enemyPieces: moves.add(createMove(square.toBitboard().backwardLeftRelativeTo(sideToMove), square, Capture)) @@ -704,13 +727,11 @@ proc generateRookMovements(self: ChessBoard, moves: var MoveList) = let sideToMove = self.getSideToMove() occupancy = self.getOccupancy() - friendlyPieces = self.getOccupancyFor(sideToMove) rooks = self.getBitboard(Rook, sideToMove) for square in rooks: let blockers = occupancy and Rook.getRelevantBlockers(square) - var moveset = getRookMoves(square, blockers) - # Can't move over our own pieces - moveset = moveset and not friendlyPieces + # Can't move over other pieces (captures are handled elsewhere) + let moveset = getRookMoves(square, blockers) and not occupancy for target in moveset: moves.add(createMove(square, target)) @@ -744,13 +765,12 @@ proc generateBishopMovements(self: ChessBoard, moves: var MoveList) = let sideToMove = self.getSideToMove() occupancy = self.getOccupancy() - friendlyPieces = self.getOccupancyFor(sideToMove) bishops = self.getBitboard(Bishop, sideToMove) for square in bishops: let blockers = occupancy and Bishop.getRelevantBlockers(square) var moveset = getBishopMoves(square, blockers) - # Can't move over our own pieces - moveset = moveset and not friendlyPieces + # Can't move over other pieces (captures are handled elsewhere) + moveset = moveset and not occupancy for target in moveset: moves.add(createMove(square, target)) @@ -795,8 +815,8 @@ proc generateQueenMovements(self: ChessBoard, moves: var MoveList) = rookMoves = getRookMoves(square, rookBlockers) bishopMoves = getBishopMoves(square, bishopBlockers) var moveset = rookMoves or bishopMoves - # Can't move over our own pieces - moveset = moveset and not friendlyPieces + # Can't move over other pieces (captures are handled elsewhere) + moveset = moveset and not occupancy for target in moveset: moves.add(createMove(square, target)) @@ -871,77 +891,17 @@ proc generateKnightMoves(self: ChessBoard, moves: var MoveList)= moves.add(createMove(square, target, Capture)) -proc checkInsufficientMaterialPieceCount(self: ChessBoard, color: PieceColor): bool = - ## Helper function for checkInsufficientMaterial - let - friendlyPawns = self.countPieces(Piece(kind: Pawn, color: color)) - friendlyRooks = self.countPieces(Piece(kind: Rook, color: color)) - friendlyQueens = self.countPieces(Piece(kind: Queen, color: color)) - friendlyKnights = self.countPieces(Piece(kind: Knight, color: color)) - friendlyBishops = self.countPieces(Piece(kind: Bishop, color: color)) - enemyPawns = self.countPieces(Piece(kind: Pawn, color: color.opposite())) - enemyRooks = self.countPieces(Piece(kind: Rook, color: color.opposite())) - enemyQueens = self.countPieces(Piece(kind: Queen, color: color.opposite())) - enemyKnights = self.countPieces(Piece(kind: Knight, color: color.opposite())) - enemyBishops = self.countPieces(Piece(kind: Bishop, color: color.opposite())) - if friendlyPawns > 0 or friendlyRooks > 0 or friendlyQueens > 0: - return false - if friendlyKnights >= 2: - return false - if friendlyKnights + friendlyBishops >= 2: - return false - if friendlyKnights >= 1 and (enemyPawns > 0 or enemyRooks > 0 or enemyBishops > 0 or enemyKnights > 0 or enemyQueens > 0): - return false - if friendlyBishops >= 1 and (enemyKnights > 0 or enemyPawns > 0): - return false - return true - - -proc checkInsufficientMaterial(self: ChessBoard): bool = - ## Checks if the given position has not enough material for either side to - ## checkmate the enemy king. Note that the criteria as implemented here are - ## not fully compliant with FIDE rules (they just define a draw by insufficient - ## material as "[...] the position is such that the opponent cannot checkmate - ## the player’s king by any possible series of legal moves.", which is really - ## tricky to implement efficiently). For more info see https://www.reddit.com/r/chess/comments/se89db/a_writeup_on_definitions_of_insufficient_material/ - if not (self.checkInsufficientMaterialPieceCount(White) and self.checkInsufficientMaterialPieceCount(Black)): - return false - let - whiteBishops = self.countPieces(Piece(kind: Bishop, color: White)) - blackBishops = self.countPieces(Piece(kind: Bishop, color: Black)) - if blackBishops + whiteBishops >= 2: - var - darkSquare = 0 - lightSquare = 0 - for bishop in self.position.pieces.black.bishops: - if bishop.isLightSquare(): - lightSquare += 1 - else: - darkSquare += 1 - for bishop in self.position.pieces.white.bishops: - if bishop.isLightSquare(): - lightSquare += 1 - else: - darkSquare += 1 - if darkSquare >= 1 and lightSquare >= 1: - return false - return true - - proc generateMoves*(self: ChessBoard, moves: var MoveList) = ## Generates the list of all possible legal moves ## in the current position + moves.clear() if self.position.halfMoveClock >= 100: # Draw by 50-move rule return # TODO: Check for draw by insufficient material - #[ - if self.checkInsufficientMaterial(): - return @[] - ]# # TODO: Check for repetitions (requires zobrist hashing + table) - self.generatePawnMoves(moves) self.generateKingMoves(moves) + self.generatePawnMoves(moves) self.generateKnightMoves(moves) self.generateSlidingMoves(moves) @@ -1031,7 +991,8 @@ proc removePiece(self: ChessBoard, square: Square) = ## Removes a piece from the board, updating necessary ## metadata var piece = self.grid[square] - doAssert piece.kind != Empty and piece.color != None + when not defined(danger): + doAssert piece.kind != Empty and piece.color != None, self.toFEN() self.removePieceFromBitboard(square) self.grid[square] = nullPiece() @@ -1042,9 +1003,10 @@ proc movePiece(self: ChessBoard, move: Move) = ## not update attacked squares metadata, just ## positional info and the grid itself let piece = self.grid[move.startSquare] - let targetSquare = self.getPiece(move.targetSquare) - if targetSquare.color != None: - raise newException(AccessViolationDefect, &"{piece} at {move.startSquare} attempted to overwrite {targetSquare} at {move.targetSquare}") + when not defined(danger): + let targetSquare = self.getPiece(move.targetSquare) + if targetSquare.color != None: + raise newException(AccessViolationDefect, &"{piece} at {move.startSquare} attempted to overwrite {targetSquare} at {move.targetSquare}: {move}") # Update positional metadata self.removePiece(move.startSquare) self.spawnPiece(move.targetSquare, piece) @@ -1055,7 +1017,7 @@ proc movePiece(self: ChessBoard, startSquare, targetSquare: Square) = self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare)) -proc doMove(self: ChessBoard, move: Move) = +proc doMove*(self: ChessBoard, move: Move) = ## Internal function called by makeMove after ## performing legality checks. Can be used in ## performance-critical paths where a move is @@ -1063,11 +1025,11 @@ proc doMove(self: ChessBoard, move: Move) = # Record final position for future reference self.positions.add(self.position) - inc(self.currPos) # Final checks let piece = self.grid[move.startSquare] - doAssert piece.kind != Empty and piece.color != None + when not defined(danger): + doAssert piece.kind != Empty and piece.color != None, &"{move} {self.toFEN()}" var halfMoveClock = self.position.halfMoveClock @@ -1089,7 +1051,7 @@ proc doMove(self: ChessBoard, move: Move) = self.position = Position(plyFromRoot: self.position.plyFromRoot + 1, halfMoveClock: halfMoveClock, fullMoveCount: fullMoveCount, - turn: self.getSideToMove().opposite, + sideToMove: self.getSideToMove().opposite, castlingRights: castlingRights, enPassantSquare: enPassantTarget, pieces: self.position.pieces @@ -1103,6 +1065,7 @@ proc doMove(self: ChessBoard, move: Move) = if move.isCapture(): # Get rid of captured pieces self.removePiece(move.targetSquare) + # Move the piece to its target square self.movePiece(move) if move.isPromotion(): @@ -1121,19 +1084,19 @@ proc doMove(self: ChessBoard, move: Move) = else: # Unreachable discard - self.updateBoard() + self.updateCheckers() proc spawnPiece(self: ChessBoard, square: Square, piece: Piece) = ## Internal helper to "spawn" a given piece at the given - ## square. Note that this will overwrite whatever piece - ## was previously located there: use with caution. Does - ## not automatically update the attacked square metadata + ## square + when not defined(danger): + doAssert self.grid[square].kind == Empty self.addPieceToBitboard(square, piece) self.grid[square] = piece -proc updateBoard*(self: ChessBoard) = +proc update*(self: ChessBoard) = ## Updates the internal grid representation ## according to the positional data stored ## in the chessboard @@ -1168,10 +1131,8 @@ proc updateBoard*(self: ChessBoard) = proc unmakeMove*(self: ChessBoard) = ## Reverts to the previous board position, ## if one exists - if self.currPos >= 0: - self.position = self.positions[self.currPos] - dec(self.currPos) - self.updateBoard() + self.position = self.positions.pop() + self.update() proc isLegal(self: ChessBoard, move: Move): bool {.inline.} = @@ -1329,418 +1290,6 @@ proc toFEN*(self: ChessBoard): string = result &= $self.getMoveCount() -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 - - var moves = MoveList() - self.generateMoves(moves) - if not bulk: - if len(moves) == 0 and self.inCheck(self.getSideToMove()): - result.checkmates = 1 - # TODO: Should we count stalemates/draws? - if ply == 0: - result.nodes = 1 - return - elif ply == 1 and bulk: - if divide: - var postfix = "" - for move in moves: - case move.getPromotionType(): - of PromoteToBishop: - postfix = "b" - of PromoteToKnight: - postfix = "n" - of PromoteToRook: - postfix = "r" - of PromoteToQueen: - postfix = "q" - else: - postfix = "" - echo &"{move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}{postfix}: 1" - if verbose: - echo "" - return (uint64(len(moves)), 0, 0, 0, 0, 0, 0) - - for move in moves: - if verbose: - let canCastle = self.canCastle(self.getSideToMove()) - echo &"Ply (from root): {self.position.plyFromRoot}" - echo &"Move: {move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}" - echo &"Turn: {self.getSideToMove()}" - echo &"Piece: {self.grid[move.startSquare].kind}" - echo &"Flags: {move.getFlags()}" - echo &"In check: {(if self.inCheck(self.getSideToMove()): \"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()}" - stdout.write("En Passant target: ") - if self.getEnPassantTarget() != nullSquare(): - echo self.getEnPassantTarget().toAlgebraic() - else: - echo "None" - echo "\n", self.pretty() - self.doMove(move) - 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(self.getSideToMove()): - # Opponent king is in check - inc(result.checks) - if verbose: - let canCastle = self.canCastle(self.getSideToMove()) - echo "\n" - echo &"Opponent in check: {(if self.inCheck(self.getSideToMove()): \"yes\" else: \"no\")}" - 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("nextpos>> ") - try: - discard readLine(stdin) - except IOError: - discard - except EOFError: - discard - let next = self.perft(ply - 1, verbose, bulk=bulk) - self.unmakeMove() - if divide and (not bulk or ply > 1): - var postfix = "" - 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.toAlgebraic()}{move.targetSquare.toAlgebraic()}{postfix}: {next.nodes}" - if verbose: - echo "" - result.nodes += next.nodes - result.captures += next.captures - result.checks += next.checks - result.promotions += next.promotions - result.castles += next.castles - result.enPassant += next.enPassant - 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": - 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: - 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}" - echo &" - Checks: {data.checks}" - echo &" - E.P: {data.enPassant}" - echo &" - Checkmates: {data.checkmates}" - 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 handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discardable.} = - 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: Square - targetSquare: Square - flags: seq[MoveFlag] - - try: - startSquare = moveString[0..1].toSquare() - except ValueError: - echo &"Error: move: invalid start square ({moveString[0..1]})" - return - try: - targetSquare = moveString[2..3].toSquare() - except ValueError: - 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, etc.) - - if board.grid[targetSquare].kind != Empty: - flags.add(Capture) - - elif board.grid[startSquare].kind == Pawn and abs(rankFromSquare(startSquare) - rankFromSquare(targetSquare)) == 2: - flags.add(DoublePush) - - if len(moveString) == 5: - # Promotion - case moveString[4]: - of 'b': - flags.add(PromoteToBishop) - of 'n': - flags.add(PromoteToKnight) - of 'q': - flags.add(PromoteToQueen) - of 'r': - flags.add(PromoteToRook) - else: - echo &"Error: move: invalid promotion type" - return - - - var move = createMove(startSquare, targetSquare, flags) - let piece = board.getPiece(move.startSquare) - if piece.kind == King and move.startSquare == board.getSideToMove().getKingStartingSquare(): - if move.targetSquare == longCastleKing(piece.color): - move.flags = move.flags or CastleLong.uint16 - elif move.targetSquare == shortCastleKing(piece.color): - move.flags = move.flags or CastleShort.uint16 - if move.targetSquare == board.getEnPassantTarget(): - move.flags = move.flags or EnPassant.uint16 - result = board.makeMove(move) - if result == nullMove(): - 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: ChessBoard - 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]]) == nullMove(): - 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]]) == nullMove(): - return - inc(j) - inc(i) - board = tempBoard - of "print": - echo board - of "pretty": - echo board.pretty() - else: - echo &"error: position: unknown subcommand '{command[1]}'" - return - - -proc handleUCICommand(board: var ChessBoard, command: seq[string]) = - echo "id name Nimfish 0.1" - echo "id author Nocturn9x & Contributors (see LICENSE)" - # TODO - echo "uciok" - - -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 castlingRights rights for each side - - check: Print if the current side to move is in check - - undo, u: Undoes the last move. 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" - - fen: Shorthand for "position fen" - - pos : Shorthand for "position " - - get : Get the piece on the given square - - atk: Print the attack bitboard of the given square for the side to move - - uci: enter UCI mode (WIP) -""" - - -proc main: int = - ## Nimfish's control interface - echo "Nimfish by nocturn9x (see LICENSE)" - var - board = newDefaultChessboard() - uciMode = false - while true: - var - cmd: seq[string] - cmdStr: string - try: - if not uciMode: - stdout.write(">>> ") - stdout.flushFile() - cmdStr = readLine(stdin).strip(leading=true, trailing=true, chars={'\t', ' '}) - if cmdStr.len() == 0: - continue - cmd = cmdStr.splitWhitespace(maxsplit=2) - - case cmd[0]: - of "uci": - handleUCICommand(board, cmd) - uciMode = true - of "clear": - echo "\x1Bc" - of "help": - echo HELP_TEXT - of "go": - handleGoCommand(board, cmd) - of "position", "pos": - handlePositionCommand(board, cmd) - of "move": - handleMoveCommand(board, cmd) - of "pretty", "print", "fen": - handlePositionCommand(board, @["position", cmd[0]]) - of "undo", "u": - board.unmakeMove() - of "turn": - echo &"Active color: {board.getSideToMove()}" - of "atk": - if len(cmd) != 2: - echo "error: atk: invalid number of arguments" - continue - try: - echo board.getAttacksTo(cmd[1].toSquare(), board.getSideToMove()) - except ValueError: - echo "error: atk: invalid square" - continue - of "ep": - let target = board.getEnPassantTarget() - if target != nullSquare(): - echo &"En passant target: {target.toAlgebraic()}" - else: - echo "En passant target: None" - of "get": - if len(cmd) != 2: - echo "error: get: invalid number of arguments" - continue - try: - echo board.getPiece(cmd[1]) - except ValueError: - echo "error: get: invalid square" - continue - of "castle": - let canCastle = board.canCastle(board.getSideToMove()) - echo &"Castling rights for {($board.getSideToMove()).toLowerAscii()}:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" - of "check": - echo &"{board.getSideToMove()} king in check: {(if board.inCheck(board.getSideToMove()): \"yes\" else: \"no\")}" - else: - echo &"Unknown command '{cmd[0]}'. Type 'help' for more information." - except IOError: - echo "" - return 0 - except EOFError: - echo "" - return 0 - - when isMainModule: @@ -1846,5 +1395,9 @@ when isMainModule: testPieceBitboard(blackQueens, blackQueenSquares) testPieceBitboard(blackKing, blackKingSquares) + setControlCHook(proc () {.noconv.} = quit(0)) - quit(main()) + + import tui + + quit(tui.commandLoop()) diff --git a/Chess/nimfish/tui.nim b/Chess/nimfish/tui.nim new file mode 100644 index 0000000..a990719 --- /dev/null +++ b/Chess/nimfish/tui.nim @@ -0,0 +1,427 @@ +import nimfish + + +import std/strformat +import std/strutils +import std/times +import std/math + + +type + CountData = tuple[nodes: uint64, captures: uint64, castles: uint64, checks: uint64, promotions: uint64, enPassant: uint64, checkmates: uint64] + + +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 + + var moves = MoveList() + self.generateMoves(moves) + if not bulk: + if len(moves) == 0 and self.inCheck(self.getSideToMove()): + result.checkmates = 1 + # TODO: Should we count stalemates/draws? + if ply == 0: + result.nodes = 1 + return + elif ply == 1 and bulk: + if divide: + var postfix = "" + for move in moves: + case move.getPromotionType(): + of PromoteToBishop: + postfix = "b" + of PromoteToKnight: + postfix = "n" + of PromoteToRook: + postfix = "r" + of PromoteToQueen: + postfix = "q" + else: + postfix = "" + echo &"{move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}{postfix}: 1" + if verbose: + echo "" + return (uint64(len(moves)), 0, 0, 0, 0, 0, 0) + + for move in moves: + if verbose: + let canCastle = self.canCastle(self.getSideToMove()) + echo &"Ply (from root): {self.getPlyFromRoot()}" + echo &"Move: {move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}" + echo &"Turn: {self.getSideToMove()}" + echo &"Piece: {self.getPiece(move.startSquare).kind}" + echo &"Flags: {move.getFlags()}" + echo &"In check: {(if self.inCheck(self.getSideToMove()): \"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()}" + stdout.write("En Passant target: ") + if self.getEnPassantTarget() != nullSquare(): + echo self.getEnPassantTarget().toAlgebraic() + else: + echo "None" + echo "\n", self.pretty() + self.doMove(move) + 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(self.getSideToMove()): + # Opponent king is in check + inc(result.checks) + if verbose: + let canCastle = self.canCastle(self.getSideToMove()) + echo "\n" + echo &"Opponent in check: {(if self.inCheck(self.getSideToMove()): \"yes\" else: \"no\")}" + 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("nextpos>> ") + try: + discard readLine(stdin) + except IOError: + discard + except EOFError: + discard + let next = self.perft(ply - 1, verbose, bulk=bulk) + self.unmakeMove() + if divide and (not bulk or ply > 1): + var postfix = "" + 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.toAlgebraic()}{move.targetSquare.toAlgebraic()}{postfix}: {next.nodes}" + if verbose: + echo "" + result.nodes += next.nodes + result.captures += next.captures + result.checks += next.checks + result.promotions += next.promotions + result.castles += next.castles + result.enPassant += next.enPassant + 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": + 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: + 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}" + echo &" - Checks: {data.checks}" + echo &" - E.P: {data.enPassant}" + echo &" - Checkmates: {data.checkmates}" + 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 handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discardable.} = + 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: Square + targetSquare: Square + flags: seq[MoveFlag] + + try: + startSquare = moveString[0..1].toSquare() + except ValueError: + echo &"Error: move: invalid start square ({moveString[0..1]})" + return + try: + targetSquare = moveString[2..3].toSquare() + except ValueError: + 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, etc.) + + if board.getPiece(targetSquare).kind != Empty: + flags.add(Capture) + + elif board.getPiece(targetSquare).kind == Pawn and abs(rankFromSquare(startSquare) - rankFromSquare(targetSquare)) == 2: + flags.add(DoublePush) + + if len(moveString) == 5: + # Promotion + case moveString[4]: + of 'b': + flags.add(PromoteToBishop) + of 'n': + flags.add(PromoteToKnight) + of 'q': + flags.add(PromoteToQueen) + of 'r': + flags.add(PromoteToRook) + else: + echo &"Error: move: invalid promotion type" + return + + + var move = createMove(startSquare, targetSquare, flags) + let piece = board.getPiece(move.startSquare) + if piece.kind == King and move.startSquare == board.getSideToMove().getKingStartingSquare(): + if move.targetSquare == longCastleKing(piece.color): + move.flags = move.flags or CastleLong.uint16 + elif move.targetSquare == shortCastleKing(piece.color): + move.flags = move.flags or CastleShort.uint16 + if move.targetSquare == board.getEnPassantTarget(): + move.flags = move.flags or EnPassant.uint16 + result = board.makeMove(move) + if result == nullMove(): + 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: ChessBoard + 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]]) == nullMove(): + 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]]) == nullMove(): + return + inc(j) + inc(i) + board = tempBoard + of "print": + echo board + of "pretty": + echo board.pretty() + else: + echo &"error: position: unknown subcommand '{command[1]}'" + return + + +proc handleUCICommand(board: var ChessBoard, command: seq[string]) = + echo "id name Nimfish 0.1" + echo "id author Nocturn9x & Contributors (see LICENSE)" + # TODO + echo "uciok" + + +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 castlingRights rights for each side + - check: Print if the current side to move is in check + - unmove, u: Unmakes the last move. Can be used in succession + - stm: Print which side is to move + - ep: Print the current en passant target + - pretty: Shorthand for "position pretty" + - print: Shorthand for "position print" + - fen: Shorthand for "position fen" + - pos : Shorthand for "position " + - get : Get the piece on the given square + - atk : Print the attack bitboard of the given square for the side to move + - skip: Swap the side to move + - uci: enter UCI mode (WIP) + - quit: exit +""" + + +proc commandLoop*: int = + ## Nimfish's control interface + echo "Nimfish by nocturn9x (see LICENSE)" + var + board = newDefaultChessboard() + uciMode = false + while true: + var + cmd: seq[string] + cmdStr: string + try: + if not uciMode: + stdout.write(">>> ") + stdout.flushFile() + cmdStr = readLine(stdin).strip(leading=true, trailing=true, chars={'\t', ' '}) + if cmdStr.len() == 0: + continue + cmd = cmdStr.splitWhitespace(maxsplit=2) + + case cmd[0]: + of "uci": + handleUCICommand(board, cmd) + uciMode = true + of "clear": + echo "\x1Bc" + of "help": + echo HELP_TEXT + of "skip": + board.setSideToMove(board.getSideToMove().opposite()) + of "go": + handleGoCommand(board, cmd) + of "position", "pos": + handlePositionCommand(board, cmd) + of "move": + handleMoveCommand(board, cmd) + of "pretty", "print", "fen": + handlePositionCommand(board, @["position", cmd[0]]) + of "unmove", "u": + board.unmakeMove() + of "stm": + echo &"Side to move: {board.getSideToMove()}" + of "atk": + if len(cmd) != 2: + echo "error: atk: invalid number of arguments" + continue + try: + echo board.getAttacksTo(cmd[1].toSquare(), board.getSideToMove()) + except ValueError: + echo "error: atk: invalid square" + continue + of "ep": + let target = board.getEnPassantTarget() + if target != nullSquare(): + echo &"En passant target: {target.toAlgebraic()}" + else: + echo "En passant target: None" + of "get": + if len(cmd) != 2: + echo "error: get: invalid number of arguments" + continue + try: + echo board.getPiece(cmd[1]) + except ValueError: + echo "error: get: invalid square" + continue + of "castle": + let canCastle = board.canCastle(board.getSideToMove()) + echo &"Castling rights for {($board.getSideToMove()).toLowerAscii()}:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" + of "check": + echo &"{board.getSideToMove()} king in check: {(if board.inCheck(board.getSideToMove()): \"yes\" else: \"no\")}" + else: + echo &"Unknown command '{cmd[0]}'. Type 'help' for more information." + except IOError: + echo "" + return 0 + except EOFError: + echo "" + return 0 \ No newline at end of file