Browse sections

root@kaus.com:~/projects$ cat neat-playground.md

NEAT Playground

active

Personal

Kbit · page guide
  1. 1 / 4

    Use the section map on long studies.

    Jump between methodology, results, experiments, limitations, and related writing without losing your place.

  2. 2 / 4

    The narrative carries the claim.

    Read each section as problem, method, evidence, and boundary. Negative results remain part of the story.

  3. 3 / 4

    Inspect figures alongside the narrative.

    Read figures in place, scroll wide charts or tables horizontally, and follow any source links for the underlying evidence.

  4. 4 / 4

    Keep exploring the corpus.

    Return to Projects or open Writing for the next thread.

NEAT neuroevolution implemented from scratch in C, evolving neural nets that play Flappy Bird, Snake, 2048, and Pac-Man, with interactive replay visualizers you can run in the browser.

A from-scratch C implementation of NEAT (NeuroEvolution of Augmenting Topologies) that evolves small neural networks to play four arcade games, using episode fitness without action labels or gradient descent. Recorded evolution histories replay in interactive browser visualizers showing the network, its sensors, and its decisions.

try it

Interactive replay visualizers, hosted right here. Pick a game and scrub through generations.

Watch the evolved agents play
  • Repository-reported throughput: ~60M agent-steps/second/core
  • 2048 learning study: 102.6 mean maximum tile across 5 final test batches
  • 2048: a late score increase after a plateau around generations 170 to 200
  • 20 retained learning runs, 4,500 generation records available as CSV

Overview

NEAT Playground trains neural networks through evolution. A population of small networks plays Flappy Bird, Snake, 2048, or Pac-Man, and selected performers reproduce with mutations to their weights and structure. Episode fitness guides the search without action labels or gradient descent. The method is NEAT (NeuroEvolution of Augmenting Topologies), built from scratch in C99, driving all four games through one shared interface. The repository reports throughput of about 60 million agent-steps per second per core; that figure was not re-benchmarked for this case study.

The repository began as a two-line README in April 2019. The retained implementation and four-game study were committed in July 2026, followed by browser replay work. Its central comparison asks whether changing the training worlds helps: train the same algorithm on one fixed world instead of a fresh random one every generation, and all four games score lower on an unseen world after fixed-world training than after random-world training in the recorded ablation.

The problem and motivation

I wanted to train agents from their performance in a game, without collecting examples of the correct action at each step. The simulator can score a completed episode, but that score does not explain which earlier decisions helped. This project uses neuroevolution to search directly over the controller and its network structure.

NEAT represents a network's weights and connections as a genome, evaluates its episode fitness, and uses selection and mutation to produce the next population. It requires neither action labels nor gradients through the simulator. Sparse or delayed rewards can still make useful candidates hard to discover: a fitness score must distinguish enough behaviors for selection to make progress. The sensor and fitness choices therefore remain part of the learning problem.

The four games form a difficulty ladder, not four unrelated demos: Flappy is a pure reflex baseline, Snake adds path planning, 2048 adds reading a whole board over many moves, and Pac-Man adds pursuing one goal while evading another. Holding the algorithm fixed and varying only the game turns each result into a question about what NEAT does under different demands.

How it evolved (2019 to 2026)

The April 2019 commit records the AiBirb name and the intention to use NEAT for Flappy Bird. The first retained implementation arrived in July 2026: a C simulator, shared-memory transport, a viewer, and a trainer. Subsequent commits moved game behavior behind the Env interface and added Snake, 2048, and Pac-Man. That interface let the same evolutionary machinery meet different observation spaces and action choices.

The study then made the training setup itself a subject of comparison: fixed versus changing worlds, fitness versus batch-based champion selection, and feedforward versus recurrent networks. Saved logs keep those questions inspectable after a run ends. The browser replay work provides another view of the recorded agents, while the CSV downloads below expose the measurements behind the learning curves and selection comparison.

System architecture

One NEAT engine drives all four games through a single shared interface, the Env vtable in include/env.h: observe, step, reset, alive, score. The engine is 2,086 lines across 7 files (genome operations, population and speciation, network compilation, HyperNEAT, config parsing, serialization, and the RNG), game-agnostic by design. Three driver binaries, a trainer, a viewer, and a server, all reach every game through that vtable, and each of the four game modules, 1,238 lines total, implements it independently: adding a fifth game means one new file behind the vtable, not touching the engine or the drivers. The whole tree is 4,698 lines across 21 files.

The evolutionary core is canonical NEAT: every new connection gets a global innovation number so differently-shaped networks can still be recombined correctly, and new structure is protected from immediate competition by speciation, grouping networks by a compatibility distance (excess genes, disjoint genes, average weight difference) and breeding mostly within each species using fitness sharing, with stagnant species removed while the top species and the all-time champion stay protected. A master seed drives separate game and evolution RNG streams. The default build avoids architecture-specific and fast-math flags, and the regression script compares the saved champions and logs from two same-seed runs of the same binary. Cross-platform byte equality has not been established. Champion-selection batches also derive their seeds from the master seed and generation number, making the selection inputs reproducible.

The browser visualizers load compressed JSON histories containing species networks and generation-specific evaluation-course seeds. A JavaScript port of the NEAT evaluator and all four environments executes the selected network and displays its activations and sensor readings. Keeping that second implementation consistent with C is a separate engineering task. The downloadable replay check below covers four diagnostic episodes; it does not certify every species and course in the histories.

One game-agnostic NEAT engine (2,086 lines across 7 files) is driven by three binaries (trainer, viewer, server). All three reach every game through the same Env vtable defined in include/env.h, four calls (observe, step, reset, score) that each of the four game modules (1,238 lines total) implements independently. Adding a fifth game means writing one new file behind that vtable, not touching the engine or the drivers.

NEAT engine (game-agnostic)genome 420 · population 470 · network 188 · hyperneat 178config 412 · serialize 365 · rng 53 — 2,086 lines, 7 filesTrainerneat_trainer_main.c · 400 linesViewerviewer_main.c · 86 linesServerserver_main.c · 73 linesEnv vtableinclude/env.hobserve · step · reset · alive · scoreenv_create(name)Flappy Birdgames/flappy.c100 lines · score: pipesSnakegames/snake.c368 lines · score: apples2048games/g2048.c300 lines · score: max tilePac-Mangames/pacman.c470 lines · score: pellets1,238 lines across the 4 game modules · 559 lines across the 3 driverssrc/*.c + src/games/*.c + include/*.h totals 4,698 lines

architecture · neat-playground

The games and evaluation design

Each game hands the network a fixed-size sensor vector and gets back one action, chosen by argmax over the output layer. In the hosted replay histories, Flappy Bird uses 5 sensors and 2 actions, glide or flap. Snake uses 31 inputs: 7 heading-relative features plus 24 ray readings, with 3 relative-turn actions. 2048 combines its 16 board cells with 8 engineered features for 24 inputs and 4 slide actions. Pac-Man combines 16 features with 24 ray readings for 40 inputs and 4 move actions. These counts describe the hosted replay runs; the historical study results below retain their separate measurement provenance.

The default training setup derives a new batch of world seeds for each generation. Flappy's pipe gaps, Snake's start cell and apple positions, 2048's tile spawns, and Pac-Man's maze (a lattice of pillars, each staying a wall about 70% of the time, always generated from a fully-connected base so no maze is unsolvable) are drawn from the run's RNG stream. The memorization ablation below compares fixed-world and random-world training on unseen worlds. Random training worlds reduce the opportunity to memorize one level, but held-out evaluation is still needed to measure generalization.

Champion selection follows the same logic. Instead of crowning whichever genome had the highest fitness ever recorded, rewarding whoever's generation served up an easy world, the trainer periodically re-tests the top genomes on a brand-new random batch and crowns whoever scores best on it. A separate verifier can re-test the saved champion across a specified seed range, reporting mean, minimum, and maximum scores. Its printed mean is integer-truncated; the selection comparison below retains those printed batch means.

Experiments and results

The memorization summary compares training on one reused world with training on newly randomized worlds. All four games have higher reported unseen-course scores under random-world training. For 2048, the fixed-world training-course and unseen-course scores are 128 and 6, compared with 64 and 42 under random-world training. Snake reports 31 and 10, compared with 12 and 25. Pac-Man reports 40 and 15, compared with 66 and 41. Flappy reports 6 and 2, compared with 54 and 54. These eight summary rows omit trial counts, seeds, evaluation horizons and raw outcomes, so they do not establish matched conditions, uncertainty or the cause of the differences. The source calls 2048's score convention maximum tile, but its aggregation protocol is absent. Values such as 6 and 42 are retained summary scores. Without the aggregation protocol, they should not be read as literal tiles from individual boards.

The learning curves average five runs at each generation. Their final logged test-batch means average 54 pipes for Flappy, 27.6 apples for Snake, and 32.8 pellets for Pac-Man. For 2048, the score is the largest tile reached in an episode, not the accumulated merge score. The trainer averages that value across its test batch and truncates it to an integer. At generation 299, the five logged values were 42, 38, 339, 40, and 54: a mean of 102.6 with a standard deviation of 118.3 across runs. That spread matters more than the mean alone. The late increase in the average curve does not establish a grokking transition or explain what the high-scoring run learned.

Two further three-seed ablations tested champion selection and recurrence. Selection on fresh test batches has slightly higher reported averages than selection on training fitness in three games: Flappy 54 versus 51, Snake 27 versus 26, and 2048 38 versus 35, with Pac-Man unchanged at 30, all rounded. These summaries do not establish statistical certainty. An early apparent recurrence advantage turned out to be confounded, as the later experiment explains.

A separate four-run, 800-generation 2048 study recorded peak logged test-batch mean maximum tiles of 46, 53, 65, and 47. The 65 is a batch average, not a tile value. That study used feedforward networks, a larger population, different observations, and longer test episodes than the five-run learning study. Its peaks and the learning study's final means are different measurements, so they cannot establish a gain from training longer.

Learning curves

Each curve averages five runs' logged test-batch means at each generation. Final values report the mean and population standard deviation across those five logged batch means. The trainer truncates each batch mean to an integer. For 2048, the episode score is its largest tile reached, not accumulated merge points; its final logged batch means are 42, 38, 339, 40, and 54. These selection-batch curves do not establish a grokking transition or an independent final evaluation.

Flappy Bird · pipes6000gen 99

final 54.0 ± 0.0 pipes (mean ± population SD across five logged batch means)

Snake · apples3000gen 249

final 27.6 ± 1.7 apples (mean ± population SD across five logged batch means)

2048 · mean max tile12000gen 299

final 102.6 ± 118.3 mean max tile (mean ± population SD across five logged batch means)

Pac-Man · pellets4000gen 249

final 32.8 ± 7.0 pellets (mean ± population SD across five logged batch means)

Historical learning runs · source ef5a8a0 · results/raw/learning_*_s1–5.csv and results/summary/headline.json · measurements rechecked September 21, 2026.

Memorization ablation

The retained summary compares one reused training world ("fixed world") with newly randomized training worlds ("random world"). Hollow bars show reported training-course scores and solid green bars show reported unseen-course scores. All four games have higher reported unseen scores under random-world training.

These eight summary rows omit trial counts, seeds, evaluation horizons and raw outcomes, so they cannot establish uncertainty or explain the differences. The source labels 2048's score convention as maximum tile, but its aggregation protocol is absent. Values such as 6 and 42 are retained summary scores. Without the aggregation protocol, they should not be read as literal tiles from individual boards. Scores use different units across games.

Flappy Bird · pipes600625454fixed worldrandom world

fixed: 6→2 unseen · random: 54→54 unseen

Snake · apples40031101225fixed worldrandom world

fixed: 31→10 unseen · random: 12→25 unseen

2048 · reported score150012866442fixed worldrandom world

fixed: 128→6 unseen · random: 64→42 unseen

Pac-Man · pellets80040156641fixed worldrandom world

fixed: 40→15 unseen · random: 66→41 unseen

Decisions and tradeoffs

Writing NEAT from scratch in C gave me control over both the evolutionary operators and the simulation cost. Small environments and batched population evaluation made repeated sweeps practical on commodity hardware. The cost is maintaining the machinery a mature library would already provide: mutation operators, serialization, visualization support, and established behavior to compare against when a run goes wrong.

Varying the training worlds is a deliberate choice: it reduces repeated exposure to one fixed layout. The reported unseen-course scores are consistent with retaining that choice, although the summary does not establish a causal effect. A related choice was what the network should see. A raw board grid failed outright on the harder games (Snake scored 0 apples every time, Pac-Man about 7), in the evaluated configuration with 148 to 251 raw inputs. Those runs motivated the hand-built feature vectors used in the reported results, without establishing that NEAT cannot learn from raw boards under other configurations.

Keeping training in C and replay in JavaScript makes the saved networks easy to inspect in a browser, but leaves two implementations to maintain. A September consistency check rebuilt the native recorder and replayed four existing diagnostic cases on seed 12345, with no new training or holdout evaluation. Across 2,039 recorded steps, it compared observations, actions, scores, neuron activations and rendered state, then each final score. The networks occur in the public histories, but this diagnostic seed is not one of their recorded courses. The downloadable inputs and checker make that bounded comparison repeatable; final-state and termination checks remain outside the recorded evidence.

The newest work in the same tree raises the engineering bar again: C++20 and CMake instead of hand-rolled Makefiles, sanitizer builds, a ctest suite, and a held-out verifier as a first-class command rather than a line in a training log, aimed at a fifth game, a Frogger-style lane crossing that adds moving-obstacle timing to the planning and board-reading the first four games already demanded.

Lessons learned

Random-world training had higher unseen-world scores in all four recorded game comparisons. That supports keeping varied training environments in this project. The exported summaries do not establish a universal cause of memorization, matched evaluation horizons, or uncertainty across repeated runs. A replicated comparison with matched training and evaluation budgets would test how robust the observed advantage is.

The second lesson is a methodology warning that cost a real result to learn. An early one-factor-at-a-time sweep showed recurrent connections nearly doubling 2048's score, a headline-looking finding. Re-running the comparison from an already-tuned baseline instead of a default one made the effect mostly disappear: feedforward and recurrent networks landed within a couple of points of each other (45 versus 44). The apparent recurrence advantage depended on the comparison baseline. These runs did not establish an independent benefit after the other settings were tuned, so the earlier headline was too strong.

A third lesson: an easy benchmark tells you almost nothing about an algorithm, since it never exercises the parts that make the algorithm interesting. Flappy ignored nearly every hyperparameter that mattered for the other three games, so judged on Flappy alone, the honest conclusion would have been that none of NEAT's knobs matter, exactly wrong for the rest.

Current limitations

The repository is private, so this page has no code link. Selected ablation summaries, source identities, a reading guide, and checksums are downloadable beside the results. Those extracts let readers inspect the reported values, but the full source and run artifacts needed to reproduce the study are not public. The games and networks are both small by design, which is what makes several hundred training runs affordable, but it also means the conclusions are about small-network neuroevolution specifically, not neuroevolution at scale. The hyperparameter sweep is one-factor-at-a-time: it identifies which single knobs matter but, as the recurrence result shows, not how they interact, and the full interaction surface was not mapped.

A few modeling choices simplify the games themselves. Pac-Man's three ghosts use straight-line Manhattan targeting at half of Pac-Man's own speed, which keeps the maze winnable, rather than the arcade original's timing-based, personality-driven chase logic. 2048's illegal-move handling, a small penalty plus a cap on consecutive illegal moves before ending the episode, is one reasonable policy among several. The checkpoint serializer is built to round-trip the project's own files, not hardened against malformed input, so it is safe for personal runs and not a trusted import format for files from elsewhere.

Where it is heading

The repository is private while some early experimental history gets cleaned up before anyone else reads it. Opening the source is the plan once that pass is done, and a code link replaces this page's current no-repo state at that point.

The newer C++20 line of work, still in the same working tree, keeps moving on its own fifth game. Its stated next steps are a raw or combined pixel-observation mode (currently hand-built feature vectors only) and river and log lanes to complete the hazard set (currently road hazards only), both open work rather than finished.

Experiments

2 experiment write-ups · continued development log

Memorization ablation: fixed vs. random training worlds · 2026-07 · completed

Setup

The retained summary compares two training regimes per game: one reused training world and newly randomized training worlds. It records training-course and unseen-course scores for each regime. The eight rows do not include trial counts, training or evaluation seeds, horizons, or raw outcomes. They do not establish that populations, hyperparameters and evaluation conditions were matched, or quantify uncertainty.

Result

All four games have higher reported unseen scores under random-world training. For 2048, the fixed-world training-course and unseen-course scores are 128 and 6, compared with 64 and 42 under random-world training. The source labels the score convention as maximum tile, but the summary lacks its aggregation protocol. Values such as 6 and 42 are retained summary scores. Without the aggregation protocol, they should not be read as literal tiles from individual boards. Snake reports 31 and 10 apples under fixed-world training, compared with 12 and 25 under random-world training. Pac-Man reports 40 and 15 pellets, compared with 66 and 41. Flappy reports 6 and 2 pipes, compared with 54 and 54. Snake's random-world unseen score exceeds its training-course score, but these summaries do not explain why.

What it changed

Generation-varying world seeds were already the trainer's default going into this ablation. The direction of the reported comparison is consistent with retaining that default. Establishing how much varied worlds help would require the missing protocol and repeated evaluations under matched conditions. The summary alone does not establish a causal benefit or replace independent held-out evaluation.

Champion selection: training fitness vs. a fresh selection batch · 2026-07 · completed

previously

The memorization summary reports higher unseen-course scores under random-world training in all four games. Missing trial records and protocol details prevent a causal conclusion from those summaries. The separate champion-selection experiment asks a narrower question: once training worlds are already random, does it still matter how the champion gets picked at the end of each generation?

Setup

Fitness on any one generation's random worlds is noisy, since some generations simply serve up easier worlds than others. The trainer re-tests its top candidates and current champion at configured intervals on a fresh random batch, seeded from the run's master seed and generation number, and keeps the highest batch mean ("testbatch"). This comparison contrasts that scheme with selecting on raw training fitness ("fitness"), across three run seeds per game. The retained experiment script specifies 150 generations and then evaluates each saved champion on the same 30 seeds, 3000 through 3029, capped at 6,000 ticks per episode. The downloadable CSV retains the verifier's integer-truncated batch means; individual episode scores are not included.

Result

Testbatch selection has a higher three-seed mean in three games. Rounded means are Flappy 54 versus 51, Snake 27 versus 26, and 2048 38 versus 35. Individual runs are mixed: Snake seed 2 scores 25 versus 28, and 2048 seed 3 scores 33 versus 38, both favoring fitness selection. Pac-Man has identical paired scores of 33, 29, and 29 under both methods, a mean of 30.3. These modest average differences do not establish a reliable advantage from three seeds.

What it changed

Fresh-batch selection remains the trainer's default, and this comparison gives a modest reason to retain it. The useful design distinction is between the score that selects a champion and the held-out score used to assess that choice. Reusing the selection batch as final evidence would erase that distinction. More independent runs would be needed to determine whether the small mean differences persist.

Watch it evolve

demos/neat · replays
open full screen ↗
interactive evolution replays

Saved networks and game rules execute in your browser. Training was recorded earlier.

  • Flappy Bird replay dashboard with live network visualization

    Flappy Bird replay dashboard with live network visualization

  • Evolved agent navigating Flappy Bird pipes

    Evolved agent navigating Flappy Bird pipes

  • Evolved Snake agent with sensor rays displayed

    Evolved Snake agent with sensor rays displayed

  • 2048 board with network output panel

    2048 board with network output panel

  • Pac-Man agent balancing pellet collection and ghost evasion

    Pac-Man agent balancing pellet collection and ghost evasion

  • Animated capture of the recorded evolution viewer: the Flappy Bird viewer's species timeline, generation slider, and per-species strategy cards

    Animated capture of the recorded evolution viewer: the Flappy Bird viewer's species timeline, generation slider, and per-species strategy cards

← All projects