From 6a548bf3723fe94212727f01f4dc931c877632a3 Mon Sep 17 00:00:00 2001 From: nocturn9x Date: Fri, 19 Apr 2024 00:03:19 +0200 Subject: [PATCH] Magic bitboards can now be found (untested) --- src/Chess/magics.nim | 156 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 129 insertions(+), 27 deletions(-) diff --git a/src/Chess/magics.nim b/src/Chess/magics.nim index 8b7748a..dbf8909 100644 --- a/src/Chess/magics.nim +++ b/src/Chess/magics.nim @@ -1,19 +1,18 @@ ## 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 pieces import std/random +import std/bitops export pieces export bitboards -randomize() - type MagicEntry = object @@ -23,8 +22,10 @@ type indexBits: uint8 -proc generateRookBlockers: array[64, Bitboard] {.compileTime.} = - ## Generates all blocker masks for rooks +# 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 @@ -36,33 +37,37 @@ proc generateRookBlockers: array[64, Bitboard] {.compileTime.} = last = makeSquare(rank, 7).toBitboard() while true: current = current.rightRelativeTo(White) - if current == last or current == 0: + 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 or current == 0: + 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 or current == 0: + 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 or current == 0: + if (current == last and blockers) or current == 0: break 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 file in 0..7: # Generate all possible movement masks @@ -95,11 +100,16 @@ func generateBishopBlockers: array[64, Bitboard] {.compileTime.} = if current == 0: break result[i] = result[i] or current - # Mask off the edges - 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) + 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.} = @@ -108,7 +118,7 @@ func getIndex(magic: MagicEntry, blockers: Bitboard): uint {.inline.} = let blockers = blockers and magic.mask hash = blockers * magic.value - index = hash shl (64 - magic.indexBits) + index = hash shr (64 - magic.indexBits) 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, # regardless of color const - ROOK_BLOCKERS* = generateRookBlockers() - BISHOP_BLOCKERS* = generateBishopBlockers() + # 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 -# func findMagic(slider: PieceKind, square: Square, indexBits: uint8): tuple[magic: MagicEntry, moves: seq[Bitboard]] = -# ## Given a slider piece, its starting square and the number of desired -# ## index bits, find a magic number that perfectly maps all the possible -# ## sliding moves for that piece at that square into an appropriately sized -# ## perfect hash table with at most 2^indexBits entries -# var mask: Bitboard -# case slider: -# of Rook: -# mask = ROOK_BLOCKERS[square.uint] +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 \ No newline at end of file