Metabolism: song as substrate, ecology grown from the track
A new top-level section building the metabolism concept: the track supplies the food field, event impulses and per-section laws; the seed picks the species (a rule set, not a parameter vector). Same song + seed => bit-identical frames; different songs => different worlds. - analysis: single FFT pass -> per-frame feature table; score (onsets folded into world geometry via voice ring + pitch-class sector), wells (food placed by the harmony), graph (band correlations -> interaction matrix), laws (per-section regimes) - sim: typed-array world, 3 castes (builder/orbit, forager/commute, spark/orbit-wide) with causality rings, persistence trails, consequence field; fully deterministic - lens: software framebuffer renderer (PNG in node, ImageData in browser) + event-reactive camera (cuts on activity change, alternating shot sizes, drift-and-push) - metrics: coupling x residual fitness (not per-frame descriptors) plus a grown-structure fingerprint; report + strip tools; 15 tests green - DESIGN.md captures the concept, invariants and build order 3 reference test signals (drone/runner/dusk) grow measurably distinct worlds while seeds of one song stay in the same biome.
This commit is contained in:
@@ -0,0 +1,632 @@
|
||||
// The ecology. Agents on a resource field, with the song's species and the
|
||||
// song's laws. No player, no realtime budget, no rendering: a step function.
|
||||
//
|
||||
// Three invariants are load-bearing and every design choice here serves them:
|
||||
//
|
||||
// PERSISTENCE agents live for seconds, not frames, and the trail grid keeps
|
||||
// what they did for tens of seconds. You can follow something.
|
||||
// CAUSALITY food arrives where and when the track put it. A bloom has an
|
||||
// audible cause; a migration has a visible one.
|
||||
// CONSEQUENCE the field is depleted, the trail accumulates. The state at t
|
||||
// is made of the state at t-1, not re-rolled each frame.
|
||||
|
||||
import { Rng } from '../rng.js';
|
||||
import { buildWells } from './wells.js';
|
||||
|
||||
const clamp01 = (x) => (x < 0 ? 0 : x > 1 ? 1 : x);
|
||||
const TAU = Math.PI * 2;
|
||||
const H = 1.6; // field samples are one and a half cells apart
|
||||
|
||||
export const FIELD = 128;
|
||||
|
||||
export class World {
|
||||
/**
|
||||
* @param {ReturnType<import('../song/song.js').buildSong>} song
|
||||
*/
|
||||
constructor(song, { fps = 30, field = FIELD, seed = song.seed } = {}) {
|
||||
this.song = song;
|
||||
this.fps = fps;
|
||||
this.dt = 1 / fps;
|
||||
this.field = field;
|
||||
this.genome = song.genome;
|
||||
this.palette = song.palette;
|
||||
this.rng = new Rng(seed >>> 0).fork('world');
|
||||
|
||||
this.resource = new Float32Array(field * field);
|
||||
this.wells = buildWells(song.summary, field);
|
||||
// The dish is delivered already furnished: colonies exist at t=0 and
|
||||
// the first bars are life forming, not a blank plate waiting for food.
|
||||
for (const well of this.wells) {
|
||||
const add = well.rate * 7;
|
||||
const idx = well.idx;
|
||||
const w = well.w;
|
||||
for (let i = 0; i < idx.length; i++) {
|
||||
this.resource[idx[i]] = Math.min(1.6, add * w[i]);
|
||||
}
|
||||
}
|
||||
this.trail = [
|
||||
new Float32Array(field * field),
|
||||
new Float32Array(field * field),
|
||||
new Float32Array(field * field),
|
||||
];
|
||||
this.activity = new Float32Array(32 * 32);
|
||||
|
||||
this.cap = this.genome.maxAgents;
|
||||
this.x = new Float32Array(this.cap);
|
||||
this.y = new Float32Array(this.cap);
|
||||
this.vx = new Float32Array(this.cap);
|
||||
this.vy = new Float32Array(this.cap);
|
||||
this.energy = new Float32Array(this.cap);
|
||||
this.age = new Float32Array(this.cap);
|
||||
this.cls = new Uint8Array(this.cap);
|
||||
this.wellOf = new Uint8Array(this.cap);
|
||||
this.pulses = [];
|
||||
this.count = 0;
|
||||
|
||||
this.newborn = new Float32Array(this.cap * 7);
|
||||
this.newbornCount = 0;
|
||||
|
||||
// Neighbour grid.
|
||||
this.gridN = 22;
|
||||
this.cellCount = new Int32Array(this.gridN * this.gridN + 1);
|
||||
this.cellStart = new Int32Array(this.gridN * this.gridN + 1);
|
||||
this.cellItems = new Int32Array(this.cap);
|
||||
|
||||
this.history = {
|
||||
alive: new Uint16Array(song.analysis.frames),
|
||||
movement: new Float32Array(song.analysis.frames),
|
||||
food: new Float32Array(song.analysis.frames),
|
||||
births: new Uint16Array(song.analysis.frames),
|
||||
deaths: new Uint16Array(song.analysis.frames),
|
||||
};
|
||||
|
||||
this.frame = -1;
|
||||
this.birthsThisFrame = 0;
|
||||
this.deathsThisFrame = 0;
|
||||
|
||||
this.seedInitial();
|
||||
}
|
||||
|
||||
/** The law for a frame. Section boundaries are the only place laws change. */
|
||||
lawForFrame(frame) {
|
||||
const laws = this.song.laws;
|
||||
const t = frame / this.fps;
|
||||
for (let i = laws.length - 1; i >= 0; i--) {
|
||||
if (t >= laws[i].start) return laws[i];
|
||||
}
|
||||
return laws[0];
|
||||
}
|
||||
|
||||
seedInitial() {
|
||||
const n = this.genome.seedAgents;
|
||||
const wells = this.wells;
|
||||
for (let i = 0; i < n && this.count + this.newbornCount < this.cap; i++) {
|
||||
// Most life starts at a well; a few are already in transit, so the
|
||||
// first frame is a world with roads, not a row of dots.
|
||||
let x = 0.5;
|
||||
let y = 0.5;
|
||||
if (i % 5 !== 4 && wells.length) {
|
||||
const w = wells[i % wells.length];
|
||||
x = w.x + this.rng.gauss() * w.r;
|
||||
y = w.y + this.rng.gauss() * w.r;
|
||||
} else {
|
||||
const r = Math.sqrt(this.rng.next()) * 0.44;
|
||||
const a = this.rng.next() * TAU;
|
||||
x = 0.5 + Math.cos(a) * r;
|
||||
y = 0.5 + Math.sin(a) * r;
|
||||
}
|
||||
this.queueAgent(
|
||||
Math.max(0.02, Math.min(0.98, x)),
|
||||
Math.max(0.02, Math.min(0.98, y)),
|
||||
this.pickClass(),
|
||||
0.52 + 0.24 * this.rng.next()
|
||||
);
|
||||
}
|
||||
this.flushNewborns();
|
||||
this.birthsThisFrame = 0;
|
||||
this.deathsThisFrame = 0;
|
||||
}
|
||||
|
||||
nearestWell(x, y) {
|
||||
let best = 0;
|
||||
let bd = Infinity;
|
||||
for (let i = 0; i < this.wells.length; i++) {
|
||||
const w = this.wells[i];
|
||||
const d = (w.x - x) ** 2 + (w.y - y) ** 2;
|
||||
if (d < bd) {
|
||||
bd = d;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
pickClass() {
|
||||
const w = this.genome.classes.map((c) => c.seed);
|
||||
let total = 0;
|
||||
for (const v of w) total += v;
|
||||
let r = this.rng.next() * total;
|
||||
for (let i = 0; i < w.length; i++) {
|
||||
r -= w[i];
|
||||
if (r <= 0) return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
queueAgent(x, y, cls, energy) {
|
||||
if (this.count + this.newbornCount >= this.cap) return false;
|
||||
const p = this.newbornCount * 7;
|
||||
this.newborn[p] = x;
|
||||
this.newborn[p + 1] = y;
|
||||
this.newborn[p + 2] = 0;
|
||||
this.newborn[p + 3] = 0;
|
||||
this.newborn[p + 4] = energy;
|
||||
this.newborn[p + 5] = 0;
|
||||
this.newborn[p + 6] = cls;
|
||||
this.newbornCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
flushNewborns() {
|
||||
for (let k = 0; k < this.newbornCount; k++) {
|
||||
if (this.count >= this.cap) break;
|
||||
const p = k * 7;
|
||||
const i = this.count++;
|
||||
this.x[i] = this.newborn[p];
|
||||
this.y[i] = this.newborn[p + 1];
|
||||
this.vx[i] = this.newborn[p + 2];
|
||||
this.vy[i] = this.newborn[p + 3];
|
||||
this.energy[i] = this.newborn[p + 4];
|
||||
this.age[i] = this.newborn[p + 5];
|
||||
this.cls[i] = this.newborn[p + 6];
|
||||
this.wellOf[i] = this.nearestWell(this.x[i], this.y[i]);
|
||||
}
|
||||
this.newbornCount = 0;
|
||||
}
|
||||
|
||||
// ---- field -------------------------------------------------------------
|
||||
|
||||
sample(fx, fy) {
|
||||
const F = this.field;
|
||||
const gx = Math.max(0, Math.min(F - 1.001, fx * (F - 1)));
|
||||
const gy = Math.max(0, Math.min(F - 1.001, fy * (F - 1)));
|
||||
const x0 = gx | 0;
|
||||
const y0 = gy | 0;
|
||||
const tx = gx - x0;
|
||||
const ty = gy - y0;
|
||||
const i = y0 * F + x0;
|
||||
const a = this.resource[i];
|
||||
const b = this.resource[i + 1];
|
||||
const c = this.resource[i + F];
|
||||
const d = this.resource[i + F + 1];
|
||||
return (a + (b - a) * tx) * (1 - ty) + (c + (d - c) * tx) * ty;
|
||||
}
|
||||
|
||||
/** Consume from the four cells under (fx, fy). Returns what was eaten. */
|
||||
eat(fx, fy, amount) {
|
||||
const F = this.field;
|
||||
const gx = Math.max(0, Math.min(F - 1.001, fx * (F - 1)));
|
||||
const gy = Math.max(0, Math.min(F - 1.001, fy * (F - 1)));
|
||||
const x0 = gx | 0;
|
||||
const y0 = gy | 0;
|
||||
const tx = gx - x0;
|
||||
const ty = gy - y0;
|
||||
const res = this.resource;
|
||||
const i0 = y0 * F + x0;
|
||||
const i1 = i0 + 1;
|
||||
const i2 = i0 + F;
|
||||
const i3 = i2 + 1;
|
||||
const w0 = (1 - tx) * (1 - ty);
|
||||
const w1 = tx * (1 - ty);
|
||||
const w2 = (1 - tx) * ty;
|
||||
const w3 = tx * ty;
|
||||
let eaten = 0;
|
||||
let take = Math.min(res[i0], amount * w0);
|
||||
res[i0] -= take; eaten += take;
|
||||
take = Math.min(res[i1], amount * w1);
|
||||
res[i1] -= take; eaten += take;
|
||||
take = Math.min(res[i2], amount * w2);
|
||||
res[i2] -= take; eaten += take;
|
||||
take = Math.min(res[i3], amount * w3);
|
||||
res[i3] -= take; eaten += take;
|
||||
return eaten;
|
||||
}
|
||||
|
||||
depositTrail(cls, x, y, amount) {
|
||||
const F = this.field;
|
||||
const ix = Math.max(0, Math.min(F - 1, (x * F) | 0));
|
||||
const iy = Math.max(0, Math.min(F - 1, (y * F) | 0));
|
||||
this.trail[cls][iy * F + ix] += amount;
|
||||
const ax = Math.max(0, Math.min(31, (x * 32) | 0));
|
||||
const ay = Math.max(0, Math.min(31, (y * 32) | 0));
|
||||
this.activity[ay * 32 + ax] += amount * 3;
|
||||
}
|
||||
|
||||
splat(x, y, spread, amount) {
|
||||
const F = this.field;
|
||||
const sigma = Math.max(0.8, spread * F);
|
||||
const r = Math.ceil(sigma * 2.4);
|
||||
const cx = Math.round(x * (F - 1));
|
||||
const cy = Math.round(y * (F - 1));
|
||||
const denom = 1 / (2 * sigma * sigma);
|
||||
let total = 0;
|
||||
const idx = [];
|
||||
const w = [];
|
||||
for (let dy = -r; dy <= r; dy++) {
|
||||
const py = cy + dy;
|
||||
if (py < 0 || py >= F) continue;
|
||||
for (let dx = -r; dx <= r; dx++) {
|
||||
const px = cx + dx;
|
||||
if (px < 0 || px >= F) continue;
|
||||
const k = Math.exp(-(dx * dx + dy * dy) * denom);
|
||||
if (k < 0.01) continue;
|
||||
idx.push(py * F + px);
|
||||
w.push(k);
|
||||
total += k;
|
||||
}
|
||||
}
|
||||
if (total <= 0) return;
|
||||
const k = amount / total;
|
||||
for (let i = 0; i < idx.length; i++) this.resource[idx[i]] += w[i] * k;
|
||||
}
|
||||
|
||||
// ---- neighbour grid ----------------------------------------------------
|
||||
|
||||
buildGrid() {
|
||||
const n = this.gridN;
|
||||
const counts = this.cellCount;
|
||||
counts.fill(0);
|
||||
for (let i = 0; i < this.count; i++) {
|
||||
const cx = Math.min(n - 1, Math.max(0, (this.x[i] * n) | 0));
|
||||
const cy = Math.min(n - 1, Math.max(0, (this.y[i] * n) | 0));
|
||||
counts[cy * n + cx + 1]++;
|
||||
}
|
||||
for (let i = 1; i <= n * n; i++) counts[i] += counts[i - 1];
|
||||
this.cellStart.set(counts);
|
||||
for (let i = 0; i < this.count; i++) {
|
||||
const cx = Math.min(n - 1, Math.max(0, (this.x[i] * n) | 0));
|
||||
const cy = Math.min(n - 1, Math.max(0, (this.y[i] * n) | 0));
|
||||
const c = cy * n + cx;
|
||||
this.cellItems[this.cellStart[c]++] = i;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- step --------------------------------------------------------------
|
||||
|
||||
step() {
|
||||
this.frame++;
|
||||
const f = this.frame;
|
||||
const law = this.lawForFrame(f);
|
||||
const cutoff = f - Math.ceil(this.fps * 1.6);
|
||||
while (this.pulses.length && this.pulses[0].frame < cutoff) this.pulses.shift();
|
||||
|
||||
this.birthsThisFrame = 0;
|
||||
this.deathsThisFrame = 0;
|
||||
|
||||
this.inject(f, law);
|
||||
this.decay(law);
|
||||
this.buildGrid();
|
||||
|
||||
this.update(law);
|
||||
this.compact();
|
||||
this.flushNewborns();
|
||||
this.record(law);
|
||||
return law;
|
||||
}
|
||||
|
||||
inject(frame, law) {
|
||||
const events = this.song.index[frame];
|
||||
if (!events) return;
|
||||
for (const e of events) {
|
||||
this.splat(e.x, e.y, e.spread, e.amount * law.spawn);
|
||||
const births = Math.round(e.spawn * law.spawn);
|
||||
for (let k = 0; k < births; k++) {
|
||||
if (this.queueAgent(
|
||||
e.x + this.rng.range(-0.02, 0.02),
|
||||
e.y + this.rng.range(-0.02, 0.02),
|
||||
e.voice,
|
||||
0.45 + 0.25 * e.strength
|
||||
)) this.birthsThisFrame++;
|
||||
}
|
||||
const ax = Math.max(0, Math.min(31, (e.x * 32) | 0));
|
||||
const ay = Math.max(0, Math.min(31, (e.y * 32) | 0));
|
||||
this.activity[ay * 32 + ax] += 0.5 + 2 * e.strength;
|
||||
// A visible consequence: the hit lands as a ring on the ground.
|
||||
if (e.strength > 0.42) {
|
||||
this.pulses.push({ x: e.x, y: e.y, frame, strength: e.strength, voice: e.voice });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
decay(law) {
|
||||
const dt = this.dt;
|
||||
const fieldK = Math.pow(0.5, dt / (this.genome.fieldHalfLife * (0.8 + 0.4 * law.spawn)));
|
||||
const trailK = Math.pow(0.5, dt / (this.genome.trailHalfLife * (0.75 + 0.35 * law.deposit)));
|
||||
const res = this.resource;
|
||||
for (let i = 0; i < res.length; i++) res[i] *= fieldK;
|
||||
for (let c = 0; c < 3; c++) {
|
||||
const t = this.trail[c];
|
||||
for (let i = 0; i < t.length; i++) t[i] *= trailK;
|
||||
}
|
||||
const act = this.activity;
|
||||
const actK = Math.pow(0.5, dt / 1.4);
|
||||
for (let i = 0; i < act.length; i++) act[i] *= actK;
|
||||
|
||||
// The wells recharge. Slowly, and to a ceiling: a well is a place with
|
||||
// a carrying capacity, not a faucet. The section's energy sets the rate,
|
||||
// so a breakdown is still a leaner world.
|
||||
const scale = (0.45 + 1.25 * law.energy) * dt;
|
||||
for (const well of this.wells) {
|
||||
const add = well.rate * scale;
|
||||
const idx = well.idx;
|
||||
const w = well.w;
|
||||
for (let i = 0; i < idx.length; i++) {
|
||||
const cell = idx[i];
|
||||
res[cell] = Math.min(1.6, res[cell] + add * w[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update(law) {
|
||||
const dt = this.dt;
|
||||
const n = this.gridN;
|
||||
const sense = 0.13;
|
||||
const sense2 = sense * sense;
|
||||
const maxNeighbours = 24;
|
||||
const g = this.genome;
|
||||
const wells = this.wells;
|
||||
|
||||
for (let i = 0; i < this.count; i++) {
|
||||
const cls = this.cls[i];
|
||||
const c = g.classes[cls];
|
||||
const x = this.x[i];
|
||||
const y = this.y[i];
|
||||
let vx = this.vx[i];
|
||||
let vy = this.vy[i];
|
||||
|
||||
let fx = 0;
|
||||
let fy = 0;
|
||||
let homeDx = 0;
|
||||
let homeDy = 0;
|
||||
|
||||
// Food gradient. A hungry agent commits harder to what it smells.
|
||||
const e = this.energy[i];
|
||||
const hunger = 0.55 + 0.85 * (1 - clamp01(e));
|
||||
const h = H / this.field;
|
||||
const gx = (this.sample(x + h, y) - this.sample(x - h, y)) / (2 * h);
|
||||
const gy = (this.sample(x, y + h) - this.sample(x, y - h)) / (2 * h);
|
||||
const grad = Math.hypot(gx, gy);
|
||||
if (grad > 1e-6) {
|
||||
fx += (gx / grad) * c.seek * hunger;
|
||||
fy += (gy / grad) * c.seek * hunger;
|
||||
}
|
||||
|
||||
// Place. Each caste has a different relation to the wells, and the
|
||||
// song sets how strongly it holds: builders orbit and paint,
|
||||
// foragers commute and draw roads, sparks orbit wide and scatter.
|
||||
if (wells.length) {
|
||||
const home = wells[this.wellOf[i]];
|
||||
const hx = home.x - x;
|
||||
const hy = home.y - y;
|
||||
const hd = Math.hypot(hx, hy) + 1e-6;
|
||||
homeDx = hx / hd;
|
||||
homeDy = hy / hd;
|
||||
if (c.role === 'commute') {
|
||||
const next = (this.wellOf[i] + 1) % wells.length;
|
||||
const w = wells[next];
|
||||
const wx = w.x - x;
|
||||
const wy = w.y - y;
|
||||
const wd = Math.hypot(wx, wy) + 1e-6;
|
||||
if (wd < 0.05) {
|
||||
this.wellOf[i] = next;
|
||||
} else {
|
||||
const pull = 1.0 + 1.7 * g.separation;
|
||||
fx += (wx / wd) * pull;
|
||||
fy += (wy / wd) * pull;
|
||||
}
|
||||
} else {
|
||||
const err = (hd - c.orbit) / c.orbit;
|
||||
const k = 1.9 * g.cohesion;
|
||||
fx += (hx / hd) * Math.max(-1.4, Math.min(1.4, err)) * k;
|
||||
fy += (hy / hd) * Math.max(-1.4, Math.min(1.4, err)) * k;
|
||||
const dir = cls === 2 ? -1 : 1;
|
||||
fx += (-hy / hd) * dir * c.spin;
|
||||
fy += (hx / hd) * dir * c.spin;
|
||||
}
|
||||
}
|
||||
|
||||
// Society.
|
||||
let alignX = 0;
|
||||
let alignY = 0;
|
||||
let cohX = 0;
|
||||
let cohY = 0;
|
||||
let sepX = 0;
|
||||
let sepY = 0;
|
||||
let neighbours = 0;
|
||||
const cx = Math.min(n - 1, Math.max(0, (x * n) | 0));
|
||||
const cy = Math.min(n - 1, Math.max(0, (y * n) | 0));
|
||||
for (let oy = -1; oy <= 1 && neighbours < maxNeighbours; oy++) {
|
||||
const gy2 = cy + oy;
|
||||
if (gy2 < 0 || gy2 >= n) continue;
|
||||
for (let ox = -1; ox <= 1 && neighbours < maxNeighbours; ox++) {
|
||||
const gx2 = cx + ox;
|
||||
if (gx2 < 0 || gx2 >= n) continue;
|
||||
const cell = gy2 * n + gx2;
|
||||
const a = this.cellStart[cell];
|
||||
const b = this.cellStart[cell + 1];
|
||||
for (let p = a; p < b; p++) {
|
||||
const j = this.cellItems[p];
|
||||
if (j === i) continue;
|
||||
const dx = this.x[j] - x;
|
||||
const dy = this.y[j] - y;
|
||||
const d2 = dx * dx + dy * dy;
|
||||
if (d2 > sense2 || d2 < 1e-12) continue;
|
||||
const d = Math.sqrt(d2);
|
||||
alignX += this.vx[j];
|
||||
alignY += this.vy[j];
|
||||
cohX += dx;
|
||||
cohY += dy;
|
||||
const push = (1 - d / sense) / d;
|
||||
sepX -= dx * push;
|
||||
sepY -= dy * push;
|
||||
const k = g.interaction[cls][this.cls[j]];
|
||||
fx += (dx / d) * k;
|
||||
fy += (dy / d) * k;
|
||||
if (++neighbours >= maxNeighbours) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (neighbours > 0) {
|
||||
const inv = 1 / neighbours;
|
||||
fx += cohX * inv * g.cohesion * 5.0;
|
||||
fy += cohY * inv * g.cohesion * 5.0;
|
||||
fx += (alignX * inv - vx) * g.alignment * 3.2;
|
||||
fy += (alignY * inv - vy) * g.alignment * 3.2;
|
||||
fx += sepX * g.separation * 1.05;
|
||||
fy += sepY * g.separation * 1.05;
|
||||
}
|
||||
// Crowding keeps bodies apart: a colony must never collapse to a
|
||||
// single blob, or the species reads as paint instead of life. The
|
||||
// push is outward, away from the well where the crowd is densest,
|
||||
// and scales with how far the crowd has overshot.
|
||||
if (neighbours > 9) {
|
||||
const c = Math.min(2.6, (neighbours - 9) * 0.30);
|
||||
fx -= homeDx * c;
|
||||
fy -= homeDy * c;
|
||||
}
|
||||
|
||||
// Jitter: what a noisy track does to a well-meaning rule.
|
||||
fx += this.rng.signed() * g.jitter * 0.26;
|
||||
fy += this.rng.signed() * g.jitter * 0.26;
|
||||
|
||||
// Soft walls: the dish has an edge, but it does not trap.
|
||||
const m = 0.035;
|
||||
if (x < m) fx += (m - x) * 14;
|
||||
if (x > 1 - m) fx -= (x - (1 - m)) * 14;
|
||||
if (y < m) fy += (m - y) * 14;
|
||||
if (y > 1 - m) fy -= (y - (1 - m)) * 14;
|
||||
|
||||
const speed = c.speed * law.speed;
|
||||
const mag = Math.hypot(fx, fy);
|
||||
if (mag > 1e-9) {
|
||||
const turn = Math.min(1, c.turn * law.turn * dt);
|
||||
vx += ((fx / mag) * speed - vx) * turn;
|
||||
vy += ((fy / mag) * speed - vy) * turn;
|
||||
} else {
|
||||
vx *= 0.94;
|
||||
vy *= 0.94;
|
||||
}
|
||||
|
||||
let nx = x + vx * dt;
|
||||
let ny = y + vy * dt;
|
||||
if (nx < 0.005) { nx = 0.005; vx = Math.abs(vx) * 0.6; }
|
||||
if (nx > 0.995) { nx = 0.995; vx = -Math.abs(vx) * 0.6; }
|
||||
if (ny < 0.005) { ny = 0.005; vy = Math.abs(vy) * 0.6; }
|
||||
if (ny > 0.995) { ny = 0.995; vy = -Math.abs(vy) * 0.6; }
|
||||
|
||||
// Metabolism.
|
||||
const eaten = this.eat(nx, ny, 0.62 * dt);
|
||||
let energy = e + eaten * 2.1;
|
||||
const work = 0.35 + 0.65 * Math.min(1, Math.hypot(vx, vy) / Math.max(1e-6, speed));
|
||||
energy -= c.drain * dt * work;
|
||||
energy = Math.min(1.7, energy);
|
||||
|
||||
this.x[i] = nx;
|
||||
this.y[i] = ny;
|
||||
this.vx[i] = vx;
|
||||
this.vy[i] = vy;
|
||||
this.energy[i] = energy;
|
||||
this.age[i] += dt;
|
||||
|
||||
// History is written into the ground, not into a list of events.
|
||||
// Ink, not fog. A body lays a line roughly its own width; if the
|
||||
// deposit rate is high enough to fill the dish, the history is lost.
|
||||
// The floor keeps a fast swarm from reading as invisible: every
|
||||
// body leaves SOMETHING, however thin.
|
||||
const deposit = (0.30 + 0.70 * c.deposit) * law.deposit * dt * 0.032;
|
||||
this.depositTrail(cls, nx, ny, deposit);
|
||||
|
||||
if (energy >= c.split * law.split && neighbours < 16) {
|
||||
this.energy[i] = energy * 0.52;
|
||||
if (this.queueAgent(
|
||||
nx + this.rng.range(-0.012, 0.012),
|
||||
ny + this.rng.range(-0.012, 0.012),
|
||||
cls,
|
||||
energy * 0.42
|
||||
)) this.birthsThisFrame++;
|
||||
} else if (energy <= 0) {
|
||||
this.energy[i] = 0;
|
||||
this.deathsThisFrame++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compact() {
|
||||
let w = 0;
|
||||
for (let i = 0; i < this.count; i++) {
|
||||
if (this.energy[i] <= 0) continue;
|
||||
if (w !== i) {
|
||||
this.x[w] = this.x[i];
|
||||
this.y[w] = this.y[i];
|
||||
this.vx[w] = this.vx[i];
|
||||
this.vy[w] = this.vy[i];
|
||||
this.energy[w] = this.energy[i];
|
||||
this.age[w] = this.age[i];
|
||||
this.cls[w] = this.cls[i];
|
||||
}
|
||||
w++;
|
||||
}
|
||||
this.count = w;
|
||||
}
|
||||
|
||||
record(law) {
|
||||
const h = this.history;
|
||||
const f = this.frame;
|
||||
if (f < 0 || f >= h.alive.length) return;
|
||||
h.alive[f] = this.count;
|
||||
let move = 0;
|
||||
for (let i = 0; i < this.count; i++) move += Math.hypot(this.vx[i], this.vy[i]);
|
||||
h.movement[f] = move;
|
||||
let food = 0;
|
||||
const res = this.resource;
|
||||
for (let i = 0; i < res.length; i += 3) food += res[i];
|
||||
h.food[f] = (food * 3) / res.length;
|
||||
h.births[f] = this.birthsThisFrame;
|
||||
h.deaths[f] = this.deathsThisFrame;
|
||||
}
|
||||
|
||||
/** The camera's view of where something just happened. */
|
||||
activityCell(x, y) {
|
||||
const ax = Math.max(0, Math.min(31, (x * 32) | 0));
|
||||
const ay = Math.max(0, Math.min(31, (y * 32) | 0));
|
||||
return this.activity[ay * 32 + ax];
|
||||
}
|
||||
|
||||
stats() {
|
||||
const h = this.history;
|
||||
let peak = 0;
|
||||
for (let i = 0; i < h.alive.length; i++) peak = Math.max(peak, h.alive[i]);
|
||||
return {
|
||||
alive: this.count,
|
||||
peak,
|
||||
fieldMass: this.sampleFieldMass(),
|
||||
trailMass: this.sampleTrailMass(),
|
||||
name: this.genome.name,
|
||||
};
|
||||
}
|
||||
|
||||
sampleFieldMass() {
|
||||
let s = 0;
|
||||
for (let i = 0; i < this.resource.length; i++) s += this.resource[i];
|
||||
return s / this.resource.length;
|
||||
}
|
||||
|
||||
sampleTrailMass() {
|
||||
let s = 0;
|
||||
for (let c = 0; c < 3; c++) {
|
||||
const t = this.trail[c];
|
||||
for (let i = 0; i < t.length; i++) s += t[i];
|
||||
}
|
||||
return s / (this.trail[0].length * 3);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user