Metabolism M2.8: rock — inert resource the song drops and builders quarry, carry and build with; livelier metabolisms
This commit is contained in:
@@ -130,6 +130,17 @@ 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.
|
||||
|
||||
### 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
|
||||
— strong low/mid hits drop stone (crushing what they land on), strong high
|
||||
hits shatter it, drops rain it, breakdowns weather it away. Food does not
|
||||
recharge through rock, so capped ground is barren until opened. Builders
|
||||
**quarry** it (rock → carried burden, and the grinding yields soil), **carry**
|
||||
it, and **build** with it — walls rise where the colony works. Rock is the
|
||||
one thing on the map that is neither food nor life: it shapes where the
|
||||
geography can go at all.
|
||||
|
||||
### Law — regime per section
|
||||
Section boundaries are found where the 4 Hz feature cloud changes its mind
|
||||
(cheap self-similarity, not an FFT of the FFT), classified into
|
||||
@@ -294,6 +305,9 @@ world behind the `Run` is being rebuilt.*
|
||||
the living well timeline. The world's economy is food-limited: colonies
|
||||
bloom on wells, starve off bare ground, and must keep moving to keep
|
||||
eating.
|
||||
- **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.
|
||||
- **M3 — lens** the camera travels (track / reveal / establish) and reserves
|
||||
cuts for musical punctuation.
|
||||
- **M4 — film** mux frames + PCM to video.
|
||||
|
||||
@@ -177,6 +177,36 @@ holds at 640×360 (~12 ms/frame sim+render). Songs still diverge ≫ seeds on
|
||||
the fingerprint (classMix now sorted — the seed permutes the casting, the
|
||||
song decides its shape).
|
||||
|
||||
### M2.8 — rock: the world gains an inert resource ✔
|
||||
|
||||
The films were alive but weightless: nothing on the map was *stuff*. Rock is
|
||||
the correction — an inert resource with mass, presence and consequence:
|
||||
|
||||
- **the field** (`world.rock`) — a rock height per cell; above 0.55 the cell
|
||||
is impassable (advection aims mass at stone and the mass stays, piling
|
||||
against the wall; diffusion only crosses open edges). Food does not recharge
|
||||
through rock: capped ground is barren until someone quarries it open.
|
||||
- **the song's weather** — the map is born with a deterministic scatter of
|
||||
stone (7–15 blobs, from the seed). Strong low/mid hits DROP rock where they
|
||||
land (and crush what they land on); strong high hits SHATTER it. The
|
||||
section regimes gained `rockfall` and `erode` operators: drops rain stone,
|
||||
breakdowns weather it away.
|
||||
- **the masons** — builders gained two VM actions: **dig** (a slow bite out
|
||||
of the rock, which becomes carried burden — and grinding yields soil, a
|
||||
little food, right where they dug) and **build** (the carried burden rises
|
||||
as a wall where the ground is clear). The carry lives in per-block registers
|
||||
of the world, so stone genuinely travels: quarries open, roads run from
|
||||
them, walls rise where the colony loiters.
|
||||
- **livelier organisms** — slower metabolisms (longer lives off food), faster
|
||||
strains, softer bare-ground starvation: agents travel farther between
|
||||
wells, meet more, and work more.
|
||||
|
||||
*Outcome:* 44 tests green, with new pins: the deterministic scatter; the
|
||||
song's rockfall outpacing a mute sky; a stone wall holding a crowd west of it
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
@@ -217,6 +217,17 @@ export function composeTrail(world, palette, exposure = 2.2) {
|
||||
sharp[i * 3 + 2] += v * c[2];
|
||||
}
|
||||
}
|
||||
// The stone, same as the live renderer draws it.
|
||||
if (world.rock) {
|
||||
const stone = palette.stone ?? [0.35, 0.36, 0.4];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const r = world.rock[i];
|
||||
if (r < 1e-3) continue;
|
||||
sharp[i * 3] += r * stone[0] * 2.6;
|
||||
sharp[i * 3 + 1] += r * stone[1] * 2.6;
|
||||
sharp[i * 3 + 2] += r * stone[2] * 2.6;
|
||||
}
|
||||
}
|
||||
const wide = boxBlur(sharp, F, 2);
|
||||
const tight = boxBlur(sharp, F, 1);
|
||||
const out = new Uint8ClampedArray(n * 4);
|
||||
|
||||
@@ -74,6 +74,17 @@ export class Renderer {
|
||||
sharp[i * 3 + 1] += f * fog[1];
|
||||
sharp[i * 3 + 2] += f * fog[2];
|
||||
}
|
||||
// The stone sits ON the ground: inert, cold, unmistakable. Heavy
|
||||
// weight — rock tone-maps to a solid grey mass, not a smudge.
|
||||
const rock = world.rock;
|
||||
const stone = palette.stone ?? [0.35, 0.36, 0.4];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const r = rock[i];
|
||||
if (r < 1e-3) continue;
|
||||
sharp[i * 3] += r * stone[0] * 2.6;
|
||||
sharp[i * 3 + 1] += r * stone[1] * 2.6;
|
||||
sharp[i * 3 + 2] += r * stone[2] * 2.6;
|
||||
}
|
||||
boxBlur(sharp, this.tmp, this.tight, F, 1);
|
||||
boxBlur(sharp, this.tmp, this.wide, F, 3);
|
||||
const out = this.trailRGBA;
|
||||
|
||||
+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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,12 +55,16 @@ export function buildPalette(summary, rng) {
|
||||
|
||||
const bg = hsl(hue + 0.5, 0.40, 0.026 + 0.012 * bright);
|
||||
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);
|
||||
|
||||
return {
|
||||
hue,
|
||||
sat,
|
||||
bg,
|
||||
fog,
|
||||
stone,
|
||||
classes: [c0, c1, c2],
|
||||
bodies,
|
||||
// Trails sit below the agents: the world is dimmer than what moves in it.
|
||||
|
||||
@@ -36,18 +36,18 @@ const lerp = (a, b, t) => a + (b - a) * t;
|
||||
* speed / growth / deposit / split / spawn
|
||||
*/
|
||||
export const REGIMES = {
|
||||
intro: { chemo: 0.7, disperse: 0.9, home: 1.6, turbulence: 0.5, speed: 0.6, growth: 0.7, deposit: 0.8, split: 0.9, spawn: 0.5, peace: 1.4 },
|
||||
build: { chemo: 0.9, disperse: 1.1, home: 0.9, turbulence: 0.8, speed: 1.0, growth: 1.2, deposit: 1.0, split: 1.0, spawn: 1.2, peace: 0.9 },
|
||||
drop: { chemo: 1.5, disperse: 1.7, home: 0.2, turbulence: 2.4, speed: 1.7, growth: 1.7, deposit: 0.6, split: 1.6, spawn: 2.4, peace: 0.25 },
|
||||
breakdown: { chemo: 0.4, disperse: 0.15, home: 2.2, turbulence: 0.15, speed: 0.4, growth: 0.3, deposit: 2.2, split: 1.4, spawn: 0.25, peace: 2.0 },
|
||||
sustain: { chemo: 0.9, disperse: 1.0, home: 0.8, turbulence: 0.8, speed: 0.9, growth: 0.9, deposit: 1.1, split: 1.0, spawn: 0.8, peace: 1.0 },
|
||||
outro: { chemo: 0.5, disperse: 0.5, home: 2.0, turbulence: 0.3, speed: 0.5, growth: 0.4, deposit: 1.8, split: 1.2, spawn: 0.3, peace: 1.6 },
|
||||
intro: { chemo: 0.7, disperse: 0.9, home: 1.6, turbulence: 0.5, speed: 0.6, growth: 0.7, deposit: 0.8, split: 0.9, spawn: 0.5, peace: 1.4, rockfall: 0.4, erode: 0.5 },
|
||||
build: { chemo: 0.9, disperse: 1.1, home: 0.9, turbulence: 0.8, speed: 1.0, growth: 1.2, deposit: 1.0, split: 1.0, spawn: 1.2, peace: 0.9, rockfall: 1.0, erode: 0.6 },
|
||||
drop: { chemo: 1.5, disperse: 1.7, home: 0.2, turbulence: 2.4, speed: 1.7, growth: 1.7, deposit: 0.6, split: 1.6, spawn: 2.4, peace: 0.25, rockfall: 2.2, erode: 0.4 },
|
||||
breakdown: { chemo: 0.4, disperse: 0.15, home: 2.2, turbulence: 0.15, speed: 0.4, growth: 0.3, deposit: 2.2, split: 1.4, spawn: 0.25, peace: 2.0, rockfall: 0.15, erode: 1.8 },
|
||||
sustain: { chemo: 0.9, disperse: 1.0, home: 0.8, turbulence: 0.8, speed: 0.9, growth: 0.9, deposit: 1.1, split: 1.0, spawn: 0.8, peace: 1.0, rockfall: 0.9, erode: 0.8 },
|
||||
outro: { chemo: 0.5, disperse: 0.5, home: 2.0, turbulence: 0.3, speed: 0.5, growth: 0.4, deposit: 1.8, split: 1.2, spawn: 0.3, peace: 1.6, rockfall: 0.3, erode: 1.4 },
|
||||
};
|
||||
|
||||
const OP_KEYS = ['chemo', 'disperse', 'home', 'turbulence', 'speed', 'growth', 'deposit', 'split', 'spawn', 'peace'];
|
||||
const OP_KEYS = ['chemo', 'disperse', 'home', 'turbulence', 'speed', 'growth', 'deposit', 'split', 'spawn', 'peace', 'rockfall', 'erode'];
|
||||
|
||||
// Ceilings so a song's character amplifies a regime without blowing it up.
|
||||
const OP_MAX = { chemo: 2.0, disperse: 2.0, home: 2.4, turbulence: 2.2, speed: 1.8, growth: 1.8, deposit: 2.4, split: 1.8, spawn: 2.2, peace: 2.4 };
|
||||
const OP_MAX = { chemo: 2.0, disperse: 2.0, home: 2.4, turbulence: 2.2, speed: 1.8, growth: 1.8, deposit: 2.4, split: 1.8, spawn: 2.2, peace: 2.4, rockfall: 2.6, erode: 2.4 };
|
||||
|
||||
/**
|
||||
* How the track's own character scales each operator family — this is what
|
||||
@@ -71,6 +71,8 @@ function songScales(summary) {
|
||||
split: 0.85 + 0.35 * density,
|
||||
spawn: 0.80 + 0.50 * beat,
|
||||
peace: 0.85 + 0.30 * tone, // tonal tracks settle easier
|
||||
rockfall: 0.55 + 0.90 * density, // percussive tracks rain rock
|
||||
erode: 0.80 + 0.40 * tone, // tonal tracks weather it
|
||||
};
|
||||
}
|
||||
|
||||
@@ -167,6 +169,16 @@ export function compileProgram(summary, genome, strains) {
|
||||
splash: strains.map((s) => SPLASH_BY_ROLE[s.role] ?? 0.5),
|
||||
voiceStrain,
|
||||
},
|
||||
// Rock: the inert resource. Strong low/mid hits DROP it, strong high
|
||||
// hits SHATTER it; builders quarry it (dig → carry → wall, and the
|
||||
// grinding yields soil). A busy, percussive track throws more stone.
|
||||
rock: {
|
||||
drop: 0.9 * (0.55 + 0.9 * clamp01(summary.onsetDensity / 6)),
|
||||
shatter: 1.1,
|
||||
dig: 0.9,
|
||||
build: 0.8 + 0.5 * clamp01(1 - summary.flatness),
|
||||
scatter: 7 + Math.round(8 * clamp01(summary.onsetDensity / 6)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@ export const S_TIME = 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 SENSE_COUNT = 12;
|
||||
export const S_ROCK = 12; // rock under this block: the quarriable ground
|
||||
export const SENSE_COUNT = 13;
|
||||
|
||||
// Gradient fields, two slots each (x, y): food, own strain, kin, foe, well.
|
||||
export const G_FOOD = 0;
|
||||
@@ -47,7 +48,9 @@ export const A_HIT = 4;
|
||||
export const A_SPAWN = 5;
|
||||
export const A_DRIFT = 6; // gradual direction change following a gradient
|
||||
export const A_ACCELERATE = 7; // speed up / slow down
|
||||
export const ACTION_COUNT = 8;
|
||||
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 REGS = 6;
|
||||
|
||||
export const MAX_INSTR = 32;
|
||||
@@ -89,23 +92,23 @@ class Asm {
|
||||
|
||||
const ROLES = {
|
||||
builder: {
|
||||
base: { speed: 0.030, deposit: 1.50, cap: 1.25, growth: 0.90, eat: 0.036 },
|
||||
base: { speed: 0.038, deposit: 1.50, cap: 1.25, growth: 0.90, eat: 0.032 },
|
||||
hungryAt: 0.50, spawnW: 0.30,
|
||||
},
|
||||
grazer: {
|
||||
base: { speed: 0.075, deposit: 0.75, cap: 1.00, growth: 1.00, eat: 0.050 },
|
||||
base: { speed: 0.095, deposit: 0.75, cap: 1.00, growth: 1.00, eat: 0.042 },
|
||||
hungryAt: 0.42, spawnW: 0.45,
|
||||
},
|
||||
scout: {
|
||||
base: { speed: 0.125, deposit: 0.18, cap: 0.80, growth: 1.05, eat: 0.055 },
|
||||
base: { speed: 0.155, deposit: 0.18, cap: 0.80, growth: 1.05, eat: 0.046 },
|
||||
hungryAt: 0.62, spawnW: 0.60,
|
||||
},
|
||||
sentinel: {
|
||||
base: { speed: 0.058, deposit: 0.55, cap: 1.00, growth: 0.85, eat: 0.045 },
|
||||
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,
|
||||
},
|
||||
breeder: {
|
||||
base: { speed: 0.050, deposit: 0.50, cap: 0.90, growth: 1.25, eat: 0.040 },
|
||||
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,
|
||||
},
|
||||
};
|
||||
@@ -189,14 +192,25 @@ export function compileStrains(summary, genome, rng) {
|
||||
const fed = asm.label();
|
||||
asm.jump(hungryPatch, fed);
|
||||
if (role === 'builder') {
|
||||
// The ink-dropper: inscribes, but never quite stops — a slow orbit
|
||||
// around its ground keeps the colony's edge alive.
|
||||
asm.act(A_EXCRETE, 3, 1.35);
|
||||
// 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.
|
||||
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);
|
||||
asm.act(A_SPAWN, 1, spec.spawnW);
|
||||
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.halt();
|
||||
} else if (role === 'grazer') {
|
||||
// Commutes: follows the food edge, leaves a road behind.
|
||||
asm.grad(4, G_FOOD, 0, 0.7);
|
||||
@@ -297,6 +311,7 @@ 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;
|
||||
const mem = memBuf;
|
||||
while (steps++ < MAX_STEPS && pc < n) {
|
||||
const ins = code[pc];
|
||||
@@ -334,6 +349,8 @@ export function runProgram(memBuf, off, senses, grads, act, code) {
|
||||
case A_SPAWN: act[5] += Math.max(0, drive) * w * 0.012; break;
|
||||
case A_DRIFT: act[0] += drive * w * 0.1; act[1] += drive * w * 0.1; break;
|
||||
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;
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
// M2.8 — rock: the inert resource. It is scattered at the world's birth,
|
||||
// dropped and shattered by the song, impassable to the population, and
|
||||
// quarried by builders — carried, walled, ground into soil.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { analyse } from '../src/audio/analyse.js';
|
||||
import { decodeWav, mono } from '../src/audio/wav.js';
|
||||
import { buildSong } from '../src/song/song.js';
|
||||
import { Run } from '../src/film.js';
|
||||
import { OP, S_ROCK, A_DIG, A_BUILD } from '../src/song/strains.js';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const BANK = resolve(HERE, '../..', 'flow-state/test/songs');
|
||||
|
||||
function load(name, seed = 12345) {
|
||||
const wav = decodeWav(readFileSync(resolve(BANK, `${name}.wav`)));
|
||||
return buildSong(analyse(mono(wav.channels), wav.sampleRate), seed);
|
||||
}
|
||||
|
||||
function run(name, seed = 12345, field = 64) {
|
||||
const song = load(name, seed);
|
||||
const r = new Run(song, { fps: 30, seed, width: 240, height: 135, field, genesis: 0 });
|
||||
return r;
|
||||
}
|
||||
|
||||
const rockMass = (w) => {
|
||||
let s = 0;
|
||||
for (let i = 0; i < w.n; i++) s += w.rock[i];
|
||||
return s;
|
||||
};
|
||||
const maskedCells = (w) => {
|
||||
let s = 0;
|
||||
for (let i = 0; i < w.n; i++) s += w.rockMask[i];
|
||||
return s;
|
||||
};
|
||||
|
||||
test('the world is born with stone in it (deterministically)', () => {
|
||||
const a = run('drone').world;
|
||||
const b = run('drone').world;
|
||||
assert.ok(rockMass(a) > 5, `scatter should hold stone (${rockMass(a).toFixed(1)})`);
|
||||
assert.ok(maskedCells(a) > 0, 'some ground is impassable from birth');
|
||||
assert.deepEqual(Array.from(a.rock), Array.from(b.rock));
|
||||
});
|
||||
|
||||
test('the song drops rock on heavy hits and shatters it on high ones', () => {
|
||||
const live = run('runner').world;
|
||||
const mute = run('runner').world;
|
||||
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)})`);
|
||||
});
|
||||
|
||||
test('rock is a wall: the population cannot cross it', () => {
|
||||
const w = run('drone').world;
|
||||
const F = w.field;
|
||||
// A solid vertical wall of stone down the middle, north to south.
|
||||
for (let y = 0; y < F; y++) {
|
||||
for (let x = 30; x < 34; x++) w.rock[y * F + x] = 1.5;
|
||||
}
|
||||
w.refreshRockMask();
|
||||
// A crowd planted on the west side, food on both sides so the only
|
||||
// reason not to spread east is the wall.
|
||||
for (let y = 8; y < 56; y++) {
|
||||
for (let x = 4; x < 26; x++) {
|
||||
const i = y * F + x;
|
||||
for (let s = 0; s < w.m; s++) w.pop[s][i] = 2;
|
||||
w.alleg[0][i] = 2 * w.m;
|
||||
w.resource[i] = 1.2;
|
||||
}
|
||||
for (let x = 40; x < 60; x++) w.resource[y * F + x] = 1.2;
|
||||
}
|
||||
w.bodies[0].cx = 0.2; w.bodies[0].cy = 0.5; w.bodies[0].alive = true;
|
||||
if (!w.aliveBodies.includes(0)) w.aliveBodies.push(0);
|
||||
w.recount();
|
||||
|
||||
for (let f = 0; f < 300; f++) w.step();
|
||||
|
||||
let east = 0;
|
||||
for (let y = 0; y < F; y++) {
|
||||
for (let x = 34; x < F; x++) east += w.density[y * F + x];
|
||||
}
|
||||
assert.ok(east < w.count * 0.02,
|
||||
`the wall must hold (${east.toFixed(1)} of ${w.count.toFixed(0)} leaked east)`);
|
||||
});
|
||||
|
||||
test('builders quarry: rock goes down, soil and burden appear', () => {
|
||||
const w = run('dusk').world;
|
||||
const F = w.field;
|
||||
// Every strain digs where rock lies.
|
||||
for (const s of w.strains) {
|
||||
s.code = [
|
||||
[OP.SENSE, 4, S_ROCK, 1],
|
||||
[OP.IFGT, 4, 0.05, 3],
|
||||
[OP.HALT],
|
||||
[OP.ACT, A_DIG, 4, 1],
|
||||
[OP.HALT],
|
||||
];
|
||||
}
|
||||
// A rich seam under the crowd.
|
||||
for (let y = 20; y < 40; y++) {
|
||||
for (let x = 20; x < 40; x++) {
|
||||
const i = y * F + x;
|
||||
w.rock[i] = 1.4;
|
||||
for (let s = 0; s < w.m; s++) w.pop[s][i] = 3;
|
||||
w.alleg[0][i] = 3 * w.m;
|
||||
}
|
||||
}
|
||||
w.refreshRockMask();
|
||||
w.bodies[0].alive = true; w.bodies[0].cx = 0.45; w.bodies[0].cy = 0.45;
|
||||
if (!w.aliveBodies.includes(0)) w.aliveBodies.push(0);
|
||||
w.recount();
|
||||
|
||||
const before = rockMass(w);
|
||||
let foodBefore = 0;
|
||||
for (let i = 0; i < w.n; i++) foodBefore += w.resource[i];
|
||||
for (let f = 0; f < 120; f++) w.step();
|
||||
const after = rockMass(w);
|
||||
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];
|
||||
|
||||
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(foodAfter > foodBefore, `grinding yields soil (${foodBefore.toFixed(1)} → ${foodAfter.toFixed(1)})`);
|
||||
});
|
||||
|
||||
test('builders build: carried rock rises as walls', () => {
|
||||
const w = run('dusk').world;
|
||||
// Every strain builds.
|
||||
for (const s of w.strains) {
|
||||
s.code = [
|
||||
[OP.SET, 4, 1],
|
||||
[OP.ACT, A_BUILD, 4, 1],
|
||||
[OP.HALT],
|
||||
];
|
||||
}
|
||||
// A loaded block in the middle of 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;
|
||||
const before = rockMass(w);
|
||||
for (let f = 0; f < 60; f++) w.step();
|
||||
const after = rockMass(w);
|
||||
assert.ok(after > before + 0.5,
|
||||
`carried rock must rise (${before.toFixed(2)} → ${after.toFixed(2)})`);
|
||||
});
|
||||
Reference in New Issue
Block a user