6.9 KiB
Magic Bitboard Design Guide for a Zig Chess Engine
Overview
This document describes the implementation strategy for sliding-piece move generation using Magic Bitboards in Zig.
The goal is:
- Extremely fast rook/bishop/queen move generation
- All move tables precomputed at startup or compile time
- Efficient runtime lookups using:
- Bit masks
- Bitwise operations
- Multiplication by magic numbers
- Table indexing
This document assumes:
- Board representation uses 64-bit bitboards (
u64) - One bitboard per piece type
- Separate white and black occupancy bitboards
- Additional metadata stored separately, such as castling and en passant
References for follow-up:
- Chess Programming Wiki, “Magic Bitboards”: https://www.chessprogramming.org/Magic_Bitboards
- Chess Programming Wiki, “Sliding Piece Attacks”: https://www.chessprogramming.org/Sliding_Piece_Attacks
- Chess Programming Wiki, “Bitboards”: https://www.chessprogramming.org/Bitboards
1. Board Representation
Use one u64 per piece type.
Example:
const Board = struct {
white_pawns: u64,
white_knights: u64,
white_bishops: u64,
white_rooks: u64,
white_queens: u64,
white_king: u64,
black_pawns: u64,
black_knights: u64,
black_bishops: u64,
black_rooks: u64,
black_queens: u64,
black_king: u64,
white_occ: u64,
black_occ: u64,
all_occ: u64,
};
2. Square Numbering
Recommended:
A1 = 0
B1 = 1
...
H1 = 7
A2 = 8
...
H8 = 63
This layout aligns naturally with white perspective, rank/file math, and bit shifting.
3. Sliding Piece Basics
Magic bitboards are only needed for:
- Rooks
- Bishops
- Queens
Knights, kings, and pawns can use precomputed attack masks or direct bit operations.
4. Relevant Occupancy Masks
Each rook/bishop square has a relevant occupancy mask.
This mask contains only squares that can block movement.
Board edges are excluded because edge blockers do not affect how far the slider can move; they create redundant occupancy states.
Example: rook on D4.
Relevant mask includes:
D5 D6 D7
D3 D2
C4 B4
E4 F4 G4
Not included:
D8
D1
A4
H4
5. Runtime Occupancy Extraction
At runtime:
const blockers = board.all_occ & rook_masks[square];
This isolates only blockers relevant to the rook.
6. Magic Bitboard Lookup Formula
Core formula:
index = (blockers * magic) >> shift;
Where:
blockers= masked occupancymagic= precomputed magic numbershift= reduces result to table index size
7. Why Multiplication Works
The multiplication mixes blocker bits into a pseudo-random pattern.
A good magic number guarantees that every possible blocker configuration maps to a usable table index. In practice, collisions are allowed only when the colliding occupancies produce the same attack bitboard.
8. Why Shifting Is Needed
Multiplication produces a 64-bit result. We only need enough bits to index the move table.
Example:
4096 occupancy states -> need 12 bits
shift = 64 - 12
Formula:
index = (blockers *% magic) >> (64 - relevant_bits);
Use Zig wrapping multiplication (*%) for magic indexing.
9. Move Tables
Each square has attack entries:
rook_attacks[64][N]
bishop_attacks[64][N]
Where N depends on occupancy combinations.
Typical upper bounds:
- Rook: up to 4096 entries per square
- Bishop: up to 512 entries per square
Each entry stores a u64 attack bitboard.
10. Runtime Move Generation
Rook:
const blockers = board.all_occ & rook_masks[square];
const index = (blockers *% rook_magics[square]) >> rook_shifts[square];
const moves = rook_attacks[square][index];
Bishop:
const blockers = board.all_occ & bishop_masks[square];
const index = (blockers *% bishop_magics[square]) >> bishop_shifts[square];
const moves = bishop_attacks[square][index];
Queen:
const queen_moves = rook_moves | bishop_moves;
Use OR (|), not AND (&).
11. Friendly Piece Removal
Sliding attacks include friendly occupied squares. Remove illegal destinations:
const legal_moves = attacks & ~friendly_occ;
12. Captures
Captures naturally remain in the move set because sliding stops on enemy blockers and includes the enemy blocker square.
To isolate captures:
const captures = legal_moves & enemy_occ;
13. Generating Relevant Occupancy Masks
Rook rays:
- North
- South
- East
- West
Bishop rays:
- North-east
- North-west
- South-east
- South-west
For magic relevant occupancy masks, exclude edge squares.
14. Generating All Occupancy Variations
If a mask has n relevant bits, then occupancy count is:
2^n
Example:
12 relevant bits -> 4096 occupancy states
Generate every occupancy subset.
15. Generating Attack Tables
For each occupancy subset:
- Simulate sliding movement.
- Stop when a blocker is encountered.
- Include the blocker square.
- Save resulting move bitboard.
16. Magic Number Generation
Magic generation is brute force.
Algorithm for a square:
- Generate all occupancies.
- Generate all attacks.
- Pick random
u64candidate magic. - Test
(blockers *% magic) >> shift. - Ensure every occupancy maps without harmful collision.
- If collision is harmful, reject magic and try another.
17. Zig-Specific Caveats
Use wrapping multiplication:
const index = (blockers *% magic) >> shift;
Normal multiplication may trap in safety-checked modes if overflow occurs.
18. Zig Integer Types
Use explicit types everywhere:
u64
u32
usize
Avoid implicit casts.
19. Compile-Time Generation
Zig can generate tables at compile time. Runtime generation is also useful while learning and debugging.
Potential workflow:
- Runtime utility searches for magics and prints/generated tables.
- Generated constants are reviewed.
- Final engine uses embedded constants and precomputed attack tables.
20. Memory Usage
Approximate attack table RAM usage:
| Table | Size |
|---|---|
| Rook attacks | ~800 KB |
| Bishop attacks | ~40 KB |
| Total | <1 MB |
21. Suggested Development Order
Phase 1
Implement:
- square numbering
- bitboard helpers
- occupancy masks
Phase 2
Implement:
- rook ray generation
- bishop ray generation
Phase 3
Implement:
- occupancy subset generation
- attack generation
Phase 4
Implement:
- brute-force magic finder
Phase 5
Implement:
- runtime lookup system
Phase 6
Validate against known positions/FENs.
22. Validation Tests
Verify:
- edge squares
- center squares
- empty board
- fully blocked board
- single blocker
- captures
- friendly blockers
- queen union correctness
23. Final Runtime Formula Summary
const blockers = board.all_occ & mask;
const index = (blockers *% magic) >> shift;
const attacks = attack_table[index];
const legal_moves = attacks & ~friendly_occ;
Queen:
const queen_moves = rook_moves | bishop_moves;