CPG/src/Chess/magics.nim

260 lines
10 KiB
Nim

## Low-level magic bitboard stuff
# Blatantly stolen from this amazing article: https://analog-hors.github.io/site/magic-bitboards/
import bitboards
import pieces
import std/random
import std/bitops
export pieces
export bitboards
type
MagicEntry = object
## A magic bitboard entry
mask: Bitboard
value: uint64
indexBits: uint8
# Yeah uh, don't look too closely at this...
proc generateRookMasks(blockers: bool = false): array[64, Bitboard] {.compileTime.} =
## Generates all movement masks for rooks (only generates
## blocker masks if blockers equals true)
for rank in 0..7:
for file in 0..7:
let
square = makeSquare(rank, file)
i = square.int
bitboard = square.toBitboard()
var
current = bitboard
last = makeSquare(rank, 7).toBitboard()
while true:
current = current.rightRelativeTo(White)
if (current == last and blockers) or current == 0:
break
result[i] = result[i] or current
current = bitboard
last = makeSquare(rank, 0).toBitboard()
while true:
current = current.leftRelativeTo(White)
if (current == last and blockers) or current == 0:
break
result[i] = result[i] or current
current = bitboard
last = makeSquare(0, file).toBitboard()
while true:
current = current.forwardRelativeTo(White)
if (current == last and blockers) or current == 0:
break
result[i] = result[i] or current
current = bitboard
last = makeSquare(7, file).toBitboard()
while true:
current = current.backwardRelativeTo(White)
if (current == last and blockers) or current == 0:
break
result[i] = result[i] or current
# Okay this is fucking clever tho. Which is obvious, considering I didn't come up with it.
# Or, well, the trick at the end isn't mine
func generateBishopMasks(blockers = false): array[64, Bitboard] {.compileTime.} =
## Generates all movement masks for bishops (only generates
## blocker masks if blockers equals true)
for rank in 0..7:
for file in 0..7:
# Generate all possible movement masks
let
square = makeSquare(rank, file)
i = square.int
bitboard = square.toBitboard()
var
current = bitboard
while true:
current = current.backwardRightRelativeTo(White)
if current == 0:
break
result[i] = result[i] or current
current = bitboard
while true:
current = current.backwardLeftRelativeTo(White)
if current == 0:
break
result[i] = result[i] or current
current = bitboard
while true:
current = current.forwardLeftRelativeTo(White)
if current == 0:
break
result[i] = result[i] or current
current = bitboard
while true:
current = current.forwardRightRelativeTo(White)
if current == 0:
break
result[i] = result[i] or current
if blockers:
# Mask off the edges
# Yeah, this is the trick. I know, not a big deal, but
# I'm an idiot so what do I know. Credit to @__arandomnoob
# on the engine programming discord server for the tip!
result[i] = result[i] and not getFileMask(0)
result[i] = result[i] and not getFileMask(7)
result[i] = result[i] and not getRankMask(0)
result[i] = result[i] and not getRankMask(7)
func getIndex(magic: MagicEntry, blockers: Bitboard): uint {.inline.} =
## Computes an index into the magic bitboard table using
## the given magic entry and the blockers bitboard
let
blockers = blockers and magic.mask
hash = blockers * magic.value
index = hash shr (64 - magic.indexBits)
return index.uint
# Magic number tables and their corresponding moves
var
ROOK_MAGICS: seq[MagicEntry]
ROOK_MOVES: array[64, seq[Bitboard]]
BISHOP_MAGICS: seq[MagicEntry]
BISHOP_MOVES: array[64, seq[Bitboard]]
proc getRookMoves(square: Square, blockers: Bitboard): Bitboard =
## Returns the move bitboard for the rook at the given
## square with the given blockers bitboard
let
magic = ROOK_MAGICS[square.uint]
moves = ROOK_MOVES[square.uint]
return moves[getIndex(magic, blockers)]
proc getBishopMoves(square: Square, blockers: Bitboard): Bitboard =
## Returns the move bitboard for the bishop at the given
## square with the given blockers bitboard
let
magic = BISHOP_MAGICS[square.uint]
moves = BISHOP_MOVES[square.uint]
return moves[getIndex(magic, blockers)]
# Precomputed blocker masks. Only pieces on these bitboards
# are actually able to block the movement of a sliding piece,
# regardless of color
const
# mfw Nim's compile time VM *graciously* allows me to call perfectly valid code: :D
ROOK_BLOCKERS = generateRookMasks(blockers=true)
BISHOP_BLOCKERS = generateBishopMasks(blockers=true)
ROOK_MOVEMENTS = generateRookMasks()
BISHOP_MOVEMENTS = generateBishopMasks()
func getRelevantBlockers(kind: PieceKind, square: Square): Bitboard =
## Returns the relevant blockers mask for the given piece
## type at the given square
case kind:
of Rook:
return ROOK_BLOCKERS[square.uint]
of Bishop:
return BISHOP_BLOCKERS[square.uint]
else:
discard
proc attemptMagicTableCreation(kind: PieceKind, square: Square, entry: MagicEntry): tuple[success: bool, table: seq[Bitboard]] =
## Tries to create a magic bitboard table for the given piece
## at the given square using the provided magic entry. Returns
## (true, table) if successful, (false, empty) otherwise
# Initialize a new sequence with capacity 2^indexBits
result.table = newSeqOfCap[Bitboard](1 shl entry.indexBits)
result.success = true
for _ in 0..result.table.capacity:
result.table.add(Bitboard(0))
# Iterate all possible blocker configurations
for blocker in entry.mask.subsets():
let index = getIndex(entry, blocker)
# Get the moves the piece can make from the given
# square with this specific blocker configuration
var moves: Bitboard
case kind:
of Rook:
moves = ROOK_MOVEMENTS[square.uint]
of Bishop:
moves = BISHOP_MOVEMENTS[square.uint]
else:
discard
if result.table[index] == 0:
# No entry here, yet, so no problem!
result.table[index] = moves
elif (result.table[index] or blocker) != moves:
# We found a non-constructive collision, fail :(
# Notes for future self: A "constructive" collision
# is one which doesn't affect the result, because some
# blocker configurations will map to the same set of
# resulting moves. This actually improves our chances
# of building our lovely perfect-hash-function-as-a-table
# because we don't actually need to map *all* blocker
# configurations uniquely, just the ones that lead to
# a different set of moves. This happens because we are
# keeping track of a lot of redundant blockers that are
# beyond squares a slider piece could go to: we could reduce
# the table size if we didn't account for those, but this
# would require us to have a loop going in every sliding
# direction to find what pieces are actually blocking the
# the slider's path and which aren't for every single lookup,
# which is the whole thing we're trying to avoid by doing all
# this magic bitboard stuff and it is basically how the old mailbox
# move generator worked anyway (thanks to Sebastian Lague on YouTube
# for the insight)
return (false, @[])
# We have found a constructive collision: all good
proc findMagic(kind: PieceKind, square: Square, indexBits: uint8): tuple[entry: MagicEntry, table: seq[Bitboard]] =
## Constructs a (sort of) perfect hash function that fits all
## the possible blocking configurations for the given piece at
## the given square into a table of size 2^indexBits
let mask = kind.getRelevantBlockers(square)
# The best way to find a good magic number? Literally just
# bruteforce the shit out of it!
var rand = initRand()
while true:
# Again, this is stolen from the article. A magic number
# is only useful if it is small (i.e. has a low number of
# bits set), so we AND together 3 random numbers to get a
# number with (hopefully) not that many bits set
let
magic = rand.next() and rand.next() and rand.next()
entry = MagicEntry(mask: mask, value: magic, indexBits: indexBits)
var attempt = attemptMagicTableCreation(kind, square, entry)
if attempt.success:
return (entry, attempt.table)
# Not successful? No problem, we'll just try again until
# the heat death of the universe!
import std/strformat
for i in 0..63:
let square = Square(i)
var result = findMagic(Rook, square, Rook.getRelevantBlockers(square).uint64.countSetBits().uint8)
echo &"Found magic bitboard for rooks at {square}"
ROOK_MAGICS.add(result.entry)
ROOK_MOVES[i] = result.table
result = findMagic(Bishop, square, Bishop.getRelevantBlockers(square).uint64.countSetBits().uint8)
echo &"Found magic bitboard for bishops at {square}"
BISHOP_MAGICS.add(result.entry)
BISHOP_MOVES[i] = result.table