From de0864c066c6f5a9d896c0e333503d4290cb3797 Mon Sep 17 00:00:00 2001 From: nocturn9x Date: Mon, 16 Oct 2023 15:25:48 +0200 Subject: [PATCH] More additions to move generation and initial sliding work --- src/Chess/board.nim | 304 ++++++++++++++++++++------------------------ 1 file changed, 136 insertions(+), 168 deletions(-) diff --git a/src/Chess/board.nim b/src/Chess/board.nim index cef9a84..8804a52 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -103,86 +103,6 @@ proc algebraicToPosition*(s: string): Location {.inline.} proc getCapture*(self: ChessBoard, move: Move): Location func emptyMove*: Move {.inline.} = Move(startSquare: emptyLocation(), targetSquare: emptyLocation(), piece: emptyPiece()) -# Movement offsets for the various pieces. They are -# different for each color because the board is an 8x8 -# grid indexed at 0, which means that white is at the -# bottom of the grid going up (indexes are decreasing) -# and black is at the top of the grid going down (indeces -# are increasing). These offsets are needed to take that -# into account when doing move generation or checking for -# captures -proc getMovementOffsets(self: ChessBoard, location: Location): seq[Location] = - let piece = self.grid[location.row, location.col] - case piece.color: - of White: - case piece.kind: - of Pawn: - # Pawns can move forward one square. In our flipped - # board configuration, that means moving up one row - # while keeping the column the same - if location.row in 1..6 and self.grid[location.row - 1, location.col].color == None: - result.add((location.row - 1, location.col)) - if self.enPassantSquare.piece.color == piece.color.opposite: - if abs(self.enPassantSquare.targetSquare.col - location.col) == 1 and abs(self.enPassantSquare.targetSquare.row - location.row) == 1: - # Only viable if the piece is on the diagonal of the target - result.add(self.enPassantSquare.targetSquare) - # They can also move on either diagonal one - # square, but only to capture - if location.col in 1..6 and location.row in 1..6: - if self.grid[location.row + 1, location.col + 1].color == Black: - # Top right diagonal (white side) - result.add((location.row + 1, location.col + 1)) - if self.grid[location.row - 1, location.col - 1].color == Black: - # Top left diagonal - result.add((location.row + 1, location.col + 1)) - # Pawn is at the right side, can only capture - # on the left one - elif location.col == 0 and location.row < 7 and self.grid[location.row + 1, location.col + 1].color == Black: - result.add((location.row + 1, location.col + 1)) - # Pawn is at the left side, can only capture - # on the right one - elif location.col == 7 and location.row < 7 and self.grid[location.row + 1, location.col - 1].color == Black: - result.add((location.row - 1, location.col - 1)) - of Bishop: - return @[] - else: - discard - of Black: - case piece.kind: - of Pawn: - # Pawns can move forward one square. In our flipped - # board configuration, that means moving down one row - # while keeping the column the same - if location.row in 1..6 and self.grid[location.row - 1, location.col].color == None: - result.add((1, 0)) - if self.enPassantSquare.piece.color == piece.color.opposite: - if abs(self.enPassantSquare.targetSquare.col - location.col) == 1 and abs(self.enPassantSquare.targetSquare.row - location.row) == 1: - # Only viable if the piece is on the diagonal of the target - result.add(self.enPassantSquare.targetSquare) - # They can also move on either diagonal one - # square, but only to capture - if location.col in 1..6 and location.row in 1..6: - if self.grid[location.row - 1, location.col - 1].color == White: - # Top right diagonal (black side) - result.add((1, 1)) - if self.grid[location.row + 1, location.col + 1].color == White: - # Top left diagonal - result.add((-1, -1)) - # Pawn is at the right side, can only capture - # on the left one - elif location.col > 0 and location.row > 0 and self.grid[location.row - 1, location.col + 1].color == White: - result.add((-1, -1)) - # Pawn is at the left side, can only capture - # on the right one - elif location.col == 7 and location.row > 0 and self.grid[location.row + 1, location.col + 1].color == White: - result.add((1, 1)) - of Bishop: - return @[] - else: - discard - else: - discard - func getStartRow(piece: Piece): int {.inline.} = ## Retrieves the starting row of @@ -205,6 +125,11 @@ func getStartRow(piece: Piece): int {.inline.} = return 0 +func getLastRow(color: PieceColor): int {.inline.} = + ## Retrieves the location of the last + ## row relative to the given color + + proc newChessboard: ChessBoard = ## Returns a new, empty chessboard new(result) @@ -452,65 +377,95 @@ proc getPiece*(self: ChessBoard, square: string): Piece = proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = - ## Generates pawn moves + ## Generates the possible moves for the pawn in the given + ## location var piece = self.grid[location.row, location.col] + locations: seq[Location] = @[] doAssert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}" - for offset in self.getMovementOffsets(location): - result.add(Move(startSquare: location, targetSquare: offset, piece: self.grid[location.row, location.col])) + case piece.color: + of White: + # Pawns can move forward one square. In our flipped + # board configuration, that means moving up one row + # while keeping the column the same + if location.row in 1..6 and self.grid[location.row - 1, location.col].color == None: + locations.add((location.row - 1, location.col)) + if self.enPassantSquare.piece.color == piece.color.opposite: + if abs(self.enPassantSquare.targetSquare.col - location.col) == 1 and abs(self.enPassantSquare.targetSquare.row - location.row) == 1: + # Only viable if the piece is on the diagonal of the target + locations.add(self.enPassantSquare.targetSquare) + # They can also move on either diagonal one + # square, but only to capture + if location.col in 1..6 and location.row in 1..6: + if self.grid[location.row + 1, location.col + 1].color == Black: + # Top right diagonal (white side) + locations.add((location.row + 1, location.col + 1)) + if self.grid[location.row - 1, location.col - 1].color == Black: + # Top left diagonal + locations.add((location.row + 1, location.col + 1)) + # Pawn is at the right side, can only capture + # on the left one + elif location.col == 0 and location.row < 7 and self.grid[location.row + 1, location.col + 1].color == Black: + locations.add((location.row + 1, location.col + 1)) + # Pawn is at the left side, can only capture + # on the right one + elif location.col == 7 and location.row < 7 and self.grid[location.row + 1, location.col - 1].color == Black: + locations.add((location.row - 1, location.col - 1)) + of Black: + # Pawns can move forward one square. In our flipped + # board configuration, that means moving down one row + # while keeping the column the same + if location.row in 1..6 and self.grid[location.row - 1, location.col].color == None: + locations.add((1, 0)) + if self.enPassantSquare.piece.color == piece.color.opposite: + if abs(self.enPassantSquare.targetSquare.col - location.col) == 1 and abs(self.enPassantSquare.targetSquare.row - location.row) == 1: + # Only viable if the piece is on the diagonal of the target + locations.add(self.enPassantSquare.targetSquare) + # They can also move on either diagonal one + # square, but only to capture + if location.col in 1..6 and location.row in 1..6: + if self.grid[location.row - 1, location.col - 1].color == White: + # Top right diagonal (black side) + locations.add((1, 1)) + if self.grid[location.row + 1, location.col + 1].color == White: + # Top left diagonal + locations.add((-1, -1)) + # Pawn is at the right side, can only capture + # on the left one + elif location.col > 0 and location.row > 0 and self.grid[location.row - 1, location.col + 1].color == White: + locations.add((-1, -1)) + # Pawn is at the left side, can only capture + # on the right one + elif location.col == 7 and location.row > 0 and self.grid[location.row + 1, location.col + 1].color == White: + locations.add((1, 1)) + else: + discard + for target in locations: + if target.row == piece.color.getLastRow(): + # Generate all promotion moves + for promotionType in [PromoteToKnight, PromoteToBishop, PromoteToRook, PromoteToQueen]: + result.add(Move(startSquare: location, targetSquare: target, piece: self.grid[location.row, location.col], flag: promotionType)) + else: + result.add(Move(startSquare: location, targetSquare: target, piece: self.grid[location.row, location.col])) proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = - ## Generates sliding moves + ## Generates sliding moves for the sliding piece in the given location var - square: Piece piece = self.grid[location.row, location.col] - newLocation: Location doAssert piece.kind in [Bishop, Rook, Queen], &"generateSlidingMoves called on a {piece.kind}" - for offset in self.getMovementOffsets(location): - newLocation = (location.row + offset.row, location.col + offset.col) - # Keep sliding until there is a friendly piece or a capture in the way - while true: - square = self.grid[newLocation.row, newLocation.col] - # Friendly piece: cannot move any further - if square.color == piece.color: - break - # Empty square or capture: can do! - if square.color == None or square.color == piece.color.opposite(): - result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece)) - # Continue in this direction - newLocation.row += offset.row - # Check if we reached the end of the board - if newLocation.row < 0 or newLocation.row > 7: - break - newLocation.col += offset.col - if newLocation.col < 0 or newLocation.col > 7: - break - proc generateMoves(self: ChessBoard, location: Location): seq[Move] = ## Returns the list of possible moves for the ## piece in the given location let piece = self.grid[location.row, location.col] - case piece.color: - of White: - case piece.kind: - of Pawn: - return self.generatePawnMoves(location) - of Bishop: - return self.generateSlidingMoves(location) - else: - discard - of Black: - case piece.kind: - of Pawn: - return self.generatePawnMoves(location) - of Bishop: - return self.generateSlidingMoves(location) - else: - discard + case piece.kind: + of Queen, Bishop, Rook: + return self.generateSlidingMoves(location) + of Pawn: + return self.generatePawnMoves(location) else: return @[] @@ -537,56 +492,69 @@ proc isCapture*(self: ChessBoard, move: Move): bool {.inline.} = return self.getCapture(move) != emptyLocation() +proc validatePawnMove(self: ChessBoard, move: Move): bool = + ## Returns true if the given pawn move is allowed + ## (internal helper to testMoveOffsets) + if move.targetSquare.col != move.startSquare.col: + # Pawn can only change column in case of capture or en passant + if self.enPassantSquare == emptyMove(): + # No en passant possible, only possibility + # is a capture + return self.isCapture(move) + # En passant is possible, check if the destination is + # its target square + if self.enPassantSquare.targetSquare != move.targetSquare: + # We still need to check for captures even if en passant + # is possible + return self.isCapture(move) + # Number of rows traveled + var rows: int + # Due to our unique board layout, we need to do this nonsense + if move.piece.color == White: + rows = move.startSquare.row - move.targetSquare.row + else: + rows = move.targetSquare.row - move.startSquare.row + if rows < 0 or rows > 2: + # Pawns don't go backwards, I'm afraid. They also can't + # go any further than 2 squares + return false + if rows == 2: + # Check if double pawn pushing is possible (only the first + # move for each pawn) + if move.startSquare.row != move.piece.getStartRow(): + # Pawn has already moved more than once, double push + # is not allowed + return false + # En passant is now possible + let targetSquare: Location = ((if move.piece.color == White: move.targetSquare.row + 1 else: move.targetSquare.row - 1), move.targetSquare.col) + self.enPassantSquare = Move(piece: move.piece, startSquare: move.startSquare, targetSquare: targetSquare) + # Captures are checked earlier, so we only need to make sure we aren't blocked by + # a piece + return self.grid[move.targetSquare.row, move.targetSquare.col].kind == Empty + + +proc validateSlidingMove(self: ChessBoard, move: Move): bool = + ## Returns true if the given pawn move is allowed + ## (internal helper to testMoveOffsets) + + var directions: seq[Location] + + proc testMoveOffsets(self: ChessBoard, move: Move): bool = ## Returns true if the piece in the given - ## move is allowed to move in the direction - ## specified. This does not take pins nor checks - ## into account, but other rules like double pawn - ## pushes and en passant are validated here. Note - ## that this is an internal method called by checkMove - ## and it does not validate whether the target square - ## is occupied or not (it is assumed the check has been - ## performed beforehand, like checkMove does) + ## move is pseudo-legal: this does not take pins + ## nor checks into account, but other rules like + ## double pawn pushes and en passant are validated + ## here. Note that this is an internal method called + ## by checkMove and it does not validate whether the + ## target square is occupied or not (it is assumed the + ## check has been performed beforehand, like checkMove + ## does) case move.piece.kind: of Pawn: - if move.targetSquare.col != move.startSquare.col: - # Pawn can only change column in case of capture or en passant - if self.enPassantSquare == emptyMove(): - # No en passant possible, only possibility - # is a capture - return self.isCapture(move) - # En passant is possible, check if the destination is - # its target square - if self.enPassantSquare.targetSquare != move.targetSquare: - # We still need to check for captures even if en passant - # is possible - return self.isCapture(move) - # Number of rows traveled - var rows: int - # Due to our unique board layout, we need to do this nonsense - if move.piece.color == White: - rows = move.startSquare.row - move.targetSquare.row - else: - rows = move.targetSquare.row - move.startSquare.row - if rows < 0 or rows > 2: - # Pawns don't go backwards, I'm afraid. They also can't - # go any further than 2 squares - return false - if rows == 2: - # Check if double pawn pushing is possible (only the first - # move for each pawn) - if move.startSquare.row != move.piece.getStartRow(): - # Pawn has already moved more than once, double push - # is not allowed - return false - # En passant is now possible - let targetSquare: Location = ((if move.piece.color == White: move.targetSquare.row + 1 else: move.targetSquare.row - 1), move.targetSquare.col) - self.enPassantSquare = Move(piece: move.piece, startSquare: move.startSquare, targetSquare: targetSquare) - # Captures are checked earlier, so we only need to make sure we aren't blocked by - # a piece - return self.grid[move.targetSquare.row, move.targetSquare.col].kind == Empty + return self.validatePawnMove(move) of Bishop: - discard + return self.validateSlidingMove(move) else: return false