YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
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 (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.
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
- 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. - 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. - 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.
- 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. - 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).
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 β returnNoneunconditionally if it doesn't, the wayenv.pydoes aboveBFS_MAX_CELLS)
To point this at a new game or puzzle:
- Implement
Environmentfor your domain (seeconnectx/env.pyfor a full worked example, including the small-board-with-an-oracle / large-board- without-one pattern). - Run
connectx.train_utils.train_stage1on your new environment to get a working encoder/dynamics/decoder β no oracle needed for this stage in any domain. - If your domain is single-agent (a puzzle, not a two-player game):
the value head can train directly against
bfs_solvelabels if you have an oracle, or viaconnectx.verifier.train_mc_value_onpolicy(dropunsolved_penalty, since a single-agent domain never has a "loss," only "unsolved") if you don't. - If your domain is adversarial (two players, like this one): bake a
fixed opponent into your
step()first (the "easy path" β seeenv.py's own module docstring) to get the pipeline working end to end, THEN settrain_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." - If your domain's rules are exactly known (not something that needs to be
learned) and the state space is too large for
bfs_solveto search exhaustively at decision time, real search over the real environment (connectx/adversarial_search.py's pattern, or the plain single-agent search inconnectx/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.