Bug fixes and huge performance improvement. Initial work on pins

This commit is contained in:
Mattia Giambirtone 2024-04-20 17:48:18 +02:00
parent 4a9deb517a
commit d5bcd15c48
7 changed files with 143 additions and 46 deletions

View File

@ -21,6 +21,7 @@ import nimfishpkg/magics
import nimfishpkg/pieces
import nimfishpkg/moves
import nimfishpkg/position
import nimfishpkg/rays
export bitboards, magics, pieces, moves, position
@ -248,7 +249,7 @@ func getKingAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bit
result = Bitboard(0)
let
king = self.getBitboard(King, attacker)
if (getKingBitboard(square) and king) != 0:
if (getKingAttacks(square) and king) != 0:
result = result or king
@ -260,7 +261,7 @@ func getKnightAttacks(self: Chessboard, square: Square, attacker: PieceColor): B
result = Bitboard(0)
for knight in knights:
let knightBB = knight.toBitboard()
if (getKnightBitboard(knight) and knightBB) != 0:
if (getKnightAttacks(knight) and knightBB) != 0:
result = result or knightBB
@ -298,10 +299,14 @@ proc getAttacksTo*(self: Chessboard, square: Square, attacker: PieceColor): Bitb
proc updateChecksAndPins(self: Chessboard) =
## Updates internal metadata about checks and
## pinned pieces
# *Ahem*, stolen from https://github.com/Ciekce/voidstar/blob/424ac4624011271c4d1dbd743602c23f6dbda1de/src/position.rs
# Can you tell I'm a *great* coder?
let
sideToMove = self.position.sideToMove
nonSideToMove = sideToMove.opposite()
friendlyKing = self.getBitboard(King, sideToMove).toSquare()
friendlyKingBB = self.getBitboard(King, sideToMove)
friendlyKing = friendlyKingBB.toSquare()
friendlyPieces = self.getOccupancyFor(sideToMove)
enemyPieces = self.getOccupancyFor(nonSideToMove)
@ -311,6 +316,24 @@ proc updateChecksAndPins(self: Chessboard) =
self.position.diagonalPins = Bitboard(0)
self.position.orthogonalPins = Bitboard(0)
let
diagonalAttackers = self.getBitboard(Queen, nonSideToMove) or self.getBitboard(Bishop, nonSideToMove)
orthogonalAttackers = self.getBitboard(Queen, nonSideToMove) or self.getBitboard(Rook, nonSideToMove)
canPinDiagonally = diagonalAttackers and getBishopMoves(friendlyKing, enemyPieces)
canPinOrthogonally = orthogonalAttackers and getRookMoves(friendlyKing, enemyPieces)
for piece in canPinDiagonally:
let pinningRay = getRayBetween(friendlyKing, piece) or piece.toBitboard()
if (pinningRay and friendlyKingBB).countSquares() > 0:
self.position.diagonalPins = self.position.diagonalPins or pinningRay
for piece in canPinOrthogonally:
let pinningRay = getRayBetween(friendlyKing, piece) or piece.toBitboard()
if (pinningRay and friendlyKingBB).countSquares() > 0:
self.position.diagonalPins = self.position.diagonalPins or pinningRay
func inCheck(self: Chessboard): bool {.inline.} =
## Returns if the current side to move is in check
@ -499,12 +522,13 @@ proc generatePawnMovements(self: Chessboard, moves: var MoveList) =
proc generatePawnCaptures(self: Chessboard, moves: var MoveList) =
## Helper of generatePawnMoves for generating all capture
## pawn moves
## Helper of generatePawnMoves for generating all capturing
## moves
let
sideToMove = self.position.sideToMove
nonSideToMove = sideToMove.opposite()
pawns = self.getBitboard(Pawn, sideToMove)
occupancy = self.getOccupancy()
# We can only capture enemy pieces (except the king)
enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove)
enemyPawns = self.getBitboard(Pawn, nonSideToMove)
@ -512,7 +536,9 @@ proc generatePawnCaptures(self: Chessboard, moves: var MoveList) =
leftMovement = pawns.forwardLeftRelativeTo(sideToMove)
epTarget = self.position.enPassantSquare
var epBitboard = if (epTarget != nullSquare()): epTarget.toBitboard() else: Bitboard(0)
epBitboard = epBitboard and enemyPawns
# TODO: Remove this. Seems like we're not keeping track of en passant targets properly and
# trying to do en passant on top of a piece
epBitboard = epBitboard and not occupancy
# Top right attacks
for square in rightMovement and enemyPieces:
moves.add(createMove(square.toBitboard().backwardLeftRelativeTo(sideToMove), square, Capture))
@ -530,7 +556,7 @@ proc generatePawnCaptures(self: Chessboard, moves: var MoveList) =
proc generatePawnPromotions(self: Chessboard, moves: var MoveList) =
## Helper of generatePawnMoves for generating all pawn promotion
## Helper of generatePawnMoves for generating all promotion
## moves
let
sideToMove = self.position.sideToMove
@ -555,14 +581,26 @@ proc generateRookMoves(self: Chessboard, moves: var MoveList) =
occupancy = self.getOccupancy()
nonSideToMove = sideToMove.opposite()
enemyPieces = self.getOccupancyFor(sideToMove.opposite()) and not self.getBitboard(King, nonSideToMove)
rooks = self.getBitboard(Rook, sideToMove) or self.getBitboard(Queen, sideToMove)
for square in rooks:
rooks = self.getBitboard(Rook, sideToMove)
queens = self.getBitboard(Queen, sideToMove)
movableRooks = not self.position.diagonalPins and (queens or rooks)
pinMask = self.position.orthogonalPins
pinnedRooks = movableRooks and pinMask
unpinnedRooks = movableRooks and not pinnedRooks
for square in pinnedRooks:
let
blockers = occupancy and Rook.getRelevantBlockers(square)
moveset = getRookMoves(square, blockers)
for target in moveset and not occupancy and pinMask:
moves.add(createMove(square, target))
for target in moveset and enemyPieces and pinMask:
moves.add(createMove(square, target, Capture))
for square in unpinnedRooks:
let
blockers = occupancy and Rook.getRelevantBlockers(square)
moveset = getRookMoves(square, blockers)
for target in moveset and not occupancy:
moves.add(createMove(square, target))
# Captures
for target in moveset and enemyPieces:
moves.add(createMove(square, target, Capture))
@ -571,11 +609,24 @@ proc generateBishopMoves(self: Chessboard, moves: var MoveList) =
## Helper of generateSlidingMoves to generate bishop moves
let
sideToMove = self.position.sideToMove
occupancy = self.getOccupancy()
nonSideToMove = sideToMove.opposite()
enemyPieces = self.getOccupancyFor(sideToMove.opposite()) and not self.getBitboard(King, nonSideToMove)
occupancy = self.getOccupancy()
bishops = self.getBitboard(Bishop, sideToMove) or self.getBitboard(Queen, sideToMove)
for square in bishops:
bishops = self.getBitboard(Bishop, sideToMove)
queens = self.getBitboard(Queen, sideToMove)
movableBishops = not self.position.orthogonalPins and (queens or bishops)
pinMask = self.position.diagonalPins
pinnedBishops = movableBishops and pinMask
unpinnedBishops = movableBishops and not pinnedBishops
for square in pinnedBishops:
let
blockers = occupancy and Bishop.getRelevantBlockers(square)
moveset = getBishopMoves(square, blockers)
for target in moveset and pinMask and not occupancy:
moves.add(createMove(square, target))
for target in moveset and enemyPieces and pinMask:
moves.add(createMove(square, target, Capture))
for square in unpinnedBishops:
let
blockers = occupancy and Bishop.getRelevantBlockers(square)
moveset = getBishopMoves(square, blockers)
@ -597,15 +648,13 @@ proc generateKingMoves(self: Chessboard, moves: var MoveList) =
let
sideToMove = self.position.sideToMove
king = self.getBitboard(King, sideToMove)
moveIdx = king.toSquare().uint64
occupancy = self.getOccupancy()
nonSideToMove = sideToMove.opposite()
enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove)
# Regular moves
for square in KING_BITBOARDS[moveIdx] and not occupancy:
bitboard = getKingAttacks(king.toSquare())
for square in bitboard and not occupancy:
moves.add(createMove(king, square))
# Captures
for square in KING_BITBOARDS[moveIdx] and enemyPieces:
for square in bitboard and enemyPieces:
moves.add(createMove(king, square, Capture))
@ -616,13 +665,14 @@ proc generateKnightMoves(self: Chessboard, moves: var MoveList)=
knights = self.getBitboard(Knight, sideToMove)
occupancy = self.getOccupancy()
nonSideToMove = sideToMove.opposite()
pinned = self.position.diagonalPins or self.position.orthogonalPins
unpinnedKnights = knights and not pinned
enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove)
for square in knights:
# Regular moves
for target in KNIGHT_BITBOARDS[square.uint64] and not occupancy:
for square in unpinnedKnights:
let bitboard = getKnightAttacks(square)
for target in bitboard and not occupancy:
moves.add(createMove(square, target))
# Captures
for target in KNIGHT_BITBOARDS[square.uint64] and enemyPieces:
for target in bitboard and enemyPieces:
moves.add(createMove(square, target, Capture))
@ -635,6 +685,14 @@ proc generateMoves*(self: Chessboard, moves: var MoveList) =
# TODO: Check for draw by insufficient material
# TODO: Check for repetitions (requires zobrist hashing + table)
self.generateKingMoves(moves)
if self.position.checkers.countSquares() > 1:
# King is in double check: no need to generate any more
# moves
return
if not self.inCheck():
# TODO: Castling
discard
self.generatePawnMoves(moves)
self.generateKnightMoves(moves)
self.generateSlidingMoves(moves)

View File

@ -52,6 +52,7 @@ func clearBit*(a: var Bitboard, bit: SomeInteger) = a.uint64.clearBit(bit)
func setBit*(a: var Bitboard, bit: SomeInteger) = a.uint64.setBit(bit)
func clearBit*(a: var Bitboard, bit: Square) = a.uint64.clearBit(bit.int)
func setBit*(a: var Bitboard, bit: Square) = a.uint64.setBit(bit.int)
func removed*(a, b: Bitboard): Bitboard = a and not b
func countSquares*(self: Bitboard): int {.inline.} =
@ -103,10 +104,9 @@ iterator subsets*(self: Bitboard): Bitboard =
var subset = Bitboard(0)
while true:
subset = (subset - self) and self
yield subset
if subset == 0:
break
yield subset
iterator pairs*(self: Bitboard): tuple[i: int, sq: Square] =
var i = 0
@ -167,8 +167,6 @@ func getDirectionMask*(bitboard: Bitboard, color: PieceColor, direction: Directi
of Right:
return bitboard shl 1
of Black:
# The directions for black are just the opposite of those for white,
# so we avoid duplicating any code
case direction:
of Forward:
return bitboard shl 8
@ -284,9 +282,9 @@ func computeKnightBitboards: array[64, Bitboard] {.compileTime.} =
const
KING_BITBOARDS* = computeKingBitboards()
KNIGHT_BITBOARDS* = computeKnightBitboards()
KING_BITBOARDS = computeKingBitboards()
KNIGHT_BITBOARDS = computeKnightBitboards()
func getKingBitboard*(square: Square): Bitboard {.inline.} = KING_BITBOARDS[square.int]
func getKnightBitboard*(square: Square): Bitboard {.inline.} = KNIGHT_BITBOARDS[square.int]
func getKingAttacks*(square: Square): Bitboard {.inline.} = KING_BITBOARDS[square.int]
func getKnightAttacks*(square: Square): Bitboard {.inline.} = KNIGHT_BITBOARDS[square.int]

View File

@ -137,8 +137,8 @@ proc getRookMoves*(square: Square, blockers: Bitboard): Bitboard =
## square with the given blockers bitboard
let
magic = ROOK_MAGICS[square.uint]
moves = ROOK_MOVES[square.uint]
return moves[getIndex(magic, blockers)]
movesAddr = addr ROOK_MOVES[square.uint]
return movesAddr[][getIndex(magic, blockers)]
proc getBishopMoves*(square: Square, blockers: Bitboard): Bitboard =
@ -146,8 +146,20 @@ proc getBishopMoves*(square: Square, blockers: Bitboard): Bitboard =
## square with the given blockers bitboard
let
magic = BISHOP_MAGICS[square.uint]
moves = BISHOP_MOVES[square.uint]
return moves[getIndex(magic, blockers)]
movesAddr = addr BISHOP_MOVES[square.uint]
return movesAddr[][getIndex(magic, blockers)]
proc getRookMagic*(square: Square): MagicEntry =
## Returns the magic entry for a rook
## on the given square
return ROOK_MAGICS[square.uint]
proc getBishopMagic*(square: Square): MagicEntry =
## Returns the magic entry for a bishop
## on the given square
return BISHOP_MAGICS[square.uint]
# Precomputed blocker masks. Only pieces on these bitboards
@ -188,7 +200,7 @@ func tryOffset(square: Square, df, dr: SomeInteger): Square =
return makeSquare(rank + dr, file + df)
proc getMoveSet*(kind: PieceKind, square: Square, blocker: Bitboard): Bitboard =
proc getMoveset*(kind: PieceKind, square: Square, blocker: Bitboard): Bitboard =
## A naive implementation of sliding attacks. Returns the moves that can
## be performed from the given piece at the given square with the given
## blocker mask
@ -223,8 +235,8 @@ proc attemptMagicTableCreation(kind: PieceKind, square: Square, entry: MagicEntr
# for several different blocker configurations, as
# many of them (while different) produce the same
# results
var moves = kind.getMoveSet(square, blocker)
if result.table[index] == 0:
var moves = kind.getMoveset(square, blocker)
if result.table[index] == Bitboard(0):
# No entry here, yet, so no problem!
result.table[index] = moves
elif result.table[index] != moves:
@ -265,7 +277,7 @@ proc findMagic(kind: PieceKind, square: Square, indexBits: uint8): tuple[entry:
# Again, this is stolen from the article. A magic number
# is only useful if it has high bit sparsity, so we AND
# together a bunch of random values to get a number that's
# hopefully better
# hopefully better than a single one
let
magic = rand.next() and rand.next() and rand.next()
entry = MagicEntry(mask: mask, value: magic, indexBits: indexBits)

View File

@ -1,11 +1,6 @@
import std/strutils
import std/strformat
import bitboards
import magics
import pieces
import moves

View File

@ -0,0 +1,34 @@
import bitboards
import magics
# Stolen from https://github.com/Ciekce/voidstar/blob/main/src/rays.rs :D
proc computeRaysBetweenSquares: array[64, array[64, Bitboard]] =
## Computes all sliding rays between each pair of squares
## in the chessboard
for i in 0..63:
let
source = Square(i)
sourceBitboard = source.toBitboard()
rooks = getRookMoves(source, Bitboard(0))
bishops = getBishopMoves(source, Bitboard(0))
for j in 0..63:
let target = Square(j)
if target == source:
result[i][j] = Bitboard(0)
else:
let targetBitboard = target.toBitboard()
if rooks.contains(target):
result[i][j] = getRookMoves(source, targetBitboard) and getRookMoves(target, sourceBitboard)
elif bishops.contains(target):
result[i][j] = getBishopMoves(source, targetBitboard) and getBishopMoves(target, sourceBitboard)
else:
result[i][j] = Bitboard(0)
let BETWEEN_RAYS = computeRaysBetweenSquares()
proc getRayBetween*(source, target: Square): Bitboard {.inline.} = BETWEEN_RAYS[source.int][target.int]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long