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 other, truce and trade versus skirmish and siege. Society is a property of
the mix, at both scales. 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 ### Rock — the inert resource
The map also carries **rock**: height per cell, impassable above a threshold. 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 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 - **M2.8 — rock** ✔ built: the inert resource — scattered at birth, dropped
and shattered by the song, impassable, quarried by builders into carried and shattered by the song, impassable, quarried by builders into carried
burden and soil, walled where the colony works. See EVOLUTION.md. 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 - **M3 — lens** the camera travels (track / reveal / establish) and reserves
cuts for musical punctuation. cuts for musical punctuation.
- **M4 — film** mux frames + PCM to video. - **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 rock rising as walls. The strips read it directly: grey masses the colonies
grow around, fronts grinding along them. 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 ### M3 — the lens: travel, don't cut
Replace the cut-to-a-point grammar with a **move grammar**: *track* (follow a 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 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; 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 wide = boxBlur(sharp, F, 2);
const tight = boxBlur(sharp, F, 1); const tight = boxBlur(sharp, F, 1);
const out = new Uint8ClampedArray(n * 4); 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 PER_SPECKS = 2.2;
const MAX_SPECKS = 60000; const MAX_SPECKS = 60000;
// Density glow: how fast a cell's population turns into brightness. // Density glow: how fast a cell's population turns into brightness.
const DENSITY_K = 0.11; const DENSITY_K = 0.07;
const DENSITY_EXPOSURE = 1.25; const DENSITY_EXPOSURE = 1.0;
export class Renderer { export class Renderer {
constructor(width, height, { exposure = 2.6, grain = 2.4, vignette = 0.55 } = {}) { 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 + 1] += r * stone[1] * 2.6;
sharp[i * 3 + 2] += r * stone[2] * 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.tight, F, 1);
boxBlur(sharp, this.tmp, this.wide, F, 3); boxBlur(sharp, this.tmp, this.wide, F, 3);
const out = this.trailRGBA; 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; return budget;
} }
+368 -76
View File
@@ -32,8 +32,9 @@ import { Rng } from '../rng.js';
import { wellPos, wellEnv, buildKernel } from './wells.js'; import { wellPos, wellEnv, buildKernel } from './wells.js';
import { import {
runProgram, SENSE_COUNT, GRAD_COUNT, ACTION_COUNT, REGS, 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, 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_FOOD, G_SELF, G_KIN, G_FOE, G_WELL, G_GARDEN,
G_FOOD, G_SELF, G_KIN, G_FOE, G_WELL, G_ROCK,
} from '../song/strains.js'; } from '../song/strains.js';
const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v); 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. // bodies are founded once and persist; war kills, it does not convert.
const COLONIZE_EVERY = 60; // ~2 s: a blessed frontier settlement may found 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 { export class World {
constructor(song, { fps = 30, field = FIELD, seed = song.seed, substeps = SUBSTEPS, genesis } = {}) { constructor(song, { fps = 30, field = FIELD, seed = song.seed, substeps = SUBSTEPS, genesis } = {}) {
this.song = song; this.song = song;
@@ -118,6 +151,24 @@ export class World {
// carry and wall with. rockMask marks the impassable cells. // carry and wall with. rockMask marks the impassable cells.
this.rock = new Float32Array(n); this.rock = new Float32Array(n);
this.rockMask = new Uint8Array(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, // Deterministic jitter: static signed noise fields from the seeded RNG,
// read every substep. The track's flatness sets how loud it is. // 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.blkBody = Array.from({ length: this.B }, () => new Float32Array(fc));
this.blkTotal = new Float32Array(fc); this.blkTotal = new Float32Array(fc);
this.blkRock = new Float32Array(fc); this.blkRock = new Float32Array(fc);
this.blkGarden = new Float32Array(fc);
this.domB = new Int8Array(fc).fill(-1); this.domB = new Int8Array(fc).fill(-1);
this.subB = new Int8Array(fc).fill(-1); this.subB = new Int8Array(fc).fill(-1);
this.fxX = Array.from({ length: this.m }, () => new Float32Array(fc)); this.fxX = Array.from({ length: this.m }, () => new Float32Array(fc));
@@ -162,12 +214,18 @@ export class World {
this.velX = new Float32Array(fc); this.velX = new Float32Array(fc);
this.velY = new Float32Array(fc); this.velY = new Float32Array(fc);
this.wellField = 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.ancX = Array.from({ length: this.m }, () => new Float32Array(fc));
this.ancY = 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.synergy = new Float32Array(this.m * fc);
this.valScratch = new Float64Array(this.m); this.valScratch = new Float64Array(this.m);
this.growScratch = new Float64Array(this.m); this.growScratch = new Float64Array(this.m);
this.hostility = new Float32Array(fc); 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.cUp = new Int32Array(fc);
this.cDown = new Int32Array(fc); this.cDown = new Int32Array(fc);
this.cLeft = 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 // Strain programs run on this decision grid: registers are the
// strains' persistent memory, the action slots carry each decision. // strains' persistent memory, the action slots carry each decision.
@@ -240,6 +305,7 @@ export class World {
this.deathsThisFrame = 0; this.deathsThisFrame = 0;
this.seedRocks(); this.seedRocks();
this.seedFlora();
this.refreshRockMask(); this.refreshRockMask();
this.foundOpeningBodies(); this.foundOpeningBodies();
this.seedInitial(); this.seedInitial();
@@ -398,6 +464,7 @@ export class World {
this.warmth = Math.max(-1, Math.min(1, this.warmth = Math.max(-1, Math.min(1,
this.program.diplomacy.base * 0.6 + 0.35 * (regime.operators.peace - 1))); this.program.diplomacy.base * 0.6 + 0.35 * (regime.operators.peace - 1)));
const living = this.framePrep(regime); const living = this.framePrep(regime);
this.updateRockField();
this.inject(this.frame, regime); this.inject(this.frame, regime);
this.refreshRockMask(); this.refreshRockMask();
for (let s = 0; s < this.substeps; s++) this.substep(regime); 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++) { for (let k = 0; k < n; k++) {
const x = this.rng.next(); const x = this.rng.next();
const y = this.rng.next(); const y = this.rng.next();
const sigma = 0.022 + 0.030 * this.rng.next(); const sigma = 0.030 + 0.045 * this.rng.next();
const amount = 3.5 + 6.5 * this.rng.next(); const amount = 5.0 + 9.0 * this.rng.next();
this.rockSplat(x, y, sigma, amount); this.rockSplat(x, y, sigma, amount);
} }
} }
@@ -538,11 +605,40 @@ export class World {
return { idx, w, k }; 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. */ /** The impassable frontier, recomputed after any rock change. */
refreshRockMask() { refreshRockMask() {
const rock = this.rock; const rock = this.rock;
const mask = this.rockMask; 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 --------------------------------------------------------------- // ---- wells ---------------------------------------------------------------
@@ -623,8 +719,10 @@ export class World {
this.birthsThisFrame = 0; this.birthsThisFrame = 0;
this.deathsThisFrame = 0; this.deathsThisFrame = 0;
this.moveAccum = 0; this.moveAccum = 0;
this.hungerMass = 0;
const living = this.framePrep(regime); const living = this.framePrep(regime);
this.updateRockField();
this.inject(f, regime); this.inject(f, regime);
this.refreshRockMask(); 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. */ /** Home anchors: each strain is pulled to its nearest living well. */
updateAnchors(living) { updateAnchors(living) {
const FC = this.FC; const FC = this.FC;
@@ -730,7 +850,8 @@ export class World {
if (e.voice === 2) { if (e.voice === 2) {
this.rockSplat(e.x, e.y, e.spread * 1.4, -e.strength * 2.4 * rockPol.shatter * ops.erode); this.rockSplat(e.x, e.y, e.spread * 1.4, -e.strength * 2.4 * rockPol.shatter * ops.erode);
} else { } 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; this.activity[ay * 32 + ax] += 1.5;
} }
@@ -751,6 +872,7 @@ export class World {
runStrains(regime) { runStrains(regime) {
const FC = this.FC; const FC = this.FC;
const blkRock = this.blkRock; const blkRock = this.blkRock;
const blkGarden = this.blkGarden;
const fc = FC * FC; const fc = FC * FC;
const m = this.m; const m = this.m;
const blkPop = this.blkPop; const blkPop = this.blkPop;
@@ -820,7 +942,17 @@ export class World {
ss[S_TOTAL] = clamp01(tot * 0.005); ss[S_TOTAL] = clamp01(tot * 0.005);
ss[S_WELL] = clamp01(wellField[b]); ss[S_WELL] = clamp01(wellField[b]);
ss[S_PEACE] = peace; 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) --------------------- // ---- gradients (sign per field × axis) ---------------------
gg[G_FOOD * 2] = Math.sign(gRx); 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] = Math.sign(wellField[rg] - wellField[lg]);
gg[G_WELL * 2 + 1] = Math.sign(wellField[dg] - wellField[ug]); 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); 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 ---------------------- // ---- actions → forces + reaction mods ----------------------
fxX[s][b] = act[0] + this.ancX[s][b] * 0.12 + gRx * 0.02 - dgx * pressure; 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 + 5 * actFC] = act[5]; // SPAWN
actBuf[off + 6 * actFC] = act[6]; // DIG actBuf[off + 6 * actFC] = act[6]; // DIG
actBuf[off + 7 * actFC] = act[7]; // BUILD 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 // Synergy: this strain breeds fastest in a mixed, same-body
// crowd — the share of the block's population that is kin. // crowd — the share of the block's population that is kin.
@@ -920,16 +1065,19 @@ export class World {
const blkBody = this.blkBody; const blkBody = this.blkBody;
const blkTotal = this.blkTotal; const blkTotal = this.blkTotal;
const blkRock = this.blkRock; const blkRock = this.blkRock;
const blkGarden = this.blkGarden;
blkR.fill(0); blkR.fill(0);
blkTotal.fill(0); blkTotal.fill(0);
blkRock.fill(0); blkRock.fill(0);
blkGarden.fill(0);
for (let s = 0; s < m; s++) blkPop[s].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 b = 0; b < this.B; b++) { blkTrail[b].fill(0); blkBody[b].fill(0); }
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
const b = coarseOf[i]; const b = coarseOf[i];
const d = density[i]; const d = density[i];
blkR[b] += res[i]; blkR[b] += res[i] + this.flora[i] * 0.5;
blkRock[b] += rock[i]; blkRock[b] += rock[i];
blkGarden[b] += this.garden[i];
if (d > 1e-6) { if (d > 1e-6) {
blkTotal[b] += d; blkTotal[b] += d;
for (let s = 0; s < m; s++) blkPop[s][b] += pop[s][i]; 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 -------------------------- // ---- 2. the strain programs decide the forces --------------------------
this.runStrains(regime); this.runStrains(regime);
// The per-block war pressure: hits × hostile presence. A body strikes // The per-block war pressure: hits land at their AIM target — where
// its neighbour only where the two actually touch. // 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 actBuf = this.stAct;
const actFC = FC * FC; const actFC = FC * FC;
const hostility = this.hostility; const hostility = this.hostility;
hostility.fill(0); 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++) { for (let b = 0; b < fc; b++) {
const dom = domB[b]; const dom = domB[b];
const other = subB[b]; const other = subB[b];
if (dom < 0 || other < 0) continue; if (dom < 0 || other < 0) continue;
const dip = this.diplomacy(dom, other); const dip = this.diplomacy(dom, other);
if (dip >= 0) continue; 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])); 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 ------------------------------------------ // ---- 3. advection + diffusion ------------------------------------------
@@ -1004,11 +1173,16 @@ export class World {
this.advect(pop[s], next[s], this.fxX[s], this.fxY[s], this.advect(pop[s], next[s], this.fxX[s], this.fxY[s],
advFrac, jitterAmt, diff, sub, true); advFrac, jitterAmt, diff, sub, true);
} }
const carry = this.carry;
const allegDiff = diff; // allegiance is frozen-in: it travels exactly const allegDiff = diff; // allegiance is frozen-in: it travels exactly
// with the population's own spreading // with the population's own spreading
for (let c = 0; c < this.B; c++) { for (let c = 0; c < this.B; c++) {
this.advect(alleg[c], allegNext[c], velX, velY, advFrac, 0, allegDiff, sub, false); 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 ----------------------------------------------------------- // ---- 4. react -----------------------------------------------------------
const societyCeil = prog.society.ceiling; const societyCeil = prog.society.ceiling;
@@ -1030,6 +1204,7 @@ export class World {
// Saturated food factor (rational approximation, no transcendentals). // Saturated food factor (rational approximation, no transcendentals).
const foodFactor = (r0 * 5) / (1 + r0 * 5); 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 // 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 // catalyst. A colony that outgrows its ground starves back and must
// wander — the world tracks its wells instead of congealing. // wander — the world tracks its wells instead of congealing.
@@ -1041,15 +1216,24 @@ export class World {
if (host > 1e-9 && dom >= 0) { if (host > 1e-9 && dom >= 0) {
const other = subB[b] >= 0 ? subB[b] : dom; const other = subB[b] >= 0 ? subB[b] : dom;
const loseShare = dom !== other const loseShare = dom !== other
? allegNext[other][i] / Math.max(1e-6, density[i]) ? clamp01(allegNext[other][i] / Math.max(1e-6, density[i]))
: 0; : 0;
casualty = host * 2.5 * sub * loseShare; casualty = Math.min(0.35, host * 2.5 * sub * loseShare);
} }
let demand = 0; let demand = 0;
for (let s = 0; s < m; s++) demand += vals[s] * this.eatR[s] * actBuf[s * ACTION_COUNT * actFC + 3 * actFC + b]; for (let s = 0; s < m; s++) demand += vals[s] * this.eatR[s] * actBuf[s * ACTION_COUNT * actFC + 3 * actFC + b];
demand *= sub; 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 starve = (1 - foodFactor) * (0.28 + 0.9 * ops.spawn) * sub;
const society = Math.max(0, 1 - density[i] / societyCeil); const society = Math.max(0, 1 - density[i] / societyCeil);
@@ -1067,7 +1251,7 @@ export class World {
// eating.) // eating.)
g = Math.max(0, p + g * society) - p * starve * this.eatR[s]; g = Math.max(0, p + g * society) - p * starve * this.eatR[s];
g = Math.max(0, g); 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; grown[s] = g;
total += 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 --------------------------------------------------------------- // ---- swap ---------------------------------------------------------------
let totalPop = 0; let totalPop = 0;
for (let s = 0; s < m; s++) { for (let s = 0; s < m; s++) {
@@ -1176,7 +1307,106 @@ export class World {
alleg[c] = allegNext[c]; alleg[c] = allegNext[c];
allegNext[c] = t; 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; 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 (target > 1e-6) {
if (S > 1e-4) { if (S > 1e-4) {
const k = target / S; const k = Math.min(1e4, target / S);
for (const b of this.aliveBodies) alleg[b][i] *= k; for (const b of this.aliveBodies) {
alleg[b][i] = Math.min(target, alleg[b][i] * k);
}
} else { } else {
const x = (i % F) / F; const x = (i % F) / F;
const y = ((i / F) | 0) / 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; for (let i = 0; i < act.length; i++) act[i] *= actK;
// Erosion: the section's erode operator sets how fast the stone // 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 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); const rockK = Math.pow(0.5, dt / rockHalf);
for (let i = 0; i < rock.length; i++) rock[i] *= rockK; 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 // Wells recharge to a ceiling — but not through stone: rock capped
// ground stays barren until someone quarries it open. // ground stays barren until someone quarries it open.
const scale = (0.22 + 0.7 * regime.energy) * dt; const scale = (0.22 + 0.7 * regime.energy) * dt;
const mask = this.rockMask;
for (const { well, env } of living) { for (const { well, env } of living) {
const add = well.rate * scale * env / well.peak; const add = well.rate * scale * env / well.peak;
const idx = well.kernel.idx; const idx = well.kernel.idx;
@@ -1448,6 +1733,13 @@ export class World {
} }
record(f) { 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; if (f < 0 || f >= this.history.alive.length) return;
const h = this.history; const h = this.history;
h.alive[f] = this.totalPop; 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); const fog = hsl(hue + 0.5, 0.6, 0.075);
// The stone: rock reads as a cold, desaturated mass — clearly of the // The stone: rock reads as a cold, desaturated mass — clearly of the
// ground, clearly not alive, clearly not food. // 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 { return {
hue, hue,
@@ -65,6 +71,8 @@ export function buildPalette(summary, rng) {
bg, bg,
fog, fog,
stone, stone,
garden,
flora,
classes: [c0, c1, c2], classes: [c0, c1, c2],
bodies, bodies,
// Trails sit below the agents: the world is dimmer than what moves in it. // 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: { rock: {
drop: 0.9 * (0.55 + 0.9 * clamp01(summary.onsetDensity / 6)), drop: 0.9 * (0.55 + 0.9 * clamp01(summary.onsetDensity / 6)),
shatter: 1.1, shatter: 1.1,
dig: 0.9, dig: 1.8,
till: 3.0,
build: 0.8 + 0.5 * clamp01(1 - summary.flatness), 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_WELL = 10; // nearest living well's pull
export const S_PEACE = 11; // section's peace operator (war ↔ truce) 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 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. // Gradient fields, two slots each (x, y): food, own strain, kin, foe, well.
export const G_FOOD = 0; export const G_FOOD = 0;
@@ -37,7 +38,9 @@ export const G_SELF = 1;
export const G_KIN = 2; export const G_KIN = 2;
export const G_FOE = 3; export const G_FOE = 3;
export const G_WELL = 4; 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 GRAD_COUNT = G_FIELDS * 2;
export const A_MOVE_X = 0; 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_ACCELERATE = 7; // speed up / slow down
export const A_DIG = 8; // quarry rock here (rock → carried → soil) export const A_DIG = 8; // quarry rock here (rock → carried → soil)
export const A_BUILD = 9; // deposit carried rock as a wall 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 REGS = 6;
export const MAX_INSTR = 32; export const MAX_INSTR = 56;
export const MAX_STEPS = 48; export const MAX_STEPS = 64;
export const OP = { export const OP = {
SET: 0, ADD: 1, MUL: 2, SUB: 3, CLAMP: 4, LERP: 5, SET: 0, ADD: 1, MUL: 2, SUB: 3, CLAMP: 4, LERP: 5,
@@ -93,23 +100,27 @@ class Asm {
const ROLES = { const ROLES = {
builder: { builder: {
base: { speed: 0.038, deposit: 1.50, cap: 1.25, growth: 0.90, eat: 0.032 }, 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: { grazer: {
base: { speed: 0.095, deposit: 0.75, cap: 1.00, growth: 1.00, eat: 0.042 }, 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: { scout: {
base: { speed: 0.155, deposit: 0.18, cap: 0.80, growth: 1.05, eat: 0.046 }, 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: { sentinel: {
base: { speed: 0.070, deposit: 0.55, cap: 1.00, growth: 0.85, eat: 0.040 }, 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: { breeder: {
base: { speed: 0.062, deposit: 0.50, cap: 0.90, growth: 1.25, eat: 0.036 }, 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())); kinPatch = asm.ifgt(4, spec.kinAt * (0.8 + 0.4 * rng.next()));
} }
// ---- hungry: follow the smell of food. A starving district trusts // ---- hungry: follow the smell of food — and WORK on the way,
// the gradient; the world's well anchor blends in on top. // because for a mason or a husbandman work IS foraging: the aimed
const hungryPatch = asm.iflt(0, spec.hungryAt * (0.85 + 0.3 * noise)); // ground pays wages in food.
const hungryPatch = asm.iflt(0, spec.hungryAt);
asm.grad(4, G_FOOD, 0, 1); asm.grad(4, G_FOOD, 0, 1);
asm.grad(5, G_FOOD, 1, 1); asm.grad(5, G_FOOD, 1, 1);
asm.act(A_MOVE_X, 4, 1); asm.act(A_MOVE_X, 4, 1);
asm.act(A_MOVE_Y, 5, 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.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. // ---- fed: the role's own work.
const fed = asm.label(); const fed = asm.label();
asm.jump(hungryPatch, fed); asm.jump(hungryPatch, fed);
if (role === 'builder') { if (role === 'builder') {
// The mason: where rock lies underfoot it quarries (rock becomes // The mason: it smells stone and walks to it; its WORK lands
// carried burden and, ground down, soil); where the ground is // AHEAD of it, along its aim — it carves the face of the wall,
// clear it walls — the carried rock rises as the colony's // not the ground beneath its feet. On fresh stone ahead it
// structure. It never quite stops orbiting its ground. // 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); asm.sense(4, S_ROCK, 1);
const quarry = asm.ifgt(4, 0.12); const quarry = asm.ifgt(4, 0.06);
asm.act(A_BUILD, 1, 1.2); // Clear ground: toward the stone.
asm.act(A_EXCRETE, 3, 1.1); asm.grad(4, G_ROCK, 0, 1);
asm.grad(4, G_SELF, 0, 0.4); asm.grad(5, G_ROCK, 1, 1);
asm.grad(5, G_SELF, 1, 0.4); asm.act(A_MOVE_X, 4, 0.6);
asm.act(A_DRIFT, 4, 0.8); 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.act(A_SPAWN, 1, spec.spawnW);
asm.halt(); 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(); const dig = asm.label();
asm.jump(quarry, dig); asm.jump(spent, dig);
asm.act(A_DIG, 4, 1); // Worked-out ground: quarry what is left for the walls.
asm.grad(4, G_FOOD, 0, 0.3); asm.act(A_DIG, 4, 1.6);
asm.act(A_MOVE_X, 4, 0.3); asm.act(A_EXCRETE, 3, 0.3);
asm.halt(); asm.halt();
} else if (role === 'grazer') { } else if (role === 'grazer') {
// Commutes: follows the food edge, leaves a road behind. // 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_EXCRETE, 3, 0.5);
asm.act(A_SPAWN, 1, spec.spawnW); asm.act(A_SPAWN, 1, spec.spawnW);
asm.halt(); 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 } else { // breeder
// Follows the crowd it feeds on; spawns when the law allows. // Follows the crowd it feeds on; spawns when the law allows.
asm.grad(4, G_KIN, 0, 1); asm.grad(4, G_KIN, 0, 1);
@@ -311,7 +419,8 @@ export function runProgram(memBuf, off, senses, grads, act, code) {
let steps = 0; let steps = 0;
const n = code.length; const n = code.length;
act[0] = 0; act[1] = 0; act[2] = 0; act[3] = 1; act[4] = 0; act[5] = 0; 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; const mem = memBuf;
while (steps++ < MAX_STEPS && pc < n) { while (steps++ < MAX_STEPS && pc < n) {
const ins = code[pc]; 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_ACCELERATE: act[3] *= drive * w + 1; break;
case A_DIG: act[6] += Math.max(0, drive) * w; 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_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; default: break;
} }
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.drop = 0;
mute.program.rock.shatter = 0; mute.program.rock.shatter = 0;
for (let f = 0; f < 600; f++) { live.step(); mute.step(); } for (let f = 0; f < 600; f++) { live.step(); mute.step(); }
assert.ok(rockMass(live) > rockMass(mute) * 1.3, // The sky's ledger: a live song drops stone, a mute one never does.
`rockfall should outpace a mute sky (live ${rockMass(live).toFixed(1)} vs ${rockMass(mute).toFixed(1)})`); 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', () => { 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; let foodAfter = 0;
for (let i = 0; i < w.n; i++) foodAfter += w.resource[i]; for (let i = 0; i < w.n; i++) foodAfter += w.resource[i];
let carry = 0; 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(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)})`); 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], [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 F = w.field;
const b = w.coarseOf[30 * F + 30]; for (let y = 26; y < 35; y++) {
for (let s = 0; s < w.m; s++) w.carry[s * w.fc + b] = 5; 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); 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); 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)})`); `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', () => { test('strains have bounded programs with real branching', () => {
const strains = load('dusk').strains; 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]))); 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.IFGT) || ops.has(OP.IFLT), 'programs branch');
assert.ok(ops.has(OP.SENSE) && ops.has(OP.ACT), 'programs sense and act'); 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, assert.ok(withGenesis.world.count > without.world.count * 1.1,
`genesis world should be more populated (${withGenesis.world.count} vs ${without.world.count})`); `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. // …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()})`); `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.history.alive[0], 0, 'genesis must not record into the film');
assert.equal(withGenesis.world.frame, -1, 'the film starts at frame 0'); assert.equal(withGenesis.world.frame, -1, 'the film starts at frame 0');