Magic bitboards can now be found (untested)
This commit is contained in:
parent
e3225bb4bc
commit
9e7ce5640e
|
@ -1,19 +1,18 @@
|
||||||
## Low-level magic bitboard stuff
|
## Low-level magic bitboard stuff
|
||||||
|
|
||||||
# Stolen from this amazing article: https://analog-hors.github.io/site/magic-bitboards/
|
# Blatantly stolen from this amazing article: https://analog-hors.github.io/site/magic-bitboards/
|
||||||
|
|
||||||
import bitboards
|
import bitboards
|
||||||
import pieces
|
import pieces
|
||||||
|
|
||||||
|
|
||||||
import std/random
|
import std/random
|
||||||
|
import std/bitops
|
||||||
|
|
||||||
|
|
||||||
export pieces
|
export pieces
|
||||||
export bitboards
|
export bitboards
|
||||||
|
|
||||||
randomize()
|
|
||||||
|
|
||||||
|
|
||||||
type
|
type
|
||||||
MagicEntry = object
|
MagicEntry = object
|
||||||
|
@ -23,8 +22,10 @@ type
|
||||||
indexBits: uint8
|
indexBits: uint8
|
||||||
|
|
||||||
|
|
||||||
proc generateRookBlockers: array[64, Bitboard] {.compileTime.} =
|
# Yeah uh, don't look too closely at this...
|
||||||
## Generates all blocker masks for rooks
|
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 rank in 0..7:
|
||||||
for file in 0..7:
|
for file in 0..7:
|
||||||
let
|
let
|
||||||
|
@ -36,33 +37,37 @@ proc generateRookBlockers: array[64, Bitboard] {.compileTime.} =
|
||||||
last = makeSquare(rank, 7).toBitboard()
|
last = makeSquare(rank, 7).toBitboard()
|
||||||
while true:
|
while true:
|
||||||
current = current.rightRelativeTo(White)
|
current = current.rightRelativeTo(White)
|
||||||
if current == last or current == 0:
|
if (current == last and blockers) or current == 0:
|
||||||
break
|
break
|
||||||
result[i] = result[i] or current
|
result[i] = result[i] or current
|
||||||
current = bitboard
|
current = bitboard
|
||||||
last = makeSquare(rank, 0).toBitboard()
|
last = makeSquare(rank, 0).toBitboard()
|
||||||
while true:
|
while true:
|
||||||
current = current.leftRelativeTo(White)
|
current = current.leftRelativeTo(White)
|
||||||
if current == last or current == 0:
|
if (current == last and blockers) or current == 0:
|
||||||
break
|
break
|
||||||
result[i] = result[i] or current
|
result[i] = result[i] or current
|
||||||
current = bitboard
|
current = bitboard
|
||||||
last = makeSquare(0, file).toBitboard()
|
last = makeSquare(0, file).toBitboard()
|
||||||
while true:
|
while true:
|
||||||
current = current.forwardRelativeTo(White)
|
current = current.forwardRelativeTo(White)
|
||||||
if current == last or current == 0:
|
if (current == last and blockers) or current == 0:
|
||||||
break
|
break
|
||||||
result[i] = result[i] or current
|
result[i] = result[i] or current
|
||||||
current = bitboard
|
current = bitboard
|
||||||
last = makeSquare(7, file).toBitboard()
|
last = makeSquare(7, file).toBitboard()
|
||||||
while true:
|
while true:
|
||||||
current = current.backwardRelativeTo(White)
|
current = current.backwardRelativeTo(White)
|
||||||
if current == last or current == 0:
|
if (current == last and blockers) or current == 0:
|
||||||
break
|
break
|
||||||
result[i] = result[i] or current
|
result[i] = result[i] or current
|
||||||
|
|
||||||
|
|
||||||
func generateBishopBlockers: array[64, Bitboard] {.compileTime.} =
|
# 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 rank in 0..7:
|
||||||
for file in 0..7:
|
for file in 0..7:
|
||||||
# Generate all possible movement masks
|
# Generate all possible movement masks
|
||||||
|
@ -95,7 +100,12 @@ func generateBishopBlockers: array[64, Bitboard] {.compileTime.} =
|
||||||
if current == 0:
|
if current == 0:
|
||||||
break
|
break
|
||||||
result[i] = result[i] or current
|
result[i] = result[i] or current
|
||||||
|
if blockers:
|
||||||
# Mask off the edges
|
# 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(0)
|
||||||
result[i] = result[i] and not getFileMask(7)
|
result[i] = result[i] and not getFileMask(7)
|
||||||
result[i] = result[i] and not getRankMask(0)
|
result[i] = result[i] and not getRankMask(0)
|
||||||
|
@ -108,7 +118,7 @@ func getIndex(magic: MagicEntry, blockers: Bitboard): uint {.inline.} =
|
||||||
let
|
let
|
||||||
blockers = blockers and magic.mask
|
blockers = blockers and magic.mask
|
||||||
hash = blockers * magic.value
|
hash = blockers * magic.value
|
||||||
index = hash shl (64 - magic.indexBits)
|
index = hash shr (64 - magic.indexBits)
|
||||||
return index.uint
|
return index.uint
|
||||||
|
|
||||||
|
|
||||||
|
@ -142,17 +152,109 @@ proc getBishopMoves(square: Square, blockers: Bitboard): Bitboard =
|
||||||
# are actually able to block the movement of a sliding piece,
|
# are actually able to block the movement of a sliding piece,
|
||||||
# regardless of color
|
# regardless of color
|
||||||
const
|
const
|
||||||
ROOK_BLOCKERS* = generateRookBlockers()
|
# mfw Nim's compile time VM *graciously* allows me to call perfectly valid code: :D
|
||||||
BISHOP_BLOCKERS* = generateBishopBlockers()
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# func findMagic(slider: PieceKind, square: Square, indexBits: uint8): tuple[magic: MagicEntry, moves: seq[Bitboard]] =
|
for i in 0..63:
|
||||||
# ## Given a slider piece, its starting square and the number of desired
|
let square = Square(i)
|
||||||
# ## index bits, find a magic number that perfectly maps all the possible
|
var result = findMagic(Rook, square, Rook.getRelevantBlockers(square).uint64.countSetBits().uint8)
|
||||||
# ## sliding moves for that piece at that square into an appropriately sized
|
echo &"Found magic bitboard for rooks at {square}"
|
||||||
# ## perfect hash table with at most 2^indexBits entries
|
ROOK_MAGICS.add(result.entry)
|
||||||
# var mask: Bitboard
|
ROOK_MOVES[i] = result.table
|
||||||
# case slider:
|
result = findMagic(Bishop, square, Bishop.getRelevantBlockers(square).uint64.countSetBits().uint8)
|
||||||
# of Rook:
|
echo &"Found magic bitboard for bishops at {square}"
|
||||||
# mask = ROOK_BLOCKERS[square.uint]
|
BISHOP_MAGICS.add(result.entry)
|
||||||
|
BISHOP_MOVES[i] = result.table
|
Loading…
Reference in New Issue