Metabolism M2.8: rock — inert resource the song drops and builders quarry, carry and build with; livelier metabolisms

This commit is contained in:
Dejvino
2026-10-28 11:36:30 +01:00
parent 8abefb16b7
commit 1d497e556e
9 changed files with 464 additions and 30 deletions
+194 -12
View File
@@ -32,7 +32,7 @@ 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_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,
} from '../song/strains.js';
@@ -113,6 +113,12 @@ export class World {
this.activity = new Float32Array(32 * 32);
// Rock (M2.8): the inert resource. A barrier the organisms cannot
// cross, that the song drops and shatters, and that builders quarry,
// carry and wall with. rockMask marks the impassable cells.
this.rock = new Float32Array(n);
this.rockMask = new Uint8Array(n);
// Deterministic jitter: static signed noise fields from the seeded RNG,
// read every substep. The track's flatness sets how loud it is.
this.noise = new Float32Array(n);
@@ -148,6 +154,7 @@ export class World {
this.blkTrail = Array.from({ length: this.B }, () => new Float32Array(fc));
this.blkBody = Array.from({ length: this.B }, () => new Float32Array(fc));
this.blkTotal = new Float32Array(fc);
this.blkRock = 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));
@@ -184,6 +191,8 @@ export class World {
}
}
this.carry = new Float32Array(this.m * fc); // quarried burden per block
// Strain programs run on this decision grid: registers are the
// strains' persistent memory, the action slots carry each decision.
this.stMem = new Float32Array(this.m * REGS * fc);
@@ -230,6 +239,8 @@ export class World {
this.birthsThisFrame = 0;
this.deathsThisFrame = 0;
this.seedRocks();
this.refreshRockMask();
this.foundOpeningBodies();
this.seedInitial();
this.runGenesis();
@@ -388,6 +399,7 @@ export class World {
this.program.diplomacy.base * 0.6 + 0.35 * (regime.operators.peace - 1)));
const living = this.framePrep(regime);
this.inject(this.frame, regime);
this.refreshRockMask();
for (let s = 0; s < this.substeps; s++) this.substep(regime);
this.decay(regime, living);
this.politics(regime, true);
@@ -463,6 +475,76 @@ export class World {
for (let i = 0; i < idx.length; i++) res[idx[i]] += w[i] * k;
}
// ---- rock ---------------------------------------------------------------
/** Scatter the world's starting stone: deterministic, from the seed. */
seedRocks() {
const n = this.program.rock.scatter;
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();
this.rockSplat(x, y, sigma, amount);
}
}
/**
* Add (or, with a negative amount, shave) rock in a Gaussian blob.
* Returns the touched cells so callers can crush or yield.
*/
rockSplat(x, y, spread, amount) {
const F = this.field;
const sigma = Math.max(0.8, spread * F);
const r = Math.ceil(sigma * 2.4);
const cx = Math.round(x * (F - 1));
const cy = Math.round(y * (F - 1));
const denom = 1 / (2 * sigma * sigma);
let total = 0;
const idx = [];
const w = [];
for (let dy = -r; dy <= r; dy++) {
const py = cy + dy;
if (py < 0 || py >= F) continue;
for (let dx = -r; dx <= r; dx++) {
const px = cx + dx;
if (px < 0 || px >= F) continue;
const k = Math.exp(-(dx * dx + dy * dy) * denom);
if (k < 0.01) continue;
idx.push(py * F + px);
w.push(k);
total += k;
}
}
if (total <= 0) return { idx: [], w: [], k: 0 };
const k = amount / total;
const rock = this.rock;
const pop = this.pop;
for (let i = 0; i < idx.length; i++) {
const cell = idx[i];
if (amount >= 0) {
const before = rock[cell];
rock[cell] = Math.min(2.2, rock[cell] + w[i] * k);
const gained = rock[cell] - before;
// A rockfall crushes what it lands on.
if (gained > 0.05) {
const crush = Math.max(0, 1 - gained * 0.55);
for (let s = 0; s < this.m; s++) pop[s][cell] *= crush;
}
} else {
rock[cell] = Math.max(0, rock[cell] + w[i] * k);
}
}
return { idx, w, k };
}
/** 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;
}
// ---- wells ---------------------------------------------------------------
/** (Re)build a living well's recharge kernel at its current position. */
@@ -545,6 +627,7 @@ export class World {
const living = this.framePrep(regime);
this.inject(f, regime);
this.refreshRockMask();
for (let s = 0; s < this.substeps; s++) this.substep(regime);
this.decay(regime, living);
this.politics(regime, false);
@@ -616,6 +699,7 @@ export class World {
if (!events) return;
const F = this.field;
const spawn = regime.operators.spawn;
const ops = regime.operators;
const vs = this.program.events.voiceStrain;
const splash = this.program.events.splash;
// The song spawns: how hard a hit births new life where it lands. The
@@ -638,6 +722,18 @@ export class World {
const ax = Math.max(0, Math.min(31, (e.x * 32) | 0));
const ay = Math.max(0, Math.min(31, (e.y * 32) | 0));
this.activity[ay * 32 + ax] += 0.5 + 2 * e.strength;
// Rock follows the song's own weather: strong low and mid hits
// DROP stone where they land (and crush what they land on);
// strong high hits SHATTER it — the percussion breaks the rock.
if (e.strength > 0.6) {
const rockPol = this.program.rock;
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.activity[ay * 32 + ax] += 1.5;
}
// The clearest causality: a hit lands as a ring on the ground.
if (e.strength > 0.42) {
this.pulses.push({ x: e.x, y: e.y, frame, strength: e.strength, voice: e.voice });
@@ -654,6 +750,7 @@ export class World {
*/
runStrains(regime) {
const FC = this.FC;
const blkRock = this.blkRock;
const fc = FC * FC;
const m = this.m;
const blkPop = this.blkPop;
@@ -723,6 +820,7 @@ 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);
// ---- gradients (sign per field × axis) ---------------------
gg[G_FOOD * 2] = Math.sign(gRx);
@@ -760,6 +858,8 @@ export class World {
actBuf[off + 3 * actFC] = act[3]; // EAT
actBuf[off + 4 * actFC] = act[4]; // HIT
actBuf[off + 5 * actFC] = act[5]; // SPAWN
actBuf[off + 6 * actFC] = act[6]; // DIG
actBuf[off + 7 * actFC] = act[7]; // BUILD
// Synergy: this strain breeds fastest in a mixed, same-body
// crowd — the share of the block's population that is kin.
@@ -811,6 +911,7 @@ export class World {
const noise = this.noise;
const noise2 = this.noise2;
const n = this.n;
const rock = this.rock;
// ---- 1. block sums ----------------------------------------------------
const blkR = this.blkR;
@@ -818,14 +919,17 @@ export class World {
const blkTrail = this.blkTrail;
const blkBody = this.blkBody;
const blkTotal = this.blkTotal;
const blkRock = this.blkRock;
blkR.fill(0);
blkTotal.fill(0);
blkRock.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];
blkRock[b] += rock[i];
if (d > 1e-6) {
blkTotal[b] += d;
for (let s = 0; s < m; s++) blkPop[s][b] += pop[s][i];
@@ -947,7 +1051,7 @@ export class World {
demand *= sub;
res[i] = demand > 0 ? Math.max(0, r0 - Math.min(r0, demand)) : r0;
const starve = (1 - foodFactor) * (0.35 + 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);
for (let s = 0; s < m; s++) {
const p = vals[s];
@@ -1005,6 +1109,59 @@ 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++) {
@@ -1036,6 +1193,7 @@ export class World {
const coarseOf = this.coarseOf;
const noise = this.noise;
const noise2 = this.noise2;
const mask = this.rockMask;
const n = this.n;
dst.fill(0);
for (let i = 0; i < n; i++) {
@@ -1053,8 +1211,12 @@ export class World {
const mx = (leave * Math.abs(vx)) / w;
const my = (leave * Math.abs(vy)) / w;
dst[i] += v - leave;
dst[vx > 0.02 ? nRight[i] : vx < -0.02 ? nLeft[i] : i] += mx;
dst[vy > 0.02 ? nDown[i] : vy < -0.02 ? nUp[i] : i] += my;
// Rock is a wall: mass aimed at a stone cell stays put and
// piles against it.
const tx = vx > 0.02 ? nRight[i] : vx < -0.02 ? nLeft[i] : i;
const ty = vy > 0.02 ? nDown[i] : vy < -0.02 ? nUp[i] : i;
dst[mask[tx] ? i : tx] += mx;
dst[mask[ty] ? i : ty] += my;
if (track) {
const ax = ((i % F) * 32 / F) | 0;
@@ -1066,13 +1228,23 @@ export class World {
dst[i] += v;
}
// Diffusion through the four neighbours (conservative).
const d = (src[nUp[i]] + src[nDown[i]] + src[nLeft[i]] + src[nRight[i]] - 4 * v) * diff * sub;
dst[i] += d * 0.5;
dst[nUp[i]] -= d * 0.125;
dst[nDown[i]] -= d * 0.125;
dst[nLeft[i]] -= d * 0.125;
dst[nRight[i]] -= d * 0.125;
// Diffusion through the OPEN neighbours only: rock reflects the
// haze instead of drinking it (still conservative overall).
let open = 0;
let nb = 0;
if (!mask[nUp[i]]) { nb += src[nUp[i]]; open++; }
if (!mask[nDown[i]]) { nb += src[nDown[i]]; open++; }
if (!mask[nLeft[i]]) { nb += src[nLeft[i]]; open++; }
if (!mask[nRight[i]]) { nb += src[nRight[i]]; open++; }
if (open > 0) {
const d = (nb - open * v) * diff * sub;
dst[i] += d * 0.5;
const share = d * 0.5 / open;
if (!mask[nUp[i]]) dst[nUp[i]] -= share;
if (!mask[nDown[i]]) dst[nDown[i]] -= share;
if (!mask[nLeft[i]]) dst[nLeft[i]] -= share;
if (!mask[nRight[i]]) dst[nRight[i]] -= share;
}
}
}
@@ -1252,14 +1424,24 @@ export class World {
const actK = Math.pow(0.5, dt / 1.4);
for (let i = 0; i < act.length; i++) act[i] *= actK;
// Wells recharge to a ceiling; the section's energy sets the rate.
// Erosion: the section's erode operator sets how fast the stone
// weathers away. Breakdowns clear the ground; drops raise it.
const rock = this.rock;
const rockHalf = 60 / (0.4 + 0.8 * ops.erode);
const rockK = Math.pow(0.5, dt / rockHalf);
for (let i = 0; i < rock.length; i++) rock[i] *= rockK;
// 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;
const w = well.kernel.w;
for (let i = 0; i < idx.length; i++) {
const cell = idx[i];
if (mask[cell]) continue;
res[cell] = Math.min(1.6, res[cell] + add * w[i]);
}
}