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 |