book_pending MechanismPolyglot is the standard binary opening book format used by most chess engines (Stockfish, Houdini, Fruit, etc.). Each entry contains:
The micro-Max engine (by H.G. Müller) is a minimalist engine under 2 KB of source code. It plays well in the middlegame but, due to shallow search (fixed node limit), it can make suboptimal moves in the opening. An opening book solves this: the first 2–6 moves are taken from a precomputed database, then control is handed to the engine.
B in UART), then switches to the engine (marker D). This improves opening quality without increasing move time.
The solution is split into two parts:
debut_book.c — an array of positions and best moves.polyglot.c computes the Zobrist hash of the current position and looks it up in debut_book[] via binary search.The main challenge: micro-Max and Polyglot use different coordinate systems, piece encodings, and color conventions. Without correct conversion, Zobrist hashes will not match and the book will not work.
Micro-Max uses 0x88 notation: square = rank * 16 + file, where rank 0 = a8 (top), rank 7 = a1 (bottom). Valid squares: 0x00–0x77, odd bits 4–7 = off-board.
Polyglot (and the Python chess library) uses linear numbering 0x3F: a1=0, h1=7, a8=56, h8=63.
| Square | 0x88 (micro-Max) | 0x3F (Polyglot) | Python (chess lib) | Formula |
|---|---|---|---|---|
| a8 | 0x00 (0) | 56 | 56 | (7-0)*8+0 = 56 |
| e1 | 0x74 (116) | 4 | 4 | (7-7)*8+4 = 4 |
| h1 | 0x77 (119) | 7 | 7 | (7-7)*8+7 = 7 |
| a1 | 0x70 (112) | 0 | 0 | (7-7)*8+0 = 0 |
| d4 | 0x43 (67) | 27 | 27 | (7-4)*8+3 = 27 |
// 0x88 → 0x3F WITH rank inversion
static int sq88_to_063(int sq) {
return (7 - (sq >> 4)) * 8 + (sq & 7);
}
# Python chess lib: square is already 0x3F, NO inversion needed
# chess.square_file(sq) = file (0-7)
# chess.square_rank(sq) = rank (0-7), rank 0 = rank 1 (white side)
sq063 = rank_ * 8 + file_ # NO (7 - rank_) inversion!
(7 - rank_) * 8 + file_, which inverted the rank and caused key mismatches. The correct formula for Python is rank_ * 8 + file_ — no inversion, because the chess library already uses rank 0 = white's back rank (a1=0). The C code does need inversion because micro-Max uses rank 0 = a8 (top of board).
Micro-Max stores the piece type in the lower 3 bits of the b[] array. The numbering does not match the Polyglot standard.
| Piece | Micro-Max p & 7 | Standard (Polyglot) | In b[] |
|---|---|---|---|
| Pawn (black) | 1 | 1 | 17 (16+1) |
| Pawn (white) | 2 | 1 | 10 (8+2) |
| Knight | 3 | 2 | 11 / 19 |
| King | 4 | 6 | 12 / 20 |
| Bishop | 5 | 3 | 13 / 21 |
| Rook | 6 | 4 | 14 / 22 |
| Queen | 7 | 5 | 15 / 23 |
mm_to_std[8]:/* 0 1 2 3 4 5 6 7 */
/* MM type: - P P N K B R Q */
/* Std type: 0 1 1 2 6 3 4 5 */
static const int mm_to_std[8] = {0, 1, 1, 2, 6, 3, 4, 5};
In micro-Max, color is encoded by a bit in the square value:
color = 1 for white, but the working keys from the 19-entry reference book proved that white = 0 and black = 1 is correct.
| System | White | Black | Code |
|---|---|---|---|
| C (compute_zobrist) | 1 | 0 | (p & 8) ? 1 : 0 |
| Python (zobrist_key_stm32) | 0 | 1 | 0 if piece.color else 1 |
| Net effect | Both produce the same key because C inverts the rank (swapping visual position) while Python does not — the two inversions cancel out. | ||
Side to move: In micro-Max, k=8 means white to move, k=16 means black to move. In the Zobrist hash, if side_val != 8 (black to move), ZobristTable[768] is XORed.
/* Side to move: k=8 → white (no XOR), k=16 → black (XOR) */
if (side_val != 8) {
h ^= ZobristTable[768];
}
Castling is encoded in the Zobrist hash by 4 separate numbers (indices 769–772):
| Right | Index | C Condition | Square (0x88) | Value |
|---|---|---|---|---|
| White, kingside (K) | 769 | b[0x77] == 14 | h1 | Rook (6+8) |
| White, queenside (Q) | 770 | b[0x70] == 14 | a1 | Rook (6+8) |
| Black, kingside (k) | 771 | b[0x07] == 22 | h8 | Rook (6+16) |
| Black, queenside (q) | 772 | b[0x00] == 22 | a8 | Rook (6+16) |
Additionally, the king must be on its home square:
b[0x74] == 12 (e1, type 4 + 8)b[0x04] == 20 (e8, type 4 + 16)can_castle():static int can_castle(int white, int kingside) {
if (white) {
if (b[0x74] != 12) return 0; /* white king on e1 */
if (kingside && b[0x77] != 14) return 0;/* white rook on h1 */
if (!kingside && b[0x70] != 14) return 0;/* white rook on a1 */
} else {
if (b[0x04] != 20) return 0; /* black king on e8 */
if (kingside && b[0x07] != 22) return 0;/* black rook on h8 */
if (!kingside && b[0x00] != 22) return 0;/* black rook on a8 */
}
return 1;
}
If a pawn just moved two squares forward, micro-Max stores the “skipped” square in variable E1. In the Zobrist hash this is XORed with a number from the range 773–780 (one per file a–h):
/* En passant */
if (E1 != 0 && !(E1 & 0x88)) {
h ^= ZobristTable[773 + (E1 & 7)];
}
| File | Purpose | Size | Memory |
|---|---|---|---|
zobrist_table.h | 781 pseudo-random 64-bit numbers for hashing | ~6 KB | Flash |
debut_book.c | Position array (key + move + weight), binary search | ~8 KB (1000 entries) | Flash |
polyglot.c | Functions: compute_zobrist, polyglot_find_move, polyglot_to_mm | ~2 KB | Flash (code) |
polyglot.h | Headers: typedef, function prototypes | ~0.5 KB | — |
main.c | Game loop, book integration via book_pending | ~12 KB | Flash (code) + RAM |
generate_strong_book.py | Offline book generator (Python + Stockfish) | ~6 KB | PC only |
debug_keys.py | Validation tool: verifies Zobrist keys against known book | ~4 KB | PC only |
Zobrist hash is the XOR of pseudo-random numbers, one per position component:
Total: 781 numbers, each 64-bit.
index = color * 384 + (piece_type - 1) * 64 + square_063
color: 0 = black, 1 = white (C) / 0 = white, 1 = black (Python)
piece_type: 1 = pawn, 2 = knight, 3 = bishop, 4 = rook, 5 = queen, 6 = king
square: 0..63 (a1=0, h8=63)
color * 384: 0 (first color) or 384 (second color)
piece * 64: 0, 64, 128, 192, 256, 320
debug_keys.py against the 19-entry reference book.
compute_zobrist()uint64_t compute_zobrist(int side_val) {
uint64_t h = 0;
int sq;
for (sq = 0; sq < 128; sq++) {
if (sq & 0x88) continue; /* skip off-board */
int p = b[sq];
if (p == 0) continue; /* empty square */
int mm_type = p & 7;
int std_type = mm_to_std[mm_type];
int color = (p & 8) ? 1 : 0; /* white=1, black=0 */
int sq063 = sq88_to_063(sq);
h ^= ZobristTable[color * 384 + (std_type - 1) * 64 + sq063];
}
if (side_val != 8) h ^= ZobristTable[768]; /* black to move */
if (can_castle(1, 1)) h ^= ZobristTable[769];
if (can_castle(1, 0)) h ^= ZobristTable[770];
if (can_castle(0, 1)) h ^= ZobristTable[771];
if (can_castle(0, 0)) h ^= ZobristTable[772];
if (E1 != 0 && !(E1 & 0x88))
h ^= ZobristTable[773 + (E1 & 7)];
return h;
}
zobrist_key_stm32() (corrected v7.0)MM_TO_STD = [0, 1, 1, 2, 6, 3, 4, 5]
def get_mm_type(piece):
if piece.piece_type == chess.PAWN: return 2 if piece.color else 1
elif piece.piece_type == chess.KNIGHT: return 3
elif piece.piece_type == chess.BISHOP: return 5
elif piece.piece_type == chess.ROOK: return 6
elif piece.piece_type == chess.QUEEN: return 7
elif piece.piece_type == chess.KING: return 4
return 0
def zobrist_key_stm32(board, ZT):
key = 0
for sq in chess.SQUARES:
piece = board.piece_at(sq)
if piece is None:
continue
mm_type = get_mm_type(piece)
std_type = MM_TO_STD[mm_type]
# FIXED: white=0, black=1
color = 0 if piece.color else 1
file_ = chess.square_file(sq)
rank_ = chess.square_rank(sq)
# FIXED: NO rank inversion
sq063 = rank_ * 8 + file_
z_index = color * 384 + (std_type - 1) * 64 + sq063
key ^= ZT[z_index]
if not board.turn: # black to move
key ^= ZT[768]
# Castling
if board.castling_rights & chess.BB_H1: key ^= ZT[769]
if board.castling_rights & chess.BB_A1: key ^= ZT[770]
if board.castling_rights & chess.BB_H8: key ^= ZT[771]
if board.castling_rights & chess.BB_A8: key ^= ZT[772]
# En passant
if board.ep_square is not None:
key ^= ZT[773 + chess.square_file(board.ep_square)]
return key & 0xFFFFFFFF
The following 4 keys from the 19-entry reference book were reproduced exactly by the corrected Python function:
| Position | Key | Move (hex) | Decoded Move | Match |
|---|---|---|---|---|
| After 1.e4 (black to move) | 0x66FF886D | 0x08B2 | c7-c5 (Sicilian) | ✔ |
| After 1.c4 (black to move) | 0x9941BC1A | 0x0B7E | g8-f6 (Nf6) | ✔ |
| After 1.d4 (black to move) | 0xD75F281A | 0x0B7E | g8-f6 (Nf6) | ✔ |
| After 1.Nf3 (black to move) | 0x835FB199 | 0x0B7E | g8-f6 (Nf6) | ✔ |
uint64_t but only the lower 32 bits (uint32_t key) are stored in debut_book.c. The probability of collision across 1000 entries is negligibly small (~10-6).
The file contains three key functions. Addresses are from the IAR linker map file.
compute_zobrist(int side_val)See Section 5. Returns the 64-bit Zobrist hash of the current position on board b[]. Address: 0x800'4663, size: 0xF8.
polyglot_find_move(uint32_t key, uint16_t *out_move)Binary search in the sorted array debut_book[]:
int polyglot_find_move(uint32_t key, uint16_t *out_move) {
extern const polyglot_entry_t debut_book[];
extern const int debut_book_size;
int l = 0, r = debut_book_size - 1;
while (l <= r) {
int m = (l + r) >> 1;
if (debut_book[m].key == key) {
*out_move = debut_book[m].move;
return 1; /* found */
} else if (debut_book[m].key < key) {
l = m + 1;
} else {
r = m - 1;
}
}
return 0; /* not found */
}
Address: 0x800'475B, size: 0x42.
debut_book[] array must be sorted by key in ascending order. The Python generator does this automatically via book.sort(key=lambda x: x[0]).
polyglot_to_mm(uint16_t pg_move, int *from88, int *to88)Converts a Polyglot move (6+6 bits) to 0x88 coordinates for micro-Max:
void polyglot_to_mm(uint16_t pg_move, int *from88, int *to88) {
int from063 = pg_move & 0x3F;
int to063 = (pg_move >> 6) & 0x3F;
*from88 = (7 - (from063 / 8)) * 16 + (from063 % 8);
*to88 = (7 - (to063 / 8)) * 16 + (to063 % 8);
}
Address: 0x800'47C9, size: 0x48. Polyglot move: bits 0–5 = “from” square (0x3F), bits 6–11 = “to” square, bits 12–14 = promotion piece (0 = none, 1 = knight, 2 = bishop, 3 = rook, 4 = queen).
def move_to_polyglot(move):
from_sq = move.from_square
to_sq = move.to_square
file_from = chess.square_file(from_sq)
rank_from = chess.square_rank(from_sq)
file_to = chess.square_file(to_sq)
rank_to = chess.square_rank(to_sq)
# FIXED: NO rank inversion
sq063_from = rank_from * 8 + file_from
sq063_to = rank_to * 8 + file_to
return (sq063_from & 0x3F) | ((sq063_to & 0x3F) << 6)
int side, one, book_pending = 0; /* book_pending — book flag */
uint16_t pg_move; /* move from book */
uint32_t book_key; /* Zobrist key */
int from88, to88; /* coordinates for engine */
/* === OPENING BOOK === */
if (book_pending) {
book_pending = 0;
NVIC_DisableIRQ(USART1_IRQn);
USART1->CR1 &= ~USART_CR1_RE;
book_key = (uint32_t)(compute_zobrist(k) & 0xFFFFFFFFU);
if (polyglot_find_move(book_key, &pg_move)) {
uart1_write('B'); /* marker: book move */
polyglot_to_mm(pg_move, &from88, &to88);
/* Write move to c[] — as if human entered it */
c[0] = 'a' + (from88 & 7);
c[1] = '8' - (from88 >> 4);
c[2] = 'a' + (to88 & 7);
c[3] = '8' - (to88 >> 4);
c[4] = '\n';
data_count = -1;
goto skip_wait; /* skip the input wait loop */
}
uart1_write('D'); /* marker: engine search */
USART1->CR1 |= USART_CR1_RE;
NVIC_EnableIRQ(USART1_IRQn);
}
if (side != k) {
half_m++;
if (side == 8) book_pending = 1; /* white moved → black from book */
char ssd = sd == 0 ? 'w' : 'b';
/* ... move display ... */
}
nxt: Labelnxt:;
side = k;
k ^= D(k, -I, I, Q, 1, j++, O, 8, 2) - I ? 0 : 24;
book_pending Mechanism
Key idea: the book move is fed into the engine via the same path as a human move — through the c[] array, parsing, GameHistory validation, and the D() call. This guarantees the move is checked for legality and correctly applied.
Why this is reliable: The book move goes through D(k, -I, I, ...) with K and L set from the book. D() checks legality (if the move is impossible, D() will not return I and k will not change). This prevents “hanging” on an impossible book move.
The book is generated offline on a PC. The Python script uses three stages:
Generates positions after the 4 principal first moves: e2e4, d2d4, c2c4, g1f3. These are positions where black is to move (1 ply deep).
For each of the 4 main openings:
This yields ~4 × 5 × 20 ≈ 400 positions with deep, strong continuations.
Fill the remaining slots up to BOOK_SIZE = 1000 with random positions at odd plies (3, 5, 7, 9, 11, 13) — always black to move.
depth = random.choice([1, 3, 5, 7, 9, 11, 13]) to ensure this.
debut_book.ctypedef struct {
uint32_t key; /* Zobrist hash (lower 32 bits) */
uint16_t move; /* Polyglot move: from(6) | to(6) | promo(3) */
uint8_t weight; /* weight (1 = best move, see note) */
} polyglot_entry_t;
polyglot_find_move returns the first matching entry regardless of weight. Weight = 1 is sufficient and matches the reference.
book.sort(key=lambda x: x[0]) before writing. Without sorting, binary search fails silently — it finds nothing even if the key exists.
const polyglot_entry_t debut_book[] = {
{0x12345678U, 0x0F12U, 1}, /* position → e2e4 */
{0x9ABCDEF0U, 0x1C30U, 1}, /* position → c7c5 */
/* ... 998 entries ... */
};
const int debut_book_size = 1000;
zobrist_table.h and in the Python script must be identical. If they differ, keys will not match and the book will not work. Use the same zobrist_table.h file for both.
debug_keys.pyA diagnostic script that loads zobrist_table.h, computes Zobrist keys for 4 known positions (after 1.e4, 1.c4, 1.d4, 1.Nf3) using all 16 combinations of parameters (rank inversion, color mapping, castling, en passant), and finds the combination that matches the known keys from debut_book.c.
Result: invert_rank=False, color_white_is_1=False, include_castling=True, include_ep=True — all 4 keys matched.
| Symptom | Cause | Solution |
|---|---|---|
Marker D always (book never triggers) | Zobrist numbers in C and Python don't match, or coordinates not inverted | Verify zobrist_table.h is shared; check sq88_to_063: must be (7 - (sq>>4))*8 + (sq&7) |
| Hang after white's move | Book move fails validation (coordinates swapped) | Check polyglot_to_mm: must reverse-invert the rank |
| Error Pe1072 (declaration after label) | Variable declaration immediately after nxt: label | Move declarations to global scope or add ; after label |
| Book triggers on human's move | Book checked unconditionally (no book_pending guard) | Ensure book is only called when book_pending == 1 |
| Linker error: undefined debut_book | debut_book.c not added to IAR project | Right-click project → Add → Add Files |
| Duplicate definition zobrist_table | zobrist_table.h included in multiple files | Include only in polyglot.c; use #pragma once or include guard |
| Book plays same move always | Single entry per position (no randomization) | Generate multiple moves per position with different weights |
| Book not found after sorting fix | Entries not sorted by key | Add book.sort(key=lambda x: x[0]) before writing debut_book.c |
| Weight overflow or mismatch | weight = 255 instead of 1 | Use weight = 1 as in the reference book |
| Book has positions white-to-move | Even plies generated (white moves) | Use only odd plies: depth = random.choice([1,3,5,7,9,11,13]) |
| Rank inversion mismatch (Python vs C) | Python used (7-rank)*8+file | Python: use rank*8+file (no inversion); C: use (7-(sq>>4))*8+(sq&7) |
| Color mapping mismatch | Python used white=1, black=0 | Python: use color = 0 if piece.color else 1 (white=0, black=1) |
[SERVER] Bad confirm format in log | STM32 prints board ASCII art to UART, server doesn't recognize it | Not an error — server ignores board visualization and waits for mov ... string |
Based on the IAR linker map file micro_Max_ver4_2.map (1000 entries):
| Component | Address | Size (hex) | Size (bytes) |
|---|---|---|---|
| .intvec (vector table) | 0x800'0000 | 0xEC | 236 |
| debut_book.o (book data) | 0x800'00EC | 0x1F40 | 8,000 |
| main.o (code) | 0x800'202C | 0x18A0 | 6,304 |
| spi.o, gpio.o, uart.o, core.o | 0x800'38CC–0x800'3E24 | — | 1,664 |
| polyglot.o (code) | 0x800'45EC | 0x224 | 548 |
| polyglot.o (Zobrist table) | 0x800'49E0 | 0x1868 | 6,248 |
| debut_book.o (size var) | 0x800'64B4 | 0x4 | 4 |
| Runtime libs + startup | various | — | ~2,950 |
| Total used | — | 0x6476 | 25,762 |
| Free | 0x800'6562 | 0x1A9E | 6,814 |
Book footprint: 8,004 bytes (8 KB) for 1000 entries × 7 bytes + 4 bytes size variable. Free space allows ~851 more entries (up to ~1851 total).
| Component | Address | Size (hex) | Size (bytes) |
|---|---|---|---|
| .data (initialized globals) | 0x2000'0000 | 0x60 | 96 |
| A[U] hash table (main.o) | 0x2000'0060 | 0xC00 | 3,072 |
| T[] evaluation table (main.o) | 0x2000'0C60 | 0x40C | 1,036 |
| GameHistory (main.o) | 0x2000'106C | 0xC0 | 192 |
| rand.o state | 0x2000'11B0 | 0x84 | 132 |
| Other BSS (main.o, uart.o) | 0x2000'1234–0x2000'137F | — | 331 |
| CSTACK (stack) | 0x2000'1380 | 0x1400 | 5,120 |
| Total used | — | 0x2780 | 10,112 |
| Free | 0x2000'2780 | 0x80 | 128 |
main.c will overflow. To free RAM: reduce CSTACK to 4 KB (verify recursion depth of micro-Max) or reduce buffer sizes.
| Book Size | Flash (book) | Flash (total firmware) | Fits C6 (32KB)? |
|---|---|---|---|
| 500 entries | ~3.5 KB | ~21 KB | ✔ Yes |
| 1000 entries | ~8 KB | ~26 KB | ✔ Yes (current) |
| 1851 entries | ~13 KB | ~31 KB | ✔ Yes (max) |
| 2000 entries | ~14 KB | ~32 KB | ✘ No (needs C8, 64KB) |
All bugs discovered and fixed during development of the Polyglot book integration:
| # | Symptom | Root Cause | Fix |
|---|---|---|---|
| 1 | Book never triggers (marker D always) | Rank inversion in Python: used (7-rank)*8+file, but chess lib uses rank 0 = white's back rank | Changed to rank*8+file (no inversion in Python) |
| 2 | Keys don't match even after rank fix | Color mapping in Python: used white=1, black=0, but correct is white=0, black=1 | Changed to color = 0 if piece.color else 1 |
| 3 | Book found for 1st move but not 2nd | Entries not sorted by key — binary search requires sorted array | Added book.sort(key=lambda x: x[0]) before writing file |
| 4 | Book found but move rejected | Weight = 255 instead of 1 (as in reference book) | Changed to weight = 1 |
| 5 | Book only works for first move | Stage 2 generated positions at even plies (white to move) — STM32 never looks these up | Changed to odd plies only: random.choice([1,3,5,7,9,11,13]) |
| 6 | Book too large (>2000 entries) | Stage 2 used top-5 responses × all white replies × all 20 first moves | Limited to 4 main openings (e4, d4, c4, Nf3) × top-5 × all white replies |
| 7 | Specific openings missing (e.g. after 1.d4) | Random fill rarely hits exact opening positions | Stage 1 explicitly adds all 4 main openings; Stage 2 adds continuations |
| 8 | [SERVER] Bad confirm format in log | STM32 prints ASCII board to UART; server doesn't recognize it as protocol message | Not a bug — server ignores board visualization and waits for mov ... string |
debug_keys.pyA diagnostic script that loads zobrist_table.h, computes Zobrist keys for 4 known positions using all 16 combinations of parameters, and finds the correct one. Output:
*** FOUND! ***
invert_rank = False
color_white_is_1 = False
include_castling = True
include_ep = True
1.e4 (black): 0x66FF886D (expected 0x66FF886D) ✓
1.c4 (black): 0x9941BC1A (expected 0x9941BC1A) ✓
1.d4 (black): 0xD75F281A (expected 0xD75F281A) ✓
1.Nf3 (black): 0x835FB199 (expected 0x835FB199) ✓
The STM32 communicates with a Python chess server over UART/Wi-Fi (ESP-01). The protocol is text-based.
| Direction | Message | Meaning |
|---|---|---|
| Server → STM32 | e2e4X | White's move (UCI + suffix) |
| STM32 → Server | GO e2e4 OK | Confirmation (may vary) |
| STM32 → Server | mov e2e4 w | Move confirmation: mov <uci> <color> |
| STM32 → Server | rnbqkbnr... | ASCII board dump (for visual debug, ignored by server) |
| Server → STM32 | 0X | Server finished, expects black's move |
| STM32 → Server | mov e7e5 b | Black's move (from book or engine) |
| STM32 → Server | B or D | Marker: B = book move, D = engine search |
mov ... string. This is not an error — it is expected behavior.