Metabolism M2.8: rock — inert resource the song drops and builders quarry, carry and build with; livelier metabolisms
This commit is contained in:
+194
-12
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user