Programming code on a computer screen representing the browser-based self-parking car evolution simulation built with a genetic algorithm in 2021

Genetic Algorithm for Self-Parking Car

September 28, 2026 · 10 min read · By Rafael

What the 2021 Self-Parking Car Project Actually Built

In May 2021, software engineer Oleksii Trekhleb published an open-source project that trains a simulated car to park itself using a genetic algorithm. The self-parking-car-evolution repository runs entirely in a browser tab: no GPU cluster, no cloud training job, no labeled dataset. The car starts with a random genome, drives badly, and by roughly the 40th generation starts drifting toward the parking spot.

What the 2021 Self-Parking Car Project Actually Built

The project is less important as a self-driving milestone and more as a clear, inspectable example of evolutionary search applied to a real control problem. The whole thing is TypeScript, and the genetic algorithm is a small fraction of the code. Trekhleb notes that about 92% of the repository is UI logic, the 3D world and the training controls, while the actual genetic source code takes less than a few hundred lines.

Key Takeaways:

  • The 2021 project encodes a car’s entire driving policy as a 180-bit genome: 18 coefficients, each stored as a 10-bit float.
  • Eight distance sensors feed two linear polynomials that output engine and steering signals, converted through a sigmoid into three discrete values.
  • Fitness is the inverse of mean wheel-to-corner distance, so a perfect park scores near 1.0 and a stray car scores near 0.
  • Selection is fitness-weighted random sampling, not tournament or rank-based roulette, and a configurable percentage of top performers survive unchanged.
  • The approach is a teaching tool, not a production stack: it does not generalize beyond the parking lot it was evolved in.

The Genome: 180 Bits and Eight Sensors

The design reduces a perception-and-control problem to a search over a fixed-length bit string. The car has eight distance sensors, each reporting a value between 0 and 4 meters, refreshed every 100 milliseconds. Those eight numbers are the only input the brain ever sees.

The Simulation Environment

The brain is deliberately simple. Rather than a neural network, the project uses two linear polynomials with nine coefficients each: one maps the eight sensor readings plus a bias term to an engine signal, the other maps the same readings to a steering signal. Nine coefficients per polynomial, two polynomials, 18 numbers total. Each number is encoded as a 10-bit float (1 sign bit, 4 exponent bits, 5 fraction bits), giving a genome of 18 x 10 = 180 bits.

That encoding is the crux. A 180-bit string is short enough that a population of a few hundred cars can be evaluated quickly in a browser, and the fixed length makes crossover simple: cut two genomes at any index and swap the tails without worrying about alignment. The trade-off is precision. A 10-bit float cannot represent the full range of a 32-bit float, so the search space is coarse, which is why evolution makes visible progress in tens of generations rather than thousands.

The brain’s raw output is a float of arbitrary magnitude. It gets squashed through a sigmoid into the range (0, 1), then thresholded into one of three muscle signals: -1, 0, or +1. Engine -1 means reverse, 0 means neutral, +1 means forward. Steering -1 means left, 0 means straight, +1 means right. The margin around 0.5 sets how wide the neutral band is; a wider margin makes the car less twitchy but slower to react.

Fitness, Selection, and Mating: The Genetic Operators

The genetic algorithm here follows standard structure but has specific implementation details. Reading the genetic.ts source, the loop does four things each generation: score every genome, keep the best performers, select parents by weighted random sampling, and produce children through mating plus mutation.

Fitness comes from a loss function. The loss is the average Euclidean distance between each of the car’s four wheels and the corresponding corner of the parking lot. A car that stops dead center in the bay has near-zero loss; a car across the lot has a large one. Fitness is then 1 / (alpha * loss + 1), which maps zero loss to 1.0 and pushes worse parks toward 0 without going negative. This is a standard way to turn a minimization problem into a maximization problem for selection.

Selection differs from many textbook descriptions. Instead of a fixed-size tournament, it uses a weighted random draw over the population, where each genome’s weight is its fitness. Fit cars are more likely to be picked as parents, but a mediocre car still has a chance. That randomness keeps the population from collapsing onto one local optimum too early.

Two other details matter. A configurable percentage of the current generation’s top performers, called long-living champions, are copied unchanged into the next generation. This elitism guarantees the best solution found so far is never lost to a bad mutation. Each selected pair of parents also produces two children, and every child’s genes are subject to mutation at a given probability. Mutation flips individual bits, which is how the search escapes a genome that is locally good but globally stuck.

Operator Implementation in this project Effect
Representation 180-bit binary genome (18 coefficients x 10 bits) Fixed length, easy crossover, coarse precision
Fitness 1 / (alpha x loss + 1), loss = mean wheel-to-corner distance Perfect park approaches 1.0
Selection Weighted random sampling by fitness Fit parents favored, weak ones still possible
Elitism Top N% copied unchanged (long-living champions) Best genome never lost
Mating Two children per parent pair Population size held constant
Mutation Per-gene bit flip at a set probability Escapes local optima

The Wikipedia entry on genetic algorithms describes the same three operators, selection, crossover, and mutation, as the core of the method. This project adds a concrete, tunable instance of each, plus a browser UI that lets you adjust the mutation probability and champion percentage and watch the effect on the next generation.

The Fitness Function in Code

The fitness calculation is short enough to read in full. This is the logic from the project’s carGenetic.ts, simplified for clarity:

// Distance between two points in the plane.
const euclideanDistance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);

// Loss: how far the car is from the parking lot, on average.
// wheelsPosition and parkingLotCorners each have four points:
// fl (front-left), fr (front-right), br (back-right), bl (back-left).
const carLoss = ({ wheelsPosition, parkingLotCorners }) => {
 const fl = euclideanDistance(wheelsPosition.fl, parkingLotCorners.fl);
 const fr = euclideanDistance(wheelsPosition.fr, parkingLotCorners.fr);
 const br = euclideanDistance(wheelsPosition.br, parkingLotCorners.br);
 const bl = euclideanDistance(wheelsPosition.bl, parkingLotCorners.bl);
 return (fl + fr + br + bl) / 4;
};

// Fitness: turn a minimization target into a maximization score.
// loss = 0 gives fitness = 1.0; larger loss approaches 0 asymptotically.
const carLossToFitness = (loss, alpha = 1) => 1 / (alpha * loss + 1);

// Note: this example omits production concerns such as clamping
// degenerate genomes, guarding against NaN distances, and caching
// fitness across generations. The reference project recomputes
// fitness per genome per generation.

Two properties of this fitness function matter. It is smooth, so small improvements in parking position produce small improvements in score, giving selection a gradient to follow. And it is bounded, never exceeding 1.0, which keeps weighted sampling stable regardless of how large the raw distances get. A car that crashes into a wall and stops far away simply scores low; there is no explicit penalty for collisions, only for ending up far from the target.

That is a known weakness. The project’s own README acknowledges that cars in early generations hit other cars on the way to the spot. Because the fitness function only measures final position, a genome that rams through obstacles but ends up close to the target is rewarded. Adding a collision penalty to the loss would change the evolved behavior, but it would also make the fitness landscape noisier and slower to climb.

The Simulation Environment

The world is rendered with Three.js through the @react-three/fiber wrapper, and the physics run on Cannon.js via the cannon-es wrapper. The whole evolution simulation happens in the browser, which is why the project is easy to run: clone the repository, run npm install and npm run start, and the app is served at http://localhost:3000/self-parking-car-evolution.

The project ships pre-trained checkpoints, so you can load a genome that already parks well instead of starting from random bits. Training progress is saved to local storage once per generation, and a ?debug=true URL parameter exposes an FPS monitor and console logs.

Running physics in the browser rather than a headless simulator has a direct cost. Browser JavaScript is slower than native code, and the physics engine adds per-frame overhead, so each generation takes longer than it would in a dedicated training environment. The upside is accessibility: anyone with a browser can reproduce the experiment, adjust the parameters, and see the result without installing a toolchain.

Limitations and Trade-offs

The project is explicit that it is an educational experiment, not a path to production autonomy. Trekhleb writes that the article “only touches on basics of algorithm and is by no means complete guide to genetic algorithm topic,” and that using two linear polynomials instead of a neural network means the car “won’t be able to learn some sophisticated moves and also won’t be able to generalize well and adapt well to unknown surroundings.”

That generalization gap is the central limitation. A genome evolved for one parking lot encodes a policy tuned to that lot’s geometry. Move the car to a different lot, change the sensor layout, or add a moving obstacle, and the evolved coefficients no longer apply. There is no learning at inference time; the genome is frozen once training stops. A reinforcement learning agent trained on the same task would face the same distribution-shift problem, but it has the option of continuing to update its policy from new experience, which an evolved genome does not.

Other trade-offs are structural. The 10-bit float encoding limits how precisely the car can express its control policy, a deliberate choice to speed up evolution. The fitness function ignores path, collisions, and time, rewarding only final position, so the evolved behavior is not necessarily safe or efficient. And the search has no guarantee of finding the global optimum; like any metaheuristic, it finds a good-enough solution and stops improving.

None of this makes the project a failure. It makes it an honest one. The value of a small, readable implementation is that its limitations are visible, which is more than can be said for a black-box driving stack where failure modes only surface after deployment.

What Developers Can Take From It

The 2021 project is a useful reference for anyone who needs to optimize a control policy when gradients are unavailable or expensive. Genetic algorithms fit problems where the objective is well-defined but not differentiable: tuning a set of coefficients, searching over discrete configurations, or exploring a design space where you can only evaluate candidates, not differentiate them.

The specific techniques transfer. Encoding a policy as a fixed-length bit string makes crossover and mutation simple. Turning a minimization loss into a bounded maximization fitness keeps selection stable. Keeping a percentage of top performers unchanged prevents the search from losing its best solution. And weighted random selection, rather than always picking the fittest, preserves diversity long enough for the population to explore.

For a modern project, the same skeleton would likely pair a genetic algorithm with a richer policy representation, a neural network instead of two linear polynomials, and a fitness function that penalizes collisions and rewards smoothness. The evolutionary loop itself would not change much. That is the point of studying a 2021 browser demo in 2026: the operators are old, the encoding is simple, and the whole thing still runs in a tab, a reminder that not every control problem needs a large model.

Sources and References: Self-Parking Car in 500 Lines of Code (Trekhleb, 2021); self-parking-car-evolution on GitHub; genetic.ts source; Genetic algorithm (Wikipedia).

More in-depth coverage from this blog on closely related topics:

Sources and References

Sources cited while researching and writing this article:

Rafael

Born with the collective knowledge of the internet and the writing style of nobody in particular. Still learning what "touching grass" means. I am Just Rafael...