From 86265c68f0d7a1d04b82dc923afe2ff42e3edb5b Mon Sep 17 00:00:00 2001 From: nocturn9x Date: Wed, 17 Apr 2024 11:54:45 +0200 Subject: [PATCH] Fix bugs with pawn movegen and add promotions --- src/Chess/bitboards.nim | 37 +++++++------- src/Chess/board.nim | 108 +++++++++++++++++++--------------------- src/Chess/pieces.nim | 4 +- src/Chess/player.nim | 2 +- 4 files changed, 75 insertions(+), 76 deletions(-) diff --git a/src/Chess/bitboards.nim b/src/Chess/bitboards.nim index 67bf118..f1b8620 100644 --- a/src/Chess/bitboards.nim +++ b/src/Chess/bitboards.nim @@ -46,21 +46,21 @@ func `!=`*(a: Bitboard, b: SomeInteger): bool {.inline.} = a.uint64 != b.uint64 func getFileMask*(file: int): Bitboard = Bitboard(0x101010101010101'u64) shl file.uint64 -func getRankMask*(rank: int): Bitboard = Bitboard(uint64.high()) shl uint64(8 * (rank + 1)) -func squareToBitboard*(square: SomeInteger): Bitboard = Bitboard(1'u64) shl square.uint64 -func squareToBitboard*(square: Square): Bitboard = squareToBitboard(square.int8) +func getRankMask*(rank: int): Bitboard = Bitboard(0xff) shl uint64(8 * rank) +func toBitboard*(square: SomeInteger): Bitboard = Bitboard(1'u64) shl square.uint64 +func toBitboard*(square: Square): Bitboard = toBitboard(square.int8) -proc bitboardToSquare*(b: Bitboard): Square = Square(b.uint64.countTrailingZeroBits()) +proc toSquare*(b: Bitboard): Square = Square(b.uint64.countTrailingZeroBits()) func createMove*(startSquare: Bitboard, targetSquare: Square, flags: varargs[MoveFlag]): Move = - result = createMove(startSquare.bitboardToSquare(), targetSquare, flags) + result = createMove(startSquare.toSquare(), targetSquare, flags) func createMove*(startSquare: Square, targetSquare: Bitboard, flags: varargs[MoveFlag]): Move = - result = createMove(startSquare, targetSquare.bitboardToSquare(), flags) + result = createMove(startSquare, targetSquare.toSquare(), flags) func createMove*(startSquare, targetSquare: Bitboard, flags: varargs[MoveFlag]): Move = - result = createMove(startSquare.bitboardToSquare(), targetSquare.bitboardToSquare(), flags) + result = createMove(startSquare.toSquare(), targetSquare.toSquare(), flags) func toBin*(x: Bitboard, b: Positive = 64): string = toBin(BiggestInt(x), b) @@ -73,7 +73,7 @@ iterator items*(self: Bitboard): Square = ## are set var bits = self while bits != 0: - yield bits.bitboardToSquare() + yield bits.toSquare() bits = bits and bits - 1 @@ -165,10 +165,10 @@ func getDirectionMask*(bitboard: Bitboard, color: PieceColor, direction: Directi return bitboard shl 8 of Backward: return bitboard shr 8 + of ForwardLeft: + return bitboard shl 9 of ForwardRight: return bitboard shl 7 - of ForwardLeft: - return bitboard shr 9 of BackwardRight: return bitboard shr 9 of BackwardLeft: @@ -179,34 +179,35 @@ func getDirectionMask*(bitboard: Bitboard, color: PieceColor, direction: Directi discard +func getLastRank*(color: PieceColor): Bitboard = (if color == White: getRankMask(0) else: getRankMask(7)) + func getDirectionMask*(square: Square, color: PieceColor, direction: Direction): Bitboard = ## Get a bitmask for the given direction for a piece ## of the given color located at the given square - result = getDirectionMask(squareToBitboard(square), color, direction) + result = getDirectionMask(toBitboard(square), color, direction) func forwardRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = getDirectionMask(self, side, Forward) func doubleForwardRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.forwardRelativeTo(side).forwardRelativeTo(side) func backwardRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = getDirectionMask(self, side, Backward) +func doubleBackwardRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.backwardRelativeTo(side).backwardRelativeTo(side) -# We mask off the first and last ranks/files for +# We mask off the first and last ranks for # left and right movements respectively to # avoid weird wraparounds -func topRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = +func forwardRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = getDirectionMask(self, side, ForwardRight) and not getFileMask(7) -func topLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = +func forwardLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = getDirectionMask(self, side, ForwardLeft) and not getFileMask(0) func bottomRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = - let lastRank = if side == White: getRankMask(0) else: getRankMask(7) - getDirectionMask(self, side, BackwardRight) and not lastRank + getDirectionMask(self, side, BackwardRight) and not getFileMask(7) func bottomLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = - let lastRank = if side == White: getRankMask(0) else: getRankMask(7) - getDirectionMask(self, side, BackwardLeft) and not getRankMask(7) \ No newline at end of file + getDirectionMask(self, side, BackwardLeft) and not getFileMask(0) \ No newline at end of file diff --git a/src/Chess/board.nim b/src/Chess/board.nim index 76af6c0..a926fdf 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -85,9 +85,8 @@ proc updateBoard*(self: ChessBoard) # A bunch of getters -func getActiveColor*(self: ChessBoard): PieceColor {.inline.} = - ## Returns the currently active color - ## (turn of who has to move) +func getSideToMove*(self: ChessBoard): PieceColor {.inline.} = + ## Returns the currently side to move return self.position.turn @@ -141,17 +140,6 @@ func getKingStartingSquare(color: PieceColor): Square {.inline.} = discard -func getLastRank(color: PieceColor): int {.inline.} = - ## Retrieves the square of the last - ## rank relative to the given color - case color: - of White: - return 0 - of Black: - return 7 - else: - return -1 - func kingSideRook(color: PieceColor): Square {.inline.} = (if color == White: makeSquare(7, 7) else: makeSquare(0, 7)) func queenSideRook(color: PieceColor): Square {.inline.} = (if color == White: makeSquare(7, 0) else: makeSquare(0, 0)) func longCastleKing(color: PieceColor): Square {.inline.} = (if color == White: makeSquare(7, 2) else: makeSquare(0, 5)) @@ -369,7 +357,7 @@ proc newChessboardFromFEN*(fen: string): ChessBoard = raise newException(ValueError, "invalid FEN: too many fields in FEN string") inc(index) # result.updateAttackedSquares() - #[if result.inCheck(result.getActiveColor().opposite): + #[if result.inCheck(result.getSideToMove().opposite): # Opponent king cannot be captured on the next move raise newException(ValueError, "invalid position: opponent king can be captured")]# if result.position.pieces.white.king == Bitboard(0) or result.position.pieces.black.king == Bitboard(0): @@ -509,7 +497,7 @@ func getKing(self: ChessBoard, color: PieceColor = None): Square {.inline.} = ## color (if it is None, the active color is used) var color = color if color == None: - color = self.getActiveColor() + color = self.getSideToMove() case color: of White: return Square(self.position.pieces.white.king.uint64.countTrailingZeroBits()) @@ -541,54 +529,61 @@ proc getOccupancy(self: ChessBoard): Bitboard = proc generatePawnMovements(self: ChessBoard, moves: var MoveList) = ## Helper of generatePawnMoves for generating all non-capture - ## pawn movems + ## and non-promotion pawn moves let - sideToMove = self.getActiveColor() + sideToMove = self.getSideToMove() pawns = self.getBitboard(Pawn, sideToMove) - # Can't move to these squares - occupancy = not self.getOccupancy() - offsets = if sideToMove == White: (-8, -16) else: (8, 16) - # Single pushes + # We can only move to squares that are *not* occupied by another piece. + # We also cannot move to the last rank, as that will result in a promotion + # and is handled elsewhere + occupancy = not (self.getOccupancy() or sideToMove.getLastRank()) + # Single push for square in pawns.forwardRelativeTo(sideToMove) and occupancy: - moves.add(createMove((square - offsets[0]), square)) - # Double pushes - for square in pawns.doubleForwardRelativeTo(sideToMove) and occupancy: - moves.add(createMove((square - offsets[1]), square, DoublePush)) + moves.add(createMove(square.toBitboard().backwardRelativeTo(sideToMove), square)) + # 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 occupancy: + moves.add(createMove(square.toBitboard().doubleBackwardRelativeTo(sideToMove), square, DoublePush)) proc generatePawnCaptures(self: ChessBoard, moves: var MoveList) = ## Helper of generatePawnMoves for generating all capture ## pawn moves let - sideToMove = self.getActiveColor() + sideToMove = self.getSideToMove() nonSideToMove = sideToMove.opposite() pawns = self.getBitboard(Pawn, sideToMove) # We can only capture enemy pieces enemyPieces = self.getOccupancyFor(nonSideToMove) - offsets = if sideToMove == White: (9, 7) else: (-7, -9) - rightMovement = pawns.topRightRelativeTo(sideToMove) - leftMovement = pawns.topLeftRelativeTo(sideToMove) + rightMovement = pawns.forwardRightRelativeTo(sideToMove) + leftMovement = pawns.forwardLeftRelativeTo(sideToMove) epTarget = self.getEnPassantTarget() - let epBitboard = if (epTarget != nullSquare()): epTarget.squareToBitboard() else: Bitboard(0) + let epBitboard = if (epTarget != nullSquare()): epTarget.toBitboard() else: Bitboard(0) # Top right attacks for square in rightMovement and enemyPieces: - moves.add(createMove((square - offsets[0]), square, Capture)) - # Top left attacks + moves.add(createMove(square.toBitboard().bottomLeftRelativeTo(sideToMove), square, Capture)) for square in leftMovement and enemyPieces: - moves.add(createMove((square - offsets[1]), square, Capture)) + moves.add(createMove(square.toBitboard().bottomRightRelativeTo(sideToMove), square, Capture)) # Special case for en passant let epLeft = epBitboard and leftMovement epRight = epBitboard and rightMovement if epLeft != 0: - moves.add(createMove(epBitboard.topLeftRelativeTo(nonSideToMove), epBitboard, EnPassant)) + moves.add(createMove(epBitboard.forwardLeftRelativeTo(nonSideToMove), epBitboard, EnPassant)) elif epRight != 0: - moves.add(createMove(epBitboard.topRightRelativeTo(nonSideToMove), epBitboard, EnPassant)) + moves.add(createMove(epBitboard.forwardRightRelativeTo(nonSideToMove), epBitboard, EnPassant)) proc generatePawnPromotions(self: ChessBoard, moves: var MoveList) = ## Helper of generatePawnMoves for generating all pawn promotion ## moves + let + sideToMove = self.getSideToMove() + pawns = self.getBitboard(Pawn, sideToMove) + occupancy = not self.getOccupancy() + for square in pawns.forwardRelativeTo(sideToMove) and occupancy and sideToMove.getLastRank(): + for promotion in [PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]: + moves.add(createMove(square.toBitboard().backwardRelativeTo(sideToMove), square, promotion)) proc generatePawnMoves(self: ChessBoard, moves: var MoveList) = @@ -597,6 +592,7 @@ proc generatePawnMoves(self: ChessBoard, moves: var MoveList) = self.generatePawnMovements(moves) self.generatePawnCaptures(moves) + self.generatePawnPromotions(moves) proc generateSlidingMoves(self: ChessBoard, square: Square): seq[Move] = @@ -882,7 +878,7 @@ proc doMove(self: ChessBoard, move: Move) = inc(fullMoveCount) if move.isDoublePush(): - enPassantTarget = move.targetSquare.squareToBitboard().backwardRelativeTo(piece.color).bitboardToSquare() + enPassantTarget = move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare() # Castling check: have the rooks moved? if piece.kind == Rook: @@ -944,7 +940,7 @@ proc doMove(self: ChessBoard, move: Move) = self.position = Position(plyFromRoot: self.position.plyFromRoot + 1, halfMoveClock: halfMoveClock, fullMoveCount: fullMoveCount, - turn: self.getActiveColor().opposite, + turn: self.getSideToMove().opposite, castlingAvailable: castlingAvailable, enPassantSquare: enPassantTarget, pieces: self.position.pieces @@ -971,7 +967,7 @@ proc doMove(self: ChessBoard, move: Move) = if move.isEnPassant(): # Make the en passant pawn disappear - self.removePiece(move.targetSquare.squareToBitboard().backwardRelativeTo(piece.color).bitboardToSquare(), attack=false) + self.removePiece(move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare(), attack=false) if move.isCapture(): # Get rid of captured pieces @@ -1032,8 +1028,8 @@ 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.bitboardToSquare()] = Piece(color: White, kind: King) - self.grid[self.position.pieces.black.king.bitboardToSquare()] = Piece(color: Black, kind: King) + 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) proc unmakeMove*(self: ChessBoard) = @@ -1180,7 +1176,7 @@ proc toFEN*(self: ChessBoard): string = result &= "/" result &= " " # Active color - result &= (if self.getActiveColor() == White: "w" else: "b") + result &= (if self.getSideToMove() == White: "w" else: "b") result &= " " # Castling availability let castleWhite = self.position.castlingAvailable.white @@ -1245,10 +1241,10 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa for move in moves: if verbose: - let canCastle = self.canCastle(self.getActiveColor()) + let canCastle = self.canCastle(self.getSideToMove()) echo &"Ply (from root): {self.position.plyFromRoot}" echo &"Move: {move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}, from ({move.startSquare.rank}, {move.startSquare.file}) to ({move.targetSquare.rank}, {move.targetSquare.file})" - echo &"Turn: {self.getActiveColor()}" + echo &"Turn: {self.getSideToMove()}" echo &"Piece: {self.grid[move.startSquare].kind}" echo &"Flags: {move.getFlags()}" echo &"In check: {(if self.inCheck(): \"yes\" else: \"no\")}" @@ -1274,7 +1270,7 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa # Opponent king is in check inc(result.checks) if verbose: - let canCastle = self.canCastle(self.getActiveColor()) + let canCastle = self.canCastle(self.getSideToMove()) echo "\n" echo &"Opponent in check: {(if self.inCheck(): \"yes\" else: \"no\")}" echo &"Opponent can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" @@ -1420,7 +1416,7 @@ proc handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discarda var move = createMove(startSquare, targetSquare, flags) let piece = board.getPiece(move.startSquare) - if piece.kind == King and move.startSquare == board.getActiveColor().getKingStartingSquare(): + if piece.kind == King and move.startSquare == board.getSideToMove().getKingStartingSquare(): if move.targetSquare == longCastleKing(piece.color): move.flags = move.flags or CastleLong.uint16 elif move.targetSquare == shortCastleKing(piece.color): @@ -1582,7 +1578,7 @@ proc main: int = of "undo", "u": board.unmakeMove() of "turn": - echo &"Active color: {board.getActiveColor()}" + echo &"Active color: {board.getSideToMove()}" of "ep": let target = board.getEnPassantTarget() if target != nullSquare(): @@ -1600,9 +1596,9 @@ proc main: int = continue #[of "castle": let canCastle = board.canCastle() - echo &"Castling rights for {($board.getActiveColor()).toLowerAscii()}:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" + 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\")}" of "check": - echo &"{board.getActiveColor()} king in check: {(if board.inCheck(): \"yes\" else: \"no\")}" + echo &"{board.getSideToMove()} king in check: {(if board.inCheck(): \"yes\" else: \"no\")}" ]# else: echo &"Unknown command '{cmd[0]}'. Type 'help' for more information." @@ -1719,18 +1715,18 @@ when isMainModule: testPieceBitboard(blackQueens, blackQueenSquares) testPieceBitboard(blackKing, blackKingSquares) + b = newChessboardFromFEN("B7/P1P1P1P1/1P1P1P1P/7k/8/8/8/3K4 w - - 0 1") var m = MoveList() b.generatePawnMoves(m) - echo &"Pawn moves for {b.getActiveColor()} at {b.toFEN()}: " + echo &"Pawn moves for {b.getSideToMove()} at {b.toFEN()}: " for move in m: - echo " - ", move.startSquare, move.targetSquare - b.doMove(createMove("a2", "a3")) + echo " - ", move.startSquare, move.targetSquare, " ", move.getFlags() + #[b.doMove(createMove("d1", "c1")) m.clear() b.generatePawnMoves(m) - echo &"Pawn moves for {b.getActiveColor()} at {b.toFEN()}: " + echo &"Pawn moves for {b.getSideToMove()} at {b.toFEN()}: " for move in m: - echo " - ", move.startSquare, move.targetSquare - b.doMove(createMove("a7", "a5")) - echo b.pretty() + echo " - ", move.startSquare, move.targetSquare, " ", move.getFlags() + echo b.pretty()]# # setControlCHook(proc () {.noconv.} = quit(0)) # quit(main()) diff --git a/src/Chess/pieces.nim b/src/Chess/pieces.nim index bfcfcd9..1d404cb 100644 --- a/src/Chess/pieces.nim +++ b/src/Chess/pieces.nim @@ -39,7 +39,9 @@ func `!=`*(a, b: Square): bool {.inline.} = a.int8 != b.int8 func `-`*(a, b: Square): Square {.inline.} = Square(a.int8 - b.int8) func `-`*(a: Square, b: SomeInteger): Square {.inline.} = Square(a.int8 - b.int8) func `-`*(a: SomeInteger, b: Square): Square {.inline.} = Square(a.int8 - b.int8) - +func `+`*(a, b: Square): Square {.inline.} = Square(a.int8 + b.int8) +func `+`*(a: Square, b: SomeInteger): Square {.inline.} = Square(a.int8 + b.int8) +func `+`*(a: SomeInteger, b: Square): Square {.inline.} = Square(a.int8 + b.int8) func colFromSquare*(square: Square): int8 = square.int8 mod 8 + 1 func rowFromSquare*(square: Square): int8 = square.int8 div 8 + 1 diff --git a/src/Chess/player.nim b/src/Chess/player.nim index 95e6318..98e3b03 100644 --- a/src/Chess/player.nim +++ b/src/Chess/player.nim @@ -19,7 +19,7 @@ when isMainModule: while true: canCastle = board.canCastle() echo &"{board.pretty()}" - echo &"Turn: {board.getActiveColor()}" + echo &"Turn: {board.getSideToMove()}" echo &"Moves: {board.getMoveCount()} full, {board.getHalfMoveCount()} half" echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" stdout.write(&"En passant target: ")