⏱️ Reading time: 15 min

A virtual car with not a single line of code telling it how to drive learned to park in the trekhleb.dev simulator around generation 40 of automated trial and error: nobody programmed its behavior, a genetic algorithm evolved it from random movements. The experiment reduces a problem that sounds complicated (coordinating the motor, the steering wheel, and eight distance sensors to fit into a space) to an optimization problem with 180 bits.

📑 En este artículo
  1. TL;DR
  2. What Is a Genetic Algorithm?
  3. Why This Matters
  4. How the Car’s Artificial Evolution Works
    1. The Genome: The Car as a Bit String
    2. Sensors and Motor: What the Brain Receives and Produces
    3. The Fitness Function: What It Rewards and What It Penalizes
    4. Selection: Who Reproduces
    5. Crossover: Combining Two Genomes into One
    6. Mutation: The Source of Novelty
    7. Elitism: Not Losing the Best
  5. Practical Examples: From Genome to Movement
    1. 1. Decoding the Genome into Movements
    2. 2. Calculating the Fitness of an Attempt
    3. 3. Crossing Two Genomes
  6. Getting Started: Running the Real Simulator
  7. Real-World Use Cases of Artificial Evolution
  8. Common Mistakes and Best Practices
  9. Comparison with Alternatives
  10. Going Deeper: Advanced Details
  11. Frequently Asked Questions
    1. Does a Genetic Algorithm Guarantee Finding the Best Possible Solution?
    2. How Many Generations Does Artificial Evolution Need to Park a Car?
    3. How Does Artificial Evolution Differ from Reinforcement Learning?
    4. Do You Need to Know Calculus or Neural Networks to Apply Evolutionary Optimization?
    5. What Happens If the Mutation Rate in an Evolutionary Search Is Too High?
    6. Is Evolutionary Computation Useful for Problems Outside Simulations and Video Games?
  12. References

This article explains step by step how that artificial evolution works, with original code examples, hand-calculated outputs, and the concepts needed to replicate it in any language, not just TypeScript.

TL;DR

  • A genetic algorithm evolves random driving genomes until one manages to park without crashing.
  • Each car translates eight distance sensors into turns and acceleration through movements = f(sensors).
  • Fitness penalizes crashes and rewards finishing close to and aligned with the parking space.
  • Selection, crossover, and mutation combine the fittest parents to create the next generation.
  • 180 bits of genome are enough for driving behavior to emerge without hand-written rules.

What Is a Genetic Algorithm?

A genetic algorithm is an optimization technique inspired by natural selection: it generates a population of candidate solutions encoded as strings of bits or numbers, measures how good each one is with a fitness function, and combines and mutates the best ones to produce generations that get closer to the best solution.

In the case of the self-parking car, each candidate solution is a binary genome that, once decoded, becomes a decision table: given what the sensors see, what to do with the motor and the steering wheel. There is no neural network trained with gradients, nor if/else rules written by a human. Just bits that survive or disappear depending on how well they maneuver.

Why This Matters

Genetic algorithms solve problems where no clean mathematical formula exists to reach the optimum, or where calculating a gradient is impossible because the success function has discontinuous jumps (a car crashed or it didn’t, there’s no derivable “half crash”). That makes them useful for optimizing routes, designing antennas, or tuning the hyperparameters of other models: tasks where testing and comparing complete variants is more practical than deriving a formula.

Unlike reinforcement learning, which adjusts a policy step by step using a reward signal at every instant, a genetic algorithm only needs one final score per complete attempt. That simplifies the implementation: there’s no need to design a reward for every instant of the simulation, it’s enough to judge the outcome when the car stops, crashes, or runs out of time.

As a teaching example, the self-parking car case is ideal: it exposes in readable code every piece (genome, fitness, selection, crossover, mutation) that later reappears, under different names, in the training of evolutionary neural networks and in optimizers that don’t rely on backpropagation.

How the Car’s Artificial Evolution Works

The entire evolutionary search cycle a genetic algorithm uses repeats generation after generation in six steps:

flowchart TD
    A["Generate initial population of random genomes"] --> B["Simulate each car, sensors every 100ms"]
    B --> C["Calculate fitness: crashes, distance and final angle"]
    C --> D["Select the fittest cars"]
    D --> E["Crossover genomes of selected parents"]
    E --> F["Mutate a small fraction of the bits"]
    F --> G["New generation of cars"]
    G --> B

The Genome: The Car as a Bit String

Each car in the population starts with a randomly generated genome: a string of bits that, in trekhleb’s original project, is 180 bits long. That genome isn’t directly interpreted as “turn left”; it’s decoded into a table that maps sensor combinations to motor and steering signals. Changing a single bit can completely change how the car reacts to the same obstacle.

The car’s genome in the trekhleb.dev experiment is 180 bits long. Foto de Андрей Сизов en Unsplash

Sensors and Motor: What the Brain Receives and Produces

The car perceives the world with eight distance sensors, each capable of measuring obstacles between 0 and 4 meters. When nothing is nearby, the sensor reports 0; the smaller the nonzero number, the closer the obstacle. These eight values are recalculated every 100 milliseconds and are the only information the car has about its environment.

The output is just as simple: two signals, one for the motor and one for the steering wheel, each with only three possible values: -1, 0, or +1. The motor interprets -1 as reverse, 0 as neutral, and +1 as forward; the steering wheel interprets -1 as turning left, 0 as going straight, and +1 as turning right. All of the car’s “intelligence” comes down to the function movements = f(sensors) that the genome encodes.

flowchart LR
    S["8 distance sensors (0 to 4 meters)"] --> B["Decoded genome: movements = f(sensors)"]
    B --> M["Motor signal: -1, 0 or +1"]
    B --> W["Steering signal: -1, 0 or +1"]
    M --> C["The car moves"]
    W --> C

The Fitness Function: What It Rewards and What It Penalizes

The fitness function turns a car’s performance into a comparable number. In this problem it has to harshly penalize crashes (a car that slams into another car shouldn’t compete with one that nearly succeeded) and reward two things at the end of the attempt: how close it ended up to the parking space and how aligned its angle was with that space. Without the second condition, a car could “win” by ending up right next to the space but sideways, without having parked in any useful sense.

⚠️ Heads up: a fitness function that only rewards speed and closeness, without penalizing crashes, trains cars that slam into the space at full speed because, technically, they end up close.

Selection: Who Reproduces

Once the simulation of the entire generation ends, each car is left with a fitness score. Selection decides which genomes move on to the next round as “parents”: the most common methods are tournament selection (pairs or small groups are drawn at random and the one with the best fitness wins) and roulette-wheel selection (the probability of being chosen is proportional to fitness). The goal is to bias reproduction toward the best individuals without completely eliminating diversity, because a population that’s too uniform stops exploring new solutions.

Crossover: Combining Two Genomes into One

Crossover takes two parent genomes and produces one offspring by combining fragments of both, usually by cutting each string at one point and joining the first half of one parent with the second half of the other. The bet is that if one parent learned to brake in time and the other learned to turn at just the right moment, the offspring can inherit both advantages.

Mutation: The Source of Novelty

Mutation randomly flips a small fraction of the offspring’s bits, typically between 1% and 5% of the genome. Without mutation, the population can only recombine variations that already existed in the initial generation, and if that initial generation didn’t include any reasonable genome, the algorithm gets stuck. Mutation is the only source of genuinely new behavior.

Elitism: Not Losing the Best

Since crossover and mutation are random processes, an entire generation can end up worse than the previous one purely from bad luck in the combinations. Elitism copies the best genome (or the best N genomes) unchanged directly into the next generation, guaranteeing that progress never fully regresses.

sequenceDiagram
    participant E as Environment
    participant Se as Sensors
    participant Ce as Brain
    participant Mu as Muscles
    E->>Se: distances to obstacles every 100ms
    Se->>Ce: 8 numbers between 0 and 4 meters
    Ce->>Mu: motor and steering signal
    Mu->>E: new car position
    Note over E,Mu: repeats every 100ms until crashing or parking

Practical Examples: From Genome to Movement

The following examples are simplified versions, written in TypeScript, of the pieces described above. Each one can be run in isolation with Node.js, and its output is what appears below, calculated exactly from the example’s values.

1. Decoding the Genome into Movements

This first example shows how a fragment of bits translates into motor or steering signals using a fixed encoding table:

type MuscleSignal = -1 | 0 | 1;

const CODE: Record<string, musclesignal=""> = { '00': 0, '01': 1, '10': -1, '11': 0 };

function decodeMove(bits: string): MuscleSignal {
  return CODE[bits] ?? 0;
}

const genomeFragment = '011000'; // 3 movements encoded in 2 bits each
const moves = [
  decodeMove(genomeFragment.slice(0, 2)),
  decodeMove(genomeFragment.slice(2, 4)),
  decodeMove(genomeFragment.slice(4, 6)),
];

console.log(moves);</string,>

The output is literally: [ 1, -1, 0 ]. The first pair of bits (01) decodes to forward, the second (10) to reverse, and the third (00) to neutral.

2. Calculating the Fitness of an Attempt

function fitness(distanciaAlEspacio: number, diferenciaAngulo: number, choco: boolean): number {
  if (choco) return 0;
  const puntajeDistancia = Math.max(0, 100 - distanciaAlEspacio * 10);
  const puntajeAngulo = Math.max(0, 50 - diferenciaAngulo);
  return puntajeDistancia + puntajeAngulo;
}

console.log(fitness(2.5, 12, false));

With a final distance of 2.5 meters and an angle difference of 12 degrees, with no crash, the output is 113: 75 points for distance (100 – 2.5×10) plus 38 for angle (50 – 12).

3. Crossing Two Genomes

function crossover(padreA: string, padreB: string, puntoCorte: number): string {
  return padreA.slice(0, puntoCorte) + padreB.slice(puntoCorte);
}

console.log(crossover('11110000', '00001111', 4));

The output is 11111111: the first 4 bits come from parent A (1111) and the last 4 from parent B (1111). A different cut point would have produced a different offspring, even with the same two parents.

Getting Started: Running the Real Simulator

The project that inspired this article is open source and runs in the browser. To try it locally on Linux or macOS with Node.js already installed:

git clone https://github.com/trekhleb/self-parking-car-evolution.git
cd self-parking-car-evolution
npm install

On Windows, the same three commands work the same way from PowerShell or a Node.js terminal. The exact script to start the simulator (depending on the repo version it might be npm start or an equivalent script) is documented in the project’s README; it’s worth checking before running anything, since it can change between versions.

💡 Tip: if you’re going to experiment with your own parameters, start with a small population (20-30 genomes) and a simple fitness function. If the car doesn’t improve after 20 generations, the problem is almost always in the fitness function, not the algorithm.

Real-World Use Cases of Artificial Evolution

The self-parking car is a didactic case, but the same recipe (genome, fitness, selection, crossover, mutation) solves real engineering problems. NASA used evolutionary algorithms to design antennas with shapes no human engineer would have drawn by hand, directly optimizing the radiation pattern instead of starting from a known geometry.

Beyond hardware, evolutionary optimization is used to schedule timetables and shifts with constraints that are hard to model analytically, to tune the hyperparameters of other machine learning models when grid search is too expensive, and to generate procedural content in video games (levels, maps, creatures) where “good” is defined by a fitness function rather than a closed-form formula.

It’s also the gateway to neuroevolution: instead of decoding the genome into a fixed table like in the car example, it can be decoded directly into a neural network’s weights, letting evolution (not gradient descent) adjust those weights. Algorithms like NEAT take that idea a step further and evolve the network’s structure as well, not just its weights.

Common Mistakes and Best Practices

  • Poorly designed fitness: if the score doesn’t capture exactly what you want (parking well, not just “being close”), the algorithm will optimize toward whatever it does measure, even if that’s the wrong thing.
  • Population too small: with few genomes, initial diversity runs out quickly and the population converges prematurely on a mediocre solution it can no longer escape.
  • Poorly calibrated mutation: too low and the algorithm stalls without exploring anything new; too high and each generation resembles its parents less and less, losing accumulated progress.
  • No elitism: if you don’t protect a generation’s best genome, the randomness of crossover and mutation can make the next generation, on average, worse than the previous one.
  • Overfitting to the test scenario: a car always trained with the same obstacle in the same position can memorize that particular case and fail as soon as the starting point changes.

Comparison with Alternatives

TechniqueWhen to Use ItAdvantageLimitation
Artificial evolutionDiscontinuous fitness or no calculable gradientDoesn’t need derivatives or step-by-step rewardRequires simulating the entire population every generation
Reinforcement learningA reward signal is available at every instantLearns policies that are more step-efficientMore complex to implement and tune
Pure random searchThe solution space is smallTrivial to implementDoesn’t learn from previous attempts, scales poorly
Hand-written rulesThe desired behavior is simple and knownPredictable and easy to debugDoesn’t adapt to cases the programmer didn’t anticipate

Going Deeper: Advanced Details

The decision to encode the genome in bits, within evolutionary computation, isn’t arbitrary: it makes crossover easier, because cutting a bit string at one point always produces two valid halves, while cutting a list of real numbers can require more care depending on how each gene is interpreted. The tradeoff is that decoding bits into useful values (like the -1, 0, +1 for motor and steering) requires a mapping table, and that table is itself a design decision that affects how easily evolution can find good solutions.

Tournament selection and roulette-wheel selection aren’t the only options: rank selection sorts the entire population by fitness and assigns probabilities based on position rather than the absolute score, which prevents a single individual with disproportionate fitness from dominating all reproduction. When a problem has more than one objective in tension (for example, parking fast and parking precisely), multi-objective algorithms like NSGA-II maintain a front of non-dominated solutions instead of reducing everything to a single fitness number.

Computational cost matters too: every generation requires simulating all the cars in the population through the entire attempt before a single fitness value can be calculated. With large populations and long simulations, that cost grows fast, and it’s one of the reasons why, for problems where a step-by-step reward signal does exist, reinforcement learning tends to be more computationally efficient than an equivalent artificial evolution.

Each car recalculates sensors and motor/steering signals every 100 milliseconds. Foto de digitale.de en Unsplash

Your next step: take the three code examples from this article, paste them into a .ts file, and modify the values in fitness() to see how the score changes before touching the full simulator.

📬 Get new articles by email

We only email about big articles (1-2 a month).

Frequently Asked Questions

Does a Genetic Algorithm Guarantee Finding the Best Possible Solution?

No. A genetic algorithm converges toward a good solution, but there’s no mathematical guarantee it’s the global optimum; it can get stuck in a local optimum if the population loses diversity too quickly.

How Many Generations Does Artificial Evolution Need to Park a Car?

It depends on the fitness function, population size, and mutation rate. In the trekhleb.dev experiment, cars start showing recognizable parking behavior around generation 40, with no guarantee that number holds with different parameters.

How Does Artificial Evolution Differ from Reinforcement Learning?

Artificial evolution evaluates the complete result of an attempt with a single fitness function at the end, while reinforcement learning adjusts a policy using a reward signal at every step of the simulation.

Do You Need to Know Calculus or Neural Networks to Apply Evolutionary Optimization?

No. The basic version only needs bit strings, a fitness function, and selection, crossover, and mutation operations; there are no derivatives or weight matrices involved, unless you decide to decode the genome into a neural network.

What Happens If the Mutation Rate in an Evolutionary Search Is Too High?

Each offspring resembles its parents less and less, so the algorithm stops accumulating progress between generations and behaves more like a random search than a directed evolution.

Is Evolutionary Computation Useful for Problems Outside Simulations and Video Games?

Yes: it’s used in antenna design, hyperparameter tuning for machine learning models, and scheduling with complex constraints, among other problems where there’s no closed-form formula for the optimum.

References

📱 Like this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day.

Featured image: Foto de Arron Choi en Unsplash

Did it work for you? Got a different error? Say so below: questions get answered and help the next reader.

Leave a comment
Categories: Tech NewsTutorials

Andrés Morales

Developer and AI researcher. Writes about language models, frameworks, developer tooling, and open source releases. Covers ML papers, the tech startup ecosystem, and programming trends.

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *

You can include code inside <code>…</code> or, for several lines, <pre><code>…</code></pre>.

This site uses Akismet to reduce spam. Learn how your comment data is processed.