diff --git a/src/Chess/board.nim b/src/Chess/board.nim index 4215286..628b3fc 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -100,6 +100,8 @@ 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 getCapture*(self: ChessBoard, move: Move): Location +proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move +proc makeMove*(self: ChessBoard, move: Move): Move 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 @@ -148,6 +150,13 @@ func getStartRow(piece: Piece): int {.inline.} = func getLastRow(color: PieceColor): int {.inline.} = ## Retrieves the location of the last ## row relative to the given color + case color: + of White: + return 0 + of Black: + return 7 + else: + return -1 proc newChessboard: ChessBoard = @@ -509,7 +518,10 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = # A friendly piece is in the way if otherPiece.color == piece.color: break - # Target square is empty or occupied by the enemy + if otherPiece.color == piece.color.opposite: + result.add(Move(startSquare: location, targetSquare: square, piece: piece)) + break + # Target square is empty result.add(Move(startSquare: location, targetSquare: square, piece: piece)) @@ -551,6 +563,53 @@ proc testMoveOffsets(self: ChessBoard, move: Move): bool = return false +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: + if move.targetSquare == loc: + result.add(move.piece) + for move in self.attacked.white: + if move.targetSquare == loc: + result.add(move.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, loc: Location): bool = + ## Returns whether the given location is attacked + ## by the current opponent + let piece = self.grid[loc.row, loc.col] + case piece.color: + of White: + for move in self.attacked.black: + if move.targetSquare == loc: + return true + of Black: + for move in self.attacked.white: + if move.targetSquare == loc: + return true + of None: + case self.turn: + of White: + for move in self.attacked.black: + if move.targetSquare == loc: + return true + of Black: + for move in self.attacked.white: + if move.targetSquare == loc: + return true + else: + 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 updateAttackedSquares(self: ChessBoard) = ## Updates internal metadata about which squares ## are attacked. Called internally by doMove @@ -580,7 +639,6 @@ proc updateAttackedSquares(self: ChessBoard) = for loc in self.pieces.black.bishops: for move in self.generateMoves(loc): self.attacked.black.add(move) - # White rooks for loc in self.pieces.white.rooks: for move in self.generateMoves(loc): @@ -599,50 +657,6 @@ proc updateAttackedSquares(self: ChessBoard) = self.attacked.black.add(move) -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 move in self.attacked.black: - if move.targetSquare == loc: - result.add(move.piece) - of Black: - for move in self.attacked.white: - if move.targetSquare == loc: - result.add(move.piece) - 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, loc: Location): bool = - ## Returns whether the given location is attacked - ## by the current inactive color - case self.turn: - of White: - for move in self.attacked.black: - if move.targetSquare == loc: - return true - return false - of Black: - for move in self.attacked.white: - if move.targetSquare == loc: - return true - return false - else: - 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 @@ -749,6 +763,19 @@ proc updatePositions(self: ChessBoard, move: Move) = self.grid[move.targetSquare.row, move.targetSquare.col] = move.piece +proc inCheck(self: ChessBoard): bool = + ## Returns whether the active color's + ## king is in check + case self.turn: + of White: + return self.isAttacked(self.pieces.white.king) + of Black: + return self.isAttacked(self.pieces.black.king) + else: + # Unreachable + discard + + proc doMove(self: ChessBoard, move: Move) = ## Internal function called by makeMove after ## performing legality checks on the given move. Can @@ -760,34 +787,57 @@ proc doMove(self: ChessBoard, move: Move) = # pawn has moved if self.enPassantSquare != emptyMove() and self.enPassantSquare.piece.color == self.turn.opposite(): self.enPassantSquare = emptyMove() - self.turn = self.turn.opposite() +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 checkMove(self: ChessBoard, startSquare, targetSquare: string): Move = + +proc checkMove(self: ChessBoard, move: Move): bool = ## Internal function called by makeMove to check a move for legality - var pieceToMove = self.getPiece(startSquare) # 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() - var destination = self.getPiece(targetSquare) + if move.piece.kind == Empty or move.piece.color != self.turn: + 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: - return emptyMove() - var - startLocation = startSquare.algebraicToPosition() - targetLocation = targetSquare.algebraicToPosition() - result = Move(startSquare: startLocation, targetSquare: targetLocation, piece: pieceToMove) - if not self.testMoveOffsets(result): + return false + if not self.testMoveOffsets(move): # 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) + return false + echo move + self.doMove(move) + defer: self.undoMove(move) + # Move would reveal an attack + # on our king: not allowed + if self.inCheck(): + echo "in check" + return false + # All checks have passed: move is legal + result = true +proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} = + ## Like the other makeMove(), but with a Move object + result = move + if not self.checkMove(move): + return emptyMove() + self.doMove(result) + self.turn = self.turn.opposite() + proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.discardable.} = ## Makes a move on the board from the chosen start square to @@ -797,11 +847,11 @@ proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.disc ## 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(): - return - self.doMove(result) + var + startLocation = startSquare.algebraicToPosition() + targetLocation = targetSquare.algebraicToPosition() + result = Move(startSquare: startLocation, targetSquare: targetLocation, piece: self.grid[startLocation.row, startLocation.col]) + return self.makeMove(result)