lifecycle.js
162 lines · in natura-src-1.0.0.zip as natura/lifecycle.js · a module · its entry in the module reference · its test: lifecycle-test.js
// lifecycle.js -- Natura LIFE-CYCLE (SYSTEMS.md #6; the LIFE domain's last unbuilt system). A creature's
// life as a state machine, and a POPULATION as the turnover of many: birth -> growth -> maturity -> aging
// -> death -> decay -> nutrients -> (the ground that feeds the next). The ontology's biological continuum
// made mechanical -- "LIFE is matter passing THROUGH life; death is its tail." Each creature ages on the
// clock, eats (energy in) against its metabolism (energy out), grows through stages by age, breeds when
// adult and fed, and dies of starvation, cold, or old age; its carcass decays back into a nutrient field.
// The emergence is the TURNOVER: individuals die, yet the population persists across generations, holds an
// age STRUCTURE, and rides its food's carrying capacity -- and the matter that made the dead returns to the
// soil. Couples to VEGETATION (food) + WEATHER (cold stress); its OUT (creatures + nutrients) feeds a viz,
// the ecosystem, and the soil. In a minded world each creature carries a MIND that drives its behaviour
// (BUILD ON). Deterministic + node-testable. UMD.
(function (root, factory) {
if (typeof module !== 'undefined' && module.exports) module.exports = factory();
else root.Lifecycle = factory();
})(typeof self !== 'undefined' ? self : this, function () {
'use strict';
const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
// stages -- EMPTY is a free pool slot; CARCASS is a dead body still decaying (occupies a slot till gone)
const EMPTY = 0, EGG = 1, JUV = 2, ADULT = 3, SEN = 4, CARCASS = 5;
function makeLifecycle(config) {
config = config || {};
const cap = config.maxCreatures || 500;
const vegName = config.vegetation || 'vegetation';
const weatherName = config.weather || 'weather';
const foodFn = config.food || null; // (x,y,ctx)->0..1; else read vegetation, else a default patchy field
const NN = config.nutrientN || 32;
const P = Object.assign({
n0: 120,
hatchDays: 6, matureDays: 30, senesceDays: 130, maxDays: 185, // stage thresholds (sim-days)
metab: 0.05, // base energy/day burned
metabJuv: 0.9, metabAdult: 1.0, metabSen: 1.25, // per-stage metabolic multiplier (young cheap, old dear)
grazeRate: 0.12, // energy/day gained per unit food (juv+adult forage)
starveRate: 0.5, coldRate: 0.5, healRate: 0.7, // health loss when starving/cold, gain when fed & warm (per day)
reproThresh: 0.7, reproCost: 0.45, reproDailyP: 0.07, // an adult, fed above threshold, breeds with this daily chance
eggEnergy: 0.5, startEnergy: 0.6,
decayDays: 18, nutrientPerBody: 1.0, nutrientDecay: 0.02, // carcass -> nutrients over decayDays; soil slowly reuptakes
coldT: 3.0 // temperature (C) below which a creature suffers cold stress
}, config.params || {});
const cellOf = (v) => clamp(Math.floor((v + 1) / 2 * NN), 0, NN - 1);
function defaultFood(x, y) { return clamp(0.45 + 0.4 * Math.sin(x * 2.7 + 0.5) * Math.cos(y * 2.3 - 0.3), 0, 1); }
return {
name: config.name || 'lifecycle',
domain: 'life',
scale: 'local',
params: config,
init(rng) {
const st = {
cap, NN,
stage: new Uint8Array(cap), age: new Float32Array(cap), energy: new Float32Array(cap),
health: new Float32Array(cap), x: new Float32Array(cap), y: new Float32Array(cap), carcassT: new Float32Array(cap),
nutrients: new Float32Array(NN * NN),
births: 0, deaths: 0, dStarve: 0, dCold: 0, dOld: 0,
nAlive: 0, nEgg: 0, nJuv: 0, nAdult: 0, nSen: 0, nCarcass: 0, meanAge: 0,
evs: []
};
for (let i = 0; i < P.n0 && i < cap; i++) {
st.x[i] = rng() * 2 - 1; st.y[i] = rng() * 2 - 1;
const a = rng(); // seed a spread of ages/stages so there is structure from the start
st.age[i] = a * P.senesceDays;
st.stage[i] = st.age[i] < P.hatchDays ? EGG : st.age[i] < P.matureDays ? JUV : ADULT;
st.energy[i] = P.startEnergy; st.health[i] = 0.9;
}
return st;
},
step(st, ctx, dt) {
const dd = dt / 86400; if (dd <= 0) return st; const s = st; s.evs.length = 0;
const rng = ctx.rng || Math.random;
const veg = ctx.fields ? ctx.fields[vegName] : null;
const wx = ctx.fields ? ctx.fields[weatherName] : null;
const foodAt = foodFn ? (x, y) => clamp(foodFn(x, y, ctx), 0, 1)
: veg && veg.biomass && veg.N ? (x, y) => { const n = veg.N, ci = clamp(Math.floor((x + 1) / 2 * n), 0, n - 1), cj = clamp(Math.floor((y + 1) / 2 * n), 0, n - 1); return clamp(veg.biomass[cj * n + ci], 0, 1); }
: defaultFood;
const tempAt = wx && wx.temp && wx.N ? (x, y) => { const n = wx.N, ci = clamp(Math.floor((x + 1) / 2 * n), 0, n - 1), cj = clamp(Math.floor((y + 1) / 2 * n), 0, n - 1); return wx.temp[cj * n + ci]; } : () => 15;
// breeding season -- if a clock exposes a year phase, breed in the warm half; else always
const yp = ctx.clock && ctx.clock.yearPhase ? ctx.clock.yearPhase() : 0.25;
const breedSeason = 0.5 + 0.5 * Math.sin(yp * Math.PI * 2); // 1 at mid-year, 0 opposite
let nEgg = 0, nJuv = 0, nAdult = 0, nSen = 0, nCar = 0, ageSum = 0, alive = 0;
const births = [];
for (let i = 0; i < cap; i++) {
const stg = s.stage[i]; if (stg === EMPTY) continue;
if (stg === CARCASS) {
s.carcassT[i] += dd;
s.nutrients[cellOf(s.y[i]) * NN + cellOf(s.x[i])] += P.nutrientPerBody * dd / P.decayDays; // decay releases matter to the soil
if (s.carcassT[i] >= P.decayDays) { s.stage[i] = EMPTY; s.evs.push({ kind: 'decayed', at: [s.x[i], s.y[i]] }); }
else nCar++;
continue;
}
// --- alive: age, feed, health ---
s.age[i] += dd;
const met = P.metab * (stg === JUV ? P.metabJuv : stg === SEN ? P.metabSen : stg === EGG ? 0 : P.metabAdult);
const gain = stg === EGG ? 0 : P.grazeRate * foodAt(s.x[i], s.y[i]);
s.energy[i] = clamp(s.energy[i] + (gain - met) * dd, 0, 1.2);
const cold = tempAt(s.x[i], s.y[i]) < P.coldT;
if (s.energy[i] < 0.05) s.health[i] -= P.starveRate * dd;
else if (cold) s.health[i] -= P.coldRate * dd * 0.6;
else s.health[i] = clamp(s.health[i] + P.healRate * (1 - s.health[i]) * dd, 0, 1);
s.health[i] = clamp(s.health[i], 0, 1);
// --- stage transitions by age ---
if (stg === EGG && s.age[i] >= P.hatchDays) { s.stage[i] = JUV; s.evs.push({ kind: 'hatch', mind: i }); }
else if (stg === JUV && s.age[i] >= P.matureDays) s.stage[i] = ADULT;
else if (stg === ADULT && s.age[i] >= P.senesceDays) s.stage[i] = SEN;
// --- death: starvation/cold (health gone) or old age ---
let died = null;
if (s.health[i] <= 0) died = (s.energy[i] < 0.05 ? 'starve' : 'cold');
else if (s.age[i] >= P.maxDays) died = 'old';
if (died) {
s.stage[i] = CARCASS; s.carcassT[i] = 0; s.deaths++;
if (died === 'starve') s.dStarve++; else if (died === 'cold') s.dCold++; else s.dOld++;
s.evs.push({ kind: 'death', mind: i, cause: died, age: s.age[i], at: [s.x[i], s.y[i]] });
continue;
}
// --- reproduce: an adult, fed above threshold, in season ---
if (s.stage[i] === ADULT && s.energy[i] > P.reproThresh && rng() < P.reproDailyP * breedSeason * dd) {
s.energy[i] -= P.reproCost;
births.push(i);
}
const ns = s.stage[i];
if (ns === EGG) nEgg++; else if (ns === JUV) nJuv++; else if (ns === ADULT) nAdult++; else if (ns === SEN) nSen++;
ageSum += s.age[i]; alive++;
}
// place the new eggs in free slots near their parent (a birth can fail if the world is full)
for (const pi of births) {
let slot = -1; for (let j = 0; j < cap; j++) if (s.stage[j] === EMPTY) { slot = j; break; }
if (slot < 0) break;
s.stage[slot] = EGG; s.age[slot] = 0; s.energy[slot] = P.eggEnergy; s.health[slot] = 1;
s.x[slot] = clamp(s.x[pi] + (rng() - 0.5) * 0.12, -1, 1); s.y[slot] = clamp(s.y[pi] + (rng() - 0.5) * 0.12, -1, 1);
s.births++; nEgg++; alive++;
s.evs.push({ kind: 'birth', mind: slot, parent: pi, at: [s.x[slot], s.y[slot]] });
}
// soil slowly reabsorbs nutrients (they feed the next growth; keeps the field from saturating)
for (let n = 0; n < NN * NN; n++) s.nutrients[n] *= (1 - P.nutrientDecay * dd);
s.nEgg = nEgg; s.nJuv = nJuv; s.nAdult = nAdult; s.nSen = nSen; s.nCarcass = nCar;
s.nAlive = alive; s.meanAge = alive ? ageSum / alive : 0;
return s;
},
fields(st) {
return {
cap: st.cap, N: st.NN, nutrients: st.nutrients,
stage: st.stage, age: st.age, energy: st.energy, health: st.health, x: st.x, y: st.y, carcassT: st.carcassT,
nAlive: st.nAlive, nEgg: st.nEgg, nJuv: st.nJuv, nAdult: st.nAdult, nSen: st.nSen, nCarcass: st.nCarcass,
meanAge: st.meanAge, births: st.births, deaths: st.deaths,
dStarve: st.dStarve, dCold: st.dCold, dOld: st.dOld,
STAGE: { EMPTY, EGG, JUV, ADULT, SEN, CARCASS }
};
},
events(st) { return st.evs; }
};
}
return { makeLifecycle, STAGE: { EMPTY, EGG, JUV, ADULT, SEN, CARCASS } };
});