| # ConnectX-WorldModel | |
| A reinforcement-learning world model (encoder + latent dynamics + value head) | |
| combined with real adversarial search and an exact endgame solver, applied to | |
| Kaggle's **[ConnectX competition](https://kaggle.com/competitions/connectx)** | |
| (ranked Connect-4, 7-wide x 6-tall board, evergreen/Knowledge-only, no | |
| deadline). | |
| Instead of a hand-written Connect-4 bot, this trains a small model to predict | |
| what happens next in latent space, then searches over that prediction to pick | |
| a move β MuZero/AlphaZero-style planning [1][2], applied to a real, | |
| Kaggle-ranked adversarial game, and trained end to end on a single laptop (RTX | |
| 4060 laptop GPU, Ryzen AI 9 HX 370, 32GB RAM β no cluster, no cloud run). Full | |
| write-up, architecture rationale, every bug found along the way, an attempted | |
| fix for a diagnosed zugzwang weakness, and honest limitations: | |
| **[`docs/WHITEPAPER.pdf`](docs/WHITEPAPER.pdf)**. | |
| This is also not a Connect-4-only architecture β it's one instance of a | |
| general latent-space-simulation pattern (the same encoder/dynamics/value/ | |
| decoder + search code has been applied to other domains β equation solving, | |
| constraint-satisfaction logic puzzles, route planning β with zero core-code | |
| changes, just a new implementation of the small interface in | |
| `connectx/environment.py`). ConnectX is documented here because it's the | |
| first genuinely *adversarial* two-player domain this architecture was applied | |
| to β see the whitepaper's Section 1 for the full framing. | |
| ## How it works, briefly | |
| 1. **A learned world model** (`connectx/model.py`) β an encoder maps a board | |
| to a latent vector, a dynamics model predicts the next latent + reward | |
| given an action, a value head estimates cost-to-go, and a diagnostic | |
| decoder checks the latent space isn't collapsing. Trained self-supervised | |
| (`scripts/train.py`, `connectx/train_utils.py`) with **no oracle** β the | |
| real 7x6 board's game tree is too large to brute-force, so the value head | |
| trains via on-policy Monte Carlo returns (`connectx/verifier.py`) instead | |
| of regression against exact labels. | |
| 2. **Real adversarial search, not latent imagination** | |
| (`connectx/adversarial_search.py`) β the board's rules are exactly known, | |
| so rather than asking the trained model to *imagine* the opponent's reply, | |
| the search enumerates the agent's real legal moves and the opponent's real | |
| legal replies directly against the actual board simulator | |
| (`connectx/env.py`), and uses the learned value head only as the leaf | |
| evaluator. This is the single biggest lever: it alone took the agent from | |
| struggling against even a fixed heuristic to reliably beating one it never | |
| trained against. | |
| 3. **An exact endgame solver**, folded into the same search β once a | |
| position narrows to a handful of legal columns (which happens naturally | |
| as a real game fills up), the remaining game tree is small regardless of | |
| how many moves are left, and gets solved exactly instead of estimated. | |
| 4. **Episodic memory + LoRA self-play fine-tuning** | |
| (`connectx/episodic_memory.py`, `connectx/lora.py`, | |
| `scripts/lora_selfplay_finetune.py`) layered on top, each confirmed to | |
| help before being kept. | |
| 5. **A first, honestly-reported attempt at a real diagnosed weakness** | |
| (Connect-4 zugzwang/parity traps) β built, measured, and shipped as an | |
| opt-in experimental parameter rather than oversold as solved. See the | |
| whitepaper's Section 4. | |
| ## Results | |
| Win rate against an independent, trusted test harness (`tests/submission_test.py` | |
| β an alternating-turn engine that does **not** reuse the environment's own | |
| bundled `step()`, deliberately avoiding the shortcut a real submission | |
| validator needs to avoid). Three opponents: random-legal play, the fixed | |
| weak heuristic trained against, and a stronger 1-ply-deeper heuristic never | |
| seen during training. | |
| | Configuration | vs random | vs weak heuristic | vs stronger heuristic | | |
| |---|---|---|---| | |
| | Latent beam search (original baseline) | 63.3% | 0.0% | 6.7% | | |
| | Real adversarial search (1 round) | 96.7% | 100.0% | 66.7% | | |
| | + episodic memory + online learner | 98.3% | 78.3%ΒΉ | 50.0% | | |
| | + a real bug fixed (loss β draw) | 100.0% | 100.0% | 80.0% | | |
| | + LoRA self-play fine-tune | β | β | 81.7% | | |
| | + curriculum self-play (best-ever) | β | β | **85.0%** | | |
| | + exact endgame solver (current, deployed) | 100.0% | 100.0% | 83.3% | | |
| ΒΉ Explained, not a mystery β that harness ran all opponent blocks | |
| sequentially in one process, so the online learner had already drifted from | |
| 60 preceding random-opponent games by the time it reached this block. See | |
| the whitepaper for the full table, the diagnosis, and every real bug found | |
| along the way (7 of them β a couple are worth knowing if you build on this). | |
| On Kaggle's own real rating: this is a TrueSkill-style score that starts | |
| uncertain and converges over dozens of real games β don't read a | |
| freshly-uploaded rating as a verdict. 7x6 Connect-4 is a mathematically | |
| **solved** game, so the real competitive pool likely includes near-perfect | |
| solvers; these results demonstrate the architecture works on this domain, | |
| not a leaderboard-rating prediction. | |
| ## Running locally | |
| Requires Python 3.10+ and PyTorch (CPU is fine β nothing here needs a GPU; | |
| the reference results above were produced on a laptop GPU but nothing in the | |
| code requires one). | |
| ```bash | |
| pip install torch | |
| # Self-test the environment (small board, exact BFS oracle) + a real-board smoke test | |
| python -m connectx.env | |
| # Verify the already-trained, shipped agent against the trusted harness | |
| python -m tests.submission_test | |
| # Train a fresh checkpoint from scratch (real 7x6 board, no oracle at this scale, ~1hr on a laptop) | |
| python -m scripts.train | |
| # LoRA self-play fine-tune an existing checkpoint (this is the step that produced the deployed one) | |
| python -m scripts.lora_selfplay_finetune | |
| # Package a checkpoint + offline self-play memory into a self-contained Kaggle submission.py | |
| python -c "from scripts.build_submission import main; main()" | |
| ``` | |
| Run everything from the repository root (not from inside `connectx/` or | |
| `scripts/`) β every command above is `python -m <package>.<module>`, matching | |
| the layout below. | |
| `submission.py` (repo root) is the actual file to upload to Kaggle as-is β | |
| it has zero dependencies beyond `torch`/`base64`/`io`/`time`, with the | |
| trained weights and episodic memory embedded directly in the file. | |
| ## Training this for a different game | |
| Nothing here is Connect-4-specific beyond `connectx/env.py`. Every other | |
| file only depends on the small interface `connectx/environment.py` defines: | |
| - `state_dim`, `num_actions`, `always_legal_actions` (properties) | |
| - `is_solved(state)`, `is_legal(state, action_idx)` | |
| - `step(state, action_idx) -> (next_state, reward, done)` | |
| - `random_problem(rng) -> (state, answer)` | |
| - `bfs_solve(state, max_depth=8) -> path or None` (an exact oracle, if one | |
| exists at a scale small enough to brute-force β return `None` | |
| unconditionally if it doesn't, the way `env.py` does above `BFS_MAX_CELLS`) | |
| To point this at a new game or puzzle: | |
| 1. Implement `Environment` for your domain (see `connectx/env.py` for a full | |
| worked example, including the small-board-with-an-oracle / large-board- | |
| without-one pattern). | |
| 2. Run `connectx.train_utils.train_stage1` on your new environment to get a | |
| working encoder/dynamics/decoder β no oracle needed for this stage in any | |
| domain. | |
| 3. **If your domain is single-agent** (a puzzle, not a two-player game): | |
| the value head can train directly against `bfs_solve` labels if you have | |
| an oracle, or via `connectx.verifier.train_mc_value_onpolicy` (drop | |
| `unsolved_penalty`, since a single-agent domain never has a "loss," only | |
| "unsolved") if you don't. | |
| 4. **If your domain is adversarial** (two players, like this one): bake a | |
| fixed opponent into your `step()` first (the "easy path" β see `env.py`'s | |
| own module docstring) to get the pipeline working end to end, THEN set | |
| `train_mc_value_onpolicy(..., unsolved_penalty=<something>)` β this is | |
| the one setting that matters most for an adversarial domain and is | |
| exactly what bug #2 in the whitepaper was about: without it, the value | |
| head never sees a single example of "this leads to losing." | |
| 5. If your domain's rules are exactly known (not something that needs to be | |
| learned) and the state space is too large for `bfs_solve` to search | |
| exhaustively at decision time, real search over the real environment | |
| (`connectx/adversarial_search.py`'s pattern, or the plain single-agent | |
| search in `connectx/search.py`) will almost always beat asking the | |
| dynamics model to imagine ahead β that was this project's single biggest | |
| result. | |
| ## Layout | |
| ``` | |
| connectx-opensource/ | |
| connectx/ The importable package -- everything domain-agnostic + ConnectX itself | |
| environment.py The generic Environment interface everything else depends on | |
| env.py ConnectX itself: board, rules, the fixed training opponent | |
| model.py Encoder / dynamics / value head / decoder | |
| train_utils.py Self-supervised stage-1 training (no oracle, no labels) | |
| verifier.py On-policy Monte Carlo value-head training (no oracle) | |
| search.py Latent-space search (comparison baseline) + load_checkpoint | |
| adversarial_search.py The REAL search actually deployed: minimax + exact endgame solver | |
| + the experimental parity-heuristic attempt (Section 4.1) | |
| episodic_memory.py k-NN memory over real self-play trajectories (won + lost) | |
| memory_build.py Builds that memory offline, at packaging time | |
| lora.py Generic LoRA wrapper | |
| scripts/ Executable entry points (run as `python -m scripts.<name>`) | |
| train.py Full training pipeline (stage 1 + stage 2 + self-play) | |
| lora_selfplay_finetune.py LoRA self-play fine-tune of an existing checkpoint | |
| build_submission.py Packages a checkpoint + memory into one self-contained submission.py | |
| tests/ | |
| submission_test.py The trusted, independent test harness | |
| checkpoints/ | |
| connectx_checkpoint.pt Trained weights | |
| docs/ | |
| WHITEPAPER.pdf Full write-up: architecture, every bug, all results, limitations | |
| build_whitepaper.py Regenerates WHITEPAPER.pdf (requires `pip install fpdf2`) | |
| submission.py THE deployed file β upload this to Kaggle as-is | |
| LICENSE | |
| .gitignore | |
| ``` | |
| ## Citations | |
| Full reference list with page/venue detail is in `docs/WHITEPAPER.pdf`. | |
| Headline credits: latent-space planning follows MuZero [1] / AlphaZero [2]; | |
| episodic memory follows Model-Free Episodic Control [3] and Neural Episodic | |
| Control [4]; LoRA fine-tuning follows Hu et al. [5]; the zugzwang/parity | |
| endgame theory referenced in the whitepaper's case study traces to Victor | |
| Allis's 1988 solution of Connect-4 [6]; the board/config schema and | |
| fixed-opponent-in-`step` convention are ported from Kaggle's own ConnectX | |
| competition [8] and the `kaggle_environments` package [9], not from any | |
| published agent's code. | |
| ## License | |
| [MIT](LICENSE). | |