Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

capped-2048

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.


The variant

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.


The agent

  • 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++.

Build

# 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

Run

./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.


What it scores

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.


Two traps that will cost you an hour

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.


What we learned, and where it stops

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.

⚠️ "Structural" here means structural to this agent, not to the game. Humans clear this ceiling — several are above it on the public leaderboard. Nothing measured here bounds what is possible.


Licence

MIT — see LICENSE.

About

n-tuple network agent for capped 2048 — the game ends the instant a 2048 tile is made. Scores 50,484, and documents why twenty interventions failed to go higher.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages