EvoSimOdin is a 2D evolution simulator written in Odin and rendered with raylib. Each organism moves around a bounded world, senses food, spends energy, attempts to mate, and carries a mutable genome that defines its neural controller. Over time, the simulation selects organisms indirectly through a configurable mating rule and uses their genomes to seed the next generation.
The project is a compact evolutionary playground for experimenting with:
- agent-based simulation
- genome mutation and crossover
- simple neural control
- selection pressure based on environment or behavior
It is not a full NEAT implementation, but it borrows NEAT-style ideas such as innovation tracking, structural mutation, and genome crossover.
When the application starts, it opens a menu where you configure:
- mating rule
- population size
- food cap
- generation duration
Once a run starts:
- The world is reset and generation
0is seeded. - Each organism receives a genome and a runtime brain compiled from that genome.
- Every frame, the world updates organisms, food, and mating opportunities.
- Organisms lose energy over time, gain energy by eating, and can create offspring if they are mature, ready, close enough to a partner, and satisfy the active selection rule.
- Offspring genomes are queued into a next-generation pool.
- When the population dies out or the generation timer expires, the current generation ends.
- The next generation is seeded by cloning and mutating genomes from the queued offspring pool. If no pool exists yet, fresh minimal genomes are created instead.
.
|-- build.sh
|-- src/
| `-- main.odin
|-- lib/
| |-- lib.main.odin
| |-- food/
| | `-- food.odin
| |-- genome/
| | `-- genome.odin
| |-- organism/
| | `-- organism.odin
| `-- world/
| `-- world.odin
`-- artefacts/
Owns the application loop and UI flow.
- creates the window
- initializes and destroys the global simulation context
- handles menu navigation and runtime hotkeys
- replenishes food
- advances the world each frame
- triggers generation rollover
- draws the menu and runtime dashboard
Defines shared simulation-level configuration and state.
- screen constants
- default run settings
SelectionCriterionfor choosing the active mating ruleRunConfigfor user-selected settingsSimulationContextfor innovation tracking, generation bookkeeping, and the next-generation genome pool
Owns the live simulation state.
- active organisms
- active food
- generated wall rectangles
- generation seeding and reset logic
- per-frame world update
- mating and eating coordination
This is the module that orchestrates most of the simulation lifecycle once a run is active.
Defines organisms and their behavior.
- organism state such as energy, age, generation, fitness, and reproduction state
- a compiled runtime
Brain - sensing functions for walls, food, and nearby mates
- movement and energy drain
- readiness checks for mating
- fitness calculation
- drawing and cleanup
Defines the inheritable representation of an organism brain.
- node genes
- connection genes
- genome cloning and destruction
- global innovation bookkeeping through the shared context
- sparse initial genome creation
- weight and bias mutation
- structural mutation by adding connections or hidden nodes
- crossover between two parent genomes
This module is the core of the evolutionary part of the project.
Defines food entities that organisms consume to restore energy.
main creates a SimulationContext, initializes a World, and generates walls with world.make_walls.
world.seed_generation creates organisms in one of two ways:
- from fresh minimal genomes when no offspring pool exists
- from cloned and mutated genomes taken from
ctx.next_generation_pool
Each organism is created by organism.make_organism, which assigns spawn position, starting energy, lifespan, and a compiled brain.
Every frame, organism.update_organism builds a 10-value input vector:
- 3 wall ray sensors
- 3 food arc sensors
- 3 mate arc sensors
- 1 normalized energy value
These inputs are fed into the organism brain.
The brain produces 4 outputs that drive:
- forward/backward movement
- steering
- eating intent
- mating intent
Movement drains energy. Turning also has a cost. Organisms die when energy reaches zero or they outlive their lifespan.
If an organism comes within EAT_RADIUS of a food item, it consumes the food, gains energy, and increments its food_eaten counter.
Mating is not purely random. An organism must:
- be old enough
- have enough energy
- signal readiness
- not be on cooldown
- satisfy the active selection criterion
- be physically close to a compatible partner
The current selectable criteria are:
RIGHT_SIDELEFT_SIDETOP_SIDEBOTTOM_SIDEHIGH_ENERGYFOOD_FORAGERS
If mating succeeds:
- both parents lose energy
- both enter cooldown
- a child genome is produced by crossover and mutation
- that genome is queued for future generation seeding
- a live child organism is also spawned immediately into the current world
Fitness is recalculated from:
- survival time
- amount of food eaten
- offspring produced
- distance travelled
- remaining energy
A generation ends when either:
- all organisms die
- generation time reaches the configured limit
At rollover:
- living organisms are destroyed
- best fitness is preserved in the context
- food is cleared
- the next generation is seeded from the queued offspring genome pool
Up/Down: select settingLeft/Right: change valueEnter: start run
P: pause/resumeR: restart runM: return to menu
The repository includes a small build script:
./build.shThis builds the executable to:
artefacts/evosim
Equivalent direct build command:
odin build src -out:artefacts/evosim- The project uses raylib for rendering and input.
- The
SimulationContextowns the innovation map used during structural mutations. - The next generation is driven by genomes stored in
next_generation_pool, not by copying full organisms.
If you want to understand the project quickly, read the files in this order:
src/main.odinlib/lib.main.odinlib/world/world.odinlib/organism/organism.odinlib/genome/genome.odinlib/food/food.odin
That order follows the same path as the runtime: app loop -> global config -> world update -> agent behavior -> evolution internals.