Metabolism M2.9: flora islands, gardens, walls, farmer strain, aimed work (VM AIM actions), food-limited ecology rebalance

This commit is contained in:
Dejvino
2026-10-28 12:49:17 +01:00
parent 1d497e556e
commit 476fa1833a
11 changed files with 675 additions and 120 deletions
+19
View File
@@ -130,6 +130,23 @@ locked with the kick pairs diggers with haulers — and it feeds the
other, truce and trade versus skirmish and siege. Society is a property of
the mix, at both scales.
### Flora, gardens, walls — the workable world
Three made things with feedback loops, each demanding its own instructions:
- **flora** — wild food in ISLANDS: a dozen scattered patches that are food,
regrow on their own, and bloom when the world starves (the pioneers seed it
back), receding under the grazing of a well-fed crowd. Never enough to
sustain a large world; always somewhere worth walking to.
- **gardens** — stone tilled by masons into cultivated beds that yield food
for a while, then set back into rock. Tended beds (the farmers' work)
yield up to threefold. Agriculture slowly consumes the world's stone.
- **walls** — quarried burden built into barriers: they defend the
settlement, enemies strike and sap them, and unmaintained walls weather
away. Nothing on this map is finished.
All work is AIMED — the VM's work actions land where the agent faces, not
beneath it — and the work-senses look the same way.
### Rock — the inert resource
The map also carries **rock**: height per cell, impassable above a threshold.
The world is born with a deterministic scatter of it; the song is its weather
@@ -308,6 +325,8 @@ world behind the `Run` is being rebuilt.*
- **M2.8 — rock** ✔ built: the inert resource — scattered at birth, dropped
and shattered by the song, impassable, quarried by builders into carried
burden and soil, walled where the colony works. See EVOLUTION.md.
- **M2.9 — flora, gardens, walls, farmers, aimed work** ✔ built: the
interactable layer — see EVOLUTION.md.
- **M3 — lens** the camera travels (track / reveal / establish) and reserves
cuts for musical punctuation.
- **M4 — film** mux frames + PCM to video.
+42
View File
@@ -207,6 +207,48 @@ for 10 s straight; quarries opening with soil and burden appearing; carried
rock rising as walls. The strips read it directly: grey masses the colonies
grow around, fronts grinding along them.
### M2.9 — flora, gardens, walls: a world of workable things ✔
After M2.8 the films had stone but nothing to DO with it, and the ecology
still lived on wells alone. This iteration added the interactable layer —
items with feedback loops, each needing its own LittleVM instructions, hence
real specialisation:
- **aimed work** — the VM gained an AIM (two pseudo-actions setting a heading;
it defaults to the direction of travel), and every work action lands at the
block the agent is FACING, never beneath its feet: masons carve the face of
the wall ahead, farmers tend the bed they walk toward, strikers hit where
the enemy stands. The work-senses (rock, garden) look where the last aim
pointed — sight follows intent.
- **gardens** — the mason TILLS fresh stone into a cultivated bed (brought to
maturity one cell at a time; a dusting of fertility would exhaust as fast
as it is laid). A bed yields food slowly for a while, then the spent soil
sets back into rock: agriculture slowly eats the world's stone, and the
song's rockfalls replenish it. Digging itself yields NO food — stone is for
building; food comes from wells, flora, events and gardens.
- **walls** — the carried burden rises as a wall (pooled per block, mortared
along a rounded footprint). Walls block movement, so they defend; enemies
strike at their aim target and SAP the stone at the front — sieges happen,
and unmaintained walls weather away. The masons are never done.
- **farmers** — a new strain role: it walks the field rows and TENDS the beds
(a tended bed yields up to threefold), opens new beds into fresh stone, and
commutes along the garden gradient. Work is foraging: the hungry branches
of mason and husbandman work the ground they aim at, because that is where
their food comes from.
- **flora** — the world's pioneers, living in ISLANDS scattered at birth
(a dozen small patches; they can never spread beyond their own seed
ground). Each stand is food — grazed down by hungry crowds, never wiped out
(seed-rain), regrowing slowly. Its vigour tracks the world's famine: a
starving or dwindling world blooms with flora and is seeded back to life;
a strong, well-fed world grazes it back to a whisper. Rendered as speckles
— a few apple trees and bushes here and there, never a garden of Eden.
*Balance notes carved into the tests:* the ecology is food-limited (the gate
is nearly shut on bare ground; colonies that outgrow their ground starve back
and wander); rock has a budget that survives a whole film (calm songs keep
sparse stone, percussive ones accumulate walls); agriculture consumes stone
instead of minting it. 44 tests green; realtime holds at 640×360.
### M3 — the lens: travel, don't cut
Replace the cut-to-a-point grammar with a **move grammar**: *track* (follow a
moving front), *reveal* (pan along a coastline), *establish* (hold wide and
+10
View File
@@ -228,6 +228,16 @@ export function composeTrail(world, palette, exposure = 2.2) {
sharp[i * 3 + 2] += r * stone[2] * 2.6;
}
}
if (world.garden) {
const soil = palette.garden ?? [0.16, 0.2, 0.1];
for (let i = 0; i < n; i++) {
const g = world.garden[i];
if (g < 1e-3) continue;
sharp[i * 3] += g * soil[0] * 2.2;
sharp[i * 3 + 1] += g * soil[1] * 2.2;
sharp[i * 3 + 2] += g * soil[2] * 2.2;
}
}
const wide = boxBlur(sharp, F, 2);
const tight = boxBlur(sharp, F, 1);
const out = new Uint8ClampedArray(n * 4);
+62 -2
View File
@@ -15,8 +15,8 @@ import { Bitmap } from './bitmap.js';
const PER_SPECKS = 2.2;
const MAX_SPECKS = 60000;
// Density glow: how fast a cell's population turns into brightness.
const DENSITY_K = 0.11;
const DENSITY_EXPOSURE = 1.25;
const DENSITY_K = 0.07;
const DENSITY_EXPOSURE = 1.0;
export class Renderer {
constructor(width, height, { exposure = 2.6, grain = 2.4, vignette = 0.55 } = {}) {
@@ -85,6 +85,27 @@ export class Renderer {
sharp[i * 3 + 1] += r * stone[1] * 2.6;
sharp[i * 3 + 2] += r * stone[2] * 2.6;
}
// The flora: a whisper of wild green under everything.
if (world.flora) {
const veg = palette.flora ?? [0.2, 0.45, 0.2];
for (let i = 0; i < n; i++) {
const fl = world.flora[i];
if (fl < 1e-2) continue;
sharp[i * 3] += fl * veg[0] * 0.15;
sharp[i * 3 + 1] += fl * veg[1] * 0.15;
sharp[i * 3 + 2] += fl * veg[2] * 0.15;
}
}
// The gardens: worked ground, dark and rich — the body's fields.
const beds = world.garden;
const soil = palette.garden ?? [0.16, 0.2, 0.1];
for (let i = 0; i < n; i++) {
const g = beds[i];
if (g < 1e-3) continue;
sharp[i * 3] += g * soil[0] * 2.2;
sharp[i * 3 + 1] += g * soil[1] * 2.2;
sharp[i * 3 + 2] += g * soil[2] * 2.2;
}
boxBlur(sharp, this.tmp, this.tight, F, 1);
boxBlur(sharp, this.tmp, this.wide, F, 3);
const out = this.trailRGBA;
@@ -224,6 +245,45 @@ export class Renderer {
}
}
}
// The flora: sparse plant-specks — the map reads as inhabited by
// vegetation, not only by bodies.
if (world.flora) {
const veg = palette.flora ?? [0.2, 0.45, 0.2];
const vr = veg[0] * 255 * 0.75;
const vg = veg[1] * 255 * 0.75;
const vb = veg[2] * 255 * 0.75;
const cx = cam.cx;
const cy = cam.cy;
for (let y = 0; y < F && budget > 0; y++) {
const wy = (y + 0.5) / F;
const sy = (wy - cy) * scale + halfH;
if (sy < -2 || sy > this.h + 2) continue;
for (let x = 0; x < F && budget > 0; x++) {
const i = y * F + x;
const fl = world.flora[i];
if (fl < 0.1) continue;
const wx = (x + 0.5) / F;
const sx = (wx - cx) * scale + halfW;
if (sx < -2 || sx > this.w + 2) continue;
const count = Math.min(budget, Math.min(3, 1 + Math.round(fl * 4)));
const a = 0.20 + 0.38 * Math.min(1, fl / 0.22);
for (let k = 0; k < count; k++) {
const seed = (i * 2654435761 + k * 2246822519) >>> 0;
const s2 = (Math.imul(seed ^ (seed >>> 13), 1274126177) ^ 2246822519) >>> 0;
const px = sx + ((s2 >>> 24) / 255 - 0.5) * 2.4;
const py = sy + (((s2 >>> 16) & 255) / 255 - 0.5) * 2.4;
const ix = px | 0;
const iy = py | 0;
if (ix < 0 || iy < 0 || ix >= this.w || iy >= this.h) continue;
const o = (iy * this.w + ix) * 4;
d[o] = d[o] + vr * a >= 255 ? 255 : d[o] + vr * a;
d[o + 1] = d[o + 1] + vg * a >= 255 ? 255 : d[o + 1] + vg * a;
d[o + 2] = d[o + 2] + vb * a >= 255 ? 255 : d[o + 2] + vb * a;
}
budget -= count;
}
}
}
return budget;
}
+368 -76
View File
@@ -32,8 +32,9 @@ import { Rng } from '../rng.js';
import { wellPos, wellEnv, buildKernel } from './wells.js';
import {
runProgram, SENSE_COUNT, GRAD_COUNT, ACTION_COUNT, REGS,
S_FOOD, S_SELF, S_KIN, S_FOE, S_TRAIL, S_LAW, S_HUNGER, S_EDGE, S_TIME, S_TOTAL, S_WELL, S_PEACE, S_ROCK,
G_FOOD, G_SELF, G_KIN, G_FOE, G_WELL,
S_FOOD, S_SELF, S_KIN, S_FOE, S_TRAIL, S_LAW, S_HUNGER, S_EDGE, S_TIME, S_TOTAL, S_WELL, S_PEACE, S_ROCK, S_GARDEN,
G_GARDEN,
G_FOOD, G_SELF, G_KIN, G_FOE, G_WELL, G_ROCK,
} from '../song/strains.js';
const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v);
@@ -51,6 +52,38 @@ const DEPOSIT_SCALE = 0.012;
// bodies are founded once and persist; war kills, it does not convert.
const COLONIZE_EVERY = 60; // ~2 s: a blessed frontier settlement may found
/** A normalised Gaussian footprint (cell indices + weights) per coarse block. */
function buildBlockKernels(F, FC) {
const kernels = [];
const bs = F / FC;
for (let b = 0; b < FC * FC; b++) {
const cx = ((b % FC) + 0.5) * bs - 0.5;
const cy = (((b / FC) | 0) + 0.5) * bs - 0.5;
const sig = Math.max(1.4, bs * 0.9);
const r = Math.ceil(sig * 2.2);
const denom = 1 / (2 * sig * sig);
const idx = [];
const w = [];
let total = 0;
for (let dy = -r; dy <= r; dy++) {
const py = Math.round(cy) + dy;
if (py < 0 || py >= F) continue;
for (let dx = -r; dx <= r; dx++) {
const px = Math.round(cx) + dx;
if (px < 0 || px >= F) continue;
const g = Math.exp(-(dx * dx + dy * dy) * denom);
if (g < 0.02) continue;
idx.push(py * F + px);
w.push(g);
total += g;
}
}
const k = total > 0 ? 1 / total : 0;
kernels.push({ idx: Int32Array.from(idx), w: Float32Array.from(w, (v) => v * k) });
}
return kernels;
}
export class World {
constructor(song, { fps = 30, field = FIELD, seed = song.seed, substeps = SUBSTEPS, genesis } = {}) {
this.song = song;
@@ -118,6 +151,24 @@ export class World {
// carry and wall with. rockMask marks the impassable cells.
this.rock = new Float32Array(n);
this.rockMask = new Uint8Array(n);
this.rockFallen = 0; // total stone the song has dropped
// Flora (M2.9): the wild plants — the world's pioneers, living in
// ISLANDS scattered at birth. They ARE food, they regrow on their
// own, and they BLOOM when the world is starving or dwindling
// (seeding it back to life), then recede once the population is
// strong and feeding itself again. Never enough to sustain a
// large world, and never anywhere but on their own seed ground.
this.flora = new Float32Array(n);
this.floraSeed = new Uint8Array(n); // the islands: where flora may live
this.famine = 0; // smoothed mean hunger of the population
this.peakPop = 0; // recent population high-water mark
this.hungerMass = 0; // per-frame accumulator (the reaction fills it)
// Cultivated ground: worked stone that slowly yields food, then
// petrifies back to rock. garden[i] is the fertility left.
this.garden = new Float32Array(n);
this.gardenSrc = new Float32Array(n);
// Recent tending, per cell: a visited garden yields far more.
this.tend = new Float32Array(n);
// Deterministic jitter: static signed noise fields from the seeded RNG,
// read every substep. The track's flatness sets how loud it is.
@@ -155,6 +206,7 @@ export class World {
this.blkBody = Array.from({ length: this.B }, () => new Float32Array(fc));
this.blkTotal = new Float32Array(fc);
this.blkRock = new Float32Array(fc);
this.blkGarden = new Float32Array(fc);
this.domB = new Int8Array(fc).fill(-1);
this.subB = new Int8Array(fc).fill(-1);
this.fxX = Array.from({ length: this.m }, () => new Float32Array(fc));
@@ -162,12 +214,18 @@ export class World {
this.velX = new Float32Array(fc);
this.velY = new Float32Array(fc);
this.wellField = new Float32Array(fc);
this.rockField = new Float32Array(fc); // smoothed stone: the far smell
this.ancX = Array.from({ length: this.m }, () => new Float32Array(fc));
this.ancY = Array.from({ length: this.m }, () => new Float32Array(fc));
this.synergy = new Float32Array(this.m * fc);
this.valScratch = new Float64Array(this.m);
this.growScratch = new Float64Array(this.m);
this.hostility = new Float32Array(fc);
this.hitAt = new Float32Array(fc);
// The last aim per (strain, block): the senses of WORK look where
// the work points, one substep back — sight follows intent.
this.aimX = new Int8Array(this.m * fc);
this.aimY = new Int8Array(this.m * fc);
this.cUp = new Int32Array(fc);
this.cDown = new Int32Array(fc);
this.cLeft = new Int32Array(fc);
@@ -191,7 +249,14 @@ export class World {
}
}
this.carry = new Float32Array(this.m * fc); // quarried burden per block
// Quarried burden per strain, a fine field advected with the
// population flux — stone is genuinely HAULED: dug at the quarry,
// carried with the crowd, walled at home.
this.carry = Array.from({ length: this.m }, () => new Float32Array(n));
this.carryNext = Array.from({ length: this.m }, () => new Float32Array(n));
// Rounded build footprints: a Gaussian kernel per coarse block, so
// walls rise organically instead of as square tiles.
this.buildKernel = buildBlockKernels(field, Math.max(16, Math.round(field / 2)));
// Strain programs run on this decision grid: registers are the
// strains' persistent memory, the action slots carry each decision.
@@ -240,6 +305,7 @@ export class World {
this.deathsThisFrame = 0;
this.seedRocks();
this.seedFlora();
this.refreshRockMask();
this.foundOpeningBodies();
this.seedInitial();
@@ -398,6 +464,7 @@ export class World {
this.warmth = Math.max(-1, Math.min(1,
this.program.diplomacy.base * 0.6 + 0.35 * (regime.operators.peace - 1)));
const living = this.framePrep(regime);
this.updateRockField();
this.inject(this.frame, regime);
this.refreshRockMask();
for (let s = 0; s < this.substeps; s++) this.substep(regime);
@@ -483,8 +550,8 @@ export class World {
for (let k = 0; k < n; k++) {
const x = this.rng.next();
const y = this.rng.next();
const sigma = 0.022 + 0.030 * this.rng.next();
const amount = 3.5 + 6.5 * this.rng.next();
const sigma = 0.030 + 0.045 * this.rng.next();
const amount = 5.0 + 9.0 * this.rng.next();
this.rockSplat(x, y, sigma, amount);
}
}
@@ -538,11 +605,40 @@ export class World {
return { idx, w, k };
}
/** Scatter the wild flora: small patches, deterministic from the seed. */
seedFlora() {
const n = 9 + Math.round(6 * clamp01(this.song.summary.onsetDensity / 6));
for (let k = 0; k < n; k++) {
const x = this.rng.next();
const y = this.rng.next();
const sigma = 0.010 + 0.011 * this.rng.next();
const F = this.field;
const cx = Math.round(x * (F - 1));
const cy = Math.round(y * (F - 1));
const sig = Math.max(1.2, sigma * F);
const r = Math.ceil(sig * 2.2);
const denom = 1 / (2 * sig * sig);
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 i = py * F + px;
if (this.rockMask[i]) continue;
const g = Math.exp(-(dx * dx + dy * dy) * denom);
this.floraSeed[i] = 1;
this.flora[i] = Math.min(0.75, this.flora[i] + g * (0.35 + 0.3 * this.rng.next()));
}
}
}
}
/** The impassable frontier, recomputed after any rock change. */
refreshRockMask() {
const rock = this.rock;
const mask = this.rockMask;
for (let i = 0; i < rock.length; i++) mask[i] = rock[i] > 0.55 ? 1 : 0;
for (let i = 0; i < rock.length; i++) mask[i] = rock[i] > 0.25 ? 1 : 0;
}
// ---- wells ---------------------------------------------------------------
@@ -623,8 +719,10 @@ export class World {
this.birthsThisFrame = 0;
this.deathsThisFrame = 0;
this.moveAccum = 0;
this.hungerMass = 0;
const living = this.framePrep(regime);
this.updateRockField();
this.inject(f, regime);
this.refreshRockMask();
@@ -660,6 +758,28 @@ export class World {
}
}
/**
* The far smell of stone: the rock field, box-blurred twice on the
* coarse grid. Adjacent-block gradients only pull at the rim; this is
* what draws a quarry crew across the map.
*/
updateRockField() {
const FC = this.FC;
const fc = FC * FC;
const f = this.rockField;
f.fill(0);
const rock = this.rock;
const coarseOf = this.coarseOf;
for (let i = 0; i < this.n; i++) f[coarseOf[i]] += rock[i];
for (let pass = 0; pass < 2; pass++) {
const prev = Float32Array.from(f);
for (let b = 0; b < fc; b++) {
f[b] = (prev[b] * 4 + prev[this.cUp[b]] + prev[this.cDown[b]] +
prev[this.cLeft[b]] + prev[this.cRight[b]]) / 8;
}
}
}
/** Home anchors: each strain is pulled to its nearest living well. */
updateAnchors(living) {
const FC = this.FC;
@@ -730,7 +850,8 @@ export class World {
if (e.voice === 2) {
this.rockSplat(e.x, e.y, e.spread * 1.4, -e.strength * 2.4 * rockPol.shatter * ops.erode);
} else {
this.rockSplat(e.x, e.y, e.spread * 1.2, e.strength * 3.2 * ops.rockfall * rockPol.drop);
this.rockFallen += e.strength * 2.8 * ops.rockfall * rockPol.drop;
this.rockSplat(e.x, e.y, e.spread * 1.3, e.strength * 2.8 * ops.rockfall * rockPol.drop);
}
this.activity[ay * 32 + ax] += 1.5;
}
@@ -751,6 +872,7 @@ export class World {
runStrains(regime) {
const FC = this.FC;
const blkRock = this.blkRock;
const blkGarden = this.blkGarden;
const fc = FC * FC;
const m = this.m;
const blkPop = this.blkPop;
@@ -820,7 +942,17 @@ export class World {
ss[S_TOTAL] = clamp01(tot * 0.005);
ss[S_WELL] = clamp01(wellField[b]);
ss[S_PEACE] = peace;
ss[S_ROCK] = clamp01(blkRock[b] * 0.35);
// The work-senses look where the work points: the block the
// strain aimed at last substep (its own feet when it stood).
const aimIdx = s * fc + b;
const axn = this.aimX[aimIdx];
const ayn = this.aimY[aimIdx];
const wb = (axn || ayn)
? Math.min(FC - 1, Math.max(0, (b % FC) + axn)) +
FC * Math.min(FC - 1, Math.max(0, ((b / FC) | 0) + ayn))
: b;
ss[S_ROCK] = clamp01(blkRock[wb] * 0.45);
ss[S_GARDEN] = clamp01(blkGarden[wb] * 0.5);
// ---- gradients (sign per field × axis) ---------------------
gg[G_FOOD * 2] = Math.sign(gRx);
@@ -847,8 +979,17 @@ export class World {
}
gg[G_WELL * 2] = Math.sign(wellField[rg] - wellField[lg]);
gg[G_WELL * 2 + 1] = Math.sign(wellField[dg] - wellField[ug]);
gg[G_ROCK * 2] = Math.sign(this.rockField[rg] - this.rockField[lg]);
gg[G_ROCK * 2 + 1] = Math.sign(this.rockField[dg] - this.rockField[ug]);
gg[G_GARDEN * 2] = Math.sign(blkGarden[rg] - blkGarden[lg]);
gg[G_GARDEN * 2 + 1] = Math.sign(blkGarden[dg] - blkGarden[ug]);
runProgram(this.stMem, s * REGS * FC + b, ss, gg, act, strains[s].code);
// Remember the aim: the next substep's work-senses look there.
let nx = act[12] !== 0 ? Math.sign(act[12]) : Math.sign(act[0]);
let ny = act[13] !== 0 ? Math.sign(act[13]) : Math.sign(act[1]);
this.aimX[aimIdx] = nx;
this.aimY[aimIdx] = ny;
// ---- actions → forces + reaction mods ----------------------
fxX[s][b] = act[0] + this.ancX[s][b] * 0.12 + gRx * 0.02 - dgx * pressure;
@@ -860,6 +1001,10 @@ export class World {
actBuf[off + 5 * actFC] = act[5]; // SPAWN
actBuf[off + 6 * actFC] = act[6]; // DIG
actBuf[off + 7 * actFC] = act[7]; // BUILD
actBuf[off + 8 * actFC] = act[8]; // TILL
actBuf[off + 9 * actFC] = act[9]; // TEND
actBuf[off + 12 * actFC] = act[12]; // AIM_X
actBuf[off + 13 * actFC] = act[13]; // AIM_Y
// Synergy: this strain breeds fastest in a mixed, same-body
// crowd — the share of the block's population that is kin.
@@ -920,16 +1065,19 @@ export class World {
const blkBody = this.blkBody;
const blkTotal = this.blkTotal;
const blkRock = this.blkRock;
const blkGarden = this.blkGarden;
blkR.fill(0);
blkTotal.fill(0);
blkRock.fill(0);
blkGarden.fill(0);
for (let s = 0; s < m; s++) blkPop[s].fill(0);
for (let b = 0; b < this.B; b++) { blkTrail[b].fill(0); blkBody[b].fill(0); }
for (let i = 0; i < n; i++) {
const b = coarseOf[i];
const d = density[i];
blkR[b] += res[i];
blkR[b] += res[i] + this.flora[i] * 0.5;
blkRock[b] += rock[i];
blkGarden[b] += this.garden[i];
if (d > 1e-6) {
blkTotal[b] += d;
for (let s = 0; s < m; s++) blkPop[s][b] += pop[s][i];
@@ -962,22 +1110,43 @@ export class World {
// ---- 2. the strain programs decide the forces --------------------------
this.runStrains(regime);
// The per-block war pressure: hits × hostile presence. A body strikes
// its neighbour only where the two actually touch.
// The per-block war pressure: hits land at their AIM target — where
// the striker is heading, i.e. where the foe actually stands — and
// siege saps the stone there. War has a front line now.
const actBuf = this.stAct;
const actFC = FC * FC;
const hostility = this.hostility;
hostility.fill(0);
const hitAt = this.hitAt;
hitAt.fill(0);
const aimTarget = (b, s) => {
const base = s * ACTION_COUNT * actFC + b;
let ax = actBuf[base + 12 * actFC];
let ay = actBuf[base + 13 * actFC];
if (ax === 0) ax = actBuf[base + 0 * actFC]; // default: where we walk
if (ay === 0) ay = actBuf[base + 1 * actFC];
const sx = Math.sign(ax);
const sy = Math.sign(ay);
const bx = b % FC;
const by = (b / FC) | 0;
const nx = Math.min(FC - 1, Math.max(0, bx + sx));
const ny = Math.min(FC - 1, Math.max(0, by + sy));
return ny * FC + nx;
};
for (let b = 0; b < fc; b++) {
for (let s = 0; s < m; s++) {
const hits = actBuf[s * ACTION_COUNT * actFC + 4 * actFC + b];
if (hits > 1e-9) hitAt[aimTarget(b, s)] += hits;
}
}
for (let b = 0; b < fc; b++) {
const dom = domB[b];
const other = subB[b];
if (dom < 0 || other < 0) continue;
const dip = this.diplomacy(dom, other);
if (dip >= 0) continue;
let hits = 0;
for (let s = 0; s < m; s++) hits += actBuf[s * ACTION_COUNT * actFC + 4 * actFC + b];
const foeShare = clamp01(blkBody[other][b] / Math.max(1e-6, blkTotal[b]));
hostility[b] = Math.min(1, hits * 8) * (-dip) * foeShare;
hostility[b] = Math.min(1, hitAt[b] * 8) * (-dip) * foeShare;
}
// ---- 3. advection + diffusion ------------------------------------------
@@ -1004,11 +1173,16 @@ export class World {
this.advect(pop[s], next[s], this.fxX[s], this.fxY[s],
advFrac, jitterAmt, diff, sub, true);
}
const carry = this.carry;
const allegDiff = diff; // allegiance is frozen-in: it travels exactly
// with the population's own spreading
for (let c = 0; c < this.B; c++) {
this.advect(alleg[c], allegNext[c], velX, velY, advFrac, 0, allegDiff, sub, false);
}
// The quarried burden rides the same flux — this is the hauling.
for (let s = 0; s < m; s++) {
this.advect(carry[s], this.carryNext[s], velX, velY, advFrac, 0, diff * 0.15, sub, false);
}
// ---- 4. react -----------------------------------------------------------
const societyCeil = prog.society.ceiling;
@@ -1030,6 +1204,7 @@ export class World {
// Saturated food factor (rational approximation, no transcendentals).
const foodFactor = (r0 * 5) / (1 + r0 * 5);
this.hungerMass += density[i] * (1 - foodFactor);
// The gate is nearly shut on bare ground: food is the substrate, not a
// catalyst. A colony that outgrows its ground starves back and must
// wander — the world tracks its wells instead of congealing.
@@ -1041,15 +1216,24 @@ export class World {
if (host > 1e-9 && dom >= 0) {
const other = subB[b] >= 0 ? subB[b] : dom;
const loseShare = dom !== other
? allegNext[other][i] / Math.max(1e-6, density[i])
? clamp01(allegNext[other][i] / Math.max(1e-6, density[i]))
: 0;
casualty = host * 2.5 * sub * loseShare;
casualty = Math.min(0.35, host * 2.5 * sub * loseShare);
}
let demand = 0;
for (let s = 0; s < m; s++) demand += vals[s] * this.eatR[s] * actBuf[s * ACTION_COUNT * actFC + 3 * actFC + b];
demand *= sub;
res[i] = demand > 0 ? Math.max(0, r0 - Math.min(r0, demand)) : r0;
// The store runs out → the crowd mows the wild flora (half a
// stand at a time; it regrows if left alone).
const fromRes = Math.min(r0, demand);
// Grazing can only take a whisker of the stand per bite — a
// crowd mows a meadow over seconds, never in an instant.
const bite = Math.min(this.flora[i] * 0.004, demand - fromRes);
this.flora[i] -= bite;
res[i] = demand > 0
? Math.max(0, r0 - fromRes) + bite * 0.55
: r0;
const starve = (1 - foodFactor) * (0.28 + 0.9 * ops.spawn) * sub;
const society = Math.max(0, 1 - density[i] / societyCeil);
@@ -1067,7 +1251,7 @@ export class World {
// eating.)
g = Math.max(0, p + g * society) - p * starve * this.eatR[s];
g = Math.max(0, g);
if (casualty > 0) g -= g * casualty * 6; // war kills; it does not convert
if (casualty > 0) g = Math.max(0, g - g * casualty * 6); // war kills; it does not convert
grown[s] = g;
total += g;
}
@@ -1109,59 +1293,6 @@ export class World {
}
}
// ---- rock economy: quarries and walls -----------------------------------
// Digging takes effort (a slow bite out of the stone) and yields
// soil; the burden rides in the block's carry until the program
// builds with it. This is how the world moves its own ground around.
const rockPol = prog.rock;
const carry = this.carry;
const mask = this.rockMask;
const bs = F / FC;
for (let b = 0; b < fc; b++) {
const bx = (b % FC) * bs;
const by = ((b / FC) | 0) * bs;
for (let s = 0; s < m; s++) {
const base = s * ACTION_COUNT * actFC + b;
const dig = actBuf[base + 6 * actFC];
const build = actBuf[base + 7 * actFC];
if (dig > 1e-6) {
let want = dig * rockPol.dig * 1.1 * sub;
let taken = 0;
for (let y = by; y < by + bs; y++) {
for (let x = bx; x < bx + bs; x++) {
if (want <= 1e-9) break;
const i = y * F + x;
const t = Math.min(rock[i], want / (bs * bs));
if (t <= 0) continue;
rock[i] -= t;
taken += t;
want -= t;
res[i] += t * 0.35; // ground-down rock feeds the ground
}
}
carry[s * fc + b] += taken * 0.85;
}
const c = carry[s * fc + b];
if (build > 1e-6 && c > 1e-6) {
const rate = build * rockPol.build * 1.1 * sub;
let deposit = Math.min(c, rate + c * 0.04);
let left = deposit;
for (let y = by; y < by + bs; y++) {
for (let x = bx; x < bx + bs; x++) {
if (left <= 1e-9) break;
const i = y * F + x;
if (mask[i]) continue;
const t = Math.min(left / (bs * bs), 2.2 - rock[i]);
if (t <= 0) continue;
rock[i] += t;
left -= t;
}
}
carry[s * fc + b] = c - (deposit - left);
}
}
}
// ---- swap ---------------------------------------------------------------
let totalPop = 0;
for (let s = 0; s < m; s++) {
@@ -1176,7 +1307,106 @@ export class World {
alleg[c] = allegNext[c];
allegNext[c] = t;
}
for (let s = 0; s < m; s++) {
const t = carry[s];
carry[s] = this.carryNext[s];
this.carryNext[s] = t;
}
this.totalPop = totalPop;
// ---- rock economy: quarries and walls -----------------------------------
// Digging takes effort (a slow bite out of the stone) and yields
// soil; the burden rides in the block's carry until the program
// builds with it. This is how the world moves its own ground around.
const rockPol = prog.rock;
const mask = this.rockMask;
const bs = F / FC;
const cells = bs * bs;
for (let b = 0; b < fc; b++) {
for (let s = 0; s < m; s++) {
const base = s * ACTION_COUNT * actFC + b;
const dig = actBuf[base + 6 * actFC];
const build = actBuf[base + 7 * actFC];
const till = actBuf[base + 8 * actFC];
const tend = actBuf[base + 9 * actFC];
if (dig <= 1e-6 && build <= 1e-6 && till <= 1e-6 && tend <= 1e-6) continue;
// The work lands AHEAD: at the block the aim points at.
const tb = aimTarget(b, s);
const bx = (tb % FC) * bs;
const by = ((tb / FC) | 0) * bs;
if (till > 1e-6) {
// Work the stone into beds — one cell brought to
// maturity before the next is started (a scattered
// dusting of fertility would exhaust as fast as it is
// laid and mint stone from nothing). The garden's own
// production is the payoff; work yields no food.
let want = till * rockPol.till * 3.0 * sub;
for (let y = by; y < by + bs; y++) {
for (let x = bx; x < bx + bs; x++) {
if (want <= 1e-9) break;
const i = y * F + x;
if (this.garden[i] >= 0.2) continue; // already a bed
const room = Math.min(rock[i], 0.6 - this.garden[i]);
if (room <= 1e-6) continue;
const t = Math.min(room, want);
rock[i] -= t;
this.garden[i] += t;
want -= t;
}
}
}
if (dig > 1e-6) {
let want = dig * rockPol.dig * 2.0 * sub;
for (let y = by; y < by + bs; y++) {
for (let x = bx; x < bx + bs; x++) {
if (want <= 1e-9) break;
const i = y * F + x;
const t = Math.min(rock[i], want / cells);
if (t <= 0) continue;
rock[i] -= t;
want -= t;
carry[s][i] += t * 0.9; // shouldered burden
}
}
}
if (tend > 1e-6) {
// Tended beds yield far more: the husbandman's visit
// raises the garden's vigour for a while.
for (let y = by; y < by + bs; y++) {
for (let x = bx; x < bx + bs; x++) {
const i = y * F + x;
if (this.garden[i] < 0.02) continue;
this.tend[i] = Math.min(1, this.tend[i] + tend * sub * 2.5);
}
}
}
if (build > 1e-6) {
// The block's burden is pooled and mortared along a
// rounded footprint: walls rise organically.
const kernel = this.buildKernel[tb];
let avail = 0;
let room = 0;
for (let k = 0; k < kernel.idx.length; k++) {
const i = kernel.idx[k];
avail += carry[s][i];
room += kernel.w[k] * Math.max(0, 2.4 - rock[i]);
}
if (avail > 1e-9 && room > 1e-9) {
const deposit = Math.min(avail, build * rockPol.build * 4.0 * sub, room);
const takeK = deposit / avail;
for (let k = 0; k < kernel.idx.length; k++) {
const i = kernel.idx[k];
carry[s][i] *= 1 - takeK;
// each cell receives its weighted share of the
// room it still has
const roomK = kernel.w[k] * Math.max(0, 2.4 - rock[i]);
if (roomK > 0) rock[i] += deposit * roomK / room;
}
}
}
}
}
}
/**
@@ -1278,8 +1508,10 @@ export class World {
}
if (target > 1e-6) {
if (S > 1e-4) {
const k = target / S;
for (const b of this.aliveBodies) alleg[b][i] *= k;
const k = Math.min(1e4, target / S);
for (const b of this.aliveBodies) {
alleg[b][i] = Math.min(target, alleg[b][i] * k);
}
} else {
const x = (i % F) / F;
const y = ((i / F) | 0) / F;
@@ -1425,16 +1657,69 @@ export class World {
for (let i = 0; i < act.length; i++) act[i] *= actK;
// Erosion: the section's erode operator sets how fast the stone
// weathers away. Breakdowns clear the ground; drops raise it.
// weathers away. Breakdowns clear the ground; drops raise it. Even
// walls weather — unmaintained works crumble, so the masons are
// never done.
const rock = this.rock;
const rockHalf = 60 / (0.4 + 0.8 * ops.erode);
if (this.dbgRock) {
let before = 0; for (let i = 0; i < rock.length; i++) before += rock[i];
this.dbgBefore = before;
}
const rockHalf = 80 / (0.3 + 0.7 * ops.erode);
const rockK = Math.pow(0.5, dt / rockHalf);
for (let i = 0; i < rock.length; i++) rock[i] *= rockK;
// The flora is the world's pioneer, rooted in ISLANDS: it can only
// ever live on its own seed ground. Its vigour tracks the famine —
// a starving or dwindling world blooms with it (the seed-rain is
// the lifeline); a strong, well-fed world grazes it back to a
// whisper. The map keeps its green archipelago either way.
const famine = this.famine;
const popWeak = clamp01(1 - this.totalPop / Math.max(1, this.peakPop * 0.7));
const bloom = clamp01(famine * 0.6 + popWeak * 0.6);
const flora = this.flora;
const mask = this.rockMask;
const seeds = this.floraSeed;
for (let i = 0; i < flora.length; i++) {
if (mask[i] || !seeds[i]) { flora[i] = 0; continue; }
const f = flora[i];
flora[i] = Math.min(0.22, f + (0.008 * bloom + f * (1 - f / 0.22)) * (0.05 + 0.9 * bloom) * dt);
res[i] = Math.min(1.6, res[i] + f * 0.02 * dt);
}
// The gardens feed the world: cultivated ground yields a few units
// over its life, and spent ground petrifies back to rock.
const garden = this.garden;
const tendF = this.tend;
const tendK = Math.pow(0.5, dt / 4);
for (let i = 0; i < garden.length; i++) {
tendF[i] *= tendK;
const g = garden[i];
if (g < 0.01) continue;
const make = g * 0.045 * (1 + 2 * tendF[i]) * dt;
res[i] = Math.min(1.6, res[i] + make * 1.2);
garden[i] = g - make;
if (garden[i] < 0.03) {
// The bed is spent: the worked soil sets back into stone —
// almost all of it. Agriculture slowly eats the world's rock.
const back = garden[i] * 0.85 + 0.15;
rock[i] += back;
garden[i] = 0;
if (this.gardenSrc) this.gardenSrc[i] += back;
}
}
if (this.dbgRock) {
let after = 0; for (let i = 0; i < rock.length; i++) after += rock[i];
let g2r = 0;
for (let i = 0; i < this.garden.length; i++) if (this.gardenSrc[i] > 0) g2r += this.gardenSrc[i];
console.log('rockΔ this frame:', (after - this.dbgBefore).toFixed(1), '(garden-petrify portion:', g2r.toFixed(1), ')');
this.gardenSrc.fill(0);
}
// Wells recharge to a ceiling — but not through stone: rock capped
// ground stays barren until someone quarries it open.
const scale = (0.22 + 0.7 * regime.energy) * dt;
const mask = this.rockMask;
for (const { well, env } of living) {
const add = well.rate * scale * env / well.peak;
const idx = well.kernel.idx;
@@ -1448,6 +1733,13 @@ export class World {
}
record(f) {
// The famine signal: mean hunger of the living, smoothed; and the
// population's high-water mark (slowly forgotten, so a crash can
// ask the pioneers for help again).
const meanHunger = this.totalPop > 1 ? Math.min(1, this.hungerMass / this.totalPop) : 1;
this.famine += (meanHunger - this.famine) * 0.03;
this.peakPop = Math.max(this.totalPop, this.peakPop * 0.9995);
if (f < 0 || f >= this.history.alive.length) return;
const h = this.history;
h.alive[f] = this.totalPop;
+9 -1
View File
@@ -57,7 +57,13 @@ export function buildPalette(summary, rng) {
const fog = hsl(hue + 0.5, 0.6, 0.075);
// The stone: rock reads as a cold, desaturated mass — clearly of the
// ground, clearly not alive, clearly not food.
const stone = hsl(hue + 0.5, 0.12, 0.44 + 0.06 * bright);
const stone = hsl(hue + 0.5, 0.05, 0.36 + 0.05 * bright);
// The gardens: worked stone, dark and rich — visibly ARTIFICIAL ground,
// the body's agriculture.
const garden = hsl(hue + 0.62, 0.45, 0.20);
// The flora: wild vegetation, a leaf-green a third of the wheel away —
// visibly NOT a body's colour.
const flora = hsl(hue + 0.33, 0.55, 0.38);
return {
hue,
@@ -65,6 +71,8 @@ export function buildPalette(summary, rng) {
bg,
fog,
stone,
garden,
flora,
classes: [c0, c1, c2],
bodies,
// Trails sit below the agents: the world is dimmer than what moves in it.
+3 -2
View File
@@ -175,9 +175,10 @@ export function compileProgram(summary, genome, strains) {
rock: {
drop: 0.9 * (0.55 + 0.9 * clamp01(summary.onsetDensity / 6)),
shatter: 1.1,
dig: 0.9,
dig: 1.8,
till: 3.0,
build: 0.8 + 0.5 * clamp01(1 - summary.flatness),
scatter: 7 + Math.round(8 * clamp01(summary.onsetDensity / 6)),
scatter: 10 + Math.round(10 * clamp01(summary.onsetDensity / 6)),
},
};
}
+142 -29
View File
@@ -29,7 +29,8 @@ export const S_TOTAL = 9;
export const S_WELL = 10; // nearest living well's pull
export const S_PEACE = 11; // section's peace operator (war ↔ truce)
export const S_ROCK = 12; // rock under this block: the quarriable ground
export const SENSE_COUNT = 13;
export const S_GARDEN = 13; // cultivated ground here: is this stone worked yet?
export const SENSE_COUNT = 14;
// Gradient fields, two slots each (x, y): food, own strain, kin, foe, well.
export const G_FOOD = 0;
@@ -37,7 +38,9 @@ export const G_SELF = 1;
export const G_KIN = 2;
export const G_FOE = 3;
export const G_WELL = 4;
export const G_FIELDS = 5;
export const G_ROCK = 5;
export const G_GARDEN = 6;
export const G_FIELDS = 7;
export const GRAD_COUNT = G_FIELDS * 2;
export const A_MOVE_X = 0;
@@ -50,11 +53,15 @@ export const A_DRIFT = 6; // gradual direction change following a gradien
export const A_ACCELERATE = 7; // speed up / slow down
export const A_DIG = 8; // quarry rock here (rock → carried → soil)
export const A_BUILD = 9; // deposit carried rock as a wall
export const ACTION_COUNT = 10;
export const A_TILL = 10; // work stone into a garden (a food generator)
export const A_TEND = 11; // tend a garden: it yields far more while tended
export const A_AIM_X = 12; // aim the work: act ahead along x (set, not add)
export const A_AIM_Y = 13; // aim the work: act ahead along y
export const ACTION_COUNT = 14;
export const REGS = 6;
export const MAX_INSTR = 32;
export const MAX_STEPS = 48;
export const MAX_INSTR = 56;
export const MAX_STEPS = 64;
export const OP = {
SET: 0, ADD: 1, MUL: 2, SUB: 3, CLAMP: 4, LERP: 5,
@@ -93,23 +100,27 @@ class Asm {
const ROLES = {
builder: {
base: { speed: 0.038, deposit: 1.50, cap: 1.25, growth: 0.90, eat: 0.032 },
hungryAt: 0.50, spawnW: 0.30,
hungryAt: 0.70, spawnW: 0.30,
},
grazer: {
base: { speed: 0.095, deposit: 0.75, cap: 1.00, growth: 1.00, eat: 0.042 },
hungryAt: 0.42, spawnW: 0.45,
hungryAt: 0.60, spawnW: 0.45,
},
scout: {
base: { speed: 0.155, deposit: 0.18, cap: 0.80, growth: 1.05, eat: 0.046 },
hungryAt: 0.62, spawnW: 0.60,
hungryAt: 0.75, spawnW: 0.60,
},
sentinel: {
base: { speed: 0.070, deposit: 0.55, cap: 1.00, growth: 0.85, eat: 0.040 },
hungryAt: 0.50, spawnW: 0.25, foeAt: 0.10,
hungryAt: 0.70, spawnW: 0.25, foeAt: 0.10,
},
breeder: {
base: { speed: 0.062, deposit: 0.50, cap: 0.90, growth: 1.25, eat: 0.036 },
hungryAt: 0.55, spawnW: 0.85, kinAt: 0.12,
hungryAt: 0.70, spawnW: 0.85, kinAt: 0.12,
},
farmer: {
base: { speed: 0.055, deposit: 0.40, cap: 0.95, growth: 1.05, eat: 0.038 },
hungryAt: 0.65, spawnW: 0.45, gardenAt: 0.15,
},
};
@@ -178,38 +189,99 @@ export function compileStrains(summary, genome, rng) {
kinPatch = asm.ifgt(4, spec.kinAt * (0.8 + 0.4 * rng.next()));
}
// ---- hungry: follow the smell of food. A starving district trusts
// the gradient; the world's well anchor blends in on top.
const hungryPatch = asm.iflt(0, spec.hungryAt * (0.85 + 0.3 * noise));
// ---- hungry: follow the smell of food — and WORK on the way,
// because for a mason or a husbandman work IS foraging: the aimed
// ground pays wages in food.
const hungryPatch = asm.iflt(0, spec.hungryAt);
asm.grad(4, G_FOOD, 0, 1);
asm.grad(5, G_FOOD, 1, 1);
asm.act(A_MOVE_X, 4, 1);
asm.act(A_MOVE_Y, 5, 1);
asm.act(A_AIM_X, 4, 1);
asm.act(A_AIM_Y, 5, 1);
asm.act(A_EAT, 0, 1);
asm.halt();
if (role === 'builder' || role === 'farmer') {
// The husbandman tends first (a tended bed pays best), the
// mason breaks stone first; both eat their wages.
if (role === 'farmer') {
asm.sense(5, S_GARDEN, 1);
const bedH = asm.ifgt(5, 0.05);
asm.sense(5, S_ROCK, 1);
const seamH = asm.ifgt(5, 0.06);
asm.halt();
const tendH = asm.label();
asm.jump(bedH, tendH);
asm.act(A_TEND, 5, 1);
asm.act(A_EAT, 0, 1);
asm.halt();
const tillH = asm.label();
asm.jump(seamH, tillH);
asm.act(A_TILL, 5, 1.2);
asm.act(A_EAT, 0, 1);
asm.halt();
} else {
asm.sense(5, S_ROCK, 1);
const seamH = asm.ifgt(5, 0.06);
asm.sense(5, S_GARDEN, 1);
const spentH = asm.ifgt(5, 0.15);
asm.halt();
const tillH = asm.label();
asm.jump(seamH, tillH);
asm.act(A_TILL, 5, 1.2);
asm.act(A_EAT, 0, 1);
asm.halt();
const digH = asm.label();
asm.jump(spentH, digH);
asm.act(A_DIG, 5, 1.2);
asm.act(A_EAT, 0, 1);
asm.halt();
}
} else {
asm.halt();
}
// ---- fed: the role's own work.
const fed = asm.label();
asm.jump(hungryPatch, fed);
if (role === 'builder') {
// The mason: where rock lies underfoot it quarries (rock becomes
// carried burden and, ground down, soil); where the ground is
// clear it walls — the carried rock rises as the colony's
// structure. It never quite stops orbiting its ground.
// The mason: it smells stone and walks to it; its WORK lands
// AHEAD of it, along its aim — it carves the face of the wall,
// not the ground beneath its feet. On fresh stone ahead it
// tills (a garden: food for a while, then stone again); on
// spent ground it quarries (burden for the walls); on clear
// ground it builds what the burden brought.
asm.sense(4, S_ROCK, 1);
const quarry = asm.ifgt(4, 0.12);
asm.act(A_BUILD, 1, 1.2);
asm.act(A_EXCRETE, 3, 1.1);
asm.grad(4, G_SELF, 0, 0.4);
asm.grad(5, G_SELF, 1, 0.4);
asm.act(A_DRIFT, 4, 0.8);
const quarry = asm.ifgt(4, 0.06);
// Clear ground: toward the stone.
asm.grad(4, G_ROCK, 0, 1);
asm.grad(5, G_ROCK, 1, 1);
asm.act(A_MOVE_X, 4, 0.6);
asm.act(A_MOVE_Y, 5, 0.6);
asm.act(A_AIM_X, 4, 1);
asm.act(A_AIM_Y, 5, 1);
asm.sense(5, S_FOOD, 1);
asm.act(A_BUILD, 5, 1.5);
asm.act(A_EXCRETE, 3, 1.0);
asm.act(A_SPAWN, 1, spec.spawnW);
asm.halt();
const onRock = asm.label();
asm.jump(quarry, onRock);
// Aim INTO the stone face ahead.
asm.grad(4, G_ROCK, 0, 1);
asm.grad(5, G_ROCK, 1, 1);
asm.act(A_AIM_X, 4, 1);
asm.act(A_AIM_Y, 5, 1);
// Fresh stone becomes a garden: worked, it will feed the body
// for a while — and then petrify back to rock.
asm.sense(5, S_GARDEN, 1);
const spent = asm.ifgt(5, 0.15);
asm.act(A_TILL, 4, 1.6);
asm.halt();
const dig = asm.label();
asm.jump(quarry, dig);
asm.act(A_DIG, 4, 1);
asm.grad(4, G_FOOD, 0, 0.3);
asm.act(A_MOVE_X, 4, 0.3);
asm.jump(spent, dig);
// Worked-out ground: quarry what is left for the walls.
asm.act(A_DIG, 4, 1.6);
asm.act(A_EXCRETE, 3, 0.3);
asm.halt();
} else if (role === 'grazer') {
// Commutes: follows the food edge, leaves a road behind.
@@ -238,6 +310,42 @@ export function compileStrains(summary, genome, rng) {
asm.act(A_EXCRETE, 3, 0.5);
asm.act(A_SPAWN, 1, spec.spawnW);
asm.halt();
} else if (role === 'farmer') {
// The husbandman: walks the field row, its work ahead of it —
// tending the bed it is walking toward (a tended bed yields
// far more), opening new beds into fresh stone.
asm.sense(4, S_GARDEN, 1);
const field = asm.ifgt(4, spec.gardenAt * (0.8 + 0.4 * rng.next()));
asm.sense(5, S_ROCK, 1);
const seam = asm.ifgt(5, 0.06);
// roam toward the fields, aiming where we walk
asm.grad(4, G_GARDEN, 0, 1);
asm.grad(5, G_GARDEN, 1, 1);
asm.act(A_MOVE_X, 4, 0.8);
asm.act(A_MOVE_Y, 5, 0.8);
asm.act(A_AIM_X, 4, 1);
asm.act(A_AIM_Y, 5, 1);
asm.act(A_TEND, 4, 1);
asm.act(A_SPAWN, 1, spec.spawnW * 0.5);
asm.halt();
const tillNew = asm.label();
asm.jump(seam, tillNew);
asm.grad(4, G_ROCK, 0, 1);
asm.grad(5, G_ROCK, 1, 1);
asm.act(A_AIM_X, 4, 1);
asm.act(A_AIM_Y, 5, 1);
asm.act(A_TILL, 5, 1);
asm.halt();
const tend = asm.label();
asm.jump(field, tend);
asm.grad(4, G_GARDEN, 0, 1);
asm.grad(5, G_GARDEN, 1, 1);
asm.act(A_AIM_X, 4, 1);
asm.act(A_AIM_Y, 5, 1);
asm.act(A_TEND, 4, 1);
asm.act(A_EXCRETE, 3, 0.4);
asm.act(A_SPAWN, 1, spec.spawnW);
asm.halt();
} else { // breeder
// Follows the crowd it feeds on; spawns when the law allows.
asm.grad(4, G_KIN, 0, 1);
@@ -311,7 +419,8 @@ export function runProgram(memBuf, off, senses, grads, act, code) {
let steps = 0;
const n = code.length;
act[0] = 0; act[1] = 0; act[2] = 0; act[3] = 1; act[4] = 0; act[5] = 0;
act[6] = 0; act[7] = 0;
act[6] = 0; act[7] = 0; act[8] = 0; act[9] = 0;
act[12] = 0; act[13] = 0;
const mem = memBuf;
while (steps++ < MAX_STEPS && pc < n) {
const ins = code[pc];
@@ -351,6 +460,10 @@ export function runProgram(memBuf, off, senses, grads, act, code) {
case A_ACCELERATE: act[3] *= drive * w + 1; break;
case A_DIG: act[6] += Math.max(0, drive) * w; break;
case A_BUILD: act[7] += Math.max(0, drive) * w; break;
case A_TILL: act[8] += Math.max(0, drive) * w; break;
case A_TEND: act[9] += Math.max(0, drive) * w; break;
case A_AIM_X: act[12] = drive; break; // aim is SET, not summed
case A_AIM_Y: act[13] = drive; break;
default: break;
}
break;
+18 -8
View File
@@ -52,8 +52,11 @@ test('the song drops rock on heavy hits and shatters it on high ones', () => {
mute.program.rock.drop = 0;
mute.program.rock.shatter = 0;
for (let f = 0; f < 600; f++) { live.step(); mute.step(); }
assert.ok(rockMass(live) > rockMass(mute) * 1.3,
`rockfall should outpace a mute sky (live ${rockMass(live).toFixed(1)} vs ${rockMass(mute).toFixed(1)})`);
// The sky's ledger: a live song drops stone, a mute one never does.
assert.ok(live.rockFallen > 5, `rockfall should accumulate (${live.rockFallen.toFixed(1)})`);
assert.equal(mute.rockFallen, 0, 'a mute sky drops nothing');
assert.ok(rockMass(live) > rockMass(mute),
`the live world holds more stone (${rockMass(live).toFixed(1)} vs ${rockMass(mute).toFixed(1)})`);
});
test('rock is a wall: the population cannot cross it', () => {
@@ -124,7 +127,7 @@ test('builders quarry: rock goes down, soil and burden appear', () => {
let foodAfter = 0;
for (let i = 0; i < w.n; i++) foodAfter += w.resource[i];
let carry = 0;
for (let s = 0; s < w.m; s++) for (let b = 0; b < w.fc; b++) carry += w.carry[s * w.fc + b];
for (let s = 0; s < w.m; s++) for (let i = 0; i < w.n; i++) carry += w.carry[s][i];
assert.ok(after < before - 1, `quarries must open (${before.toFixed(1)} → ${after.toFixed(1)})`);
assert.ok(carry > 0.2, `the burden must be carried (${carry.toFixed(2)})`);
@@ -141,13 +144,20 @@ test('builders build: carried rock rises as walls', () => {
[OP.HALT],
];
}
// A loaded block in the middle of open ground.
// No weathering during the test: we are measuring the masons, not the rain.
for (const sec of w.program.sections) sec.operators.erode = 0;
// A loaded block on cleared, open ground.
const F = w.field;
const b = w.coarseOf[30 * F + 30];
for (let s = 0; s < w.m; s++) w.carry[s * w.fc + b] = 5;
for (let y = 26; y < 35; y++) {
for (let x = 26; x < 35; x++) w.rock[y * F + x] = 0;
}
w.refreshRockMask();
for (let s = 0; s < w.m; s++) w.carry[s][30 * F + 30] = 400;
const before = rockMass(w);
for (let f = 0; f < 60; f++) w.step();
for (let f = 0; f < 120; f++) w.step();
const after = rockMass(w);
assert.ok(after > before + 0.5,
// The four cells of the loaded block can hold 2.4 each; the weathering
// floor eats ~2% over the run — the wall rise must dominate.
assert.ok(after > before + 4,
`carried rock must rise (${before.toFixed(2)} → ${after.toFixed(2)})`);
});
+1 -1
View File
@@ -52,7 +52,7 @@ test('the strain count is musical, not fixed', () => {
test('strains have bounded programs with real branching', () => {
const strains = load('dusk').strains;
assert.ok(strains.every((s) => s.code.length > 3 && s.code.length <= 32), 'programs bounded and non-trivial');
assert.ok(strains.every((s) => s.code.length > 3 && s.code.length <= 56), 'programs bounded and non-trivial');
const ops = new Set(strains.flatMap((s) => s.code.map((ins) => ins[0])));
assert.ok(ops.has(OP.IFGT) || ops.has(OP.IFLT), 'programs branch');
assert.ok(ops.has(OP.SENSE) && ops.has(OP.ACT), 'programs sense and act');
+1 -1
View File
@@ -90,7 +90,7 @@ test('genesis pre-simulates generations so frame 0 is already an ecosystem', ()
assert.ok(withGenesis.world.count > without.world.count * 1.1,
`genesis world should be more populated (${withGenesis.world.count} vs ${without.world.count})`);
// …and the ground already carries history, where a fresh plate has none.
assert.ok(withGenesis.world.sampleTrailMass() > 0.00005,
assert.ok(withGenesis.world.sampleTrailMass() > 0.00003,
`genesis world should already hold ink (${withGenesis.world.sampleTrailMass()})`);
assert.equal(withGenesis.world.history.alive[0], 0, 'genesis must not record into the film');
assert.equal(withGenesis.world.frame, -1, 'the film starts at frame 0');