Introduction
This book serves as both the documentation for the simulation implemented in this project, as well as a reflection of thoughts and ideas from throughout the development process.
The simulation is an implementation of the work by Simulife Hub. The inspiring project was originally implemented in the Processing software, using Java. This version takes a more complex approach, using the Bevy game engine, in Rust.
The goal of this project is is to learn more about Bevy and how it can aid in the development of Simulations.
Since Bevy provides powerful parallelism and an entity-component system (ECS), it should, in theory, be much more flexible to implement such (fast) simulations.
About the Simulation
As previously mentioned, this project uses the Bevy game engine. While Rust is a fast and very powerful language, it can be difficult to do many thing, such as work with large mutable structures, or display optimised and interactive graphics.
I wanted to work with Rust, which meant I needed to overcome sever challenges. Managing large state can be difficult – especially when working iteratively. I did not want to be required to refactor large sections whenever I added a feature or made a change. Additionally, I didn’t want to worry about graphics too much. This led me to consider using a game engine for this project.
The biggest reason for using Bevy, however, is due to its’ powerful ECS. Simulations are all about responding to state, and ECS makes adding features significantly simpler.
I will not go into details here as to how the Bevy engine or ECS works.
Architecture
Before getting into the exact outline of the simulation, I want to describe how using the Bevy ECS helps to structure the simulation nicely.
Use of Bevy
The Bevy ECS introduces both nice solutions as well as new complications. Unlike the original simulation, a Rust-based simulation cannot rely on mutable global state; especially when using Bevy for graphics rendering. By using systems and components, many aspects of the simulation can be decomposed and parallelised.
To give an example, suppose that you have a simulation with cells that, at each step, will either:
- consume 1 energy to move to a random adjacent tile, if there is empty space
- collect energy from the surrounding 3x3 tile environment
At the very least, you might expect something similar to the following:
pub struct CellGrid(pub HashMap<(u32, u32), Cell>);
pub struct EnvironmentEnergy(pub HashMap<(u32, u32), u32>);
pub struct Cell {
pub position: (u32, u32),
pub energy: u32,
}
In languages such as C++, such approaches might work well, assuming you can reason well about the simulation state. However, even in such languages, parallelising the cell actions might be difficult or impossible with this approach.
Considering Rust, it won’t be easy (or modular) to implement concurrently running steps if
both actions require mutable access to the Cell type. There are ways to do so, but it
make things very complex.
Using Bevy, we instead decouple things into components. We might, for example, do something like:
#[derive(Component, Clone, Debug)]
pub struct Position {
x: u32,
y: u32
}
#[derive(Component, Clone, Debug)]
pub struct Energy(u32);
#[derive(Component, Clone, Debug)]
pub struct Cell;
#[derive(Component, Clone, Debug)]
pub struct Moving;
Using Bevy, we can now write systems. For example, if we have entities that has a
Cell, Position, and Energy component, we could make a system like:
pub fn move_cells_system(
mut positions: Query<&mut Position, With<(Cell, Moving)>>,
) {
for (mut position) in positions.iter_mut() {
// update the position
}
}
pub fn cell_gather_energy_system(
mut cell_energies: Query<&mut Energy, (With<Cell>, Without<Moving>)>,
) {
for (mut energy) in cell_energies.iter_mut() {
energy.0 += 1;
}
}
With this setup, both systems are able to mutably access their required components in parallel. Furthermore, Bevy will automatically parallelise wherever possible.
Simulation Details
In this section, we will discuss the different cell types and their roles in the simulation.
Briefly, there are 6 kinds of cells:
- Sprout
- Leaf
- Root
- Branch
- Antenna
- Seed
We will cover each cell in its own section.
Also, we have several environmental energy resources:
- Sunlight
- Organic matter
- Electrical charge
The Genome
The following shows the structure of one of the 52 genes composing of a genome.
Genome Actions
The genome controls how sprout and seed cells behave.
- The spawn action block indicates what cells should be spawned in the left, forward, and rightwards positions. This block is executed only if no conditions blocks are present.
- Condition blocks are used to direct the flow of behaviour. If both of the given predicates are true, then a command block is executed (there are separate blocks for sprouts and seeds). Similarly, if either yield false, there is a command block that executes in that case.
- Commands perform an action (hence there are separate blocks for sprouts and lone/disconnected sprouts). Depending on whether the command is not present, whether it cannot be completed, or if it completed successfully, a next active gene is set.
- The active gene is simply the index of a gene in the full genome.
Together, these features control the expressiveness of the system. Below is a demonstration of a sprout cell spawning new sprouts in its facing direction/to its right, and spawning leaf, root, and antenna cells to its left.
Cells
Cells each contribute a behaviour to an organism. It may collect a particular kind of energy, or it might produce additional cells. Together, the interaction of cells within an organism (and between organisms) is what produces complex systems.
In this section, we will go into the details of each cell, and its role within the organism.
Cell Types
Sprout
Sprouts are the only cell whose behaviour is directly influenced by a genome. As the simulation progresses, the geneome undergoes random mutations. These mutations adjust the behaviour of cells.
The idea is simple: when a genome produces complex behaviours to the environment (or to other cells), the system approaches a kind of equilibrium. If a simulation is stable, cells are able to survive indefinitely. In such cases, and one random mutation is unlikely to destabalise the entire system. Thus, unuseful, random mutations rarely contribute to the overall system. It is only when many mutations are collectively considered that destablising behaviours may emerge (ideally).
More details regarding the genome and how it effects cells can be found in the genome section.
Leaf
Leaf cells collect energy via photosynthesis. Simply, it produces an amount of energy that is proportional to the amount of sunlight at its position.
Root
Root cells collect organic energy from the environment. It is the only cell that is not effected by toxic levels of organic matter.
Antenna
Antenna cells collect electrical charge from the environment. It is the only cell that is not effected by toxic levels of charge energy.
Branch
Branch cells transfer energy between cells within the same organism.
Seed
Seeds can be fired by sprouts and can turn into a sprout.
Behaviour
Cell behaviour roughly boils down to whether it is a genome-executing cell (a Sprout or
Seed), a energy producing cell (Leaf, Root, or Antenna), or a Branch cell.
Since we are using an ECS, any system that mutably accesses components will not be parallelisable. Additionally, in order to enforce that all cells have fair chance, we want to be certain that actions are performed in a certain order, and done so fairly. For example, if a cell wants to acquire energy from a cell to its left, which happens to be where a root cell is collecting energy, both cells should be given only half the energy each, where any remainder is left in the environment.
Because of this, a we are required to publish “requests”, which can then be processed and applied later on. This way, all requesting actions can access cells immutable and in parallel, and then responding systems that apply these request can implement any kind of logic to ensure fairness (such as dividing energy evenly).
By doing this, the processing of requests can also potentially be parallelised.
The general idea overall is that ECS allows you to query the current state as read-only, and then publish your intended action without yet executing the task. Once all requests are made, the requests can be processed as necessary, either in order or potentially in parallel.
Genome Actions Reference
Note
This file was generated using Claude
Detailed breakdown of all genome-driven actions in the CLANS3 simulation, extracted from
the Processing source code (Cells.pde, constant.pde).
Overview
Only APEX (Sprout) cells execute genome commands. The genome system has three execution paths depending on cell state:
- Growth — when no conditions are defined in the active gene
- Body commands — when the APEX has a parent (is part of a multicellular organism)
- Lone commands — when the APEX has no parent (is a free-roaming single cell)
Execution Flow (per tick)
APEX evaluates active gene:
├─ Check condition 1 (bytes 3-4)
├─ Check condition 2 (bytes 5-6)
│
├─ Both conditions unused (gene value > 67)?
│ └─ GROW using bytes 0-2 → APEX becomes WOOD
│
├─ At least one condition met (none return -1)?
│ ├─ Has parent? → execute body command (byte 9)
│ │ └─ success → gene byte 10, failure → gene byte 11
│ ├─ No parent? → execute lone command (byte 15)
│ │ └─ success → gene byte 16, failure → gene byte 17
│ └─ No valid command? → jump to gene byte 7
│
└─ At least one condition NOT met?
├─ Has parent? → execute body command (byte 12)
│ └─ success → gene byte 13, failure → gene byte 14
├─ No parent? → execute lone command (byte 18)
│ └─ success → gene byte 19, failure → gene byte 20
└─ No valid command? → jump to gene byte 8
Growth (No Conditions Active)
Who: APEX cells (both body and lone) When: Both condition slots in the active gene have values > 67 (maxIF) Effect: The APEX creates up to 3 new cells (left/forward/right) based on bytes 0-2, then transforms into WOOD.
Growth Byte Encoding (each of bytes 0, 1, 2)
| Value Range | Cell Created | Notes |
|---|---|---|
| 0-63 | APEX | Active gene = value % 32 |
| 64-75 | LEAF | |
| 76-85 | ANTN | |
| 86-95 | ROOT | |
| 96-255 | Nothing | No growth in this direction |
Growth Requirements
- Energy cost: (WORK + ORGANIC_CELL) = 20 energy per new cell
- Total energy must be >=
cell_count * 20or growth is skipped entirely - Target position must be empty (cellsIndx == 0), otherwise that branch is silently skipped
- The growing APEX becomes WOOD after growth
- If no branches can be created and cell is alone, active gene resets to 0
Growth Side Effects
- New APEX children: parent WOOD sets
energyTo[]flag toward them (will send energy) - New LEAF/ROOT/ANTN children: child sets
energyTo[]flag toward parent (will send energy back) - Mutation: ~1% chance per new APEX child — copies genome, changes 1 random byte
Body Commands (APEX with parent, gene value 0-14)
These commands are available when the APEX is part of a multicellular organism (has a parent).
| Cmd | Name | Description | Returns | Details |
|---|---|---|---|---|
| 0 | Skip | Do nothing | true | No-op |
| 1 | Fly Seed | Transform into flying seed | true | Sets type=SEED, calls destroyAllLinks(), move=true, restTime=8 |
| 2 | Stationary Seed | Transform into stationary seed | true | Sets type=SEED, move=false, restTime=8 |
| 3 | Delayed Fly Seed | Transform into seed that flies later | true | Sets type=SEED, move=true, restTime=8. Note: does NOT call destroyAllLinks() — stays attached until detach threshold |
| 4 | Die | Voluntary death | true | Sets energyTo[parent]=1 before dying (tries to send energy to parent), then calls die() |
| 5 | Detach | Break away from parent | true | Calls destroyAllLinks() — severs all parent/child/energy connections |
| 6 | Push Energy Left | Move soil energy to left | true | Transfers all EnergyMap at cell position to the cell one step left (relative to facing) |
| 7 | Push Energy Right | Move soil energy to right | true | Same as above, rightward |
| 8 | Push Energy Ahead | Move soil energy forward | true | Same as above, forward |
| 9 | Push Organic Left | Move soil organic to left | true | Transfers all OrganicMap at cell position to cell one step left |
| 10 | Push Organic Right | Move soil organic to right | true | Same, rightward |
| 11 | Push Organic Ahead | Move soil organic forward | true | Same, forward |
| 12 | Fire | Launch bullet projectile | true/false | Creates a SEED with 30 energy, restTime=30 ticks, move=true. Costs ORGANIC_CELL+WORK+30 = 50 energy. Fails if insufficient energy or target is wall |
| 13 | Seed | Create reproductive seed | true/false | Creates SEED with most of parent’s energy, restTime=5+random(40), move=true. Parent keeps 30 energy. Costs ORGANIC_CELL+WORK+30 = 50 minimum |
| 14 | Scatter Organic | Convert energy to soil organic | true | Distributes floor((energy-3)/9) organic to each of 9 surrounding cells. Cell retains 3 energy. Skipped if energy < 12 |
Fire/Seed Collision Behavior (setNewSEED)
When the target position (one step ahead) is occupied:
- If occupied by same genome → adds (ORGANIC_CELL+WORK+30) energy to that cell
- If occupied by different genome → marks that cell for death
- Either way, costs the creating cell (ORGANIC_CELL+WORK+30) energy
- If target is a wall (cellsIndx < 0) → fails, returns false
Lone Commands (APEX without parent, gene value 0-17)
These commands are available when the APEX is a free-roaming single cell (no parent).
| Cmd | Name | Description | Returns | Details |
|---|---|---|---|---|
| 0 | Move Forward | Step one cell ahead | true/false | Costs 1 energy (moveApexPrice). Fails if target occupied (any cell or wall) |
| 1 | Turn Right | Rotate 90° clockwise | true | direction += 1 (mod 4) |
| 2 | Turn Left | Rotate 90° counter-clockwise | true | direction -= 1 (mod 4) |
| 3 | Turn Around | Rotate 180° | true | direction += 2 (mod 4) |
| 4 | Turn Right + Move | Rotate 90° CW then step | true/false | Turn always succeeds, move may fail |
| 5 | Turn Left + Move | Rotate 90° CCW then step | true/false | Same |
| 6 | Turn Around + Move | Rotate 180° then step | true/false | Same |
| 7 | Parasite | Attach to adjacent WOOD cell | true/false | Checks cell ahead: if it’s WOOD, sets self.parent to that direction and registers on the WOOD’s energyTo/children arrays. Fails if no WOOD ahead |
| 8 | Random Turn | Turn randomly | true | 30% right, 30% left, 40% no turn |
| 9 | Random Turn + Move | Random turn then step | true/false | Same random turn logic, then moveApex() |
| 10 | Drag Organic from Left | Pull organic under self | true/false | Moves all OrganicMap from left cell to current position. Returns false if source had 0 organic |
| 11 | Drag Organic from Ahead | Pull organic under self | true/false | Same, from ahead |
| 12 | Drag Organic from Right | Pull organic under self | true/false | Same, from right |
| 13 | Drag Energy from Left | Pull energy under self | true/false | Moves all EnergyMap from left cell to current position. Returns false if source had 0 energy |
| 14 | Drag Energy from Ahead | Pull energy under self | true/false | Same, from ahead |
| 15 | Drag Energy from Right | Pull energy under self | true/false | Same, from right |
| 16 | Eat Neighbors | Kill and absorb adjacent cells | true/false | Costs 1 energy. Checks all 8 directions (cardinal + diagonal). Kills all cells with type < WOOD (i.e., APEX, LEAF, ANTN, ROOT). Absorbs their energy + engP + engM + org. Returns false if nothing eaten |
| 17 | Absorb Soil Energy | Extract energy from ground | true/false | Takes up to ALONE_CAN=6 energy from EnergyMap at current position. Returns false if soil had less than 6 energy (still takes what’s there) |
Conditions (68 total, IDs 0-67)
Conditions are checked before commands execute. Each gene has two condition slots (bytes 3-4 and 5-6). A condition slot is “unused” if the gene value > 67 (maxIF).
Return Values
1— condition met-1— condition not met0— condition slot unused (gene value > maxIF)
Condition Table
| ID | Condition | Parameter Usage |
|---|---|---|
| Resource at Position | ||
| 0 | Organic at cell < threshold | param * 2 |
| 1 | Organic at cell >= threshold | param * 2 (NOTE: source code is identical to 0 — likely a bug, SIMULATION.md says >=) |
| 2 | Cell energy > threshold | param * 2 |
| 3 | Cell energy < threshold | param * 2 |
| Level-Based | ||
| 4 | param % (level+1) == 0 | param value directly |
| 5 | level % (param+1) == 0 | param value directly |
| 6 | level > param | param value directly |
| 7 | level < param | param value directly |
| Energy Trends | ||
| 8 | Energy rising (current >= previous) | — |
| 9 | Energy falling (current < previous) | — |
| Area Resources (9 cells around position) | ||
| 10 | Organic(9) > threshold | param * 18 |
| 11 | Organic(9) < threshold | param * 18 |
| 12 | Energy(9) > threshold | param * 18 |
| 13 | Energy(9) < threshold | param * 18 |
| 14 | Energy(9) > Organic(9) | — |
| 15 | Energy(9) < Organic(9) | — |
| Spatial Awareness | ||
| 16 | Edible cells nearby | Checks 5 directions (left, front-left, front, front-right, right). “Edible” = type < WOOD (APEX, LEAF, ANTN, ROOT) |
| 17 | Area free (left + center + right) | All three relative directions must be empty |
| 18 | Free left | — |
| 19 | Free center (ahead) | — |
| 20 | Free right | — |
| 21 | Obstacle left | — |
| 22 | Obstacle center (ahead) | — |
| 23 | Obstacle right | — |
| 24 | Has parent | — |
| 25 | Random | true if random(256) > param |
| Light Comparisons (organic at 3 cells distance, excluding poisoned) | ||
| 26 | Light center > light right | — |
| 27 | Light right > light center | — |
| 28 | Light center > light left | — |
| 29 | Light left > light center | — |
| 30 | Light left > light right | — |
| 31 | Light right > light left | — |
| Energy(9) Directional Comparisons | ||
| 32 | Energy(9) center > right | — |
| 33 | Energy(9) right > center | — |
| 34 | Energy(9) center > left | — |
| 35 | Energy(9) left > center | — |
| 36 | Energy(9) left > right | — |
| 37 | Energy(9) right > left | — |
| 38 | Energy(9) right > threshold | param * 18 |
| 39 | Energy(9) center > threshold | param * 18 |
| 40 | Energy(9) left > threshold | param * 18 |
| Organic(9) Directional Comparisons | ||
| 41 | Organic(9) center > right | — |
| 42 | Organic(9) right > center | — |
| 43 | Organic(9) center > left | — |
| 44 | Organic(9) left > center | — |
| 45 | Organic(9) left > right | — |
| 46 | Organic(9) right > left | — |
| 47 | Organic(9) center > threshold | param * 18 |
| 48 | Organic(9) right > threshold | param * 18 |
| 49 | Organic(9) left > threshold | param * 18 |
| Free Space(9) Directional Comparisons | ||
| 50 | Free(9) center > right | — |
| 51 | Free(9) right > center | — |
| 52 | Free(9) center > left | — |
| 53 | Free(9) left > center | — |
| 54 | Free(9) left > right | — |
| 55 | Free(9) right > left | — |
| 56 | Free(9) center > threshold | param % 10 |
| 57 | Free(9) right > threshold | param % 10 |
| 58 | Free(9) left > threshold | param % 10 |
| Poison Detection | ||
| 59 | Organic poison ahead | OrganicMap >= 512 one step ahead |
| 60 | Organic poison left | OrganicMap >= 512 one step left |
| 61 | Organic poison right | OrganicMap >= 512 one step right |
| 62 | Energy poison ahead | EnergyMap >= 512 one step ahead |
| 63 | Energy poison left | EnergyMap >= 512 one step left |
| 64 | Energy poison right | EnergyMap >= 512 one step right |
| 65 | Any poison ahead | Either map >= 512 one step ahead |
| 66 | Any poison left | Either map >= 512 one step left |
| 67 | Any poison right | Either map >= 512 one step right |
Non-Genome Cell Behaviors (Automatic, Every Tick)
These are not genome-driven — they happen automatically based on cell type.
SEED
- Costs 0.5 energy/tick
- Dies if energy < 0
- If energy > 512 while attached → detaches (
destroyAllLinks()) - If detached and restTime > 0: counts down, moves 1 cell/tick if
move=true- Collision: kills the hit cell, seed stops moving (move=false, returns false → seed dies)
- Movement costs 1 energy/step
- When restTime reaches 0: transforms into APEX at gene 0, level=0, age=AGE
WOOD (Transport)
- Costs 0.04 energy/tick
- If energy > 0: transmits energy to all cells marked in
energyTo[], split evenly - If energy <= 0: loses 1 age tick (starts at AGE=3)
- Dies when age reaches 0
- If detached (no parent) and has no children → dies immediately
LEAF (Green)
- Photosynthesis:
OrganicMap[x][y] * free_neighbor_count * 0.0008- free_neighbor_count starts at LIGHTENERGY=10
- Each occupied neighbor (8 checked): -1 from count
- If ANY adjacent cell is LEAF: returns 0 (mutual shading — complete shutdown)
- Costs 0.04 energy/tick
- Transmits surplus energy toward parent
- Dies when age reaches 0 OR if parent == -1 (detached)
ROOT (Red)
- Extracts up to ROOT_CAN=1 organic/tick from OrganicMap at position
- Immune to organic poisoning (can survive OrganicMap >= 512)
- Costs 0.04 energy/tick
- Transmits surplus energy toward parent
- Dies when age reaches 0 OR if detached
ANTN (Blue)
- Extracts up to ANTN_CAN=1 energy/tick from EnergyMap at position
- Immune to energy poisoning (can survive EnergyMap >= 512)
- Costs 0.04 energy/tick
- Transmits surplus energy toward parent
- Dies when age reaches 0 OR if detached
APEX (Sprout)
- Costs 1 energy/tick
- Dies if energy + engP + engM < 0
- If energy > 1024 while attached → detaches and resets to gene 0
- If detached → level resets to 0
Energy Transfer System
- Uses double-buffer (
engP/engM) with alternating phase (EnergyTransportPeriodflips between +1/-1 each tick) - Cells receive buffered energy at start of tick
transmitEnergy(): splits cell’s energy evenly among all flaggedenergyTo[]directions- If nowhere to send and has parent: redirects to parent, also tells parent to stop sending energy back
- If nowhere to send and no parent: dumps energy into EnergyMap, loses 1 age tick
Key Constants
| Constant | Value | Used By |
|---|---|---|
| ROOT_CAN | 1 | ROOT extraction rate |
| ANTN_CAN | 1 | ANTN extraction rate |
| ALONE_CAN | 6 | Lone APEX soil energy absorption |
| ORGANIC_EXCESS | 512 | Poison threshold (organic) |
| ENERGY_EXCESS | 512 | Poison threshold (energy) |
| AGE | 3 | Base lifespan ticks (when no energy) |
| Energy4Life | 0.04 | LEAF/ROOT/ANTN/WOOD living cost |
| SeedEnergy4Life | 0.5 | SEED living cost |
| ApexEnergy4Life | 1 | APEX living cost |
| moveApexPrice | 1 | APEX movement cost |
| LIGHTENERGY | 10 | Base free-neighbor count for photosynthesis |
| LIGHTCOEF | 0.0008 | Photosynthesis coefficient |
| ORGANIC_CELL | 15 | Organic deposited into soil on death; organic cost per new cell |
| WORK | 5 | Energy cost of creating a cell |
| MAX_APEX_ENERGY | 1024 | APEX detach threshold |
| MAX_SEED_ENERGY | 512 | SEED detach threshold |
Source Code Notes
- Bug in condition 1: The code for conditions 0 and 1 is identical (
< param*2).SIMULATION.mddescribes condition 1 as>=, so this appears to be a copy-paste bug in the original Processing source. - “Light” conditions (26-31): “Light” actually measures organic matter at 3 cells distance in the given direction, excluding poisoned cells (organic >= 512 counts as 0). This is because photosynthesis depends on soil organic.
- Directional (9) scans (32-58): These sample a 3x3 grid offset 2 cells in the given direction, not the 3x3 around the cell itself.
- Eat (lone cmd 16): Checks all 8 neighbors (4 cardinal + 4 diagonal via 0.5-step increments). Only eats types < WOOD (APEX=0, LEAF=1, ANTN=2, ROOT=3). WOOD and SEED are spared.
- Command 3 vs Command 1: Both set move=true and restTime=8, but only command 1 calls
destroyAllLinks(). Command 3 stays attached, meaning it won’t actually fly until either energy exceeds MAX_SEED_ENERGY (512) causing auto-detach, or it dies.
Systems
Refactor Plan: Request/Resolve Architecture for Cellular Simulation
Note
This file was generated using Claude
Context
The simulation is a Rust/Bevy port of the CLANS3 Processing/Java artificial life simulation. The current implementation has core infrastructure (genome execution, energy environments, rendering) but is missing critical mechanics (growth, death recycling, energy transport topology, seed movement, mutation) and uses a flat system chain instead of the parallel request/resolve architecture described in the book documentation.
The goal is to restructure around immutable request systems (parallel) followed by mutable resolve systems (sequential), enabling Bevy’s automatic parallelism while maintaining fairness guarantees.
SIMULATION.md Discrepancies
Before starting work, these errors/discrepancies in clan/SIMULATION.md should be noted:
- Leaf formula description is slightly misleading: Says “free_neighbor_count starts
at 10”. The original code uses
LIGHTENERGY=10as a base multiplier that decrements by 1 per occupied neighbor. Functionally equivalent but “count” is misleading since it’s a light efficiency factor, not a literal count of free neighbors. - No other significant errors found – the document is accurate regarding genome structure (32x21=672 bytes), growth costs (WORK=5 + ORGANIC_CELL=15 = 20), poison thresholds (512), and seed mechanics.
Key Differences: Rust Implementation vs Original
- Rust genome uses 52
GenomeEntrystructs (high-level abstraction) vs original’s 32 raw 21-byte genes. This is intentional. - Rust
GenomePreconditionhas 8 variants vs original’s 68 condition types. Many missing. - Rust toxicity thresholds are 100/90 vs original’s 512/512. May be intentional tuning.
- Rust energy environments initialized to 50/20 vs original’s 200/200. May be intentional.
Source Reference Key
All line references are to the original Processing source in clan/:
- Cells.pde — Cell class, step logic, commands, conditions, energy transport, growth
- constant.pde — All simulation constants
- func.pde — Helper functions, dispersal, simulation step, initialization
- CLANS3eng.pde — Main loop, setup, rendering
Work Items
Phase 0: Core Infrastructure
Everything else depends on these foundational pieces.
-
0.1 —
EnergyEnvironment::deposit()method (src/energy/mod.rs)Add
deposit(x, y, amount)to write energy back into the grid. Currently onlycollectandpeekexist. Needed for: death recycling, organic scatter, energy dump from isolated cells, soil manipulation commands.The original writes directly to the global arrays (
OrganicMap[x][y] += value,EnergyMap[x][y] += value) in many places:transmitEnergy()dumps to soil:Cells.pde:1028OrganicAround()scatters organic:Cells.pde:1216-1224moveZarad*()moves charge between tiles:Cells.pde:1133-1168moveOrganic*()moves organic between tiles:Cells.pde:1172-1208
-
0.2 —
EnergyEnvironment::distribute_around()method (src/energy/mod.rs)Add 9-cell averaging distribution matching the original’s dispersal logic. Two functions in the original:
distributeOrganic(x, y, E)(func.pde:143-158):- Sums the existing values in the 3x3 neighborhood plus
E - Integer divides by 9 (base share
b), remaindercstays at center - Each of the 9 cells gets
b, center getsb + c - Note: this replaces existing values, it does not add to them
distributeZarad(x, y, E)(func.pde:163-178):- Same logic but for energy (float in original)
- Sums 3x3 + E, divides by 9, distributes evenly
- All 9 cells set to
b(remainder handling differs slightly from organic)
Both are called during
die()(Cells.pde:1038-1039). - Sums the existing values in the 3x3 neighborhood plus
-
0.3 — Live
SimulationGridspatial index (src/simulation.rs,src/main.rs)SimulationGridexists but is never inserted as a resource or maintained. Changecellsfield toHashMap<(usize, usize), Entity>. Insert as resource on startup. Add/remove/update entries on spawn, death, and movement.The original uses
cellsIndx[X][Y](Cells.pde:288, 1072, 1104, etc.) as a global 2D array mapping grid positions to cell indices. This is checked:- During growth to verify target is empty:
Cells.pde:288 - During movement to check collisions:
Cells.pde:1072, 1104 - During seed collision:
Cells.pde:347 - In
calculateSunEnergy()for neighbor detection:Cells.pde:1505-1536 - In
findIndexFromRelDirection():Cells.pde:1468-1482 - In
isFreeInRelDirection():Cells.pde:1422-1436 - In condition checks for obstacles/edible cells:
Cells.pde:500-562 - Updated on death:
Cells.pde:1053(cellsIndx[X][Y] = 0) - Updated on movement:
Cells.pde:1107-1109
- During growth to verify target is empty:
-
0.4 —
CellAgecomponent (src/cells/mod.rs)u32, default =AGE(3) (constant.pde:8). Decrements when cell energy reaches zero. Cell dies when age reaches 0.Age decrement happens in multiple cell types:
- WOOD:
Cells.pde:103(age--whenenergy < 0) - LEAF:
Cells.pde:115(same pattern) - ROOT:
Cells.pde:135(same pattern) - ANTN:
Cells.pde:155(same pattern) - Death check:
Cells.pde:106, 120, 140, 160(if(age <= 0) die())
Also decremented when energy can’t be transmitted:
Cells.pde:1029(no parent, dump to soil). - WOOD:
-
0.5 —
CellLevelcomponent (src/cells/mod.rs)u32. Tracks growth depth from organism root.Set during cell creation:
Cells.pde:298(level = level + 1). Reset when alone:Cells.pde:167(if(parent == -1) level = 0). Also reset when seed hatches:Cells.pde:91(level = 0). Used in conditions 4-7 (Cells.pde:439-457):- Cond 4:
param % (level+1) == 0 - Cond 5:
level % (param+1) == 0 - Cond 6:
level > param - Cond 7:
level < param
- Cond 4:
-
0.6 —
CellOrganiccomponent (src/cells/mod.rs)u32, default =ORGANIC_CELL(15) (constant.pde:17). Released to soil on death viadistributeOrganic()(Cells.pde:1038).Set on cell creation:
Cells.pde:296(org = ORGANIC_CELL). Consumed byConsumeNeighbourscommand:Cells.pde:974(absorbsorgfrom killed neighbors). -
0.7 —
PreviousEnergycomponent (src/energy/mod.rs)Track previous tick’s energy for conditions 8/9. The original stores
energyOldimplicitly — conditions 8/9 compare currentenergytoenergyOld:- Condition 8 (
Cells.pde:460):energy >= energyOld→ rising - Condition 9 (
Cells.pde:464):energy < energyOld→ falling
The original doesn’t explicitly copy
energyOld = energyat tick start —energyOldis set when energy changes during transmission. For simplicity in Bevy, copyCellEnergyintoPreviousEnergyat the start of each tick. - Condition 8 (
-
0.8 — Activate
CellRelation(src/cells/mod.rs)Component exists but is never inserted. Every spawned cell must have correct parent/children. The original uses
parent(direction to parent, -1 if none) andchildren[4](flags for each cardinal direction).Set during growth:
Cells.pde:300-301(child getsparent = invert(absDir), parent getschildren[absDir] = 1). Broken bydestroyAllLinks()(Cells.pde:1543-1557):- Iterates all 4 directions
- If
children[i] == 1: set child’sparent = -1 - Clears parent’s
energyTotoward this cell - Sets own
parent = -1
In Bevy, store parent as
Option<Entity>and children asVec<Entity>(already the case in the existingCellRelationstruct). -
0.9 — Wire
EnergyTransferertoCellRelationtopology (src/energy/mod.rs)Branch cells set
energyTobased on children. LEAF/ROOT/ANTN point toward parent.Set during growth (
Cells.pde:327-333):- Parent (now WOOD):
energyTo[absDir] = 1if child is APEX - Child (LEAF/ROOT/ANTN):
energyTo[invert(absDir)] = 1(toward parent)
When transmission fails (
Cells.pde:1021-1025):- Cell sets
energyTo[parent] = 1(start sending to parent) - Tells parent to stop sending back: parent’s
energyTo[invert(parent)] = 0
- Parent (now WOOD):
-
0.10 —
EnergyTransportPhaseresource (src/energy/mod.rs)Flips between
+1and-1each tick. Toggled insimulationStep()(func.pde:69):EnergyTransportPeriod *= -1.At the start of each cell’s
step()(Cells.pde:64-65):- If phase is
+1:energy += engM; engM = 0 - If phase is
-1:energy += engP; engP = 0
During
transmitEnergy()(Cells.pde:1005-1018):- If phase is
+1: writes to recipient’sengP - If phase is
-1: writes to recipient’sengM
This ensures energy propagates at most 1 cell per tick.
- If phase is
-
0.11 — Fix toroidal grid wrapping in
GridPosition::offset()(src/main.rs:58-63)Currently uses
.max(0)which clamps instead of wrapping.GridBoundary::Wrapexists insimulation.rsbut is unused.The original wraps in
X()andY()(func.pde:193-203):if(x >= W) x = x - W; else if(x < 0) x = W + x;GridPosition::offset()needs access to grid dimensions. Options:- Take
SimulationSettingsor(width, height)as parameter - Store dimensions in a global or make
offseta method onSimulationGrid
- Take
Phase 1: Request/Resolve Architecture
Restructure the system execution pipeline per the book’s PlantUML diagram
(book/src/02_details/systems.md).
-
1.1 — Define Bevy
SystemSets (src/main.rs)GenomeActionSet(parallel) — genome execution produces request componentsEnergyProducerSet(parallel) — Root/Antenna/Leaf energy collection + transfer requestsResolveRequestSet(sequential) — process move, take, spawn, death requestsBranchTransferSet— Branch cells produce deposit requestsResolveDepositSet(sequential) — process deposit transfersMaintenanceSet(sequential) — energy costs, age, death checks, cleanup
-
1.2 — Define request component types (
src/cells/mod.rsor new module)RequestMove { target_position: GridPosition }— seed/apex movementRequestSpawnCell { direction: RelativeDirection, cell_type: Cell, genome: Genome, active_gene: GenomeID, level: u32 }— growthRequestTakeEnergy { source_position: GridPosition, energy_type: Energy, amount: u32 }— pulling from soilRequestDepositEnergy { to_entity: Entity, amount: u32 }— energy transfer via topologyRequestDeath— scheduled death (replaces current ad-hocCellIsDying)RequestDetach— break parent-child linksRequestMoveEnvironment { from_pos: GridPosition, to_pos: GridPosition, energy_type: Energy }— soil manipulation
These replace the existing marker-based approach (
CellRequestSolarEnergy, etc.). -
1.3 — Make
invoke_cell_genome_actions_systemread-only (src/cells/systems.rs)Instead of directly mutating
CellandGenomeID, attach request components. The genome execution (genome.execute()) is already pure. The match arms should emit requests instead oftodo!()or direct mutation.Key change: the system needs
Commandsaccess to insert request components on entities but should NOT mutateCellorGenomeIDdirectly. Those mutations happen in resolve systems.The existing
cell_positions: HashSet<GridPosition>collection for obstacle detection should be replaced withSimulationGridreads (Phase 0.3). -
1.4 —
resolve_move_requests_system(src/cells/systems.rs)Process
RequestMove. CheckSimulationGridfor collisions.Original movement logic:
- APEX movement (
Cells.pde:1093-1113): DeductmoveApexPrice(1.0) energy. Check target cell empty. If occupied → fail. If free → updatecellsIndxat old and new positions, update X/Y. - Seed movement (
Cells.pde:1061-1089): Deduct 1 energy. Check target. If occupied → kill the occupying cell and stop (seed stays,restTime = 0). If free → move.
The resolve system should:
- Query all entities with
RequestMove - For each, check
SimulationGridat target position - Handle collision per cell type (seed kills target; apex fails)
- Update
GridPosition,Transform, andSimulationGrid - Remove
RequestMovecomponent - Track success/failure for genome next-gene branching
- APEX movement (
-
1.5 —
resolve_spawn_requests_system(src/cells/systems.rs)Process
RequestSpawnCell. Original growth logic (Cells.pde:274-334):- Parent APEX becomes WOOD:
Cells.pde:275(type = WOOD) - Calculate absolute direction from relative:
Cells.pde:279-286 - Check target cell is empty:
Cells.pde:288 - Create new cell with:
age = AGE(3):Cells.pde:295org = ORGANIC_CELL(15):Cells.pde:296typefrom gene mapping:Cells.pde:297level = parent.level + 1:Cells.pde:298direction = absDir:Cells.pde:300parent = invert(absDir):Cells.pde:301adam = parent.adam:Cells.pde:303gn = parent.gn:Cells.pde:311
- Set parent’s
children[absDir] = 1:Cells.pde:327 - Set parent’s
energyTo[absDir] = 1if child is APEX:Cells.pde:329 - Set child’s
energyTo[invert(absDir)] = 1if LEAF/ROOT/ANTN:Cells.pde:331-332 - Mutation check (1% for APEX children):
Cells.pde:314-323 - Deduct energy from parent:
needEnergy = count * (WORK + ORGANIC_CELL)perCells.pde:229-230
- Parent APEX becomes WOOD:
-
1.6 —
resolve_death_requests_system(src/cells/systems.rs)Process
RequestDeath+CellIsDying. Original death sequence (Cells.pde:1036-1058):transmitEnergy()— final energy transfer to parent/soil:Cells.pde:1037distributeOrganic(X, Y, org)— spread 15 organic to 3x3:Cells.pde:1038distributeZarad(X, Y, energy+engP+engM)— spread energy to 3x3:Cells.pde:1039destroyAllLinks()— break all parent/child connections- Clear
cellsIndx[X][Y]:Cells.pde:1053 - Remove from linked list:
Cells.pde:1054-1056 - Return to free pool:
Cells.pde:1042-1043
In Bevy: despawn entity, update
SimulationGrid, distribute resources viadistribute_around, breakCellRelationlinks on parent/children entities. -
1.7 —
resolve_detach_requests_system(src/cells/systems.rs)Process
RequestDetach. OriginaldestroyAllLinks()(Cells.pde:1543-1557):- For each direction (0-3):
- If
children[i] == 1: setcells[childIndex].parent = -1 - Set neighbor’s
energyTo[invert(i)] = 0(stop them sending energy to us)
- If
- Clear all own
energyTo[]flags - Set
parent = -1
In Bevy: remove this entity from parent’s
CellRelation.children, clear parent’sEnergyTransfererentry for this entity, set ownCellRelation.parent = None, clear ownEnergyTransferer. - For each direction (0-3):
-
1.8 —
resolve_environment_move_system(src/energy/systems.rs)Process
RequestMoveEnvironmentfor soil manipulation commands 6-11.Original implementations:
moveZaradLeft/Ahead/Right()(Cells.pde:1133-1168): Move ALL energy from cell’s position to the target position.EnergyMap[target] += EnergyMap[X][Y]; EnergyMap[X][Y] = 0moveOrganicLeft/Ahead/Right()(Cells.pde:1172-1208): Same for organic.
These are “push” operations — they move the resource from the cell’s own tile to an adjacent tile.
-
1.9 — Rewire
main.rssystem registration (src/main.rs:180-198)Replace flat
.chain()with system sets and proper ordering constraints.
Phase 2: Energy System Completion
-
2.1 — Fix solar energy formula (
src/energy/systems.rs)Current implementation (
src/energy/systems.rs:72-81) just addssunlightvalue to cell energy. The correct formula fromcalculateSunEnergy()(Cells.pde:1502-1538):mn = LIGHTENERGY // 10 for each of 8 neighbors: if neighbor is LEAF → return 0 // complete shading if neighbor exists (any cell) → mn -= 1 return OrganicMap[X][Y] * mn * LIGHTCOEF // organic * (10 - obstructions) * 0.0008Key details:
- Checks all 8 cardinal + diagonal neighbors:
Cells.pde:1505-1536 - If ANY neighbor is a LEAF → energy is zero (mutual shading rule)
- Non-leaf occupied neighbors reduce
mnby 1 each mncan go to 0 if all 8 neighbors are occupied (but not LEAF)- Requires
SimulationGridfor neighbor cell type lookup - The
SunlightCycleresource is NOT used in the original — sunlight is purely derived from soil organic content. TheSunlightCycleappears to be a custom addition.
- Checks all 8 cardinal + diagonal neighbors:
-
2.2 — Energy cost system (
src/energy/systems.rs)Per-tick costs from
constant.pde:9-11:ApexEnergy4Life = 1.0(Sprout):Cells.pde:171SeedEnergy4Life = 0.5(Seed):Cells.pde:74Energy4Life = 0.04(WOOD, LEAF, ROOT, ANTN):Cells.pde:102, 113, 134, 154
When energy goes negative:
age--; energy = 0:Cells.pde:103, 115, 135, 155- APEX/SEED check
energy + engP + engM < 0instead (includes buffers):Cells.pde:73, 166
Death trigger:
if(age <= 0) die():Cells.pde:106, 120, 140, 160- LEAF/ROOT/ANTN also die if
parent == -1:Cells.pde:120, 140, 160
Note: since Rust uses
u32for energy (not float), fractional costs (0.04, 0.5) need either: (a) switch tof32, (b) accumulate a fractional counter, or (c) scale all energy values by 100 and use integer math. -
2.3 — Ping-pong energy transfer for Branch cells (
src/energy/systems.rs)Original
transmitEnergy()(Cells.pde:1001-1031):n = count of energyTo[] flags set to 1 if n > 0: en = energy / n if EnergyTransportPeriod == 1: for each target with energyTo[i] == 1: cells[target].engP += en energy = 0 else: for each target with energyTo[i] == 1: cells[target].engM += en energy = 0At step start (
Cells.pde:64-65):if EnergyTransportPeriod == 1: energy += engM; engM = 0 else: energy += engP; engP = 0In Bevy, add two buffer components (e.g.,
EnergyBufferP(f32),EnergyBufferM(f32)) or a singleEnergyTransferBuffer { p: f32, m: f32 }. TheEnergyTransportPhaseresource determines which buffer to write to and which to read from.Called by: LEAF (
Cells.pde:116), ROOT (Cells.pde:136), ANTN (Cells.pde:156), WOOD (Cells.pde:104). All energy-producing and transport cells call this when they have positive energy. -
2.4 — “No recipients” energy fallback (
src/energy/systems.rs)When
n == 0(noenergyTotargets) intransmitEnergy()(Cells.pde:1021-1030):if parent != -1: // Start sending to parent next time energyTo[parent] = 1 // Tell parent to stop sending energy back to us cells[parentIndex].energyTo[invert(parent)] = 0 else: // Dump to soil EnergyMap[X][Y] += energy age-- energy = 0This is the self-correcting mechanism: cells that can’t transmit redirect toward their parent, and isolated cells lose energy to the soil with an age penalty.
-
2.5 —
PreviousEnergytracking system (src/energy/systems.rs)At tick start, copy
CellEnergytoPreviousEnergy. Used by conditions 8/9 (Cells.pde:459-467). Simple system that runs before all others in the tick.
Phase 3: Genome Execution Completion
-
3.1 — Implement 6 remaining multi-cell commands (
src/cells/systems.rs)All from
command()(Cells.pde:795-867):Cmd Rust Enum Original Logic Source 0 SkipTurnNo-op Cells.pde:7991 BecomeASeed(flying)type=SEED, destroyAllLinks(), move=true, restTime=8Cells.pde:802-8082 BecomeASeed(stationary)type=SEED, move=false, restTime=8Cells.pde:810-8133 BecomeADetachedSeedtype=SEED, move=true, restTime=8Cells.pde:815-8194 DieenergyTo[parent]=1, die()Cells.pde:821-8245 SeparateFromOrganismdestroyAllLinks()Cells.pde:826-8286-8 TransportSoilEnergy(dir)moveZaradLeft/Right/Ahead()Cells.pde:831-8419-11 TransportSoilOrganicMatter(dir)moveOrganicLeft/Right/Ahead()Cells.pde:843-85212 ShootSeed { high_energy: false }setNewSEED(0, 30)— bullet: 30 energy, 30 tick flightCells.pde:855-85713 ShootSeed { high_energy: true }setNewSEED(1, 5+random(40))— reproductive: all energy, 5-44 tick flightCells.pde:859-86114 DistributeEnergyAsOrganicMatterOrganicAround()Cells.pde:863-865OrganicAround()detail (Cells.pde:1213-1226):if energy < 12: return (fail) ee = floor((energy - 3) / 9) for each of 9 cells in 3x3: OrganicMap[cell] += ee energy = 3setNewSEED(en, rt)detail (Cells.pde:339-399):- Energy check:
if(energy < ORGANIC_CELL + WORK + 30) return false(line 340) - Check forward cell (line 347):
- If occupied by same genome → add energy to it
- If occupied by different genome → kill it
- Subtract
ORGANIC_CELL + WORKfrom parent (line 353) - Create SEED entity with:
type = SEED,level = 0,parent = -1(lines 363-370)direction = parent's direction(line 372)restTime = rt(line 386),move = true(line 387)adam = parent.adam(line 375),gn = parent.gn(line 376)
- Energy transfer (lines 389-396):
- If
en == 0(bullet): seed gets 30 energy, rest stays with parent - If
en == 1(reproductive): seed gets ALL remaining parent energy
- If
moveZarad*/moveOrganic*detail (Cells.pde:1133-1208):- Calculates target position (left/ahead/right relative to facing)
- Moves ALL of that resource from cell’s position to target:
Map[target] += Map[X][Y]; Map[X][Y] = 0 - Always returns
true
- Energy check:
-
3.2 — Implement 18 single-cell commands (
src/cells/systems.rs)All from
command_alone()(Cells.pde:873-996):Cmd Rust Enum Original Logic Source 0 MoveForwardmoveApex()Cells.pde:876-8781 TurnRightdirection += 1; direction %= 4Cells.pde:880-8832 TurnLeftdirection -= 1; if < 0 then += 4Cells.pde:885-8883 TurnArounddirection += 2; direction %= 4Cells.pde:890-8934 TurnRightAndMoveTurn right + moveApex()Cells.pde:895-8995 TurnLeftAndMoveTurn left + moveApex()Cells.pde:901-9056 TurnAroundAndMoveTurn around + moveApex()Cells.pde:907-9117 ParasitiseAttach to forward WOOD cell Cells.pde:913-9248 TurnRandomr=random(0,10); if r<3 right, elif r<6 left, else nothingCells.pde:926-9329 MoveRandomRandom turn (same as 8) + moveApex()Cells.pde:934-93710 PullOrganicFromLeftpushOrganicFromLeft()Cells.pde:940-94211 PullOrganicFromForwardpushOrganicFromAhead()Cells.pde:944-94612 PullOrganicFromRightpushOrganicFromRight()Cells.pde:948-95013 PullChargeFromLeftpushZaradFromLeft()Cells.pde:952-95414 PullChargeFromForwardpushZaradFromAhead()Cells.pde:956-95815 PullChargeFromRightpushZaradFromRight()Cells.pde:960-96216 ConsumeNeighboursKill adjacent non-WOOD/non-SEED cells Cells.pde:965-98217 TakeEnergyFromSoilAbsorb up to 6 from soil Cells.pde:985-994moveApex()detail (Cells.pde:1093-1113):- Deduct
moveApexPrice(1.0): line 1094 - Calculate forward position: lines 1098-1101
- If occupied →
return false: line 1104 - Update
cellsIndx(clear old, set new): lines 1107-1109 - Update X, Y: lines 1110-1111
Parasitisedetail (Cells.pde:913-924):- Find cell in forward direction:
findIndexFromDirection(direction) - If that cell exists AND is WOOD:
- Set own
parent = direction(face toward host) - Set host’s
children[invert(direction)] = 1 - Set host’s
energyTo[invert(direction)] = 1(host feeds parasite) return true
- Set own
- Otherwise
return false
ConsumeNeighboursdetail (Cells.pde:965-982):- Cost:
energy -= 1(line 966) - Check 8 directions (0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5 — includes diagonals): line 968
- For each direction:
findIndexFromDirection(d)(line 969) - If cell exists AND
type < WOOD(i.e., APEX, LEAF, ANTN, ROOT): line 971- Absorb:
energy += target.energy + target.engP + target.engM + target.org(line 974) - Kill target:
target.die()(line 975)
- Absorb:
return true
Note: the original checks 8 directions including diagonals (0.5-step increments). The
findIndexFromDirection()function handles these fractional directions (Cells.pde:1487-1497).pushOrganicFrom*detail (Cells.pde:1231-1276):- Calculate source position (left/ahead/right of cell)
- If
OrganicMap[source] <= 0→return false - Move ALL organic:
OrganicMap[X][Y] += OrganicMap[source]; OrganicMap[source] = 0 return true
pushZaradFrom*detail (Cells.pde:1280-1324):- Same as organic but for EnergyMap
TakeEnergyFromSoildetail (Cells.pde:985-994):ALONE_CAN = 6(constant.pde:4)- If
EnergyMap[X][Y] > ALONE_CAN: take 6,return true - Else: take all remaining,
return false
- Deduct
-
3.3 — Command success/failure tracking (
src/cells/systems.rs)Commands return
bool(res) in the original. This determines genome branching:- Body cell, conditions met: success →
aGen = GN[gn][aG+10] % 32, fail →aGen = GN[gn][aG+11] % 32(Cells.pde:188-189) - Body cell, conditions not met: success →
GN[gn][aG+13] % 32, fail →GN[gn][aG+14] % 32(Cells.pde:208-209) - Lone cell, conditions met: success →
GN[gn][aG+16] % 32, fail →GN[gn][aG+17] % 32(Cells.pde:192-193) - Lone cell, conditions not met: success →
GN[gn][aG+19] % 32, fail →GN[gn][aG+20] % 32(Cells.pde:212-213)
In the request/resolve model, the genome system emits both
success_next_genomeandfail_next_genome(already inCellGenomeCommand). The resolve system determines success/failure and writes the appropriateGenomeID. This requires a mechanism for resolve systems to updateGenomeID— either:- A
PendingGenomeUpdate { success_id, fail_id }component set by genome system, resolved after commands execute - Or the resolve systems directly write
GenomeID
- Body cell, conditions met: success →
-
3.4 — Growth system (
src/cells/systems.rs)The “no conditions” branch in APEX gene execution (
Cells.pde:217-231):// Count expected branches from gene bytes 0-2 tempCellCount = 0 for i in 0..3: if GN[gn][aG + i] <= 95: tempCellCount++ needEnergy = tempCellCount * (WORK + ORGANIC_CELL) // count * 20 if energy >= needEnergy: grow() // calls setNewSegment for each directionIn Rust,
GenomeSpawnalready stores the cell types for forward/left/right. The growth system should:- Check if
GenomeEntry.conditionals.preconditionsis empty (no conditions trigger) - Count non-empty spawn directions from
GenomeSpawn - Check energy >=
count * 20 - Emit
RequestSpawnCellfor each direction - Emit a request to convert self from Sprout → Branch
The gene-to-cell-type mapping (
Cells.pde:247-262):- 0-63 → APEX (active gene =
value % 32) - 64-75 → LEAF
- 76-85 → ANTN
- 86-95 → ROOT
- 96+ → no growth
This mapping is already abstracted away by
GenomeSpawnin Rust — theCellenum variant is stored directly. - Check if
-
3.5 — APEX/Seed detach-on-excess (
src/cells/systems.rs)APEX detach (
Cells.pde:173-176):if energy > MAX_APEX_ENERGY (1024) && parent != -1: destroyAllLinks() aGen = 0Seed detach (
Cells.pde:76):if energy > MAX_SEED_ENERGY (512) && parent != -1: destroyAllLinks()Constants from
constant.pde:20-21.
Phase 4: Precondition Expansion
-
4.1 — Expand
GenomePrecondition(src/genes.rs)Currently 8 variants, original has 68 (
Cells.pde:409-789). Full mapping:ID Condition Params Source 0 OrganicAtPosition < param*2organic at (X,Y), param Cells.pde:419-4231 OrganicAtPosition >= param*2same Cells.pde:424-4272 CellEnergy > param*2cell energy, param Cells.pde:429-4333 CellEnergy < param*2same Cells.pde:434-4374 param % (level+1) == 0param, level Cells.pde:439-4435 level % (param+1) == 0level, param Cells.pde:444-4476 level > paramlevel, param Cells.pde:449-4537 level < paramlevel, param Cells.pde:454-4578 energy >= energyOld(rising)energy, prev energy Cells.pde:459-4639 energy < energyOld(falling)same Cells.pde:464-46710 organicCount9(X,Y) > param*183x3 organic sum, param Cells.pde:469-47311 organicCount9(X,Y) < param*18same Cells.pde:474-47712 zaradCount9(X,Y) > param*183x3 energy sum, param Cells.pde:479-48313 zaradCount9(X,Y) < param*18same Cells.pde:484-48714 zaradCount9 > organicCount9both 3x3 sums Cells.pde:489-49315 zaradCount9 < organicCount9same Cells.pde:494-49716 Edible cells nearby (5 dirs) grid lookup Cells.pde:500-51817 Area is free (left+center+right) grid lookup Cells.pde:520-53218-20 Free space left/center/right grid + poison check Cells.pde:533-54721-23 Obstacle left/center/right inverse of 18-20 Cells.pde:549-56224 Has parent parent != -1 Cells.pde:564-56725 Random random(256) > param Cells.pde:569-57226-31 Light comparisons (3 dirs) findLight3FromRelDirectionCells.pde:575-60332-37 Energy 9-cell comparisons (3 dirs) findZarad9FromRelDirectionCells.pde:605-63538-40 Energy 9-cell thresholds findZarad9FromRelDirectionCells.pde:637-65041-46 Organic 9-cell comparisons (3 dirs) findOrganic9FromRelDirectionCells.pde:652-68247-49 Organic 9-cell thresholds findOrganic9FromRelDirectionCells.pde:684-69650-55 Free space 9-cell comparisons (3 dirs) howManySpace9InRelDirectionCells.pde:698-72856-58 Free space 9-cell thresholds howManySpace9InRelDirectionCells.pde:730-74259-61 Organic poison ahead/left/right yadFromRelDirection(0)Cells.pde:744-76262-64 Energy poison ahead/left/right yadFromRelDirection(1)Cells.pde:764-77565-67 Any poison ahead/left/right yadFromRelDirection(2)Cells.pde:777-787Note: the Rust genome uses a different encoding (high-level enum vs raw bytes), so not all 68 need 1:1 mapping. But the categories that matter most for evolution are:
- Spatial awareness: 16-24, 50-58 (free space, obstacles, edible cells)
- Resource sensing: 10-15, 26-49 (directional organic/energy gradients)
- Poison avoidance: 59-67
- Organism state: 2-9, 24 (energy, level, parent)
-
4.2 — Expand
PreconditionParameters(src/genes.rs)Currently:
#![allow(unused)] fn main() { pub struct PreconditionParameters { pub organic_energy: NeighbouringEnergy, pub charge_energy: NeighbouringEnergy, pub cell_energy_has_increased: bool, pub obstacles: ObstacleInfo, pub rng_value: u8, } }Needs additions:
has_parent: boollevel: u32cell_energy: u32(for conditions 2-3)previous_energy: u32(for conditions 8-9, replacescell_energy_has_increased)organic_at_position: u32(for conditions 0-1)organic_9cell: u32(for conditions 10-11)charge_9cell: u32(for conditions 12-13)- Directional 9-cell data (for conditions 26-58):
organic_9_forward,organic_9_left,organic_9_right, and same for charge and free-space - Directional light data (for conditions 26-31):
light3_forward,light3_left,light3_right - Poison data (for conditions 59-67):
poison_forward,poison_left,poison_right(each as flags for organic/energy/any) edible_cells_nearby: bool(for condition 16)area_is_free: bool(for condition 17)- Free space in each direction (for conditions 18-20)
-
4.3 — Directional 9-cell scans (
src/energy/mod.rs)The original’s directional scans are NOT the same as the current
NeighbouringEnergy3x3:findOrganic9FromRelDirection(relDir)(Cells.pde:1383-1398):- Converts relative direction to absolute
- Calculates a 3x3 block that is offset 1-3 cells ahead in that direction
- Sums OrganicMap values in that 3x3 block
findZarad9FromRelDirection(relDir)(Cells.pde:1403-1417):- Same but for EnergyMap
howManySpace9InRelDirection(relDir)(Cells.pde:1441-1463):- Counts free cells in the offset 3x3 block
- “Free” means:
cellsIndx == 0 AND OrganicMap < EXCESS AND EnergyMap < EXCESS(isFreeInRelDirection,Cells.pde:1422-1436)
findLight3FromRelDirection(relDir)(Cells.pde:1330-1348):- Sums OrganicMap for 3 cells ahead in the relative direction
- Excludes cells where OrganicMap >= ORGANIC_EXCESS (poison)
These helper functions compute data used by conditions 26-58.
Phase 5: Seed Mechanics
-
5.1 —
SeedRestTimecomponent (src/cells/mod.rs)u32. Set during seed creation:- Bullet (command 12):
restTime = 30(Cells.pde:855→setNewSEED(0, 30)) - Reproductive (command 13):
restTime = 5 + random(40)(Cells.pde:859→setNewSEED(1, 5+random(40))) - BecomeASeed commands:
restTime = 8(Cells.pde:806, 812, 818)
At 0, seed transforms into Sprout at gene 0:
Cells.pde:89-91(age=AGE, aGen=0, type=APEX, level=0). - Bullet (command 12):
-
5.2 — Seed flight (
src/cells/mod.rs)moveSeed()(Cells.pde:1061-1089):- Deduct 1 energy: line 1062
- Calculate forward position: lines 1066-1069
- If occupied: kill the occupying cell (
cells[inx].die()), stop moving (restTime = 0, move = false): lines 1073-1076 - If wall (negative index): stop moving: lines 1078-1080
- If free: move to position, update
cellsIndx: lines 1083-1087
Only moves if
parent == -1 AND move == true AND restTime > 0:Cells.pde:81-82. -
5.3 —
seed_behavior_system(src/cells/systems.rs)Full seed step logic (
Cells.pde:72-95):if(energy + engP + engM < 0) die() energy -= SeedEnergy4Life // 0.5 if(energy > MAX_SEED_ENERGY && parent != -1) destroyAllLinks() GN[gn][673] = 1 // mark genome in use if parent == -1: restTime-- if move && restTime > 0: moveSeed() if restTime <= 0: age = AGE aGen = 0 type = APEX level = 0
Phase 6: Death and Recycling
-
6.1 — Death recycling (
src/cells/systems.rs)Full death sequence (
Cells.pde:1036-1058):transmitEnergy()— try to send energy to connected cells firstdistributeOrganic(X, Y, org)— spread cell’s organic (15) to 3x3 soildistributeZarad(X, Y, energy+engP+engM)— spread remaining energy to 3x3 soil- Return cell to free pool:
freeCells[freeCellsPointer] = index - Reset all fields, remove from linked list
Note:
distributeOrganicanddistributeZaradaverage with existing soil values (they sum the 3x3 + deposit, then divide by 9 and redistribute). They do NOT simply add to existing values. -
6.2 — Orphan death (
src/cells/systems.rs)LEAF/ROOT/ANTN die if
parent == -1:- LEAF:
Cells.pde:120(if(age <= 0 || parent == -1) die()) - ROOT:
Cells.pde:140(same) - ANTN:
Cells.pde:160(same)
- LEAF:
-
6.3 — Isolated Branch death (
src/cells/systems.rs)WOOD:
Cells.pde:99-101:if parent == -1: has_children = children[0] + children[1] + children[2] + children[3] if has_children == 0: die()
Phase 7: Mutation
-
7.1 — Mutation on APEX child creation (
src/genes.rs)Original (
Cells.pde:314-323):if stg == APEX && freeGNPointer < TOTAL_GENOM_COUNT - 1000: if random(0, 100) <= 1: // ~1% chance g = findFreeGenom() arrayCopy(GN[gn], GN[g]) // clone genome mutGen = floor(random(0, 673)) // pick random byte GN[g][mutGen] = floor(random(0, 256)) // randomize it GN[g][673] = 1 // mark in use cells[newIndex].gn = gIn Rust, since genomes are per-entity
Components (not a global pool), there’s no genome count limit. The 1% check applies. “One byte change” maps to: pick a randomGenomeEntry(0-51), pick a random field within it (spawn direction, precondition, action, genome pointer, command), and randomize that field. -
7.2 —
Genome::mutate()method (src/genes.rs)Design decision: the original’s 673 bytes correspond to 32 genes × 21 bytes. In Rust’s 52-entry
GenomeEntrystruct, each entry has:spawn: GenomeSpawn(3 fields: forward/left/right cell types)conditionals: GenomeConditional:preconditions: Vec<GenomePrecondition>(0-2 entries)preconditions_met_action: CellAction(Command or ChangeGenome)preconditions_unmet_action: CellActionfallback_genome: GenomeID
A reasonable mapping: pick a random entry, pick a random sub-field, generate a new random value for that field using the existing
Distribution<T>impls (which are already defined for all types).
Phase 8: Initialization
-
8.1 — Fix
initialize_sprouts_system(src/main.rs:247-274)Original
createNewLife()(func.pde:3-12):for x = 10; x < W; x += 20: for y = 10; y < H; y += 20: createCell(x, y, APEX, 500 energy, random genome, unique adam)Current Rust version has bugs:
- All sprouts share ONE genome (line 252: genome generated once)
- Energy is 10 instead of 500
- Positions are random instead of grid-aligned every 20 cells
-
8.2 — Adjust initial energy values (
src/main.rs:130,134)Original:
setOrganicZarad(200, 200)(CLANS3eng.pde:83,func.pde:32-40). Current Rust: organic=50, charge=20. -
8.3 — Remove
add_test_cells(src/main.rs:351-411)Replace with real initialization.
-
8.4 — Wire CLI parsing (
src/cli.rs,src/main.rs)Clistruct exists but is unused.
Phase 9: Ancestor/Clan Tracking
-
9.1 —
CellAncestorcomponent (src/cells/mod.rs)usize. Each initial cell gets unique ID. Children inherit parent’s ancestor.Original:
adamfield, set duringcreateNewLife()(func.pde:9: incrementing counter) and inherited during growth (Cells.pde:303:adam = parent.adam).Used for lineage visualization and statistics:
func.pde:217-231counts cells per ancestor.
Phase 10: Verification
- 10.1 Unit test: growth produces correct child cells from
GenomeSpawn - 10.2 Unit test: ping-pong energy transfer moves energy 1 cell/tick
- 10.3 Unit test: death recycling distributes correct amounts to 9-cell neighborhood
- 10.4 Unit test: solar formula matches original (organic × free_count × 0.0008, zero if adjacent leaf)
- 10.5 Unit test: seed movement + collision behavior
- 10.6 Unit test: mutation produces genome with exactly 1 difference
- 10.7 Integration test: single APEX with known genome grows correctly over N ticks
- 10.8 Verify all single-cell and multi-cell commands emit correct requests
Recommended Implementation Order
- Phase 0 — foundation everything depends on
- Phase 1 — sets the architectural pattern
- Phase 2 — energy is critical for cell survival
- Phase 6 — closes the resource loop
- Phase 3 — core genome mechanics
- Phase 5 — reproduction
- Phase 7 — evolution
- Phase 4 — richer behavior
- Phase 8 — real simulation runs
- Phase 9 — visualization
- Phase 10 — verification throughout, especially at end
Critical Files
| File | Role |
|---|---|
src/cells/systems.rs | Genome execution, command implementations, resolve systems |
src/energy/mod.rs | Energy environment methods, transfer buffers, new components |
src/energy/systems.rs | Energy collection, transfer, costs |
src/genes.rs | Precondition expansion, mutation, genome execution |
src/main.rs | System sets, initialization, ordering |
src/simulation.rs | SimulationGrid as live spatial index |
src/cells/mod.rs | New components (Age, Level, Organic, Ancestor, requests) |
Quick Reference: Original Constants
From constant.pde:
| Constant | Value | Used In |
|---|---|---|
ROOT_CAN | 1.0 | ROOT extraction per tick |
ANTN_CAN | 1.0 | ANTN extraction per tick |
ALONE_CAN | 6 | Lone APEX soil absorption |
ORGANIC_EXCESS | 512 | Poison threshold (organic) |
ENERGY_EXCESS | 512 | Poison threshold (energy) |
AGE | 3 | Default cell lifespan |
Energy4Life | 0.04 | WOOD/LEAF/ROOT/ANTN cost/tick |
SeedEnergy4Life | 0.5 | Seed cost/tick |
ApexEnergy4Life | 1.0 | APEX/Sprout cost/tick |
moveApexPrice | 1.0 | Movement cost |
LIGHTENERGY | 10 | Leaf base light factor |
LIGHTCOEF | 0.0008 | Leaf light coefficient |
ORGANIC_CELL | 15 | Organic per cell (death release) |
WORK | 5 | Energy cost to create cell |
MAX_APEX_ENERGY | 1024 | APEX detach threshold |
MAX_SEED_ENERGY | 512 | Seed detach threshold |