An n-tuple network agent for capped 2048 — 2048 on a 4×4 board where the game ends the instant a move creates a 2048 tile, and the objective is to maximise score at that instant.
Best certified organic score: 50,484. Best known human score: 58,348.
Standard 2048 mechanics: swipe in one of four directions, tiles slide, equal adjacent tiles merge into
one of double value, each tile merges at most once per move, merges resolve from the edge you swipe
toward. A move is legal only if it changes the board. After each legal move one tile spawns in a
uniformly random empty cell — a 2 with probability 0.9, a 4 with probability 0.1.
The variant: the game ends immediately when a move produces a 2048 tile. So the largest tile that can exist on a live board is 1024, and there is no "keep going" — you are scored on the position at the instant the 2048 appears. The game also ends if no legal move exists.
score = Σ over tiles of v·(log₂v − 1) − 4 × (number of 4-tiles spawned during the game)
equivalently the sum of all merge rewards, where merging two tiles of value v pays 2v.
Tile-sum is monotone non-decreasing — merges conserve it exactly, spawns only add, nothing removes it. A consequence worth knowing before you start optimising anything:
arrival tile-sum ≈ 2.2 × moves r = +0.998, per-episode ratio p10 2.185 / p90 2.218
Arrival tile-sum is very nearly a re-expression of episode length. "Arrive higher" and "survive longer" are the same instruction to within about 1.5%, and the residual is the spawn draw.
- n-tuple network afterstate value function — 8 tuples × 8 symmetries, multi-stage (stage indexed by the number of 1024 tiles), ~191M weights, trained by TD(0) on self-play from an empty board.
- Depth-limited expectimax at play time over that value function, with adaptive depth (deeper when few empty cells) and depth escalation once several 1024 tiles are present.
- A hand-tuned leaf evaluator including a snake-ordered "plan target" term.
Single C++ file, no dependencies, builds with MSVC or g++.
# Linux / macOS
g++ -O2 -std=c++17 -pthread -o big9 src/train2048_big9.cpp
# Windows, MSVC
cl /O2 /EHsc /std:c++17 /D_CRT_SECURE_NO_WARNINGS /Fe:big9.exe src\train2048_big9.cpp
./big9 --eval --stages=8 --tupleset=big8 --threads=4 --depth=2 --plan --adapt \
--deep1024=4 --deep1024at=3 --plock=3600 --tailcost=0.25 --nofin=99999 \
--survtrigger=4 --survms=500 --survbudget=4000000 \
--games=100 --seed=1 --load=weights_master.bin
The trained network is a release asset (764 MB), not a file in the tree — see docs/WEIGHTS.md for the download and the checks to run on it.
Add --trajmoves=<path> for a full per-move trajectory dump (~56 MB per 500 games); the format is
documented in docs/WEIGHTS.md.
Measured over 1,100 fresh organic games from empty, deployed configuration:
| typical game (mean) | ~44,500 |
| median | ~45,000 |
| p90 | ~49,900 |
| best of 500 | ~50,350 |
| best over ~15,000 certified episodes | 50,484 |
The maximum saturates. Best-of-2,000 → 50,400. Best-of-10,000 → 50,400. 1,100 fresh games → 50,368. More games do not buy more score; this is a ceiling, not variance.
1. --eval returns a nonzero exit code on success under Windows. 127 under Git Bash,
0xC0000374 (STATUS_HEAP_CORRUPTION) under PowerShell, 0 under Linux. The cause is a real defect:
runEval calls free(W) without a !g_wMapped guard, and --load memory-maps the weights read-only,
so free() is handed a mapped pointer. The crash is strictly after the last flush and file close, so
all output is complete and correct. Gate on SUMMARY being present and TE rows == --games, never on
the exit code.
2. --eval prints no banner to stderr — that is expert-mode only. It prints two config lines to
stdout: an ARGV(n): echo and an eval: resolved line. Only the eval: line describes what the
engine will actually do. The ARGV echo omits bare flags entirely (--plan and --adapt never appear
in it) and can carry flags that were never applied.
Also: every filename flag must use --flag=value. Passed bare, it does not error — the output lands in
a file named 1.
See docs/FINDINGS.md for the full account. The short version:
Twenty distinct interventions have moved the mean and none has moved the ceiling. Search depth (6 → 10 plies), plan-term weight, plan-term aggregation, plan-target phase, MCTS over the value function, best-of-N, adaptive multilevel splitting, Go-Explore restarts, TD(λ) and Monte-Carlo targets — all closed.
The wall is crowding → alignment → forced detonation. The board fills; at the end roughly 93% of terminations are positions where every legal move creates a 2048. Arrival tile-sum stops at 5,610 over 75,000 episodes.
MIT — see LICENSE.