diff --git a/src/Chess/board.nim b/src/Chess/board.nim index 87e5b2d..9a89b7d 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -100,7 +100,6 @@ type ## A chess board object grid: Matrix[Piece] position: Position - positionIndex: int # List of reached positions positions: seq[Position] @@ -114,7 +113,7 @@ for _ in countup(0, 63): 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.} +proc algebraicToLocation*(s: string): Location {.inline.} proc getCapture*(self: ChessBoard, move: Move): Location proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move proc makeMove*(self: ChessBoard, move: Move): Move @@ -208,6 +207,11 @@ proc getCastlingInformation*(self: ChessBoard): tuple[queen, king: bool] = discard +proc getEnPassantTarget*(self: ChessBoard): Location = + ## Returns the current en passant target square + return self.position.enPassantSquare.targetSquare + + func getStartRow(piece: Piece): int {.inline.} = ## Retrieves the starting row of ## the given piece inside our 8x8 @@ -251,7 +255,6 @@ proc newChessboard: ChessBoard = move: emptyMove(), turn: White) - result.positionIndex = 0 proc newChessboardFromFEN*(state: string): ChessBoard = @@ -371,7 +374,7 @@ proc newChessboardFromFEN*(state: string): ChessBoard = # Field is already uninitialized to the correct state discard else: - result.position.enPassantSquare.targetSquare = state[index..index+1].algebraicToPosition() + result.position.enPassantSquare.targetSquare = state[index..index+1].algebraicToLocation() # Just for cleanliness purposes, we fill in the other metadata as # well result.position.enPassantSquare.piece.color = result.getActiveColor() @@ -464,7 +467,7 @@ func rankToColumn(rank: int): int = return indeces[rank - 1] -proc algebraicToPosition*(s: string): Location {.inline.} = +proc algebraicToLocation*(s: string): Location = ## Converts a square location from algebraic ## notation to its corresponding row and column ## in the chess grid (0 indexed) @@ -483,10 +486,16 @@ proc algebraicToPosition*(s: string): Location {.inline.} = return (rank, file) +proc locationToAlgebraic*(loc: Location): string = + ## Converts a location from our internal row, column + ## notation to a square in algebraic notation + return &"{char(uint8(loc.col) + uint8('a'))}{char(uint8(loc.row) + uint8('0'))}" + + proc getPiece*(self: ChessBoard, square: string): Piece = ## Gets the piece on the given square ## in algebraic notation - let loc = square.algebraicToPosition() + let loc = square.algebraicToLocation() return self.grid[loc.row, loc.col] @@ -570,7 +579,7 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = 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 moves for the sliding piece in the given location @@ -695,7 +704,7 @@ proc generateMoves(self: ChessBoard, location: Location): seq[Move] = proc getAttackers*(self: ChessBoard, square: string): seq[Piece] = ## Returns all the attackers of the given square - let loc = square.algebraicToPosition() + let loc = square.algebraicToLocation() for move in self.position.attacked.black: if move.targetSquare == loc: result.add(move.piece) @@ -707,7 +716,7 @@ proc getAttackers*(self: ChessBoard, square: string): seq[Piece] = proc getAttackersFor*(self: ChessBoard, square: string, color: PieceColor): seq[Piece] = ## Returns all the attackers of the given square ## for the given color - let loc = square.algebraicToPosition() + let loc = square.algebraicToLocation() case color: of White: for move in self.position.attacked.black: @@ -755,7 +764,7 @@ proc isAttacked*(self: ChessBoard, loc: Location): bool = proc isAttacked*(self: ChessBoard, square: string): bool = ## Returns whether the given square is attacked ## by its opponent - return self.isAttacked(square.algebraicToPosition()) + return self.isAttacked(square.algebraicToLocation()) proc updateAttackedSquares(self: ChessBoard) = @@ -850,14 +859,6 @@ proc removePiece(self: ChessBoard, location: Location) = discard -proc handleCapture(self: ChessBoard, move: Move) = - ## Handles capturing (assumes the move is valid) - let targetPiece = self.grid[move.targetSquare.row, move.targetSquare.col] - assert self.position.captured == emptyPiece(), "capture: last capture is non-empty" - self.position.captured = move.piece - self.removePiece(move.targetSquare) - - proc movePiece(self: ChessBoard, move: Move) = ## Internal helper to move a piece. Does ## not update attacked squares, just position @@ -925,11 +926,7 @@ proc updatePositions(self: ChessBoard, move: Move) = ## the pieces on the board after a move let capture = self.getCapture(move) if capture != emptyLocation(): - # 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.handleCapture(move) + self.position.captured = self.grid[capture.row, capture.col] # Update the positional metadata of the moving piece self.movePiece(move) @@ -970,10 +967,6 @@ proc doMove(self: ChessBoard, move: Move) = self.position.halfMoveClock = 0 if (self.position.halfMoveClock and 1) == 0: # Equivalent to (x mod 2) == 0, just much faster inc(self.position.fullMoveCount) - # En passant is possible only immediately after the - # pawn has moved - if self.position.enPassantSquare != emptyMove() and self.position.enPassantSquare.piece.color == self.getActiveColor().opposite(): - self.position.enPassantSquare = emptyMove() # TODO: Castling self.position.move = move @@ -987,16 +980,24 @@ proc doMove(self: ChessBoard, move: Move) = # Create new position with var newPos = Position(plyFromRoot: self.position.plyFromRoot + 1, captured: emptyPiece(), - turn: self.position.turn.opposite(), + turn: self.getActiveColor().opposite, # Inherit values from current position # (they are already up to date by this point) castling: self.position.castling, - enPassantSquare: self.position.enPassantSquare, - attacked: (@[], @[]) + attacked: self.position.attacked, + # Updated at the next call to doMove() + move: emptyMove(), + pieces: self.position.pieces, ) + # 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, + startSquare: (move.startSquare.row, move.startSquare.col), + targetSquare: move.targetSquare + move.piece.bottomSide()) + else: + newPos.enPassantSquare = emptyMove() self.position = newPos - inc(self.positionIndex) proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) = @@ -1042,43 +1043,41 @@ proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) = proc undoLastMove*(self: ChessBoard): Move {.discardable.} = - ## Undoes the last move, restoring any captured pieces, - ## as well castling and en passant status. If there are - ## no moves to undo, this is a no-op. Returns the move - ## that was performed (which may be an empty move) + ## 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) result = emptyMove() if self.positions.len() == 0: return - let positionIndex = max(0, self.positionIndex - 1) - if positionIndex in 0..self.positions.high(): - self.positionIndex = positionIndex - self.position = self.positions[positionIndex] - let - currentMove = self.position.move - oppositeMove = Move(piece: currentMove.piece, targetSquare: currentMove.startSquare, startSquare: currentMove.targetSquare) - self.spawnPiece(currentMove.startSquare, currentMove.piece) - if self.position.captured != emptyPiece(): - self.spawnPiece(self.position.move.targetSquare, self.position.captured) + var + previous = self.positions[^1] + oppositeMove = Move(piece: previous.move.piece, targetSquare: previous.move.startSquare, startSquare: previous.move.targetSquare) + self.removePiece(previous.move.startSquare) + self.position = previous + if previous.move != emptyMove(): + self.spawnPiece(previous.move.startSquare, previous.move.piece) self.updateAttackedSquares() self.updatePositions(oppositeMove) - return self.position.move + if previous.captured != emptyPiece(): + self.spawnPiece(previous.move.targetSquare, previous.captured) + discard self.positions.pop() + return self.position.move +proc isLegal(self: ChessBoard, move: Move): bool = + ## Returns whether the given move is legal -proc checkMove(self: ChessBoard, move: Move): bool = - ## Internal function called by makeMove to check a move for legality # 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 move.piece.kind == Empty or move.piece.color != self.getActiveColor(): + # or it's not this player's turn to move + if (move.piece.kind == Empty and move.targetSquare != self.getEnPassantTarget()) or move.piece.color != self.getActiveColor(): return false var destination = self.grid[move.targetSquare.row, move.targetSquare.col] - # Destination square is occupied by a piece of the same color as the piece - # being moved: illegal! + # Destination square is occupied by a friendly piece if destination.kind != Empty and destination.color == self.getActiveColor(): return false if move notin self.generateMoves(move.startSquare): # Piece cannot arrive to destination (blocked, - # pinned, or otherwise invalid move) + # pinned or otherwise invalid move) return false self.doMove(move) defer: self.undoLastMove() @@ -1093,12 +1092,11 @@ proc checkMove(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.checkMove(move): + if not self.isLegal(move): return emptyMove() self.doMove(result) - 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 @@ -1108,8 +1106,8 @@ proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.disc ## 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) var - startLocation = startSquare.algebraicToPosition() - targetLocation = targetSquare.algebraicToPosition() + startLocation = startSquare.algebraicToLocation() + targetLocation = targetSquare.algebraicToLocation() result = Move(startSquare: startLocation, targetSquare: targetLocation, piece: self.grid[startLocation.row, startLocation.col]) return self.makeMove(result) @@ -1120,8 +1118,6 @@ proc makeMove*(self: ChessBoard, startSquare, targetSquare: Location): Move {.di return self.makeMove(result) - - proc `$`*(self: ChessBoard): string = result &= "- - - - - - - -" for i, row in self.grid: diff --git a/src/Chess/player.nim b/src/Chess/player.nim index 4420f91..f11ef01 100644 --- a/src/Chess/player.nim +++ b/src/Chess/player.nim @@ -9,7 +9,7 @@ import std/strutils when isMainModule: setControlCHook(proc () {.noconv.} = echo ""; quit(0)) var - board = newChessboardFromFEN("rnbqkbnr/8/8/8/8/8/8/RNBQKBNR w KQkq - 0 1") + board = newChessboardFromFEN("rnbqkbnr/2p/8/8/8/8/P7/RNBQKBNR w KQkq - 0 1") data: string move: Move @@ -17,9 +17,17 @@ when isMainModule: while true: echo &"{board.pretty()}" echo &"Turn: {board.getActiveColor()}" + stdout.write(&"En passant target: ") + if board.getEnPassantTarget() != emptyLocation(): + echo board.getEnPassantTarget() + else: + echo "None" + stdout.write(&"Check: ") if board.inCheck(): - echo &"Check!" - stdout.write("Move (from, to) -> ") + echo &"Yes" + else: + echo "No" + stdout.write("\nMove -> ") try: data = readLine(stdin).strip(chars={'\0', ' '}) except IOError: