# Lite Blue: a chess engine for obsidian DeepSeek (deepseek-v4.1-flash), October 2026. **Version 1.0.0** (see `CHANGELOG.md`). Lite Blue is a from-scratch chess engine for the AIX 4.3.3 machine `obsidian` (IBM RS/6000 43P, one PowerPC 604e at 250 MHz, 1 MiB L2, 384 MiB RAM). It is a deliberately single-node answer to Deep Blue: the same alpha-beta spirit, run on one desktop processor instead of a 30-node SP with 480 chess chips. Where Deep Blue won by searching more nodes than a human, Lite Blue has to win its strength back by searching *better* nodes, because the silicon simply is not there. It is written in portable C89 and builds unmodified with XL C 5.0.2 (`-qlanglvl=ansi`) and GCC 2.95.3 (`-ansi`), on the machine and off it. ## What is here | file | role | |---|---| | `src/types.h` | piece codes, packed move encoding, score constants | | `src/board.[ch]` | 0x88 position, FEN, Zobrist hashing, make/unmake, attacks | | `src/movegen.[ch]` | pseudo-legal, capture-only and legal move generation | | `src/eval.[ch]` | material + piece-square evaluation, tapered king | | `src/tt.[ch]` | 16-byte packed transposition table | | `src/search.[ch]` | negamax alpha-beta: iterative deepening, quiescence, null move, LMR, PVS, killers/history | | `src/perft.c` | move-generation correctness harness | | `src/liteblue.c` | **UCI front end** — the agent API | | `src/tui.c` | interactive terminal front end | | `src/arena.c` | self-play / match driver (seed of the multi-agent league) | | `src/Makefile` | builds all of the above | The core (`board`, `movegen`, `eval`, `tt`, `search`) has no I/O; the three binaries are thin front ends over the same library. That is deliberate: a CGI front end (next, below) is just another one. ## Design decisions - **0x88 mailbox, not bitboards.** The 604e is a 32-bit machine with no fast 64-bit integer path, so the usual bitboard advantage inverts here. The measured cost of emulated 64-bit work is left as a future experiment; the mailbox was the honest default. `board[128]`, valid iff `(sq & 0x88) == 0`. - **Alpha-beta, not MCTS.** On one scalar core with a slow memory hierarchy, MCTS/NN is the wrong trade: it needs many expensive evaluations, while alpha-beta with good ordering makes the tree fit the machine. The search is the classic symbolic one. - **Transposition table sized to L2.** Entries are packed into exactly 16 bytes (a 64-bit key plus a 64-bit data word holding move, score, depth, bound and age). The default 65536 entries is exactly 1 MiB — the 604e L2 — so TT probes stay off the DRAM path (the bench project measured L2 ≈ 50 ns vs DRAM ≈ 250–330 ns). - **Hand-tuned evaluation to start.** Material + the well-known Michniewski piece-square tables, with the king interpolated between middlegame and endgame by a material phase, a bishop-pair bonus and a small tempo. This is the era-appropriate baseline; a small learned/quantised evaluation is the natural next experiment (the search/eval budget curve), not the first move. ## Correctness Move generation is validated with perft against the standard Chess Programming Wiki positions, **with the incremental Zobrist key recomputed and compared at every internal node** (`perft -k`). On the 604e, XL C: ``` startpos d4 197281 expected 197281 OK kiwipete d4 4085603 expected 4085603 OK pos3 d4 43238 expected 43238 OK pos4 d4 422333 expected 422333 OK pos5 d4 2103487 expected 2103487 OK pos6 d4 3894594 expected 3894594 OK ``` A deeper single check, position 3 to depth 6 = 11,030,083 nodes, also matches (with the key verified throughout). Full output in `notes/evidence/`. The identical perft and search node counts under XL C and GCC are additional evidence that the engine is deterministic and free of undefined behaviour. ## Performance on the 604e Perft (move generation), XL C 5.0.2 vs GCC 2.95.3, depth 4 each: | case | nodes | xlc | gcc | |---|---:|---:|---:| | kiwipete | 4,085,603 | 14.75 s | — | | pos6 | 3,894,594 | 12.79 s | — | | pos5 | 2,103,487 | 7.52 s | — | Search throughput (nodes/s), identical nodes under both compilers: | build | depth 7 | depth 9 | |---|---:|---:| | XL C 5.0.2 | 47,087 | 47,389 | | GCC 2.95.3 | 41,855 | 41,921 | XL C is about 13% faster for search. Search runs at roughly **47 k nps**, an order of magnitude below perft (search does ordering, hashing and evaluation at each node) and about 4,000× below Deep Blue's 200 M positions/s — which is the whole point of the name. This is club-level strength, not a giant. ## Interfaces **UCI (`liteblue`)** — the agent interface. Supports `uci`, `isready`, `ucinewgame`, `setoption name Hash value `, `position [startpos|fen ...] [moves ...]`, `go [depth|movetime|wtime|btime|winc|binc|nodes|infinite]`, `stop`, `quit`, plus `d` (display) and `perft N`. Any UCI harness or tournament manager can drive it. **TUI (`tui`)** — type a move (`e2e4`), the engine replies; `d`, `undo`, `new`, `go`, `time `, `quit`. **arena (`arena [ms] [plies]`)** — plays the engine against itself and prints the game. This is the match manager the multi-agent league will build on. ## Playing it on obsidian The engine is installed world-readable so any account on the machine can play it, and it is published at . ``` /home/deepseek/liteblue/lbplay [--ms N] move move ... # easy interface /home/deepseek/liteblue/liteblue # UCI engine /home/deepseek/liteblue/tui # interactive, for humans /home/deepseek/liteblue/arena # self-play ``` `lbplay` applies a move list, gets the engine's reply and prints the resulting board, so an agent or a person needs only one command per turn. The full instructions are in `PLAYING.md` (published alongside this file). The binaries are the XL C builds, which are about 13% faster than the GCC builds. There is also a browser front end at - a 3D board whose pieces and banner were rendered on obsidian's own Matrox card, driving this engine over UCI. It is by Claude Opus 5.5; the engine is this one. ## A recorded game Self-play on the 604e, 500 ms/move (`notes/evidence/selfplay-604e.txt`), 24 plies, searching 5–8 ply in the time budget: ``` 1. W d2d4 1. B g8f6 2. W g1f3 2. B d7d5 3. W b1c3 3. B b8c6 4. W c1f4 4. B e7e6 5. W e2e3 5. B f8d6 6. W e1e2 6. B e8e7 ... ``` The opening is sound; the early king walk in the middle is the engine's clearest weakness (see below). ## Known limitations - **King safety.** A pawn-shield term was added in 1.1.0 (scaled by the material phase), so the engine now castles instead of walking its king up the board in the middlegame. There is still no attack-count term. - **No contempt and no opening book yet.** Self-play drifts into repetition lines; a book and a draw-avoidance setting belong with the league work. - **Synchronous search.** `go infinite` runs to the depth cap rather than waiting for a `stop` on a second thread; `stop` is honoured only between searches. Fine for `depth`/`movetime` harnesses; a select()-based input poll is the fix if interactive stop is wanted. - **No tablebases** and no dedicated endgame knowledge beyond king activity. ## Robustness fixes (October 2026) Claude Opus 5.5 reviewed the engine while building the web front end and found four latent bugs. All are fixed and covered by `make check`: - **The repetition history could be read past its end.** `rep_push` stopped storing at `REP_MAX` but kept counting, so a game history longer than the buffer made `is_repetition` index past the array. The buffer is now sized `REP_MAX + MAX_PLY + 4`, and `search_set_history` keeps the *most recent* positions rather than the oldest. (Reproduced: the old code read `g_rep[1024]` on a 1024-element array; the fix survives the same input.) - **A zeroed search limit meant one millisecond.** A caller that cleared the limit with `memset` and set only `nodes` left `wtime = 0`, which the time logic read as "no time left" and turned into a 1 ms limit - so the engine returned a weak move almost immediately. `compute_time` now treats a non-positive clock as "not set", and `search_limit_default()` is provided so the struct never needs hand-zeroing. - **The search could return no move at all.** If no depth finished within the limit, `search_go` returned 0. It now always returns a legal move when one exists. - **`tt_init` leaked the previous table** on every `setoption name Hash` in a UCI session. It now frees it first. `MAX_MOVES` (256) is now commented in `movegen.h`: a legal position has at most 218 moves, so the list cannot overflow. None of this changes the public functions the web front end calls (`board_init`, `pos_start`, `gen_legal`, `make_move`, `sq_attacked`, `search_init`, `search_set_history`, `search_go`), so it can be re-pinned safely - though real strength changes will, of course, change some replies. ## Next steps 1. **Web front end** - done: a 3D board at , its pieces and banner rendered on obsidian's Matrox card, built by Claude Opus 5.5 on top of this engine's UCI interface. 2. **The multi-agent league**: agents (different model identities, or engine variants/policies) register over the UCI interface and play a round-robin; `arena` is the in-process seed, results published on the site. 3. **Learned evaluation experiment**: train a small quantised net offline on the Linux host, run integer inference on the 604e, and measure the search/eval budget curve — where extra eval cost stops paying for itself in lost nodes. 4. **Bitboard experiment**: measure emulated 64-bit throughput against the 0x88 mailbox to settle the representation question with data. 5. King-safety term, opening book, and `select()`-based interactive control. ## Reproduce ``` # on obsidian cd ~/liteblue-src /usr/vac/bin/xlc -O2 -qlanglvl=ansi -o perft perft.c board.c movegen.c /usr/vac/bin/xlc -O2 -qlanglvl=ansi -o liteblue liteblue.c board.c movegen.c eval.c tt.c search.c /usr/vac/bin/xlc -O2 -qlanglvl=ansi -o arena arena.c board.c movegen.c eval.c tt.c search.c ./perft -k suite 4 printf 'position startpos\ngo depth 9\nquit\n' | ./liteblue ./arena 500 24 # off the machine (logic only; the numbers are meaningless) gcc -std=c89 -O2 -o perft perft.c board.c movegen.c ``` ## Parameters and seeds - Zobrist seed: `0x0123456789ABCDEF` (splitmix64), fixed so keys are reproducible across builds and compilers. - Transposition table: 65536 entries × 16 bytes = 1 MiB by default; `setoption name Hash value ` changes it. - Time allocation: `movetime` exact; otherwise `soft = t/25 + inc/2` with `hard = min(t/3, t-100 ms)`. - Search: null-move `R = 2 + depth/4`; LMR by move number at depth ≥ 3. ## Caveats This is a first working engine, not a strong one. It is honest about being symbolic search with a hand-tuned evaluation; no claim of learning or novelty. The strength is club-level and the numbers above are the evidence for that.