No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Kshitij Aucharmal 336ac28298 Figure-8 neuroevolution car simulation (pygame + genetic algorithm)
...
Cars with 5 ray sensors and a small MLP brain evolve via elitism,
tournament selection, crossover and mutation. Colors are inherited
from the fitter parent to visualise lineage takeover. Sidebar UI with
pause, restart generation, reset simulation, speed, sensors, mutation
controls, progress and lineage charts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 10:02:07 +05:30
docs Figure-8 neuroevolution car simulation (pygame + genetic algorithm) 2026-10-29 10:02:07 +05:30
scripts Figure-8 neuroevolution car simulation (pygame + genetic algorithm) 2026-10-29 10:02:07 +05:30
.gitignore Figure-8 neuroevolution car simulation (pygame + genetic algorithm) 2026-10-29 10:02:07 +05:30
config.py Figure-8 neuroevolution car simulation (pygame + genetic algorithm) 2026-10-29 10:02:07 +05:30
main.py Figure-8 neuroevolution car simulation (pygame + genetic algorithm) 2026-10-29 10:02:07 +05:30
pyproject.toml Figure-8 neuroevolution car simulation (pygame + genetic algorithm) 2026-10-29 10:02:07 +05:30
README.md Figure-8 neuroevolution car simulation (pygame + genetic algorithm) 2026-10-29 10:02:07 +05:30
sim.py Figure-8 neuroevolution car simulation (pygame + genetic algorithm) 2026-10-29 10:02:07 +05:30
track.py Figure-8 neuroevolution car simulation (pygame + genetic algorithm) 2026-10-29 10:02:07 +05:30
uv.lock Figure-8 neuroevolution car simulation (pygame + genetic algorithm) 2026-10-29 10:02:07 +05:30

NeuroEvolution Car

A small pygame simulation where a population of cars learns to drive one full lap around a figure-eight track. Nobody tells them how to drive: each car is controlled by a tiny neural network, and a genetic algorithm breeds the best drivers from one generation to the next.

Every car is a colored rectangle. Generation 1 gets random colors, and each child inherits its color from its fitter parent, with a little drift. Watching the colors on the track and in the lineage chart shows which family of drivers takes over the population.

Generation 1: random brains ~30 generations later
Generation 1 Trained

Trained population, full UI

Run it

Requires uv (it installs Python, pygame-ce and numpy for you):

uv run main.py

Or with plain pip:

pip install pygame-ce numpy
python main.py

Controls

Every action has a button in the sidebar and a keyboard shortcut:

Key Action
Space Pause / resume
N End this generation now and evolve the next one
R Restart the current generation (same brains, back to the start line)
Shift+R Reset the whole simulation (new random population and colors)
- / + (or ↓ / ↑) Simulation speed: 1x to 64x
S Sensor rays: off / leader only / all cars
D Show or hide crashed cars
[ / ] Decrease / increase the mutation rate
Esc / Q Quit

On the track, the yellow outline marks the car furthest along, and a white outline marks the elites carried over unchanged from the previous generation.

How it works

The car

  • 5 distance sensors at -90°, -45°, 0°, +45° and +90°, each ray-cast up to 170 px against the track edge.
  • Brain: a 6 → 8 → 2 neural network (5 sensors + current speed in, tanh activations), 74 weights in total. The weights are the car's genome.
  • Outputs: steering (turn left/right) and throttle (accelerate when positive, brake when negative).
  • A car dies when any corner leaves the road, or when it makes no forward progress for 70 frames.

Fitness

Progress is measured along the track's centre line, and only forward progress inside a small look-ahead window counts. That means turning onto the wrong branch at the crossing, or driving backwards, earns nothing.

fitness = 100 × progress                          (didn't finish)
fitness = 200 − 100 × lap_time / time_limit       (finished: faster laps score higher)

Genetic algorithm

Each generation:

  1. Elitism: the top 3 cars are copied unchanged.
  2. Selection: parents are picked by tournament selection (size 2).
  3. Crossover: each gene comes from the fitter parent with probability 0.7, otherwise from the other parent.
  4. Mutation: each weight is nudged by Gaussian noise (σ = 0.35) with probability 0.10.
  5. Color + lineage: the child takes the fitter parent's color, plus a small hue drift.

What to watch

  • Per-gen chart: best progress, average progress, and the percentage of cars that finished. The first finisher usually shows up between generations 3 and 20. After that, the average climbs as the winning behavior spreads.
  • Lineage chart: each column is one generation, stacked by founder. You'll see dozens of colors collapse into a few families, and usually one family ends up owning the whole population.

Tuning

Every parameter is in config.py: track size and width, car physics, sensor range, network size, population size, elitism, mutation and the time limits. They are set so that evolution is gradual and easy to see. With a narrower track (around 46 px) the population often gets stuck at the crossing for many generations. With a wider one (60 px or more) it solves the track in 2–3 generations.

Project layout

main.py              pygame app: rendering, sidebar, buttons, keyboard
sim.py               vectorised car physics, sensors, neural nets, genetic algorithm
track.py             figure-eight track, collision mask, progress centre line
config.py            all tunable parameters
scripts/make_media.py  regenerates the screenshots/GIFs in docs/ (headless)

To regenerate the media:

SDL_VIDEODRIVER=dummy uv run --with pillow scripts/make_media.py