389 lines
6.9 KiB
Markdown
389 lines
6.9 KiB
Markdown
# 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:
|
|
|
|
```zig
|
|
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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
D5 D6 D7
|
|
D3 D2
|
|
C4 B4
|
|
E4 F4 G4
|
|
```
|
|
|
|
Not included:
|
|
|
|
```text
|
|
D8
|
|
D1
|
|
A4
|
|
H4
|
|
```
|
|
|
|
## 5. Runtime Occupancy Extraction
|
|
|
|
At runtime:
|
|
|
|
```zig
|
|
const blockers = board.all_occ & rook_masks[square];
|
|
```
|
|
|
|
This isolates only blockers relevant to the rook.
|
|
|
|
## 6. Magic Bitboard Lookup Formula
|
|
|
|
Core formula:
|
|
|
|
```zig
|
|
index = (blockers * magic) >> shift;
|
|
```
|
|
|
|
Where:
|
|
|
|
- `blockers` = masked occupancy
|
|
- `magic` = precomputed magic number
|
|
- `shift` = 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:
|
|
|
|
```text
|
|
4096 occupancy states -> need 12 bits
|
|
shift = 64 - 12
|
|
```
|
|
|
|
Formula:
|
|
|
|
```zig
|
|
index = (blockers *% magic) >> (64 - relevant_bits);
|
|
```
|
|
|
|
Use Zig wrapping multiplication (`*%`) for magic indexing.
|
|
|
|
## 9. Move Tables
|
|
|
|
Each square has attack entries:
|
|
|
|
```zig
|
|
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:
|
|
|
|
```zig
|
|
const blockers = board.all_occ & rook_masks[square];
|
|
const index = (blockers *% rook_magics[square]) >> rook_shifts[square];
|
|
const moves = rook_attacks[square][index];
|
|
```
|
|
|
|
Bishop:
|
|
|
|
```zig
|
|
const blockers = board.all_occ & bishop_masks[square];
|
|
const index = (blockers *% bishop_magics[square]) >> bishop_shifts[square];
|
|
const moves = bishop_attacks[square][index];
|
|
```
|
|
|
|
Queen:
|
|
|
|
```zig
|
|
const queen_moves = rook_moves | bishop_moves;
|
|
```
|
|
|
|
Use OR (`|`), not AND (`&`).
|
|
|
|
## 11. Friendly Piece Removal
|
|
|
|
Sliding attacks include friendly occupied squares. Remove illegal destinations:
|
|
|
|
```zig
|
|
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:
|
|
|
|
```zig
|
|
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:
|
|
|
|
```text
|
|
2^n
|
|
```
|
|
|
|
Example:
|
|
|
|
```text
|
|
12 relevant bits -> 4096 occupancy states
|
|
```
|
|
|
|
Generate every occupancy subset.
|
|
|
|
## 15. Generating Attack Tables
|
|
|
|
For each occupancy subset:
|
|
|
|
1. Simulate sliding movement.
|
|
2. Stop when a blocker is encountered.
|
|
3. Include the blocker square.
|
|
4. Save resulting move bitboard.
|
|
|
|
## 16. Magic Number Generation
|
|
|
|
Magic generation is brute force.
|
|
|
|
Algorithm for a square:
|
|
|
|
1. Generate all occupancies.
|
|
2. Generate all attacks.
|
|
3. Pick random `u64` candidate magic.
|
|
4. Test `(blockers *% magic) >> shift`.
|
|
5. Ensure every occupancy maps without harmful collision.
|
|
6. If collision is harmful, reject magic and try another.
|
|
|
|
## 17. Zig-Specific Caveats
|
|
|
|
Use wrapping multiplication:
|
|
|
|
```zig
|
|
const index = (blockers *% magic) >> shift;
|
|
```
|
|
|
|
Normal multiplication may trap in safety-checked modes if overflow occurs.
|
|
|
|
## 18. Zig Integer Types
|
|
|
|
Use explicit types everywhere:
|
|
|
|
```zig
|
|
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:
|
|
|
|
1. Runtime utility searches for magics and prints/generated tables.
|
|
2. Generated constants are reviewed.
|
|
3. 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
|
|
|
|
```zig
|
|
const blockers = board.all_occ & mask;
|
|
const index = (blockers *% magic) >> shift;
|
|
const attacks = attack_table[index];
|
|
const legal_moves = attacks & ~friendly_occ;
|
|
```
|
|
|
|
Queen:
|
|
|
|
```zig
|
|
const queen_moves = rook_moves | bishop_moves;
|
|
```
|