zig-chess/docs/magic-bitboards-design.md

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:

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 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:

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:

  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:

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:

  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

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;