diff --git a/src/Chess/board.nim b/src/Chess/board.nim index 087a3f5..87e5b2d 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -21,11 +21,12 @@ import std/strformat type # Useful type aliases - Location = tuple[row, col: int] + Location* = tuple[row, col: int] Pieces = tuple[king: Location, queens: seq[Location], rooks: seq[Location], bishops: seq[Location], knights: seq[Location], pawns: seq[Location]] + Castling* = tuple[white, black: tuple[queen, king: bool]] PieceColor* = enum ## A piece color enumeration @@ -65,11 +66,15 @@ type targetSquare*: Location flag*: MoveFlag - ChessBoard* = ref object - ## A chess board object - grid: Matrix[Piece] - # Currently active color - turn: PieceColor + Position* = ref object + ## A chess position + move: Move + # Stores castling metadata + castling: Castling + # Number of half-moves that were performed + # to reach this position starting from the + # root of the tree + plyFromRoot: int # Number of half moves since # last piece capture or pawn movement. # Used for the 50-move rule @@ -77,8 +82,6 @@ type # Full move counter. Increments # every 2 ply fullMoveCount: int - # Stores metadata for castling. - castling: tuple[white, black: tuple[queen, king: bool]] # 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 @@ -87,6 +90,19 @@ type pieces: tuple[white: Pieces, black: Pieces] # Potential attacking moves for black and white attacked: tuple[white: seq[Move], black: seq[Move]] + # Has any piece been captured to reach this position? + captured: Piece + # Active color + turn: PieceColor + + + ChessBoard* = ref object + ## A chess board object + grid: Matrix[Piece] + position: Position + positionIndex: int + # List of reached positions + positions: seq[Position] # Initialized only once, copied every time @@ -177,7 +193,19 @@ func topRightKnightMove(piece: Piece, long: bool = true): Location {.inline.} = proc getActiveColor*(self: ChessBoard): PieceColor = ## Returns the currently active color ## (turn of who has to move) - return self.turn + return self.position.turn + + +proc getCastlingInformation*(self: ChessBoard): tuple[queen, king: bool] = + ## Returns whether castling is possible + ## for the given color + case self.getActiveColor(): + of White: + return self.position.castling.white + of Black: + return self.position.castling.black + else: + discard func getStartRow(piece: Piece): int {.inline.} = @@ -218,9 +246,12 @@ proc newChessboard: ChessBoard = new(result) # Turns our flat sequence into an 8x8 grid result.grid = newMatrixFromSeq[Piece](empty, (8, 8)) - result.attacked = (@[], @[]) - result.enPassantSquare = emptyMove() - result.turn = White + result.position = Position(attacked: (@[], @[]), + enPassantSquare: emptyMove(), + move: emptyMove(), + turn: White) + + result.positionIndex = 0 proc newChessboardFromFEN*(state: string): ChessBoard = @@ -261,33 +292,33 @@ proc newChessboardFromFEN*(state: string): ChessBoard = of Black: case piece.kind: of Pawn: - result.pieces.black.pawns.add((row, column)) + result.position.pieces.black.pawns.add((row, column)) of Bishop: - result.pieces.black.bishops.add((row, column)) + result.position.pieces.black.bishops.add((row, column)) of Knight: - result.pieces.black.knights.add((row, column)) + result.position.pieces.black.knights.add((row, column)) of Rook: - result.pieces.black.rooks.add((row, column)) + result.position.pieces.black.rooks.add((row, column)) of Queen: - result.pieces.black.queens.add((row, column)) + result.position.pieces.black.queens.add((row, column)) of King: - result.pieces.black.king = (row, column) + result.position.pieces.black.king = (row, column) else: discard of White: case piece.kind: of Pawn: - result.pieces.white.pawns.add((row, column)) + result.position.pieces.white.pawns.add((row, column)) of Bishop: - result.pieces.white.bishops.add((row, column)) + result.position.pieces.white.bishops.add((row, column)) of Knight: - result.pieces.white.knights.add((row, column)) + result.position.pieces.white.knights.add((row, column)) of Rook: - result.pieces.white.rooks.add((row, column)) + result.position.pieces.white.rooks.add((row, column)) of Queen: - result.pieces.white.queens.add((row, column)) + result.position.pieces.white.queens.add((row, column)) of King: - result.pieces.white.king = (row, column) + result.position.pieces.white.king = (row, column) else: discard else: @@ -310,9 +341,9 @@ proc newChessboardFromFEN*(state: string): ChessBoard = # Active color case c: of 'w': - result.turn = White + result.position.turn = White of 'b': - result.turn = Black + result.position.turn = Black else: raise newException(ValueError, "invalid active color identifier in FEN string") of 2: @@ -324,13 +355,13 @@ proc newChessboardFromFEN*(state: string): ChessBoard = # by default discard of 'K': - result.castling.white.king = true + result.position.castling.white.king = true of 'Q': - result.castling.white.queen = true + result.position.castling.white.queen = true of 'k': - result.castling.black.king = true + result.position.castling.black.king = true of 'q': - result.castling.black.queen = true + result.position.castling.black.queen = true else: raise newException(ValueError, "invalid castling availability in FEN string") of 3: @@ -340,11 +371,11 @@ proc newChessboardFromFEN*(state: string): ChessBoard = # Field is already uninitialized to the correct state discard else: - result.enPassantSquare.targetSquare = state[index..index+1].algebraicToPosition() - # Just for cleanliness purposes, we fill in the other positional metadata as + result.position.enPassantSquare.targetSquare = state[index..index+1].algebraicToPosition() + # Just for cleanliness purposes, we fill in the other metadata as # well - result.enPassantSquare.piece.color = if result.turn == Black: White else: Black - result.enPassantSquare.piece.kind = Pawn + result.position.enPassantSquare.piece.color = result.getActiveColor() + result.position.enPassantSquare.piece.kind = Pawn # Square metadata is 2 bytes long inc(index) of 4: @@ -356,14 +387,14 @@ proc newChessboardFromFEN*(state: string): ChessBoard = # Backtrack so the space is seen by the # next iteration of the loop dec(index) - result.halfMoveClock = parseInt(s) + result.position.halfMoveClock = parseInt(s) of 5: # Fullmove number var s = "" while index <= state.high(): s.add(state[index]) inc(index) - result.fullMoveCount = parseInt(s) + result.position.fullMoveCount = parseInt(s) else: raise newException(ValueError, "too many fields in FEN string") inc(index) @@ -382,15 +413,15 @@ proc countPieces*(self: ChessBoard, kind: PieceKind, color: PieceColor): int = of White: case kind: of Pawn: - return self.pieces.white.pawns.len() + return self.position.pieces.white.pawns.len() of Bishop: - return self.pieces.white.bishops.len() + return self.position.pieces.white.bishops.len() of Knight: - return self.pieces.white.knights.len() + return self.position.pieces.white.knights.len() of Rook: - return self.pieces.white.rooks.len() + return self.position.pieces.white.rooks.len() of Queen: - return self.pieces.white.queens.len() + return self.position.pieces.white.queens.len() of King: # There shall be only one, forever return 1 @@ -399,15 +430,15 @@ proc countPieces*(self: ChessBoard, kind: PieceKind, color: PieceColor): int = of Black: case kind: of Pawn: - return self.pieces.black.pawns.len() + return self.position.pieces.black.pawns.len() of Bishop: - return self.pieces.black.bishops.len() + return self.position.pieces.black.bishops.len() of Knight: - return self.pieces.black.knights.len() + return self.position.pieces.black.knights.len() of Rook: - return self.pieces.black.rooks.len() + return self.position.pieces.black.rooks.len() of Queen: - return self.pieces.black.queens.len() + return self.position.pieces.black.queens.len() of King: # In perpetuity return 1 @@ -468,7 +499,7 @@ proc getCapture*(self: ChessBoard, move: Move): Location = result = emptyLocation() let target = self.grid[move.targetSquare.row, move.targetSquare.col] if target.color == None: - if move.targetSquare != self.enPassantSquare.targetSquare: + if move.targetSquare != self.position.enPassantSquare.targetSquare: return else: return ((if move.piece.color == White: move.targetSquare.row + 1 else: move.targetSquare.row - 1), move.targetSquare.col) @@ -497,10 +528,10 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = # 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: + if self.position.enPassantSquare.piece.color == piece.color.opposite: + if abs(self.position.enPassantSquare.targetSquare.col - location.col) == 1 and abs(self.position.enPassantSquare.targetSquare.row - location.row) == 1: # Only viable if the piece is on the diagonal of the target - locations.add(self.enPassantSquare.targetSquare) + locations.add(self.position.enPassantSquare.targetSquare) # They can also move on either diagonal one # square, but only to capture if location.col in 1..6: @@ -665,10 +696,10 @@ 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() - for move in self.attacked.black: + for move in self.position.attacked.black: if move.targetSquare == loc: result.add(move.piece) - for move in self.attacked.white: + for move in self.position.attacked.white: if move.targetSquare == loc: result.add(move.piece) @@ -679,11 +710,11 @@ proc getAttackersFor*(self: ChessBoard, square: string, color: PieceColor): seq[ let loc = square.algebraicToPosition() case color: of White: - for move in self.attacked.black: + for move in self.position.attacked.black: if move.targetSquare == loc: result.add(move.piece) of Black: - for move in self.attacked.white: + for move in self.position.attacked.white: if move.targetSquare == loc: result.add(move.piece) else: @@ -700,21 +731,21 @@ proc isAttacked*(self: ChessBoard, loc: Location): bool = let piece = self.grid[loc.row, loc.col] case piece.color: of White: - for move in self.attacked.black: + for move in self.position.attacked.black: if move.targetSquare == loc: return true of Black: - for move in self.attacked.white: + for move in self.position.attacked.white: if move.targetSquare == loc: return true of None: - case self.turn: + case self.getActiveColor(): of White: - for move in self.attacked.black: + for move in self.position.attacked.black: if move.targetSquare == loc: return true of Black: - for move in self.attacked.white: + for move in self.position.attacked.white: if move.targetSquare == loc: return true else: @@ -735,46 +766,46 @@ proc updateAttackedSquares(self: ChessBoard) = # 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) + self.position.attacked.white.setLen(0) + self.position.attacked.black.setLen(0) # Go over each piece one by one and see which squares # it currently attacks # Pawns - for loc in self.pieces.white.pawns: + for loc in self.position.pieces.white.pawns: for move in self.generateMoves(loc): - self.attacked.white.add(move) + self.position.attacked.white.add(move) # Bishops - for loc in self.pieces.white.bishops: + for loc in self.position.pieces.white.bishops: for move in self.generateMoves(loc): - self.attacked.white.add(move) - # rooks - for loc in self.pieces.white.rooks: + self.position.attacked.white.add(move) + # Rooks + for loc in self.position.pieces.white.rooks: for move in self.generateMoves(loc): - self.attacked.white.add(move) + self.position.attacked.white.add(move) # Queens - for loc in self.pieces.white.queens: + for loc in self.position.pieces.white.queens: for move in self.generateMoves(loc): - self.attacked.white.add(move) + self.position.attacked.white.add(move) # King - for move in self.generateMoves(self.pieces.white.king): - self.attacked.white.add(move) + for move in self.generateMoves(self.position.pieces.white.king): + self.position.attacked.white.add(move) # Same for black - for loc in self.pieces.black.pawns: + for loc in self.position.pieces.black.pawns: for move in self.generateMoves(loc): - self.attacked.black.add(move) - for loc in self.pieces.black.bishops: + self.position.attacked.black.add(move) + for loc in self.position.pieces.black.bishops: for move in self.generateMoves(loc): - self.attacked.black.add(move) - for loc in self.pieces.black.rooks: + self.position.attacked.black.add(move) + for loc in self.position.pieces.black.rooks: for move in self.generateMoves(loc): - self.attacked.black.add(move) - for loc in self.pieces.black.queens: + self.position.attacked.black.add(move) + for loc in self.position.pieces.black.queens: for move in self.generateMoves(loc): - self.attacked.black.add(move) - for move in self.generateMoves(self.pieces.black.king): - self.attacked.black.add(move) + self.position.attacked.black.add(move) + for move in self.generateMoves(self.position.pieces.black.king): + self.position.attacked.black.add(move) proc removePiece(self: ChessBoard, location: Location) = @@ -786,15 +817,15 @@ proc removePiece(self: ChessBoard, location: Location) = of White: case piece.kind: of Pawn: - self.pieces.white.pawns.delete(self.pieces.white.pawns.find(location)) + self.position.pieces.white.pawns.delete(self.position.pieces.white.pawns.find(location)) of Bishop: - self.pieces.white.pawns.delete(self.pieces.white.bishops.find(location)) + self.position.pieces.white.pawns.delete(self.position.pieces.white.bishops.find(location)) of Knight: - self.pieces.white.pawns.delete(self.pieces.white.knights.find(location)) + self.position.pieces.white.pawns.delete(self.position.pieces.white.knights.find(location)) of Rook: - self.pieces.white.rooks.delete(self.pieces.white.rooks.find(location)) + self.position.pieces.white.rooks.delete(self.position.pieces.white.rooks.find(location)) of Queen: - self.pieces.white.queens.delete(self.pieces.white.queens.find(location)) + self.position.pieces.white.queens.delete(self.position.pieces.white.queens.find(location)) of King: doAssert false, "removePiece: attempted to remove the white king" else: @@ -802,15 +833,15 @@ proc removePiece(self: ChessBoard, location: Location) = of Black: case piece.kind: of Pawn: - self.pieces.black.pawns.delete(self.pieces.white.pawns.find(location)) + self.position.pieces.black.pawns.delete(self.position.pieces.white.pawns.find(location)) of Bishop: - self.pieces.black.bishops.delete(self.pieces.black.bishops.find(location)) + self.position.pieces.black.bishops.delete(self.position.pieces.black.bishops.find(location)) of Knight: - self.pieces.black.knights.delete(self.pieces.black.knights.find(location)) + self.position.pieces.black.knights.delete(self.position.pieces.black.knights.find(location)) of Rook: - self.pieces.black.rooks.delete(self.pieces.black.rooks.find(location)) + self.position.pieces.black.rooks.delete(self.position.pieces.black.rooks.find(location)) of Queen: - self.pieces.black.queens.delete(self.pieces.black.queens.find(location)) + self.position.pieces.black.queens.delete(self.position.pieces.black.queens.find(location)) of King: doAssert false, "removePiece: attempted to remove the black king" else: @@ -819,6 +850,76 @@ 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 + ## metadata and the grid itself + case move.piece.color: + of White: + case move.piece.kind: + of Pawn: + # The way things are structured, we don't care about the order + # of this list, so we can add and remove entries as we please + self.position.pieces.white.pawns.delete(self.position.pieces.white.pawns.find(move.startSquare)) + self.position.pieces.white.pawns.add(move.targetSquare) + of Bishop: + self.position.pieces.white.bishops.delete(self.position.pieces.white.bishops.find(move.startSquare)) + self.position.pieces.white.bishops.add(move.targetSquare) + of Knight: + self.position.pieces.white.knights.delete(self.position.pieces.white.knights.find(move.startSquare)) + self.position.pieces.white.knights.add(move.targetSquare) + of Rook: + self.position.pieces.white.rooks.delete(self.position.pieces.white.rooks.find(move.startSquare)) + self.position.pieces.white.rooks.add(move.targetSquare) + of Queen: + try: + self.position.pieces.white.queens.delete(self.position.pieces.white.queens.find(move.startSquare)) + except: + echo self.position.pieces.white.queens + echo move.startSquare + raise getCurrentException() + self.position.pieces.white.queens.add(move.targetSquare) + of King: + self.position.pieces.white.king = move.targetSquare + else: + discard + of Black: + case move.piece.kind: + of Pawn: + self.position.pieces.black.pawns.delete(self.position.pieces.black.pawns.find(move.startSquare)) + self.position.pieces.black.pawns.add(move.targetSquare) + of Bishop: + self.position.pieces.black.bishops.delete(self.position.pieces.black.bishops.find(move.startSquare)) + self.position.pieces.black.bishops.add(move.targetSquare) + of Knight: + self.position.pieces.black.knights.delete(self.position.pieces.black.knights.find(move.startSquare)) + self.position.pieces.black.knights.add(move.targetSquare) + of Rook: + self.position.pieces.black.rooks.delete(self.position.pieces.black.rooks.find(move.startSquare)) + self.position.pieces.black.rooks.add(move.targetSquare) + of Queen: + self.position.pieces.black.queens.delete(self.position.pieces.black.queens.find(move.startSquare)) + self.position.pieces.black.queens.add(move.targetSquare) + of King: + self.position.pieces.black.king = move.targetSquare + else: + discard + else: + 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 + + proc updatePositions(self: ChessBoard, move: Move) = ## Internal helper to update the position of ## the pieces on the board after a move @@ -828,59 +929,9 @@ proc updatePositions(self: ChessBoard, move: Move) = # 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) + self.handleCapture(move) # Update the positional metadata of the moving piece - case move.piece.color: - of White: - case move.piece.kind: - of Pawn: - # The way things are structured, we don't care about the order - # of this list, so we can add and remove entries as we please - self.pieces.white.pawns.delete(self.pieces.white.pawns.find(move.startSquare)) - self.pieces.white.pawns.add(move.targetSquare) - of Bishop: - self.pieces.white.bishops.delete(self.pieces.white.bishops.find(move.startSquare)) - self.pieces.white.bishops.add(move.targetSquare) - of Knight: - self.pieces.white.knights.delete(self.pieces.white.knights.find(move.startSquare)) - self.pieces.white.knights.add(move.targetSquare) - of Rook: - self.pieces.white.rooks.delete(self.pieces.white.rooks.find(move.startSquare)) - self.pieces.white.rooks.add(move.targetSquare) - of Queen: - self.pieces.white.queens.delete(self.pieces.white.queens.find(move.startSquare)) - self.pieces.white.queens.add(move.targetSquare) - of King: - self.pieces.white.king = move.targetSquare - else: - discard - of Black: - case move.piece.kind: - of Pawn: - self.pieces.black.pawns.delete(self.pieces.black.pawns.find(move.startSquare)) - self.pieces.black.pawns.add(move.targetSquare) - of Bishop: - self.pieces.black.bishops.delete(self.pieces.black.bishops.find(move.startSquare)) - self.pieces.black.bishops.add(move.targetSquare) - of Knight: - self.pieces.black.knights.delete(self.pieces.black.knights.find(move.startSquare)) - self.pieces.black.knights.add(move.targetSquare) - of Rook: - self.pieces.black.rooks.delete(self.pieces.black.rooks.find(move.startSquare)) - self.pieces.black.rooks.add(move.targetSquare) - of Queen: - self.pieces.black.queens.delete(self.pieces.black.queens.find(move.startSquare)) - self.pieces.black.queens.add(move.targetSquare) - of King: - self.pieces.black.king = move.targetSquare - else: - discard - else: - 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 + self.movePiece(move) proc inCheck*(self: ChessBoard, color: PieceColor = None): bool = @@ -890,15 +941,15 @@ proc inCheck*(self: ChessBoard, color: PieceColor = None): bool = ## for the active color's king case color: of White: - return self.isAttacked(self.pieces.white.king) + return self.isAttacked(self.position.pieces.white.king) of Black: - return self.isAttacked(self.pieces.black.king) + return self.isAttacked(self.position.pieces.black.king) of None: - case self.turn: + case self.getActiveColor(): of White: - return self.isAttacked(self.pieces.white.king) + return self.isAttacked(self.position.pieces.white.king) of Black: - return self.isAttacked(self.pieces.black.king) + return self.isAttacked(self.position.pieces.black.king) else: # Unreachable discard @@ -909,46 +960,128 @@ proc doMove(self: ChessBoard, move: Move) = ## performing legality checks on the given move. Can ## be used in performance-critical paths where ## a move is already known to be legal - self.updatePositions(move) - self.updateAttackedSquares() + + # Final checks + + # Needed to detect draw by the 50 move rule + if move.piece.kind != Pawn and not self.isCapture(move): + inc(self.position.halfMoveClock) + else: + 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.enPassantSquare != emptyMove() and self.enPassantSquare.piece.color == self.turn.opposite(): - self.enPassantSquare = emptyMove() - self.turn = self.turn.opposite() + if self.position.enPassantSquare != emptyMove() and self.position.enPassantSquare.piece.color == self.getActiveColor().opposite(): + self.position.enPassantSquare = emptyMove() + # TODO: Castling + + self.position.move = move + + # Update position and attack metadata + self.updatePositions(move) + self.updateAttackedSquares() + + # Record final position for future reference + self.positions.add(self.position) + # Create new position with + var newPos = Position(plyFromRoot: self.position.plyFromRoot + 1, + captured: emptyPiece(), + turn: self.position.turn.opposite(), + # Inherit values from current position + # (they are already up to date by this point) + castling: self.position.castling, + enPassantSquare: self.position.enPassantSquare, + attacked: (@[], @[]) + ) + + self.position = newPos + inc(self.positionIndex) +proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) = + ## Internal helper to "spawn" a given piece at the given + ## location. Note that this will overwrite whatever piece + ## was previously located there: use with caution. Does + ## not automatically update the attacked square metadata + ## or other positional information + doAssert piece.kind != King, "spawnPiece: cannot spawn a king" + case piece.color: + of White: + case piece.kind: + of Pawn: + self.position.pieces.white.pawns.add(location) + of Knight: + self.position.pieces.white.knights.add(location) + of Bishop: + self.position.pieces.white.bishops.add(location) + of Rook: + self.position.pieces.white.rooks.add(location) + of Queen: + self.position.pieces.white.queens.add(location) + else: + discard + of Black: + case piece.kind: + of Pawn: + self.position.pieces.black.pawns.add(location) + of Knight: + self.position.pieces.black.knights.add(location) + of Bishop: + self.position.pieces.black.bishops.add(location) + of Rook: + self.position.pieces.black.rooks.add(location) + of Queen: + self.position.pieces.black.queens.add(location) + else: + discard + else: + # Unreachable + discard + self.grid[location.row, location.col] = piece -proc undoMove(self: ChessBoard, move: Move): Move {.discardable.} = - ## Undoes the given move if possible - result = Move(piece: move.piece, startSquare: move.targetSquare, targetSquare: move.startSquare) - let castling = self.castling - let enPassant = self.enPassantSquare - # Swap start and target square and do the move in reverse - self.doMove(result) - # We need to reset the entire position when the move is undone! - self.enPassantSquare = enPassant - #self.turn = self.turn.opposite() - self.castling = castling + +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) + 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) + self.updateAttackedSquares() + self.updatePositions(oppositeMove) + return self.position.move + 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.turn: + if move.piece.kind == Empty 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! - if destination.kind != Empty and destination.color == self.turn: + 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) return false self.doMove(move) - defer: self.undoMove(move) + defer: self.undoLastMove() # Move would reveal an attack # on our king: not allowed if self.inCheck(move.piece.color): @@ -962,11 +1095,6 @@ proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} = result = move if not self.checkMove(move): return emptyMove() - # 50 move rule - if move.piece.kind != Pawn and not self.isCapture(move): - inc(self.halfMoveClock) - else: - self.halfMoveClock = 0 self.doMove(result) @@ -986,6 +1114,13 @@ proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.disc return self.makeMove(result) +proc makeMove*(self: ChessBoard, startSquare, targetSquare: Location): Move {.discardable.} = + ## Like the other makeMove(), but with two locations + result = Move(startSquare: startSquare, targetSquare: targetSquare, piece: self.grid[startSquare.row, startSquare.col]) + return self.makeMove(result) + + + proc `$`*(self: ChessBoard): string = result &= "- - - - - - - -" diff --git a/src/Chess/player.nim b/src/Chess/player.nim index eeb3537..4420f91 100644 --- a/src/Chess/player.nim +++ b/src/Chess/player.nim @@ -10,8 +10,6 @@ when isMainModule: setControlCHook(proc () {.noconv.} = echo ""; quit(0)) var board = newChessboardFromFEN("rnbqkbnr/8/8/8/8/8/8/RNBQKBNR w KQkq - 0 1") - startSquare: string - targetSquare: string data: string move: Move @@ -27,12 +25,13 @@ when isMainModule: except IOError: echo "" break + if data == "undo": + echo &"Undo: {board.undoLastMove()}" + continue if len(data) != 4: continue - startSquare = data[0..1] - targetSquare = data[2..3] try: - move = board.makeMove(startSquare, targetSquare) + move = board.makeMove(data[0..1], data[2..3]) except ValueError: echo &"Error: {getCurrentExceptionMsg()}" if move == emptyMove():