diff --git a/src/Chess/board.nim b/src/Chess/board.nim index 8804a52..e5c7e24 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -69,7 +69,7 @@ type ## A chess board object grid: Matrix[Piece] # Currently active color - turn*: PieceColor + turn: PieceColor # Number of half moves since # last piece capture or pawn movement. # Used for the 50-move rule @@ -87,7 +87,7 @@ type pieces: tuple[white: Pieces, black: Pieces] # Locations of all attacked squares and their # respective attackers - attacked*: tuple[white: seq[tuple[attacker: Piece, loc: Location]], black: seq[tuple[attacker: Piece, loc: Location]]] + attacked: tuple[white: seq[tuple[attacker: Piece, loc: Location]], black: seq[tuple[attacker: Piece, loc: Location]]] # Initialized only once, copied every time @@ -102,6 +102,21 @@ func opposite*(c: PieceColor): PieceColor {.inline.} = (if c == White: Black els proc algebraicToPosition*(s: string): Location {.inline.} proc getCapture*(self: ChessBoard, move: Move): Location func emptyMove*: Move {.inline.} = Move(startSquare: emptyLocation(), targetSquare: emptyLocation(), piece: emptyPiece()) +func `+`*(a, b: Location): Location = (a.row + b.row, a.col + b.col) +func isValid*(a: Location): bool = a.row in 0..7 and a.col in 0..7 +proc generateMoves(self: ChessBoard, location: Location): seq[Move] + +func topLeftDiagonal(piece: Piece): Location {.inline.} = (if piece.color == White: (-1, -1) else: (1, 1)) +func topRightDiagonal(piece: Piece): Location {.inline.} = (if piece.color == White: (-1, 1) else: (1, -1)) +func forward(piece: Piece): Location {.inline.} = (if piece.color == Black: (1, 0) else: (-1, 0)) +func doublePush(piece: Piece): Location {.inline.} = (if piece.color == Black: (2, 0) else: (-2, 0)) +proc testMoveOffsets(self: ChessBoard, move: Move): bool + + +proc getActiveColor*(self: ChessBoard): PieceColor = + ## Returns the currently active color + ## (turn of who has to move) + return self.turn func getStartRow(piece: Piece): int {.inline.} = @@ -376,6 +391,29 @@ proc getPiece*(self: ChessBoard, square: string): Piece = return self.grid[loc.row, loc.col] +proc getCapture*(self: ChessBoard, move: Move): Location = + ## Returns the location that would be captured if this + ## move were played on the board, taking en passant and + ## other things into account (the move is assumed to be + ## already valid). An empty location is returned if no + ## piece is captured by the given move + result = emptyLocation() + let target = self.grid[move.targetSquare.row, move.targetSquare.col] + if target.color == None: + if move.targetSquare != self.enPassantSquare.targetSquare: + 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(): + return move.targetSquare + + +proc isCapture*(self: ChessBoard, move: Move): bool {.inline.} = + ## Returns whether the given move is a capture + ## or not + return self.getCapture(move) != emptyLocation() + + proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = ## Generates the possible moves for the pawn in the given ## location @@ -383,78 +421,83 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = piece = self.grid[location.row, location.col] locations: seq[Location] = @[] doAssert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}" - 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 + # Pawns can move forward one square + let forwardOffset = piece.forward() + let forward = (forwardOffset + location) + if forward.isValid() and self.grid[forward.row, forward.col].color == None: + locations.add(forwardOffset) + # If the pawn is on its first rank, it can push two squares + if location.row == piece.getStartRow(): + locations.add(piece.doublePush()) + 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: + # Top right diagonal + locations.add(piece.topRightDiagonal()) + if location.row in 1..6: + # Top left diagonal + locations.add(piece.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.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.topRightDiagonal()) + var + newLocation: Location + targetPiece: Piece for target in locations: - if target.row == piece.color.getLastRow(): + 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 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])) + 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)) - proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = - ## Generates sliding moves for the sliding piece in the given location + ## Generates moves for the sliding piece in the given location var piece = self.grid[location.row, location.col] doAssert piece.kind in [Bishop, Rook, Queen], &"generateSlidingMoves called on a {piece.kind}" + var directions: seq[Location] = @[piece.topRightDiagonal(), piece.topLeftDiagonal()] + echo piece, " ", location + for direction in directions: + echo "dir ", direction + # Slide in this direction as long as it's possible + var + square: Location = location + otherPiece: Piece + while true: + square = square + direction + # End of board reached + if not square.isValid(): + break + echo square + otherPiece = self.grid[square.row, square.col] + # A friendly piece is in the way + if otherPiece.color == piece.color: + break + # Target square is empty or occupied by the enemy + result.add(Move(startSquare: location, targetSquare: square, piece: piece)) proc generateMoves(self: ChessBoard, location: Location): seq[Move] = @@ -470,76 +513,6 @@ proc generateMoves(self: ChessBoard, location: Location): seq[Move] = return @[] -proc getCapture*(self: ChessBoard, move: Move): Location = - ## Returns the location that would be captured if this - ## move were played on the board, taking en passant and - ## other things into account. An empty location is returned - ## if no piece is captured by the given move - result = emptyLocation() - let target = self.grid[move.targetSquare.row, move.targetSquare.col] - if target.color == None: - if move.targetSquare != self.enPassantSquare.targetSquare: - 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() and move in self.generateMoves(move.startSquare): - return move.targetSquare - - -proc isCapture*(self: ChessBoard, move: Move): bool {.inline.} = - ## Returns whether the given move is a capture - ## or not - 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 pseudo-legal: this does not take pins @@ -552,9 +525,15 @@ proc testMoveOffsets(self: ChessBoard, move: Move): bool = ## does) case move.piece.kind: of Pawn: - return self.validatePawnMove(move) - of Bishop: - return self.validateSlidingMove(move) + return move in self.generatePawnMoves(move.startSquare) + of Bishop, Queen, Rook: + return move in self.generateSlidingMoves(move.startSquare) + of Knight: + # TODO + discard + of King: + # TODO + discard else: return false @@ -610,10 +589,9 @@ proc getAttackers*(self: ChessBoard, square: string): seq[Piece] = # We don't use getAttackers because this one only cares about whether # the square is attacked or not (and can therefore exit earlier than # getAttackers) -proc isAttacked*(self: ChessBoard, square: string): bool = - ## Returns whether the given square is attacked - ## by one of the enemy pieces - let loc = square.algebraicToPosition() +proc isAttacked*(self: ChessBoard, loc: Location): bool = + ## Returns whether the given location is attacked + ## by the current inactive color case self.turn: of White: for (attacker, location) in self.attacked.black: @@ -629,6 +607,12 @@ proc isAttacked*(self: ChessBoard, square: string): bool = discard +proc isAttacked*(self: ChessBoard, square: string): bool = + ## Returns whether the given square is attacked + ## by the current inactive color + return self.isAttacked(square.algebraicToPosition()) + + proc removePiece(self: ChessBoard, location: Location) = ## Removes a piece from the board, updating necessary ## metadata @@ -646,7 +630,7 @@ proc removePiece(self: ChessBoard, location: Location) = of Rook: self.pieces.white.rooks.delete(self.pieces.white.rooks.find(location)) of Queen: - self.pieces.white.queens.delete(self.pieces.white.rooks.find(location)) + self.pieces.white.queens.delete(self.pieces.white.queens.find(location)) of King: doAssert false, "removePiece: attempted to remove the white king" else: @@ -654,15 +638,15 @@ proc removePiece(self: ChessBoard, location: Location) = of Black: case piece.kind: of Pawn: - self.pieces.black.pawns.delete(self.pieces.black.pawns.find(location)) + self.pieces.black.pawns.delete(self.pieces.white.pawns.find(location)) of Bishop: - self.pieces.black.pawns.delete(self.pieces.black.bishops.find(location)) + self.pieces.black.bishops.delete(self.pieces.black.bishops.find(location)) of Knight: - self.pieces.black.pawns.delete(self.pieces.black.knights.find(location)) + self.pieces.black.knights.delete(self.pieces.black.knights.find(location)) of Rook: self.pieces.black.rooks.delete(self.pieces.black.rooks.find(location)) of Queen: - self.pieces.black.queens.delete(self.pieces.black.rooks.find(location)) + self.pieces.black.queens.delete(self.pieces.black.queens.find(location)) of King: doAssert false, "removePiece: attempted to remove the black king" else: @@ -674,13 +658,12 @@ proc removePiece(self: ChessBoard, location: Location) = proc updatePositions(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(): - # Move has captured a piece: remove the destination square's piece as well. - # We call a helper instead of doing it ourselves because there's a bunch - # of metadata that needs to be updated to do this properly and I thought - # it'd fit into its neat little function + # Move has captured a piece: remove it as well. We call a helper instead + # of doing it ourselves because there's a bunch of metadata that needs + # to be updated to do this properly and I thought it'd fit into its neat + # little function self.removePiece(capture) # Update the positional metadata of the moving piece case move.piece.color: diff --git a/src/Chess/player.nim b/src/Chess/player.nim index abc5075..6e3876d 100644 --- a/src/Chess/player.nim +++ b/src/Chess/player.nim @@ -1,5 +1,22 @@ import board as chess +import std/strformat +import std/strutils -var board = newDefaultChessboard() -echo board.pretty() +var + board = newChessboardFromFEN("rnbqkbnr/8/8/8/8/8/8/RNBQKBNR w KQkq - 0 1") + startSquare: string + targetSquare: string + move: Move + +while true: + echo board.pretty() + echo &"Turn: {board.getActiveColor()}" + stdout.write("From -> ") + startSquare = readLine(stdin).strip(chars={'\0', ' '}) + stdout.write("To -> ") + targetSquare = readLine(stdin) + try: + move = board.makeMove(startSquare, targetSquare) + except ValueError: + echo &"Error: {getCurrentExceptionMsg()}" \ No newline at end of file