- Python 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
...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> |
||
| docs | ||
| scripts | ||
| .gitignore | ||
| config.py | ||
| main.py | ||
| pyproject.toml | ||
| README.md | ||
| sim.py | ||
| track.py | ||
| uv.lock | ||
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 |
|---|---|
![]() |
![]() |
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,
tanhactivations), 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:
- Elitism: the top 3 cars are copied unchanged.
- Selection: parents are picked by tournament selection (size 2).
- Crossover: each gene comes from the fitter parent with probability 0.7, otherwise from the other parent.
- Mutation: each weight is nudged by Gaussian noise (σ = 0.35) with probability 0.10.
- 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


