From a07e9cc47586cd7937b918ad29ee95505ae4d070 Mon Sep 17 00:00:00 2001 From: nocturn9x Date: Wed, 17 Apr 2024 20:27:39 +0200 Subject: [PATCH] Added knight movegen. Updated attack tracking. Fix bugs --- src/Chess/bitboards.nim | 123 +++++++++++++++++++++++++++++----------- src/Chess/board.nim | 67 +++++++++++++++++----- 2 files changed, 142 insertions(+), 48 deletions(-) diff --git a/src/Chess/bitboards.nim b/src/Chess/bitboards.nim index 2b98497..5c48b74 100644 --- a/src/Chess/bitboards.nim +++ b/src/Chess/bitboards.nim @@ -112,30 +112,6 @@ func pretty*(self: Bitboard): string = func `$`*(self: Bitboard): string = self.pretty() -func computeDiagonalBitboards: array[14, Bitboard] {.compileTime.} = - ## Precomputes all the bitboards for diagonals - result[0] = Bitboard(0x8040201008040201'u64) - var - col = 1 - i = 0 - # Left to right - while col < 8: - result[col] = Bitboard(0x8040201008040201'u64) shl (8 * col) - inc(col) - inc(i) - result[i] = Bitboard(0x102040810204080'u64) - inc(i) - col = 1 - # Right to left - while col < 7: - result[i] = Bitboard(0x102040810204080'u64) shr (8 * col) - inc(i) - inc(col) - - -const diagonalBitboards = computeDiagonalBitboards() - - func getDirectionMask*(bitboard: Bitboard, color: PieceColor, direction: Direction): Bitboard = ## Get a bitmask relative to the given bitboard ## for the given direction for a piece of the @@ -185,6 +161,8 @@ func getDirectionMask*(bitboard: Bitboard, color: PieceColor, direction: Directi func getLastRank*(color: PieceColor): Bitboard = (if color == White: getRankMask(0) else: getRankMask(7)) func getFirstRank*(color: PieceColor): Bitboard = (if color == White: getRankMask(7) else: getRankMask(0)) +func getLeftmostFile*(color: PieceColor): Bitboard = (if color == White: getFileMask(0) else: getFileMask(7)) +func getRightmostFile*(color: PieceColor): Bitboard = (if color == White: getFileMask(7) else: getFileMask(0)) func getDirectionMask*(square: Square, color: PieceColor, direction: Direction): Bitboard = @@ -199,27 +177,104 @@ func doubleForwardRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self func backwardRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = getDirectionMask(self, side, Backward) func doubleBackwardRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.backwardRelativeTo(side).backwardRelativeTo(side) -func leftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = getDirectionMask(self, side, Left) and not getFileMask(0) -func rightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = getDirectionMask(self, side, Right) and not getFileMask(7) +func leftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = getDirectionMask(self, side, Left) and not getRightmostFile(side) +func rightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = getDirectionMask(self, side, Right) and not getLeftmostFile(side) -# We mask off the first and last ranks for -# left and right movements respectively to -# avoid weird wraparounds +# We mask off the opposide files to make sure there are +# no weird wraparounds when moving diagonally func forwardRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = - getDirectionMask(self, side, ForwardRight) and not getFileMask(7) + getDirectionMask(self, side, ForwardRight) and not getLeftmostFile(side) func forwardLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = - getDirectionMask(self, side, ForwardLeft) and not getFileMask(0) + getDirectionMask(self, side, ForwardLeft) and not getRightmostFile(side) func backwardRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = - getDirectionMask(self, side, BackwardRight) + getDirectionMask(self, side, BackwardRight) and not getLeftmostFile(side) func backwardLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = - getDirectionMask(self, side, BackwardLeft) + getDirectionMask(self, side, BackwardLeft) and not getRightmostFile(side) -# func forwardRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = getDirectionMask(self, side, Forward) +func longKnightUpLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.doubleForwardRelativeTo(side).leftRelativeTo(side) +func longKnightUpRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.doubleForwardRelativeTo(side).rightRelativeTo(side) +func longKnightDownLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.doubleBackwardRelativeTo(side).leftRelativeTo(side) +func longKnightDownRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.doubleBackwardRelativeTo(side).rightRelativeTo(side) + +func shortKnightUpLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.forwardRelativeTo(side).leftRelativeTo(side).leftRelativeTo(side) +func shortKnightUpRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.forwardRelativeTo(side).rightRelativeTo(side).rightRelativeTo(side) +func shortKnightDownLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.backwardRelativeTo(side).leftRelativeTo(side).leftRelativeTo(side) +func shortKnightDownRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.backwardRelativeTo(side).rightRelativeTo(side).rightRelativeTo(side) + +# We precompute as much stuff as possible: lookup tables are fast! + +func computeDiagonalBitboards: array[14, Bitboard] {.compileTime.} = + ## Precomputes all the bitboards for diagonals + result[0] = Bitboard(0x8040201008040201'u64) + var + col = 1 + i = 0 + # Left to right + while col < 8: + result[col] = Bitboard(0x8040201008040201'u64) shl (8 * col) + inc(col) + inc(i) + result[i] = Bitboard(0x102040810204080'u64) + inc(i) + col = 1 + # Right to left + while col < 7: + result[i] = Bitboard(0x102040810204080'u64) shr (8 * col) + inc(i) + inc(col) + + +func computeKingBitboards: array[64, Bitboard] = + ## Precomputes all the movement bitboards for the king + for i in 0'u64..63: + let king = i.toBitboard() + # It doesn't really matter which side we generate + # the move for, they're identical for both + var movements = king.forwardRelativeTo(White) + movements = movements or king.forwardLeftRelativeTo(White) + 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.backwardRightRelativeTo(White) + movements = movements or king.backwardLeftRelativeTo(White) + # We don't *need* to mask the king off: the engine already masks off + # the board's occupancy when generating moves, but it may be useful for + # other parts of the movegen for this stuff not to say "the king can just + # stay still", so we do it anyway + movements = movements and not king + result[i] = movements + + +func computeKnightBitboards: array[64, Bitboard] = + ## Precomputes all the movement bitboards for knights + for i in 0'u64..63: + let knight = i.toBitboard() + # It doesn't really matter which side we generate + # the move for, they're identical for both + var movements = knight.longKnightDownLeftRelativeTo(White) + movements = movements or knight.longKnightDownRightRelativeTo(White) + movements = movements or knight.longKnightUpLeftRelativeTo(White) + movements = movements or knight.longKnightUpRightRelativeTo(White) + movements = movements or knight.shortKnightDownLeftRelativeTo(White) + movements = movements or knight.shortKnightDownRightRelativeTo(White) + movements = movements or knight.shortKnightUpLeftRelativeTo(White) + movements = movements or knight.shortKnightUpRightRelativeTo(White) + result[i] = movements + + +# Precomputing stuff is *very* helpful for chess, it turns out +const DIAGONAL_BITBOARDS* = computeDiagonalBitboards() +# For some reason nim freaks out if I try to call computeKingBitboards() +# at compile-time. ¯\_(ツ)_/¯ +let + KING_BITBOARDS* = computeKingBitboards() + KNIGHT_BITBOARDS* = computeKnightBitboards() diff --git a/src/Chess/board.nim b/src/Chess/board.nim index 37a90b5..f3b6b2a 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -539,6 +539,29 @@ proc getPawnAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bit return pawns and (bottomLeft or bottomRight) +proc getKingAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard = + ## Returns the attack bitboard for the given square from + ## the king of the given side + result = Bitboard(0) + let + sq = square.toBitboard() + king = self.getBitboard(King, attacker) + if (sq and KING_BITBOARDS[square.uint64]) != 0: + result = result or sq + + +proc getKnightAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard = + ## Returns the attack bitboard for the given square from + ## the knights of the given side + let + sq = square.toBitboard() + knights = self.getBitboard(Knight, attacker) + result = Bitboard(0) + for knight in knights: + if (sq and KNIGHT_BITBOARDS[knight.uint64]) != 0: + result = result or knight.toBitboard() + + proc getAttacksTo(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard = ## Computes the attack bitboard for the given square from ## the given side @@ -546,8 +569,17 @@ proc getAttacksTo(self: ChessBoard, square: Square, attacker: PieceColor): Bitbo let squareBitboard = square.toBitboard() result = result or self.getPawnAttacks(square, attacker) + result = result or self.getKingAttacks(square, attacker) + result = result or self.getKnightAttacks(square, attacker) +proc getCapturablePieces(self: ChessBoard, side: PieceColor): Bitboard {.inline.} = + ## Returns the set of pieces of the given color that can + ## be captured + + # Just a handy helper to filter out the king and avoid code duplication + return self.getOccupancyFor(side) and not self.getBitboard(King, side) + proc generatePawnMovements(self: ChessBoard, moves: var MoveList) = ## Helper of generatePawnMoves for generating all non-capture @@ -576,7 +608,7 @@ proc generatePawnCaptures(self: ChessBoard, moves: var MoveList) = nonSideToMove = sideToMove.opposite() pawns = self.getBitboard(Pawn, sideToMove) # We can only capture enemy pieces (except the king) - enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove) + enemyPieces = self.getCapturablePieces(nonSideToMove) rightMovement = pawns.forwardRightRelativeTo(sideToMove) leftMovement = pawns.forwardLeftRelativeTo(sideToMove) epTarget = self.getEnPassantTarget() @@ -625,26 +657,33 @@ proc generateKingMoves(self: ChessBoard, moves: var MoveList) = let sideToMove = self.getSideToMove() king = self.getBitboard(King, sideToMove) + moveIdx = king.toSquare().uint64 allowedSquares = not self.getOccupancy() nonSideToMove = sideToMove.opposite() - enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove) - var movements = king.forwardRelativeTo(sideToMove) - movements = movements or king.rightRelativeTo(sideToMove) - movements = movements or king.backwardRelativeTo(sideToMove) - movements = movements or king.leftRelativeTo(sideToMove) - movements = movements or king.forwardRightRelativeTo(sideToMove) - movements = movements or king.forwardLeftRelativeTo(sideToMove) + enemyPieces = self.getCapturablePieces(nonSideToMove) # Regular moves - for square in movements and allowedSquares: + for square in KING_BITBOARDS[moveIdx] and allowedSquares: moves.add(createMove(king, square)) # Captures - for square in movements and enemyPieces: + for square in KING_BITBOARDS[moveIdx] and enemyPieces: moves.add(createMove(king, square, Capture)) proc generateKnightMoves(self: ChessBoard, moves: var MoveList)= ## Generates all the legal knight moves for the side to move - + let + sideToMove = self.getSideToMove() + knights = self.getBitboard(Knight, sideToMove) + allowedSquares = not self.getOccupancy() + nonSideToMove = sideToMove.opposite() + enemyPieces = self.getCapturablePieces(nonSideToMove) + for square in knights: + # Regular moves + for target in KNIGHT_BITBOARDS[square.uint64] and allowedSquares: + moves.add(createMove(square, target)) + # Captures + for target in KNIGHT_BITBOARDS[square.uint64] and enemyPieces: + moves.add(createMove(square, target, Capture)) proc checkInsufficientMaterialPieceCount(self: ChessBoard, color: PieceColor): bool = @@ -718,6 +757,7 @@ proc generateMoves*(self: ChessBoard, moves: var MoveList) = # TODO: Check for repetitions (requires zobrist hashing + table) self.generatePawnMoves(moves) self.generateKingMoves(moves) + self.generateKnightMoves(moves) # TODO: all pieces @@ -1755,13 +1795,12 @@ when isMainModule: testPieceBitboard(blackQueens, blackQueenSquares) testPieceBitboard(blackKing, blackKingSquares) - b = newChessboardFromFEN("B7/P1P1P1P1/1P1P1P1P/7k/8/8/8/3K4 w - - 0 1") - b.doMove(createMove("d1", "e1")) var m = MoveList() b.generateMoves(m) - echo &"Legal moves for {b.getSideToMove()} at {b.toFEN()}: " + echo &"There are {len(m)} legal moves for {b.getSideToMove()} at {b.toFEN()}: " for move in m: echo " - ", move.startSquare, move.targetSquare, " ", move.getFlags() echo b.pretty() + echo b.getAttacksTo("f3".toSquare(), White) # setControlCHook(proc () {.noconv.} = quit(0)) # quit(main())