diff --git a/src/Chess/board.nim b/src/Chess/board.nim index 06be12b..cef9a84 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -47,19 +47,29 @@ type ## A chess piece color*: PieceColor kind*: PieceKind - - Move* = ref object + MoveFlag* = enum + ## An enumeration of move flags + Default, # Move is a regular move + XRay, # Move is an X-ray attack + # Move is a pawn promotion + PromoteToQueen, + PromoteToRook, + PromoteToBishop, + PromoteToKnight + + Move* = object ## A chess move piece*: Piece startSquare*: Location targetSquare*: Location + flag*: MoveFlag ChessBoard* = ref object ## 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 @@ -72,12 +82,12 @@ type # En passant target square (see https://en.wikipedia.org/wiki/En_passant) # If en passant is not possible, both the row and # column of the position will be set to -1 - enPassantSquare: Move + enPassantSquare*: Move # Locations of all pieces 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 @@ -90,12 +100,110 @@ func emptyPiece*: Piece {.inline.} = Piece(kind: Empty, color: None) func emptyLocation*: Location {.inline.} = (-1 , -1) func opposite*(c: PieceColor): PieceColor {.inline.} = (if c == White: Black else: White) proc algebraicToPosition*(s: string): Location {.inline.} -func `==`(a, b: Location): bool {.inline.} = a.row == b.row and a.col == b.col proc getCapture*(self: ChessBoard, move: Move): Location +func emptyMove*: Move {.inline.} = Move(startSquare: emptyLocation(), targetSquare: emptyLocation(), piece: emptyPiece()) -var emptyMove = 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 + ## the given piece inside our 8x8 + ## grid + case piece.color: + of None: + return -1 + of White: + case piece.kind: + of Pawn: + return 6 + else: + return 5 + of Black: + case piece.kind: + of Pawn: + return 1 + else: + return 0 + proc newChessboard: ChessBoard = ## Returns a new, empty chessboard @@ -103,7 +211,7 @@ proc newChessboard: ChessBoard = # Turns our flat sequence into an 8x8 grid result.grid = newMatrixFromSeq[Piece](empty, (8, 8)) result.attacked = (@[], @[]) - result.enPassantSquare = emptyMove + result.enPassantSquare = emptyMove() result.turn = White @@ -293,6 +401,7 @@ proc countPieces*(self: ChessBoard, kind: PieceKind, color: PieceColor): int = of Queen: return self.pieces.black.queens.len() of King: + # In perpetuity return 1 else: discard @@ -342,6 +451,70 @@ proc getPiece*(self: ChessBoard, square: string): Piece = return self.grid[loc.row, loc.col] +proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = + ## Generates pawn moves + var + piece = self.grid[location.row, location.col] + 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])) + + + +proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = + ## Generates sliding moves + 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 + else: + 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 @@ -353,9 +526,8 @@ proc getCapture*(self: ChessBoard, move: Move): Location = if move.targetSquare != self.enPassantSquare.targetSquare: return else: - return (self.enPassantSquare.targetSquare.row + 1, - self.enPassantSquare.targetSquare.col) - if target.color == move.piece.color.opposite(): + 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 @@ -379,7 +551,7 @@ proc testMoveOffsets(self: ChessBoard, move: Move): bool = 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: + if self.enPassantSquare == emptyMove(): # No en passant possible, only possibility # is a capture return self.isCapture(move) @@ -403,15 +575,18 @@ proc testMoveOffsets(self: ChessBoard, move: Move): bool = if rows == 2: # Check if double pawn pushing is possible (only the first # move for each pawn) - let startRow = if move.piece.color == White: 6 else: 1 - if move.startSquare.row != startRow: + 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 - self.enPassantSquare = Move(piece: move.piece, startSquare: move.startSquare, - targetSquare: (move.targetSquare.row - 1, move.targetSquare.col)) - return true + 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 + of Bishop: + discard else: return false @@ -420,10 +595,70 @@ 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.attacked.white.setLen(0) + self.attacked.black.setLen(0) # Go over each piece one by one and see which squares # it currently attacks - for piece in self.pieces.white.pawns: - discard + + # White pawns + for loc in self.pieces.white.pawns: + for move in self.generateMoves(loc): + self.attacked.white.add((move.piece, move.targetSquare)) + # Black pawns + for loc in self.pieces.black.pawns: + for move in self.generateMoves(loc): + self.attacked.black.add((move.piece, move.targetSquare)) + # White bishops + for loc in self.pieces.white.bishops: + for move in self.generateMoves(loc): + self.attacked.white.add((move.piece, move.targetSquare)) + # Black bishops + for loc in self.pieces.black.bishops: + for move in self.generateMoves(loc): + self.attacked.black.add((move.piece, move.targetSquare)) + + +proc getAttackers*(self: ChessBoard, square: string): seq[Piece] = + ## Returns the attackers of the given square. + ## If the square has no attackers, an empty + ## seq is returned + let loc = square.algebraicToPosition() + case self.turn: + of White: + for (attacker, location) in self.attacked.black: + if location == loc: + result.add(attacker) + of Black: + for (attacker, location) in self.attacked.white: + if location == loc: + result.add(attacker) + else: + return @[] + +# 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() + case self.turn: + of White: + for (attacker, location) in self.attacked.black: + if location == loc: + return true + return false + of Black: + for (attacker, location) in self.attacked.white: + if location == loc: + return true + return false + else: + discard proc removePiece(self: ChessBoard, location: Location) = @@ -472,8 +707,6 @@ proc updatePositions(self: ChessBoard, move: Move) = ## Internal helper to update the position of ## the pieces on the board after a move - # Empty out the starting square - self.grid[move.startSquare.row, move.startSquare.col] = emptyPiece() let capture = self.getCapture(move) if capture != emptyLocation(): # Move has captured a piece: remove the destination square's piece as well. @@ -505,7 +738,7 @@ proc updatePositions(self: ChessBoard, move: Move) = of King: self.pieces.white.king = move.targetSquare else: - discard + discard of Black: case move.piece.kind: of Pawn: @@ -528,7 +761,9 @@ proc updatePositions(self: ChessBoard, move: Move) = else: discard else: - discard + discard + # 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 @@ -542,8 +777,8 @@ proc doMove(self: ChessBoard, move: Move) = self.updateAttackedSquares() # En passant is possible only immediately after the # pawn has moved - if self.enPassantSquare != emptyMove and self.enPassantSquare.piece.color == self.turn.opposite(): - self.enPassantSquare = emptyMove + if self.enPassantSquare != emptyMove() and self.enPassantSquare.piece.color == self.turn.opposite(): + self.enPassantSquare = emptyMove() self.turn = self.turn.opposite() @@ -554,12 +789,12 @@ proc checkMove(self: ChessBoard, startSquare, targetSquare: string): Move = # Start square doesn't contain a piece (and it isn't the en passant square) # or it is of the wrong color for which turn it is to move if pieceToMove.kind == Empty or pieceToMove.color != self.turn: - return emptyMove + return emptyMove() var destination = self.getPiece(targetSquare) # Destination square is occupied by a piece of the same color as the piece # being moved: illegal! if destination.kind != Empty and destination.color == self.turn: - return emptyMove + return emptyMove() var startLocation = startSquare.algebraicToPosition() targetLocation = targetSquare.algebraicToPosition() @@ -568,7 +803,7 @@ proc checkMove(self: ChessBoard, startSquare, targetSquare: string): Move = # Piece cannot arrive to destination (either # because it is blocked or because the moving # pattern is incorrect) - return emptyMove + return emptyMove() # TODO: Check for checks and pins (moves are currently pseudo-legal) @@ -583,7 +818,7 @@ proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.disc ## too) and the locations will both be set to the tuple (-1, -1) result = self.checkMove(startSquare, targetSquare) - if result == emptyMove: + if result == emptyMove(): return self.doMove(result) diff --git a/src/Chess/player.nim b/src/Chess/player.nim index 3472564..abc5075 100644 --- a/src/Chess/player.nim +++ b/src/Chess/player.nim @@ -1,14 +1,5 @@ import board as chess -# En passant my beloved var board = newDefaultChessboard() -board.makeMove("a2", "a4") -echo board.pretty() -board.makeMove("h7", "h5") -echo board.pretty() -board.makeMove("a4", "a5") -echo board.pretty() -board.makeMove("b7", "b5") -echo board.pretty() -board.makeMove("a5", "b6") + echo board.pretty()