Reading the Board β a GF(256) integer model that learns Connect Four
A small neural model whose trunk does XOR and table lookups instead of float multiply-accumulates β and an honest measurement of how good it actually is.
Author: Christof Ckal
Gold is the human, blue is the model.*
How good is it, actually?
600 games per arm, against the released checkpoint:
| Opponent | Model's win rate | Verdict |
|---|---|---|
| Random player | 85.2 % | beats it |
| Centre-column policy | 90.5 % | beats it head-to-head |
| 1-ply heuristic | 37.2 % | loses to it |
| 2-ply lookahead | 24.3 % | loses to it |
| Minimax | 41.8 % | loses to it |
That gap is the point of the project. The model agrees with its teacher on 59.6 % of training positions and 41.0 % of a board-disjoint holdout β and it still loses to a three-line rule that only checks "can I win now?" and "can I block now?". Teacher-agreement and game strength are different things, and this repo measures both.
βΆ Play it against the model
A real, session-isolated game β every visitor gets their own board. Deployed on CPU. Verified isolation + security claims.
Or play it inside the Hugging Face Space: https://huggingface.co/spaces/Chris4K/connect-four-gf256 (the Space is static and frames the live server; the server allows that origin explicitly β never a wildcard).
The dataset
The training and evaluation data is published as a separate dataset repository:
`Chris4K/connect-four-gf256`
git clone https://huggingface.co/datasets/Chris4K/connect-four-gf256
| File | Rows | What |
|---|---|---|
train/corpus_320k.txt |
320,000 | training games, v2 byte transcript (10 bytes per ply) |
train/perply_targets.npz |
320,000 | per-ply 7-way targets β (N, 19, 7) float32, plus ply_of, teacher, n_plies |
holdout/corpus.txt |
4,000 | board-disjoint holdout games |
holdout/perply_targets.npz |
4,000 | the holdout's per-ply targets |
SHA256SUMS |
β | checksums for all four artifacts |
Everything in it is synthetic: games generated by a search teacher playing itself. No personal data, no scraped text, no third-party content.
The labels come from two solvers, recorded per slot in the teacher
array:
teacher |
Solver | Share of labelled slots |
|---|---|---|
1 |
bounded exact solver (max_empty=7) β returns the full tie set |
47,209 (1.0 %) |
2 |
negamax alpha-beta search teacher β returns one move | 4,516,452 (99.0 %) |
The exact solver's slots are the only multi-label ones (65.6 % of them spread mass over several equally-best moves); the alpha-beta teacher's targets are one-hot.
The teacher is a search policy, not a proof of optimality. Connect Four is a solved game, but this model was never taught the full solution β it imitates a bounded search. No label in the dataset should be read as "the set of optimal moves".
The published corpus is the repaired one. An earlier corpus had a corrupt trailing field in 126,537 of 320,000 rows (39.5 %); the trainer now refuses such a corpus outright.
The model
The trunk is a GF(2βΈ) integer program: XOR, learned S-box table lookups and integer adds. No floating-point multiply-accumulate in the trunk.
board βββΊ byte transcript βββΊ GF(256) trunk βββΊ read-out βββΊ column logits
(v2 format) (XOR + S-box) (float32) (7 columns)
- Layers: 3 Β·
d: 32 Β· lags:[0,1,2,3,4,5,6,7,8,9,10,20] - Context: 192 Β· Batch: 16 Β· Seed: 32
- Params: emb 8,192 Β· coef 288 Β· sbox 24,576 Β· wiring 576 Β· readout 8,388,608 Β· bias 256
- Size: 32.3 MB, 8,422,496 parameters, CPU-only inference.
Honest scoping of "zero FLOPs": the trunk has no floating-point multiply-accumulate. The read-out is float32 and is 99.6 % of the parameters. The complete model is therefore not a zero-FLOP program.
Supervision
Every ply of every training game is labelled with the move chosen by a negamax alpha-beta search teacher, with ties preserved as sets rather than collapsed. The exact data β corpus, per-ply targets and the board-disjoint holdout β is published in the dataset repository (see The dataset above).
The teacher is a search policy, not a proof of optimality. Connect Four is a solved game, but this model was never taught the full solution β it imitates a bounded search. The repo's
src/c4/exact_solver.pyis a separate, bounded endgame solver (max_empty=7); it supplied 1.0 % of the corpus labels and check positions, it did not label the corpus.
Results
The convergence curve
| Step | Teacher agreement (train) | Holdout (board-disjoint) | vs 1-ply heuristic | vs random |
|---|---|---|---|---|
| 0 | 16.3 % | 11.9 % | 9-39 | 36-12 |
| 2000 | 40.2 % | 27.9 % | 10-38 | 37-11 |
| 6000 | 50.9 % | 36.7 % | 15-33 | 38-10 |
| 10000 | 56.0 % | 40.8 % | 19-29 | 37-11 |
| 14000 | 57.5 % | 38.1 % | 23-25 | 44-4 |
| 18000 | 59.6 % | 41.0 % | 16-32 | 41-7 |
| 26000 | 58.7 % | 39.1 % | 15-33 | 38-10 |
| 30000 | 59.4 % | 42.0 % | 18-30 | 38-10 |
| 34000 | 58.5 % | 41.8 % | 14-34 | 43-5 |
What the model learned
- It reads the board. Its picks spread across all seven columns
(holdout:
[39, 55, 94, 175, 78, 48, 23]), with non-zero per-column recall for every column. It is not a degenerate constant policy. - It beats random 85.2 % of the time (n=600, 3 seeds).
- It agrees with the teacher on 59.6 % of training positions and 41.0 % of held-out positions.
What it did not learn
- It does not beat a 1-ply heuristic. Net-negative at every measured step of the entire run. The best point is 23-25 β near parity, never ahead.
- Agreement plateaus after ~step 14000. More steps stopped helping.
- The ~18 pp train/holdout gap shows a real memorisation component.
Independent strength check
The canonical src.c4.duel harness, 600 games per arm per seed Γ 3 seeds,
model greedy (Ξ΅ = 0), opponent explores (Ξ΅ = 0.3) so trajectories are
genuinely distinct:
- vs. random: W1460 L337 D3 of 1800 (p β 3.2 Γ 10β»ΒΉβΆβΆ)
- vs. the best single-column constant: wins in 2 of 5 opponent settings β not a sweep
- seat check: model gap +7.5 pp, heuristic gap +4.8 pp, gap-vs-gap p = 0.402 β no model-specific seat asymmetry
Read the model yourself
The checkpoint is hosted on Hugging Face rather than committed to this repository, so a clone stays small:
# 1. Download the checkpoint (32.3 MB)
curl -L -o best_hard.pt \
https://huggingface.co/Chris4K/connect-four-gf256/resolve/main/best_hard.pt
# (or: huggingface-cli download Chris4K/connect-four-gf256 best_hard.pt)
# 2. Verify it before using it
sha256sum best_hard.pt
# 134035637b637f87f3837aa8929dc5ec5db3bb0f0c9526f93fe1449be77672e2
# 3. Play it (the server takes --model; --checkpoint does not exist)
# Run this from a clone of https://github.com/Ckal/connect-four-gf256
python -m src.c4.ui_server --model best_hard.pt --port 7860 --device cpu
# open http://127.0.0.1/
best_hard.pt is the step-18,000 hard export. The UI shows the model's
next-move distribution over all seven columns (rendered from the hard
path), the legal-move set, and lets you undo and reset. The 100 % figure on
a column is a probability of being optimal, not a chance of winning.
The public deployment uses per-session boards. The upstream research UI keeps a single board in a module global and is not safe for multi-user hosting as shipped.
Security verification
The public deployment is gated by a script that runs against the deployed process over public HTTPS β not against a local server.
git clone https://github.com/Ckal/connect-four-gf256.git
python scripts/verify_public_isolation.py https://c4.tensorgarten.de
# PASSED: 19 FAILED: 0
Embedding (the Hugging Face Space)
The Space at
Chris4K/connect-four-gf256
is static β it contains only a page that frames the live server. The
server permits exactly the framing origins it needs:
Content-Security-Policy: β¦; frame-ancestors https://chris4k-connect-four-gf256.static.hf.space https://huggingface.co
The allowlist is explicit and closed:
- Default is deny. With no
--allow-embeddingflag the response carriesX-Frame-Options: DENYandframe-ancestors 'none'. - Only
https://origins are accepted. Anhttp://ancestor could be intercepted; ahttps://*wildcard is refused outright, because it would let any site clickjack the game. - The allowlist replaces
X-Frame-Optionswhen it is in force β XFO can only allow or deny a single origin, while CSP can name several. - State-changing requests are origin-checked. Because the session cookie
must be
SameSite=Noneto work inside a cross-site frame, the server validates theOriginheader on every mutating request instead of relying onSameSite=Laxfor CSRF protection. A request from an origin that is not the site or an allowlisted ancestor is refused with HTTP 403.
Run it yourself:
python -m src.c4.ui_server --model best_hard.pt --public \
--allow-embedding https://chris4k-connect-four-gf256.static.hf.space
It asserts, and all of these currently hold:
| Assertion | Result |
|---|---|
GET / returns 200 with a real page |
PASS |
Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, HSTS |
PASS |
| Two clients receive different session cookies | PASS |
The session cookie is HttpOnly and Secure |
PASS |
| A move in one session is invisible in another β no shared board | PASS |
/modelinfo and /reload are closed in public mode (HTTP 403) |
PASS |
| A mutating request from a non-allowlisted origin is refused (HTTP 403) | PASS |
The session cookie as served when cross-site embedding is enabled:
Set-Cookie: tg_sid=<opaque id>; Path=/; HttpOnly; SameSite=None; Secure; Max-Age=7200
Without an embedding allowlist it is SameSite=Lax; Secure.
The checkpoint served by the public container is byte-identical to the one
on Hugging Face β SHA-256
134035637b637f87f3837aa8929dc5ec5db3bb0f0c9526f93fe1449be77672e2.
Reproduce it
The source, tests and measurement scripts are in the GitHub repository:
git clone https://github.com/Ckal/connect-four-gf256.git
cd connect-four-gf256
# the data
git clone https://huggingface.co/datasets/Chris4K/connect-four-gf256.git
pip install --extra-index-url https://download.pytorch.org/whl/cpu -r requirements.txt
# Train (seed 32, 100k steps, batch 16, ctx 192)
V24_TAG=v24 bash run_v24.sh
# Score any checkpoint against its owner corpus
python -u tools/measure_promotion_scorer.py \
--corpus-run runs/_probe/v24 \
runs/_probe/v24/checkpoints/best_hard.pt
# Full regression suite
bash tools/run_regression_sweep.sh
The scorer refuses to guess which corpus belongs to a checkpoint: pass
--corpus-run explicitly, or let it find an unambiguous ancestor owner
run. Without one it exits non-zero rather than scoring against the wrong
data.
Every published number traces to a command and an artifact β see
RESULTS.md.
Bugs that had to be fixed first
This is the part most write-ups skip. Each of these produced a plausible-looking number that was wrong.
| # | Bug | Impact |
|---|---|---|
| 1 | Terminal-state check truthy on terminal boards | Wrong game-over detection |
| 2 | Corrupt trailing field in 126,537 / 320,000 rows (39.5 %) | 1,567,958 affected plies; trainer now rejects corrupt corpora |
| 3 | Sidecar read by batch position instead of sampled corpus row | ~41.3 % of supervised positions had all-zero targets |
| 4 | Promotion key = lowest byte-bpb | Selected step 1500 over the final step 3923 |
| 5 | Final exports omitted transcript version | Read as v1 β scored 24.4 % instead of 91.3 % |
| 6 | Historical per-ply index arithmetic | Early positions t < k assigned to row kβ1 |
What this invalidates. The project's earlier conclusions β "dense per-ply supervision is worse", "mirror consistency is net negative", "symmetry is not worth it" β were all drawn before the row-index repair. They are not evidence about the corrected model and are not cited in the paper.
Fixing this also invalidated the old "always play column 3" control: it had actually been a centre-first staircase, not a constant policy.
Reproducibility guarantees
| Property | Evidence |
|---|---|
| Bit-exact resume | max weight Ξ 0.000e+00, identical Adam moments (12/12 checks) |
| Full regression suite | 27 PASS, 0 FAIL β reproduced by independent sweeps |
| Live UI parity | 40/40 played columns, next-move distribution L1 = 0.000e+00 |
| Atomic checkpoint publication | 9/9 checks; 0 corrupt reads in 769 reads |
| Transcript stamping | 3/3 checks |
Component suites: live parity v3/v4/v5 Β· UI hot reload 14/14 Β· resume state 19/19 Β· resume equivalence 12/12 Β· single-server UI 8/8 Β· atomic save 9/9 Β· transcript stamp 3/3 Β· watcher 12/12 Β· sessions 47/47 Β· embedding + origin policy 18/18.
Layout
The full source tree lives in the GitHub repository β this model repository holds only the checkpoint, the paper and the card.
.
βββ README.md this file
βββ CONNECT_FOUR_PAPER.md the Connect Four manuscript
βββ LICENSE MIT
βββ best_hard.pt the checkpoint (step 18,000), 32.3 MB
βββ .gitattributes
Everything else β source, tests, papers, measurement scripts, the demo recording β lives in the GitHub repository (https://github.com/Ckal/connect-four-gf256), and the training data lives in the dataset repository (https://huggingface.co/datasets/Chris4K/connect-four-gf256).
The source tree, for reference:
src/ βββ c4/ β βββ train.py trainer: dense per-ply supervision, holdout, promotion β βββ agent.py hard export, prompts, greedy policy, legal masking β βββ board.py board + legal moves + terminal check β βββ duel.py canonical strength harness β βββ opponents.py random / centre / heuristic / lookahead / minimax β βββ transcript_v2.py canonical transcript decoder β βββ corpus_integrity.py corpus + transcript validation β βββ sessions.py per-session boards and cookies β βββ ui_server.py the game UI (see the multi-user note above) βββ gf256_markov_lm_v6.py the shipped architecture βββ generate.py text-generation driver
tests/ the regression suite tools/ the authoritative measurement scripts scripts/verify_public_isolation.py the public deployment gate docs/ the full research record
## Limitations
1. **Net-negative vs. the 1-ply heuristic** β the headline negative result.
2. **Teacher-agreement β game strength** β the central finding.
3. **Holdout solver coverage is 37.3 %** (560/1,500). The other 940
positions have no proved label and are excluded, not guessed.
4. **~18 pp train/holdout gap** indicates memorisation in the headline
number.
5. **Architecture-level superiority is unproven** β the controlled A/B arms
reached only 0.30 epoch-equivalents and are provisional.
6. **Mirror/symmetry questions remain open** for the corrected model.
## Citation
```bibtex
@misc{ckal2026gf256_connectfour,
title = {Reading the Board: A GF(256) Integer Model for Connect Four},
author = {Ckal, Christof},
year = {2026},
url = {https://github.com/Ckal/connect-four-gf256}
}
License
Code: MIT (see LICENSE). Paper text: Β© 2026 Christof Ckal.
Model weights: MIT.