From c6cc98a296bcfe524537df70fce252c7b75f3147 Mon Sep 17 00:00:00 2001 From: nocturn9x Date: Fri, 20 Oct 2023 02:23:07 +0200 Subject: [PATCH] Fixes to generation and added basic debugger --- src/Chess/board.nim | 532 +++++++++++++++++++++++++------------------- 1 file changed, 308 insertions(+), 224 deletions(-) diff --git a/src/Chess/board.nim b/src/Chess/board.nim index fc2c0bf..85beed5 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -17,6 +17,7 @@ export matrix import std/strutils import std/strformat +import std/sequtils type @@ -50,12 +51,16 @@ type MoveFlag* = enum ## An enumeration of move flags - Default = 0'i8, # Move is a regular move - # Castling + Default = 0'i8, # No flag + Capture, # Move is a capture + EnPassant, # Move is an en passant + Backwards, # Move is a backward move generated by undoMove + DoublePush, # Move is a double pawn push + # Castling metadata CastleLong, CastleShort, XRay, # Move is an X-ray attack - # Move is a pawn promotion + # Pawn promotion metadata PromoteToQueen, PromoteToRook, PromoteToBishop, @@ -125,9 +130,12 @@ func `+`*(a, b: Location): Location = (a.row + b.row, a.col + b.col) func isValid*(a: Location): bool {.inline.} = a.row in 0..7 and a.col in 0..7 proc generateMoves(self: ChessBoard, location: Location): seq[Move] proc isAttacked*(self: ChessBoard, loc: Location): bool -proc undoLastMove*(self: ChessBoard): Move {.discardable.} -proc isLegal(self: ChessBoard, move: Move): bool +proc undoMove*(self: ChessBoard, move: Move): Move {.discardable.} +proc isLegal(self: ChessBoard, move: Move, keep: bool = false): bool proc doMove(self: ChessBoard, move: Move) +proc isLegalFast(self: ChessBoard, move: Move, keep: bool = false): bool +proc makeMoveFast*(self: ChessBoard, move: Move): Move {.discardable.} +proc pretty*(self: ChessBoard): string # Due to our board layout, directions of movement are reversed for white/black so # we need these helpers to avoid going mad with integer tuples and minus signs @@ -142,10 +150,10 @@ func topSide(color: PieceColor): Location {.inline.} = (if color == White: (-1, func bottomSide(color: PieceColor): Location {.inline.} = (if color == White: (1, 0) else: (-1, 0)) func forward(color: PieceColor): Location {.inline.} = (if color == White: (-1, 0) else: (1, 0)) func doublePush(color: PieceColor): Location {.inline.} = (if color == White: (-2, 0) else: (2, 0)) -func longCastleKing(color: PieceColor): Location {.inline.} = (0, -2) -func shortCastleKing(color: PieceColor): Location {.inline.} = (0, 2) -func longCastleRook(color: PieceColor): Location {.inline.} = (0, 3) -func shortCastleRook(color: PieceColor): Location {.inline.} = (0, -2) +func longCastleKing: Location {.inline.} = (0, -2) +func shortCastleKing: Location {.inline.} = (0, 2) +func longCastleRook: Location {.inline.} = (0, 3) +func shortCastleRook: Location {.inline.} = (0, -2) func kingSideRook(color: PieceColor): Location {.inline.} = (if color == White: (7, 7) else: (0, 7)) func queenSideRook(color: PieceColor): Location {.inline.} = (if color == White: (7, 0) else: (0, 0)) @@ -155,25 +163,25 @@ func bottomLeftKnightMove(color: PieceColor, long: bool = true): Location {.inli if long: return (2, -1) else: - return (-1, -2) + return (1, -2) elif color == Black: if long: - return (-2, 1) + return (2, 1) else: - return (1, -2) + return (2, -1) func bottomRightKnightMove(color: PieceColor, long: bool = true): Location {.inline.} = if color == White: if long: - return (2, -1) + return (2, 1) else: return (1, 2) elif color == Black: if long: - return (2, 1) + return (-2, -1) else: - return (1, 2) + return (-1, -2) func topLeftKnightMove(color: PieceColor, long: bool = true): Location {.inline.} = @@ -199,7 +207,7 @@ func topRightKnightMove(color: PieceColor, long: bool = true): Location {.inline if long: return (2, -1) else: - return (-1, 2) + return (1, -2) func getActiveColor*(self: ChessBoard): PieceColor {.inline.} = @@ -266,7 +274,8 @@ proc newChessboard: ChessBoard = result.position = Position(attacked: (@[], @[]), enPassantSquare: emptyMove(), move: emptyMove(), - turn: White) + turn: White, + fullMoveCount: 1) @@ -347,8 +356,8 @@ proc newChessboardFromFEN*(state: string): ChessBoard = column = 0 of '0'..'9': # Skip x columns - let x = int(uint8(c) - uint8('0')) - 1 - if x > 7: + let x = int(uint8(c) - uint8('0')) + if x > 8: raise newException(ValueError, "invalid skip value (> 8) in FEN string") column += int8(x) else: @@ -530,7 +539,7 @@ func getCapture*(self: ChessBoard, move: Move): Location = return else: return ((if move.piece.color == White: move.targetSquare.row + 1 else: move.targetSquare.row - 1), move.targetSquare.col) - if target.color == move.piece.color.opposite(): + elif target.color == move.piece.color.opposite(): return move.targetSquare @@ -545,20 +554,17 @@ proc inCheck*(self: ChessBoard, color: PieceColor = None): bool = ## king is in check. If the color is ## set to None, checks are checked ## for the active color's king + var color = color + if color == None: + color = self.getActiveColor() case color: of White: return self.isAttacked(self.position.pieces.white.king) of Black: return self.isAttacked(self.position.pieces.black.king) - of None: - case self.getActiveColor(): - of White: - return self.isAttacked(self.position.pieces.white.king) - of Black: - return self.isAttacked(self.position.pieces.black.king) - else: - # Unreachable - discard + else: + # Unreachable + discard proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: bool] {.inline.} = @@ -647,57 +653,51 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = var piece = self.grid[location.row, location.col] locations: seq[Location] = @[] + flags: seq[MoveFlag] = @[] doAssert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}" # Pawns can move forward one square let forwardOffset = piece.color.forward() let forward = (forwardOffset + location) + # Only if the square is empty though if forward.isValid() and self.grid[forward.row, forward.col].color == None: locations.add(forwardOffset) + flags.add(Default) # If the pawn is on its first rank, it can push two squares if location.row == piece.getStartRow(): - locations.add(piece.color.doublePush()) + let doubleOffset = piece.color.doublePush() + let double = location + doubleOffset + # Check if both squares are available + if double.isValid() and self.grid[forward.row, forward.col].color == None and self.grid[double.row, double.col].color == None: + locations.add(piece.color.doublePush()) + flags.add(DoublePush) if self.position.enPassantSquare.piece.color == piece.color.opposite: if abs(self.position.enPassantSquare.targetSquare.col - location.col) == 1 and abs(self.position.enPassantSquare.targetSquare.row - location.row) == 1: # Only viable if the piece is on the diagonal of the target locations.add(self.position.enPassantSquare.targetSquare) + flags.add(EnPassant) # They can also move on either diagonal one # square, but only to capture - if location.col in 1..6: - # Top right diagonal - locations.add(piece.color.topRightDiagonal()) - if location.row in 1..6: - # Top left diagonal - locations.add(piece.color.topLeftDiagonal()) - - # Pawn is at the right side, can only capture - # on the left one - if location.col == 7 and location.row < 7: - locations.add(piece.color.topLeftDiagonal()) - # Pawn is at the left side, can only capture - # on the right one - if location.col == 0 and location.row < 7: - locations.add(piece.color.topRightDiagonal()) + var diagonal = piece.color.topRightDiagonal() + if (diagonal + location).isValid() and self.isCapture(Move(piece: piece, startSquare: location, targetSquare: location + diagonal)): + locations.add(diagonal) + flags.add(Capture) + diagonal = piece.color.topLeftDiagonal() + if (diagonal + location).isValid() and self.isCapture(Move(piece: piece, startSquare: location, targetSquare: location + diagonal)): + locations.add(diagonal) + flags.add(Capture) + var newLocation: Location targetPiece: Piece - for target in locations: + for (target, flag) in zip(locations, flags): newLocation = location + target - if not newLocation.isValid(): - continue targetPiece = self.grid[newLocation.row, newLocation.col] - if targetPiece.color == piece.color: - # Can't move over a friendly piece - continue - if location.col != newLocation.col and not self.isCapture(Move(piece: piece, startSquare: location, targetSquare: newLocation)): - # Can only move diagonally when capturing - continue if newLocation.row == piece.color.getLastRow(): - # Generate all promotion moves + # 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: newLocation, piece: piece, flag: promotionType)) continue - # Move is just a pawn push - result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece)) + result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece, flag: flag)) proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = @@ -734,7 +734,7 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = if otherPiece.color == piece.color.opposite: # Target square contains an enemy piece: capture # it and stop going any further - result.add(Move(startSquare: location, targetSquare: square, piece: piece)) + result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: Capture)) break # Target square is empty result.add(Move(startSquare: location, targetSquare: square, piece: piece)) @@ -756,21 +756,25 @@ proc generateKingMoves(self: ChessBoard, location: Location): seq[Move] = # Castling let canCastle = self.canCastle() if canCastle.queen: - directions.add(piece.color.longCastleKing()) + directions.add(longCastleKing()) if canCastle.king: - directions.add(piece.color.shortCastleKing()) + directions.add(shortCastleKing()) var flag = Default for direction in directions: - if direction == piece.color.longCastleKing(): + if direction == longCastleKing(): flag = CastleLong - elif direction == piece.color.shortCastleKing(): + elif direction == shortCastleKing(): flag = CastleShort + else: + flag = Default # Step in this direction once let square: Location = location + direction # End of board reached if not square.isValid(): continue let otherPiece = self.grid[square.row, square.col] + if otherPiece.color == self.getActiveColor.opposite(): + flag = Capture # A friendly piece is in the way, move onto the next # direction if otherPiece.color == piece.color: @@ -806,7 +810,7 @@ proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] = if otherPiece.color == piece.color.opposite: # Target square contains an enemy piece: capture # it - result.add(Move(startSquare: location, targetSquare: square, piece: piece)) + result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: Capture)) continue # Target square is empty result.add(Move(startSquare: location, targetSquare: square, piece: piece)) @@ -829,14 +833,6 @@ proc generateMoves(self: ChessBoard, location: Location): seq[Move] = return @[] -proc generateLegalMoves*(self: ChessBoard, location: Location): seq[Move] = - ## Returns the list of possible legal chess moves for the - ## piece in the given location - for move in self.generateMoves(location): - if self.isLegal(move): - result.add(move) - - proc generateAllMoves*(self: ChessBoard): seq[Move] = ## Returns the list of all possible pseudo-legal moves ## in the current position @@ -848,18 +844,6 @@ proc generateAllMoves*(self: ChessBoard): seq[Move] = result.add(move) - -proc countLegalMoves*(self: ChessBoard, ply: int): int = - ## Counts the number of legal positions reached after - ## the given number of half moves - if ply == 0: - return 1 - for move in self.generateAllMoves(): - if self.isLegal(move): - #echo &"{move.piece.color} {move.piece.kind} {move.startSquare.locationToAlgebraic()} {move.targetSquare.locationtoAlgebraic()}" - result += self.countLegalMoves(ply - 1) - - proc getAttackers*(self: ChessBoard, square: Location): seq[Piece] = ## Returns all the attackers of the given square for move in self.position.attacked.black: @@ -910,12 +894,7 @@ proc updateAttackedSquares(self: ChessBoard) = ## Updates internal metadata about which squares ## are attacked. Called internally by doMove - # We refresh the attack metadata at every move. This is an - # O(1) operation, because we're only updating the length - # field without deallocating the memory, which will promptly - # be reused by us again. Neat! - self.position.attacked.white.setLen(0) - self.position.attacked.black.setLen(0) + # Go over each piece one by one and see which squares # it currently attacks @@ -988,7 +967,7 @@ proc removePiece(self: ChessBoard, location: Location) = of Black: case piece.kind: of Pawn: - self.position.pieces.black.pawns.delete(self.position.pieces.white.pawns.find(location)) + self.position.pieces.black.pawns.delete(self.position.pieces.black.pawns.find(location)) of Bishop: self.position.pieces.black.bishops.delete(self.position.pieces.black.bishops.find(location)) of Knight: @@ -1005,10 +984,11 @@ proc removePiece(self: ChessBoard, location: Location) = discard -proc movePiece(self: ChessBoard, move: Move) = - ## Internal helper to move a piece. Does - ## not update attacked squares, just position - ## metadata and the grid itself +proc movePiece(self: ChessBoard, move: Move, attack: bool = true) = + ## Internal helper to move a piece. If attack + ## is set to false, then this function does + ## not update attacked squares metadata, just + ## positional info and the grid itself case move.piece.color: of White: case move.piece.kind: @@ -1059,21 +1039,30 @@ proc movePiece(self: ChessBoard, move: Move) = # Empty out the starting square self.grid[move.startSquare.row, move.startSquare.col] = emptyPiece() # Actually move the piece - self.grid[move.targetSquare.row, move.targetSquare.col] = move.piece + self.grid[move.targetSquare.row, move.targetSquare.col] = move.piece + if attack: + self.updateAttackedSquares() -proc updatePositions(self: ChessBoard, move: Move) = +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, + piece: self.grid[startSquare.row, startSquare.col], + ), + attack + ) + + +proc updateLocations(self: ChessBoard, move: Move) = ## Internal helper to update the position of ## the pieces on the board after a move let capture = self.getCapture(move) if capture != emptyLocation(): self.position.captured = self.grid[capture.row, capture.col] - if capture != self.getEnPassantTarget(): - # En passant is handled elsewhere - self.removePiece(capture) + self.removePiece(capture) # Update the positional metadata of the moving piece self.movePiece(move) - + proc doMove(self: ChessBoard, move: Move) = ## Internal function called by makeMove after @@ -1083,7 +1072,7 @@ proc doMove(self: ChessBoard, move: Move) = # Final checks - # Record the move in the position + # Record the final move in the position self.position.move = move # Needed to detect draw by the 50 move rule @@ -1091,110 +1080,115 @@ proc doMove(self: ChessBoard, move: Move) = halfMoveClock = self.position.halfMoveClock fullMoveCount = self.position.fullMoveCount castlingAvailable = self.position.castlingAvailable - if move.piece.kind == Pawn or self.isCapture(move): - halfMoveClock = 0 - else: - inc(halfMoveClock) - if move.piece.color == Black: - inc(fullMoveCount) - # Castling check: have the rooks moved? - if move.piece.kind == Rook: - case move.piece.color: - of White: - if move.startSquare.row == move.piece.getStartRow(): - if move.startSquare.col == 0: - # Queen side - castlingAvailable.white.queen = false - elif move.startSquare.col == 7: - # King side - castlingAvailable.white.king = false - of Black: - if move.startSquare.row == move.piece.getStartRow(): - if move.startSquare.col == 0: - # Queen side - castlingAvailable.black.queen = false - elif move.startSquare.col == 7: - # King side - castlingAvailable.black.king = false - else: - discard - # Has a rook been captured? - let capture = self.getCapture(move) - if capture != emptyLocation(): - let piece = self.grid[capture.row, capture.col] - if piece.kind == Rook: - case piece.color: - of White: - if capture == piece.color.queenSideRook(): - # Queen side - castlingAvailable.white.queen = false - elif capture == piece.color.kingSideRook(): - # King side - castlingAvailable.white.king = false - of Black: - if capture == piece.color.queenSideRook(): - # Queen side - castlingAvailable.black.queen = false - elif capture == piece.color.kingSideRook(): - # King side - castlingAvailable.black.king = false - else: - # Unreachable - discard - # Has the king moved? - if move.piece.kind == King: - case move.piece.color: - of White: - castlingAvailable.white.king = false - castlingAvailable.white.queen = false - of Black: - castlingAvailable.black.king = false - castlingAvailable.black.queen = false - else: - discard - # Update position and attack metadata - self.updatePositions(move) - self.updateAttackedSquares() - - var location: Location - if move.flag in [CastleShort, CastleLong]: - # Move the rook onto the - # correct file - var - location: Location - target: Location - if move.flag == CastleShort: - location = move.piece.color.kingSideRook() - target = move.piece.color.shortCastleRook() + if move.flag != Backwards: + if move.piece.kind == Pawn or self.isCapture(move): + halfMoveClock = 0 else: - location = move.piece.color.queenSideRook() - target = move.piece.color.longCastleRook() - let rook = self.grid[location.row, location.col] - let move = Move(startSquare: location, targetSquare: location + target, piece: rook, flag: move.flag) - self.updatePositions(move) - self.updateAttackedSquares() + inc(halfMoveClock) + if move.piece.color == Black: + inc(fullMoveCount) + # Castling check: have the rooks moved? + if move.piece.kind == Rook: + case move.piece.color: + of White: + if move.startSquare.row == move.piece.getStartRow(): + if move.startSquare.col == 0: + # Queen side + castlingAvailable.white.queen = false + elif move.startSquare.col == 7: + # King side + castlingAvailable.white.king = false + of Black: + if move.startSquare.row == move.piece.getStartRow(): + if move.startSquare.col == 0: + # Queen side + castlingAvailable.black.queen = false + elif move.startSquare.col == 7: + # King side + castlingAvailable.black.king = false + else: + discard + # Has a rook been captured? + let capture = self.getCapture(move) + if capture != emptyLocation(): + let piece = self.grid[capture.row, capture.col] + if piece.kind == Rook: + case piece.color: + of White: + if capture == piece.color.queenSideRook(): + # Queen side + castlingAvailable.white.queen = false + elif capture == piece.color.kingSideRook(): + # King side + castlingAvailable.white.king = false + of Black: + if capture == piece.color.queenSideRook(): + # Queen side + castlingAvailable.black.queen = false + elif capture == piece.color.kingSideRook(): + # King side + castlingAvailable.black.king = false + else: + # Unreachable + discard + # Has the king moved? + if move.piece.kind == King: + case move.piece.color: + of White: + castlingAvailable.white.king = false + castlingAvailable.white.queen = false + of Black: + castlingAvailable.black.king = false + castlingAvailable.black.queen = false + else: + discard + + let previous = self.position + if move.flag != Backwards: + # Record final position for future reference + self.positions.add(previous) + # Create new position + self.position = Position(plyFromRoot: self.position.plyFromRoot + 1, + halfMoveClock: halfMoveClock, + fullMoveCount: fullMoveCount, + captured: emptyPiece(), + turn: self.getActiveColor().opposite, + castlingAvailable: castlingAvailable, + # Updated at the next call to doMove() + move: emptyMove(), + pieces: previous.pieces, + ) + + var location: Location + if move.flag in [CastleShort, CastleLong]: + # Move the rook onto the + # correct file + var + location: Location + target: Location + if move.flag == CastleShort: + location = move.piece.color.kingSideRook() + target = shortCastleRook() + else: + location = move.piece.color.queenSideRook() + target = longCastleRook() + let rook = self.grid[location.row, location.col] + let move = Move(startSquare: location, targetSquare: location + target, piece: rook, flag: move.flag) + self.movePiece(move, attack=false) + + # Update position and attack metadata + if move.flag == Backwards: + self.movePiece(move.targetSquare, move.startSquare) + else: + self.movePiece(move) - # Record final position for future reference - self.positions.add(self.position) - # Create new position with - var newPos = Position(plyFromRoot: self.position.plyFromRoot + 1, - halfMoveClock: halfMoveClock, - fullMoveCount: fullMoveCount, - captured: emptyPiece(), - turn: self.getActiveColor().opposite, - castlingAvailable: castlingAvailable, - # Updated at the next call to doMove() - move: emptyMove(), - ) # Check for double pawn push - if move.piece.kind == Pawn and abs(move.startSquare.row - move.targetSquare.row) == 2: - newPos.enPassantSquare = Move(piece: move.piece, + if move.flag == DoublePush: + self.position.enPassantSquare = Move(piece: move.piece, startSquare: (move.startSquare.row, move.startSquare.col), targetSquare: move.targetSquare + move.piece.color.bottomSide()) else: - newPos.enPassantSquare = emptyMove() - - self.position = newPos + self.position.enPassantSquare = emptyMove() proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) = @@ -1242,41 +1236,59 @@ proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) = self.grid[location.row, location.col] = piece -proc undoLastMove*(self: ChessBoard): Move {.discardable.} = - ## Undoes the last move, restoring the previous position. - ## If there are no positions to roll back to to, this is a - ## no-op. Returns the move that was performed (may be empty) +proc undoMove*(self: ChessBoard, move: Move): Move {.discardable.} = + ## Undoes the given move if possible result = emptyMove() if self.positions.len() == 0: return - var - previous = self.positions[^1] - oppositeMove = Move(piece: previous.move.piece, targetSquare: previous.move.startSquare, startSquare: previous.move.targetSquare) - self.removePiece(previous.move.startSquare) + var move: Move = move + let previous = self.positions.pop() + move.flag = Backwards + self.doMove(move) self.position = previous - if previous.move != emptyMove(): - self.spawnPiece(previous.move.startSquare, previous.move.piece) - self.updateAttackedSquares() - self.updatePositions(oppositeMove) - if previous.captured != emptyPiece(): - self.spawnPiece(previous.move.targetSquare, previous.captured) - discard self.positions.pop() - return self.position.move + self.position.move = move + #self.updateLocations(move) + return move -proc isLegal(self: ChessBoard, move: Move): bool = +proc isLegal(self: ChessBoard, move: Move, keep: bool = false): bool = ## Returns whether the given move is legal var move = move - if move.piece.kind == King and move.piece.color.longCastleKing() + move.startSquare == move.targetSquare: + if move.piece.kind == King and longCastleKing() + move.startSquare == move.targetSquare: move.flag = CastleLong - elif move.piece.kind == King and move.piece.color.shortCastleKing() + move.startSquare == move.targetSquare: + elif move.piece.kind == King and shortCastleKing() + move.startSquare == move.targetSquare: move.flag = CastleShort + if move.piece.kind == Pawn and move.piece.color.doublePush() + move.startSquare == move.targetSquare: + move.flag = DoublePush if move notin self.generateMoves(move.startSquare): # Piece cannot arrive to destination (blocked # or otherwise invalid move) return false self.doMove(move) - defer: self.undoLastMove() + if not keep: + defer: self.undoMove(move) + # Move would reveal an attack + # on our king: not allowed + if self.inCheck(move.piece.color): + return false + # All checks have passed: move is legal + result = true + + +proc isLegalFast(self: ChessBoard, move: Move, keep: bool = false): bool = + ## Returns whether the given move is legal + ## assuming that the input move is pseudo legal + var move = move + if move.piece.kind == King and longCastleKing() + move.startSquare == move.targetSquare: + move.flag = CastleLong + elif move.piece.kind == King and shortCastleKing() + move.startSquare == move.targetSquare: + move.flag = CastleShort + if move.piece.kind == Pawn and move.piece.color.doublePush() + move.startSquare == move.targetSquare: + move.flag = DoublePush + self.position.move = move + self.doMove(move) + if not keep: + defer: self.undoMove(move) # Move would reveal an attack # on our king: not allowed if self.inCheck(move.piece.color): @@ -1287,11 +1299,26 @@ proc isLegal(self: ChessBoard, move: Move): bool = proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} = ## Like the other makeMove(), but with a Move object - result = move - if not self.isLegal(move): + self.position.move = move + if move.flag == Backwards: + self.doMove(result) + let legal = self.isLegal(move, keep=true) + if not legal: return emptyMove() - self.doMove(result) + result = self.position.move + + +proc makeMoveFast*(self: ChessBoard, move: Move): Move {.discardable.} = + ## Like the other makeMove(), but uses isLegalFast + result = move + self.position.move = move + if move.flag == Backwards: + self.doMove(result) + let legal = self.isLegalFast(move, keep=true) + if not legal: + return emptyMove() + result = self.position.move proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.discardable.} = @@ -1360,6 +1387,63 @@ proc pretty*(self: ChessBoard): string = result &= "\x1b[0m" + + +proc countLegalMoves*(self: ChessBoard, ply: int, verbose: bool = false, verboseIllegal: bool = false, current: int = 0): int = + ## Counts (and debugs) the number of legal positions reached after + ## the given number of half moves + if ply == 0: + result = 1 + else: + var now: string + for move in self.generateAllMoves(): + now = self.pretty() + if self.isLegalFast(move, keep=true): + if verbose: + let canCastle = self.canCastle() + echo "\x1Bc" + echo &"Ply: {self.position.plyFromRoot} (move {current + result + 1})" + echo &"Move: {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()} (({move.startSquare.row}, {move.startSquare.col}) -> ({move.targetSquare.row}, {move.targetSquare.col}))" + echo &"Turn: {move.piece.color}" + echo &"Piece: {move.piece.kind}" + echo &"In check: {(if self.inCheck(move.piece.color): \"yes\" else: \"no\")}" + echo "Legal: yes" + echo &"Flag: {move.flag}" + echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" + echo "\nBefore:" + echo now + echo "\nNow: " + echo self.pretty() + try: + discard readLine(stdin) + except IOError: + discard + except EOFError: + discard + result += self.countLegalMoves(ply - 1, verbose, verboseIllegal, result) + elif verboseIllegal: + let canCastle = self.canCastle() + echo "\x1Bc" + echo &"Ply: {self.position.plyFromRoot} (move {current + result + 1})" + echo &"Move: {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()} (({move.startSquare.row}, {move.startSquare.col}) -> ({move.targetSquare.row}, {move.targetSquare.col}))" + echo &"Turn: {move.piece.color}" + echo &"Piece: {move.piece.kind}" + echo &"In check: {(if self.inCheck(move.piece.color): \"yes\" else: \"no\")}" + echo "Legal: no" + echo &"Flag: {move.flag}" + echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" + echo "\nBefore:" + echo now + echo "\nNow: " + echo self.pretty() + try: + discard readLine(stdin) + except IOError: + discard + except EOFError: + discard + self.undoMove(move) + when isMainModule: proc testPiece(piece: Piece, kind: PieceKind, color: PieceColor) = doAssert piece.kind == kind and piece.color == color, &"expected piece of kind {kind} and color {color}, got {piece.kind} / {piece.color} instead" @@ -1417,6 +1501,6 @@ when isMainModule: when compileOption("profiler"): import nimprof - echo b.countLegalMoves(3) - + b = newChessboardFromFEN("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - -") + echo b.countLegalMoves(2, verbose=true, verboseIllegal=true) echo "All tests were successful" \ No newline at end of file