From 049604716428b3b665dff3a5bb2e65920a9138d1 Mon Sep 17 00:00:00 2001 From: Mattia Giambirtone Date: Fri, 19 Apr 2024 17:05:18 +0200 Subject: [PATCH] Fix bugs in move handling --- Chess/nimfish/nimfish.nim | 207 ++++++------------------- Chess/nimfish/nimfishpkg/bitboards.nim | 3 +- 2 files changed, 50 insertions(+), 160 deletions(-) diff --git a/Chess/nimfish/nimfish.nim b/Chess/nimfish/nimfish.nim index 1c7e8ff..1a76072 100644 --- a/Chess/nimfish/nimfish.nim +++ b/Chess/nimfish/nimfish.nim @@ -35,7 +35,7 @@ type ## A chess position # Castling metadata. Updated on every move - castling: array[64, uint8] + castlingRights: array[64, uint8] # Number of half-moves that were performed # to reach this position starting from the # root of the tree @@ -81,8 +81,8 @@ proc pretty*(self: ChessBoard): string proc spawnPiece(self: ChessBoard, square: Square, piece: Piece) proc toFEN*(self: ChessBoard): string proc unmakeMove*(self: ChessBoard) -proc movePiece(self: ChessBoard, move: Move, attack: bool = true) -proc removePiece(self: ChessBoard, square: Square, attack: bool = true) +proc movePiece(self: ChessBoard, move: Move) +proc removePiece(self: ChessBoard, square: Square) proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} = @@ -163,6 +163,7 @@ proc newChessboard: ChessBoard = new(result) for i in 0..63: result.grid[i] = nullPiece() + result.currPos = -1 result.position = Position(enPassantSquare: nullSquare(), turn: White) # Indexing operations @@ -326,18 +327,18 @@ proc newChessboardFromFEN*(fen: string): ChessBoard = discard of 'K': discard - # result.position.castlingAvailable.white.king = true + # result.position.castlingRightsAvailable.white.king = true of 'Q': discard - # result.position.castlingAvailable.white.queen = true + # result.position.castlingRightsAvailable.white.queen = true of 'k': discard - # result.position.castlingAvailable.black.king = true + # result.position.castlingRightsAvailable.black.king = true of 'q': discard - # result.position.castlingAvailable.black.queen = true + # result.position.castlingRightsAvailable.black.queen = true else: - raise newException(ValueError, &"invalid FEN: unknown symbol '{c}' found in castling availability section") + raise newException(ValueError, &"invalid FEN: unknown symbol '{c}' found in castlingRights availability section") of 3: # En passant target square case c: @@ -472,7 +473,7 @@ func isCastling*(move: Move): bool {.inline.} = func getCastlingType*(move: Move): MoveFlag {.inline.} = - ## Returns the castling type of the given move. + ## Returns the castlingRights type of the given move. ## The return value of this function is only valid ## if isCastling() returns true for flag in [CastleLong, CastleShort]: @@ -644,7 +645,7 @@ proc generatePawnMovements(self: ChessBoard, moves: var MoveList) = for square in pawns.forwardRelativeTo(sideToMove) and allowedSquares: moves.add(createMove(square.toBitboard().backwardRelativeTo(sideToMove), square)) # Double push - let rank = sideToMove.getFirstRank() # Only pawns on their starting rank can double push + let rank = if sideToMove == White: getRankMask(6) else: getRankMask(1) # Only pawns on their starting rank can double push for square in (pawns and rank).doubleForwardRelativeTo(sideToMove) and allowedSquares: moves.add(createMove(square.toBitboard().doubleBackwardRelativeTo(sideToMove), square, DoublePush)) @@ -964,8 +965,8 @@ proc removePieceFromBitboard(self: ChessBoard, square: Square) = self.position.pieces.white.queens.uint64.clearBit(square.int8) of King: self.position.pieces.white.king.uint64.clearBit(square.int8) - else: - discard + of Empty: + doAssert false, &"cannot remove empty white piece from {square}" of Black: case piece.kind: of Pawn: @@ -979,11 +980,11 @@ proc removePieceFromBitboard(self: ChessBoard, square: Square) = of Queen: self.position.pieces.black.queens.uint64.clearBit(square.int8) of King: - self.position.pieces.black.king.uint64.clearBit(square.int8) - else: - discard + self.position.pieces.black.king.uint64.clearBit(square.int8) + of Empty: + doAssert false, &"cannot remove empty black piece from {square}" else: - discard + doAssert false, &"cannot remove empty piece from colorless square {square}" proc addPieceToBitboard(self: ChessBoard, square: Square, piece: Piece) = @@ -1026,73 +1027,16 @@ proc addPieceToBitboard(self: ChessBoard, square: Square, piece: Piece) = discard -proc removePiece(self: ChessBoard, square: Square, attack: bool = true) = +proc removePiece(self: ChessBoard, square: Square) = ## Removes a piece from the board, updating necessary ## metadata var piece = self.grid[square] - self.grid[square] = nullPiece() + doAssert piece.kind != Empty and piece.color != None self.removePieceFromBitboard(square) - #[if attack: - self.updateAttackedSquares()]# + self.grid[square] = nullPiece() -proc updateMovepieces(self: ChessBoard, move: Move) = - ## Updates our bitboard representation after a move: note that this - ## does *not* handle captures, en passant, promotions etc. as those - ## are already called by helpers such as removePiece() and spawnPiece() - var bitboard: uint64 - let piece = self.grid[move.startSquare] - # TODO: Should we use our helpers or is it faster to branch only once? - case piece.color: - of White: - case piece.kind: - of Pawn: - self.position.pieces.white.pawns.uint64.setBit(move.targetSquare.int8) - self.position.pieces.white.pawns.uint64.clearBit(move.startSquare.int8) - of Bishop: - self.position.pieces.white.bishops.uint64.setBit(move.targetSquare.int8) - self.position.pieces.white.bishops.uint64.clearBit(move.startSquare.int8) - of Knight: - self.position.pieces.white.knights.uint64.setBit(move.targetSquare.int8) - self.position.pieces.white.knights.uint64.clearBit(move.startSquare.int8) - of Rook: - self.position.pieces.white.rooks.uint64.setBit(move.targetSquare.int8) - self.position.pieces.white.rooks.uint64.clearBit(move.startSquare.int8) - of Queen: - self.position.pieces.white.queens.uint64.setBit(move.targetSquare.int8) - self.position.pieces.white.queens.uint64.clearBit(move.startSquare.int8) - of King: - self.position.pieces.white.king.uint64.setBit(move.targetSquare.int8) - self.position.pieces.white.king.uint64.clearBit(move.startSquare.int8) - else: - discard - of Black: - case piece.kind: - of Pawn: - self.position.pieces.black.pawns.uint64.setBit(move.targetSquare.int8) - self.position.pieces.black.pawns.uint64.clearBit(move.startSquare.int8) - of Bishop: - self.position.pieces.black.bishops.uint64.setBit(move.targetSquare.int8) - self.position.pieces.black.bishops.uint64.clearBit(move.startSquare.int8) - of Knight: - self.position.pieces.black.knights.uint64.setBit(move.targetSquare.int8) - self.position.pieces.black.knights.uint64.clearBit(move.startSquare.int8) - of Rook: - self.position.pieces.black.rooks.uint64.setBit(move.targetSquare.int8) - self.position.pieces.black.rooks.uint64.clearBit(move.startSquare.int8) - of Queen: - self.position.pieces.black.queens.uint64.setBit(move.targetSquare.int8) - self.position.pieces.black.queens.uint64.clearBit(move.startSquare.int8) - of King: - self.position.pieces.black.king.uint64.setBit(move.targetSquare.int8) - self.position.pieces.black.king.uint64.clearBit(move.startSquare.int8) - else: - discard - else: - discard - - -proc movePiece(self: ChessBoard, move: Move, attack: bool = true) = +proc movePiece(self: ChessBoard, move: Move) = ## Internal helper to move a piece. If attack ## is set to false, then this function does ## not update attacked squares metadata, just @@ -1100,20 +1044,15 @@ proc movePiece(self: ChessBoard, move: Move, attack: bool = true) = let piece = self.grid[move.startSquare] let targetSquare = self.getPiece(move.targetSquare) if targetSquare.color != None: - raise newException(AccessViolationDefect, &"attempted to overwrite a piece! {move}") + raise newException(AccessViolationDefect, &"{piece} at {move.startSquare} attempted to overwrite {targetSquare} at {move.targetSquare}") # Update positional metadata - self.updateMovePieces(move) - # Empty out the starting square - self.grid[move.startSquare] = nullPiece() - # Actually move the piece on the board - self.grid[move.targetSquare] = piece - #[if attack: - self.updateAttackedSquares()]# + self.removePiece(move.startSquare) + self.spawnPiece(move.targetSquare, piece) -proc movePiece(self: ChessBoard, startSquare, targetSquare: Square, attack: bool = true) = +proc movePiece(self: ChessBoard, startSquare, targetSquare: Square) = ## Like the other movePiece(), but with two squares - self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare), attack) + self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare)) proc doMove(self: ChessBoard, move: Move) = @@ -1124,14 +1063,16 @@ proc doMove(self: ChessBoard, move: Move) = # Record final position for future reference self.positions.add(self.position) + inc(self.currPos) # Final checks let piece = self.grid[move.startSquare] + doAssert piece.kind != Empty and piece.color != None var halfMoveClock = self.position.halfMoveClock fullMoveCount = self.position.fullMoveCount - castling = self.position.castling + castlingRights = self.position.castlingRights enPassantTarget = nullSquare() # Needed to detect draw by the 50 move rule if piece.kind == Pawn or move.isCapture() or move.isEnPassant(): @@ -1144,69 +1085,26 @@ proc doMove(self: ChessBoard, move: Move) = if move.isDoublePush(): enPassantTarget = move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare() - # Castling check: have the rooks moved? - if piece.kind == Rook: - discard - # case piece.color: - # of White: - # if rowFromSquare(move.startSquare) == piece.getStartRank(): - # if columnFromSquare(move.startSquare) == 0: - # # Queen side - # castlingAvailable.white.queen = false - # elif columnfromSquare(move.startSquare) == 7: - # # King side - # castlingAvailable.white.king = false - # of Black: - # if rowFromSquare(move.startSquare) == piece.getStartRank(): - # if columnFromSquare(move.startSquare) == 0: - # # Queen side - # castlingAvailable.black.queen = false - # elif columnFromSquare(move.startSquare) == 7: - # # King side - # castlingAvailable.black.king = false - # else: - # discard - # Has a rook been captured? - - # Create new position self.position = Position(plyFromRoot: self.position.plyFromRoot + 1, halfMoveClock: halfMoveClock, fullMoveCount: fullMoveCount, turn: self.getSideToMove().opposite, - castling: castling, + castlingRights: castlingRights, enPassantSquare: enPassantTarget, pieces: self.position.pieces ) # Update position metadata - if move.isCastling(): - # Move the rook onto the - # correct file when castling - var - square: Square - target: Square - flag: MoveFlag - if move.getCastlingType() == CastleShort: - square = piece.color.kingSideRook() - target = shortCastleRook(piece.color) - flag = CastleShort - else: - square = piece.color.queenSideRook() - target = longCastleRook(piece.color) - flag = CastleLong - let rook = self.grid[square] - self.movePiece(createMove(square, target, flag), attack=false) - if move.isEnPassant(): # Make the en passant pawn disappear - self.removePiece(move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare(), attack=false) + self.removePiece(move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare()) if move.isCapture(): # Get rid of captured pieces - self.removePiece(move.targetSquare, attack=false) - # Move the piece to its target square and update attack metadata - self.movePiece(move, attack=false) + self.removePiece(move.targetSquare) + # Move the piece to its target square + self.movePiece(move) if move.isPromotion(): # Move is a pawn promotion: get rid of the pawn # and spawn a new piece @@ -1223,7 +1121,7 @@ proc doMove(self: ChessBoard, move: Move) = else: # Unreachable discard - #self.updateAttackedSquares() + self.updateBoard() proc spawnPiece(self: ChessBoard, square: Square, piece: Piece) = @@ -1261,26 +1159,18 @@ proc updateBoard*(self: ChessBoard) = self.grid[sq] = Piece(color: White, kind: Queen) for sq in self.position.pieces.black.queens: self.grid[sq] = Piece(color: Black, kind: Queen) - self.grid[self.position.pieces.white.king.toSquare()] = Piece(color: White, kind: King) - self.grid[self.position.pieces.black.king.toSquare()] = Piece(color: Black, kind: King) + for sq in self.position.pieces.white.king: + self.grid[sq] = Piece(color: White, kind: King) + for sq in self.position.pieces.black.king: + self.grid[sq] = Piece(color: Black, kind: King) proc unmakeMove*(self: ChessBoard) = ## Reverts to the previous board position, ## if one exists - if self.currPos > 0: + if self.currPos >= 0: + self.position = self.positions[self.currPos] dec(self.currPos) - self.position = self.positions[self.currPos] - self.updateBoard() - - -proc redoMove*(self: ChessBoard) = - ## Reverts to the next board position, if one - ## exists. Only makes sense after a call to - ## unmakeMove - if self.positions.high() > self.currPos: - inc(self.currPos) - self.position = self.positions[self.currPos] self.updateBoard() @@ -1386,7 +1276,6 @@ proc pretty*(self: ChessBoard): string = result &= "\x1b[0m" - proc toFEN*(self: ChessBoard): string = ## Returns a FEN string of the current ## position in the chessboard @@ -1413,8 +1302,8 @@ proc toFEN*(self: ChessBoard): string = result &= " " # Castling availability result &= "-" - # let castleWhite = self.position.castlingAvailable.white - # let castleBlack = self.position.castlingAvailable.black + # let castleWhite = self.position.castlingRightsAvailable.white + # let castleBlack = self.position.castlingRightsAvailable.black # if not (castleBlack.king or castleBlack.queen or castleWhite.king or castleWhite.queen): # result &= "-" # else: @@ -1623,13 +1512,13 @@ proc handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discarda # Since the user tells us just the source and target square of the move, # we have to figure out all the flags by ourselves (whether it's a double - # push, a capture, a promotion, castling, etc.) + # push, a capture, a promotion, etc.) if board.grid[targetSquare].kind != Empty: flags.add(Capture) - #elif board.grid[startSquare].kind == Pawn and abs(rowFromSquare(startSquare) - rowFromSquare(targetSquare)) == 2: - # flags.add(DoublePush) + elif board.grid[startSquare].kind == Pawn and abs(rankFromSquare(startSquare) - rankFromSquare(targetSquare)) == 2: + flags.add(DoublePush) if len(moveString) == 5: # Promotion @@ -1759,7 +1648,7 @@ const HELP_TEXT = """Nimfish help menu: - position fen "..." moves a2a3 a7a6 - clear: Clear the screen - move : Perform the given move in algebraic notation - - castle: Print castling rights for each side + - castle: Print castlingRights rights for each side - check: Print if the current side to move is in check - undo, u: Undoes the last move. Can be used in succession - turn: Print which side is to move @@ -1836,7 +1725,7 @@ proc main: int = echo board.getPiece(cmd[1]) except ValueError: echo "error: get: invalid square" - continue + continue of "castle": let canCastle = board.canCastle(board.getSideToMove()) echo &"Castling rights for {($board.getSideToMove()).toLowerAscii()}:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" diff --git a/Chess/nimfish/nimfishpkg/bitboards.nim b/Chess/nimfish/nimfishpkg/bitboards.nim index a0b2f26..c51fbc4 100644 --- a/Chess/nimfish/nimfishpkg/bitboards.nim +++ b/Chess/nimfish/nimfishpkg/bitboards.nim @@ -240,7 +240,7 @@ func computeKingBitboards: array[64, Bitboard] = movements = movements or king.leftRelativeTo(White) movements = movements or king.rightRelativeTo(White) movements = movements or king.backwardRelativeTo(White) - movements = movements or king.forwardLeftRelativeTo(White) + movements = movements or king.forwardRightRelativeTo(White) movements = movements or king.backwardRightRelativeTo(White) movements = movements or king.backwardLeftRelativeTo(White) # We don't *need* to mask the king off: the engine already masks off @@ -265,6 +265,7 @@ func computeKnightBitboards: array[64, Bitboard] = movements = movements or knight.shortKnightDownRightRelativeTo(White) movements = movements or knight.shortKnightUpLeftRelativeTo(White) movements = movements or knight.shortKnightUpRightRelativeTo(White) + movements = movements and not knight result[i] = movements