diff --git a/src/Chess/board.nim b/src/Chess/board.nim index 0f91847..ed901da 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -49,7 +49,7 @@ type kind*: PieceKind - Move* = object + Move* = ref object ## A chess move piece*: Piece startSquare*: Location @@ -88,10 +88,13 @@ for _ in countup(0, 63): func emptyPiece*: Piece {.inline.} = Piece(kind: Empty, color: None) func emptyLocation*: Location {.inline.} = (-1 , -1) -func emptyMove*: Move {.inline.} = Move(startSquare: emptyLocation(), targetSquare: emptyLocation(), piece: emptyPiece()) 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 + +var emptyMove = Move(startSquare: emptyLocation(), targetSquare: emptyLocation(), piece: emptyPiece()) + proc newChessboard: ChessBoard = @@ -100,7 +103,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 @@ -339,11 +342,27 @@ 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. 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 (self.enPassantSquare.targetSquare.row + 1, + self.enPassantSquare.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 - let target = self.grid[move.targetSquare.row, move.targetSquare.col] - return target.color == move.piece.color.opposite() + return self.getCapture(move) != emptyLocation() proc testMoveOffsets(self: ChessBoard, move: Move): bool = @@ -360,13 +379,13 @@ 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) # En passant is possible, check if the destination is # its target square - elif self.enPassantSquare.targetSquare != move.targetSquare: + if self.enPassantSquare.targetSquare != move.targetSquare: # We still need to check for captures even if en passant # is possible return self.isCapture(move) @@ -383,12 +402,15 @@ proc testMoveOffsets(self: ChessBoard, move: Move): bool = return false if rows == 2: # Check if double pawn pushing is possible (only the first - # move) + # move for each pawn) let startRow = if move.piece.color == White: 6 else: 1 if move.startSquare.row != startRow: # 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 else: return false @@ -408,6 +430,7 @@ proc removePiece(self: ChessBoard, location: Location) = ## Removes a piece from the board, updating necessary ## metadata var piece = self.grid[location.row, location.col] + self.grid[location.row, location.col] = emptyPiece() case piece.color: of White: case piece.kind: @@ -451,12 +474,13 @@ proc updatePositions(self: ChessBoard, move: Move) = # Empty out the starting square self.grid[move.startSquare.row, move.startSquare.col] = emptyPiece() - if self.isCapture(move): - # Move has captured a piece: remove the destination square' piece as well. + 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 - self.removePiece(move.targetSquare) + self.removePiece(capture) # Update the positional metadata of the moving piece case move.piece.color: of White: @@ -514,42 +538,50 @@ proc doMove(self: ChessBoard, move: Move) = ## a move is already known to be legal self.updatePositions(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 self.turn = self.turn.opposite() + proc checkMove(self: ChessBoard, startSquare, targetSquare: string): Move = ## Internal function called by makeMove to check a move for legality var pieceToMove = self.getPiece(startSquare) - # Start square doesn't contain a piece or it is of the wrong color for which - # turn it is to 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() + targetLocation = targetSquare.algebraicToPosition() result = Move(startSquare: startLocation, targetSquare: targetLocation, piece: pieceToMove) if not self.testMoveOffsets(result): - # Piece cannot move in this direction - return emptyMove() + # Piece cannot arrive to destination (either + # because it is blocked or because the moving + # pattern is incorrect) + return emptyMove # TODO: Check for checks and pins (moves are currently pseudo-legal) -proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move = +proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.discardable.} = ## Makes a move on the board from the chosen start square to ## the chosen target square, ensuring it is legal (turns are - ## taken into account). This function returns a Move object: if the move + ## taken into account!). This function returns a Move object: if the move ## is legal and has been performed, the fields will be populated properly. ## For efficiency purposes, no exceptions are raised if the move is ## illegal, but the move's piece kind will be Empty (its color will be None ## 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) @@ -561,7 +593,7 @@ proc `$`*(self: ChessBoard): string = result &= "\n" for piece in row: if piece.kind == Empty: - result &= " " + result &= "x " continue if piece.color == White: result &= &"{char(piece.kind).toUpperAscii()} " @@ -572,6 +604,33 @@ proc `$`*(self: ChessBoard): string = result &= "\na b c d e f g h" +proc pretty*(self: ChessBoard): string = + ## Returns a colorized version of the + ## board for easier visualization + result &= "- - - - - - - -" + + for i, row in self.grid: + result &= "\n" + for j, piece in row: + if piece.kind == Empty: + result &= "\x1b[36;1mx" + # Avoids the color overflowing + # onto the numbers + if j < 7: + result &= " \x1b[0m" + else: + result &= "\x1b[0m " + continue + if piece.color == White: + result &= &"\x1b[37;1m{char(piece.kind).toUpperAscii()}\x1b[0m " + else: + result &= &"\x1b[30;1m{char(piece.kind)} " + result &= &"\x1b[33;1m{rankToColumn(i + 1) + 1}\x1b[0m" + + result &= "\n- - - - - - - -" + result &= "\n\x1b[31;1ma b c d e f g h" + result &= "\x1b[0m" + when isMainModule: proc testPiece(piece: Piece, kind: PieceKind, color: PieceColor) = diff --git a/src/Chess/player.nim b/src/Chess/player.nim index bf56e7a..3472564 100644 --- a/src/Chess/player.nim +++ b/src/Chess/player.nim @@ -1,8 +1,14 @@ import board as chess - +# En passant my beloved var board = newDefaultChessboard() -discard board.makeMove("a2", "a4") -discard board.makeMove("b7", "b5") -discard board.makeMove("a4", "b5") - +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()