A clear, practical introduction to magic numbers in chess programming — what they are, why they exist, and how to put them to work.
This article aims to be concise and accessible. For those who prefer a deeper dive, the following resources cover the topic with great rigour:
In a chess engine, computing the legal moves for sliding pieces — bishops, rooks, and queens — during a deep search is one of the most performance-critical operations. Naïvely iterating over the board square by square on every node is prohibitively slow.
The solution is precomputation. We calculate all possible moves for every piece on every square ahead of time and store them as 64-bit integers called bitboard masks. During the actual search, retrieving the moves for any position then becomes a simple table lookup — extremely fast.
This technique applies to bishops (moving diagonally) and rooks (moving along ranks and files), and therefore to queens as well (since queen moves are the union of the two). In deep searches with millions of nodes, the performance gain is substantial.
The key insight that makes the problem tractable is to break it into independent sub-problems. We solve it one square at a time: if we can find the magic for a bishop sitting on E4, we can apply the same method to all 64 squares independently. We also treat bishops and rooks separately — their movement patterns are different, but the approach is identical.
Start with an empty board and place a bishop on E4. The squares it can reach are shown below (the black dots represent all potential destination squares, ignoring any blocking pieces for now):
In a real game, other pieces will stand on those diagonals and block the bishop's path. Place some blocking pieces on the board. The blue dots now mark the actually legal moves — the squares the bishop can reach before being stopped by a blocker:
Note that we do not distinguish between friendly and enemy pieces at this stage.
Any piece stops the bishop's ray. We can filter out captures of our own pieces
afterward with a simple bitmask: moves & ~ourPieces.
What we care about here is purely where the bishop can go given the occupancy.
Now imagine enumerating every possible arrangement of blocking pieces on the bishop's diagonals — every permutation of occupancy — and recording the corresponding set of legal moves. This gives us a two-column table:
| Blocker occupancy (input) | Legal bishop moves (output) |
|---|---|
| Blockers on A8, B7, C6 … | Can move to D5, F3 … |
| Blockers on B1, F5 … | Can move to C2, D3, D5 … |
| … all remaining occupancy permutations … | |
This table is our ground truth. A table generation tool is available if you want to inspect the data directly or export it to a database.
The table works — but querying a general database during a search is far too slow. We need a mathematical function that maps any occupancy bitboard instantly to the correct row of legal moves. That function is what the magic number provides.
Expand the table by inserting a middle column — the computed index. This column will serve as the array index into our in-memory move table:
| Blocker occupancy | Computed index | Legal moves |
|---|---|---|
| occupancy₁ | — | moveset₁ |
| occupancy₂ | — | moveset₂ |
| … all permutations … | ||
The goal is to find a mathematical function that fills in the index column such that no two distinct occupancies that produce different move sets share the same index. Collisions between occupancies that happen to produce identical legal moves are acceptable — they can share a slot.
The search procedure works as follows:
The underlying goal is to find an array large enough to accommodate one unique entry per distinct move set, indexed without destructive collisions. Allowing a larger array (more bits) makes valid magics easier to find; a tighter array requires rarer, harder-to-find numbers. Once a valid magic is found, the move array is populated once at engine startup and then queried in constant time during the search.
Why multiplication? It is the ideal operator for this purpose: it produces a result that is partly proportional and partly pseudo-random, spreading different inputs across a wide range of outputs efficiently. A subsequent right bit-shift then compresses the large 64-bit product down to a small array index.
The full formula for retrieving the bishop's legal moves from any position is:
Both MagicNumber and shiftNumber for each square can be
found by random trial: generate a 64-bit random number with few set bits, run the
validation procedure above, and repeat until a valid magic is found. Alternatively,
use precomputed values from established chess engines or generate them with a dedicated
tool — a
magic generator
is available, and the source code is also on
GitHub.
A common optimisation is to exclude the edge squares of the diagonals from the occupancy mask. A piece on the board edge always stops the ray regardless, so it carries no information — including it only wastes index bits. This is why you will often see the inner 6×6 region (B2–G7) used as the relevant occupancy area, rather than the full diagonals. The resulting tables are somewhat smaller and the magics slightly easier to find.
The magic number technique is not limited to bishops and rooks. It generalises to any situation where a lookup from a compact, occupancy-derived key is needed — including advanced pawn move generation, pin detection, and check evasion masks. Many parts of a modern chess engine can be accelerated using the same principle.
Furthermore, there are alternative hash functions beyond plain multiplication. Some engines use XOR-folding, PEXT (a BMI2 hardware instruction available on modern x86 CPUs that extracts bits directly), or other bit-manipulation tricks. Each has trade-offs in portability, table size, and CPU-specific speed.
| Reality % | Claim / Aspect — Description |
|---|---|
| 98 % | The technique genuinely works. Magic bitboards are not theoretical curiosities — they are deployed in Stockfish, Komodo, and virtually every competitive open-source engine. The performance benefit over naive loop-based move generation is real and significant, typically 2–5× faster move generation throughput. |
| 60 % | Finding magic numbers is "quick and easy." For most squares it is fast. However, some squares — particularly corners and edges where the occupancy mask is dense — can require millions of random candidates before a valid magic is found. On slower hardware this may take minutes. Pre-published magic tables (e.g. from Stockfish) are the practical answer; generating your own is an interesting exercise, not a requirement. |
| 70 % | Memory cost is negligible. Partially true. A standard magic move table for bishops and rooks combined occupies roughly 800 KB–2 MB depending on table-sharing strategy. On a modern desktop this is trivial. On embedded hardware, microcontrollers, or cache-sensitive architectures it can matter meaningfully — the table may not fit in L1 or L2 cache, causing cache misses that erode the speed advantage. |
| 40 % |
Multiplication is the best possible hash function for this.
It is a good general-purpose choice, but on CPUs that support Intel BMI2's
PEXT instruction, hardware bit-extraction replaces the multiply-and-shift
with a single instruction — no magic number needed at all. Modern x86-64 engines
often compile two code paths and choose at runtime. The multiply approach remains
the most portable option, but it is not universally optimal.
|
| 75 % | The concept generalises easily to other move types. The principle extends well to rooks, queens (bishops + rooks), and some pawn operations. However, for pieces with fixed, non-sliding movement patterns (knights, kings) there is no occupancy dependency, and a simple direct-indexed table without any magic is already optimal. The technique is powerful precisely where ray-casting through blockers is involved. |
| 35 % | Implementing magic bitboards is beginner-friendly. The high-level concept is approachable, but a correct implementation requires solid understanding of bitwise arithmetic, 64-bit integer semantics, unsigned overflow behaviour (especially in C/C++), and careful initialisation order. Off-by-one errors in the shift count or incorrect occupancy masking produce silent, hard-to-debug move generation bugs. Recommended only after you have a working, tested naive move generator to validate against. |
| 95 % | Precomputing at startup is the right architecture. Generating all magic tables once when the engine initialises — rather than embedding them as compile-time constants — is sound engineering. It adds a small start-up cost (typically under 100 ms) but keeps the binary smaller and allows the engine to adapt the table size or strategy based on available RAM and CPU features detected at runtime. |
MagicNumber — the precomputed magic constant for this square
shiftNumber — (64 − number of index bits required for this square)
MoveTable — array populated at engine startup