# Copyright 2024 Mattia Giambirtone & All Contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ## 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 import std/tables import std/os import jsony export pieces export bitboards type MagicEntry = object ## A magic bitboard entry mask: Bitboard value: uint64 shift: uint8 # Yeah uh, don't look too closely at this... proc generateRookBlockers: array[64, Bitboard] {.compileTime.} = ## Generates all blocker masks for rooks 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 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: 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: 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: 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 generateBishopBlockers: array[64, Bitboard] {.compileTime.} = ## Generates all blocker masks for bishops 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 # 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 magic.shift return index.uint # Magic number tables and their corresponding moves var ROOK_MAGICS: array[64, MagicEntry] ROOK_MOVES: array[64, seq[Bitboard]] BISHOP_MAGICS: array[64, 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] movesAddr = addr ROOK_MOVES[square.uint] return movesAddr[][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] 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 # 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 = generateRookBlockers() BISHOP_BLOCKERS = generateBishopBlockers() func getRelevantBlockers*(kind: PieceKind, square: Square): Bitboard {.inline.} = ## 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 # Thanks analog :D const ROOK_DELTAS = [(1, 0), (0, -1), (-1, 0), (0, 1)] BISHOP_DELTAS = [(1, 1), (1, -1), (-1, -1), (-1, 1)] # These are technically (file, rank), but it's all symmetric anyway func tryOffset(square: Square, df, dr: SomeInteger): Square = let file = fileFromSquare(square) rank = rankFromSquare(square) if file + df notin 0..7: return nullSquare() if rank + dr notin 0..7: return nullSquare() return makeSquare(rank + dr, file + df) 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 result = Bitboard(0) let deltas = if kind == Rook: ROOK_DELTAS else: BISHOP_DELTAS for (file, rank) in deltas: var ray = square while not blocker.contains(ray): if (let shifted = ray.tryOffset(file, rank); shifted) != nullSquare(): ray = shifted result = result or ray.toBitboard() else: break 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 the right capacity result.table = newSeqOfCap[Bitboard](1 shl (64'u8 - entry.shift)) # Just a fast way of doing 2 ** n result.success = true for _ in 0..