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
|
other, truce and trade versus skirmish and siege. Society is a property of
|
||||||
the mix, at both scales.
|
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
|
### Law — regime per section
|
||||||
Section boundaries are found where the 4 Hz feature cloud changes its mind
|
Section boundaries are found where the 4 Hz feature cloud changes its mind
|
||||||
(cheap self-similarity, not an FFT of the FFT), classified into
|
(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
|
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
|
bloom on wells, starve off bare ground, and must keep moving to keep
|
||||||
eating.
|
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
|
- **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.
|
||||||
|
|||||||
@@ -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
|
the fingerprint (classMix now sorted — the seed permutes the casting, the
|
||||||
song decides its shape).
|
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
|
### 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
|
||||||
|
|||||||
@@ -217,6 +217,17 @@ export function composeTrail(world, palette, exposure = 2.2) {
|
|||||||
sharp[i * 3 + 2] += v * c[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 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);
|
||||||
|
|||||||
@@ -74,6 +74,17 @@ export class Renderer {
|
|||||||
sharp[i * 3 + 1] += f * fog[1];
|
sharp[i * 3 + 1] += f * fog[1];
|
||||||
sharp[i * 3 + 2] += f * fog[2];
|
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.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;
|
||||||
|
|||||||
+194
-12
@@ -32,7 +32,7 @@ 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_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,
|
G_FOOD, G_SELF, G_KIN, G_FOE, G_WELL,
|
||||||
} from '../song/strains.js';
|
} from '../song/strains.js';
|
||||||
|
|
||||||
@@ -113,6 +113,12 @@ export class World {
|
|||||||
|
|
||||||
this.activity = new Float32Array(32 * 32);
|
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,
|
// 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.
|
||||||
this.noise = new Float32Array(n);
|
this.noise = new Float32Array(n);
|
||||||
@@ -148,6 +154,7 @@ export class World {
|
|||||||
this.blkTrail = Array.from({ length: this.B }, () => new Float32Array(fc));
|
this.blkTrail = Array.from({ length: this.B }, () => new Float32Array(fc));
|
||||||
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.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));
|
||||||
@@ -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
|
// 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.
|
||||||
this.stMem = new Float32Array(this.m * REGS * fc);
|
this.stMem = new Float32Array(this.m * REGS * fc);
|
||||||
@@ -230,6 +239,8 @@ export class World {
|
|||||||
this.birthsThisFrame = 0;
|
this.birthsThisFrame = 0;
|
||||||
this.deathsThisFrame = 0;
|
this.deathsThisFrame = 0;
|
||||||
|
|
||||||
|
this.seedRocks();
|
||||||
|
this.refreshRockMask();
|
||||||
this.foundOpeningBodies();
|
this.foundOpeningBodies();
|
||||||
this.seedInitial();
|
this.seedInitial();
|
||||||
this.runGenesis();
|
this.runGenesis();
|
||||||
@@ -388,6 +399,7 @@ export class World {
|
|||||||
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.inject(this.frame, regime);
|
this.inject(this.frame, regime);
|
||||||
|
this.refreshRockMask();
|
||||||
for (let s = 0; s < this.substeps; s++) this.substep(regime);
|
for (let s = 0; s < this.substeps; s++) this.substep(regime);
|
||||||
this.decay(regime, living);
|
this.decay(regime, living);
|
||||||
this.politics(regime, true);
|
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;
|
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 ---------------------------------------------------------------
|
// ---- wells ---------------------------------------------------------------
|
||||||
|
|
||||||
/** (Re)build a living well's recharge kernel at its current position. */
|
/** (Re)build a living well's recharge kernel at its current position. */
|
||||||
@@ -545,6 +627,7 @@ export class World {
|
|||||||
const living = this.framePrep(regime);
|
const living = this.framePrep(regime);
|
||||||
|
|
||||||
this.inject(f, regime);
|
this.inject(f, regime);
|
||||||
|
this.refreshRockMask();
|
||||||
for (let s = 0; s < this.substeps; s++) this.substep(regime);
|
for (let s = 0; s < this.substeps; s++) this.substep(regime);
|
||||||
this.decay(regime, living);
|
this.decay(regime, living);
|
||||||
this.politics(regime, false);
|
this.politics(regime, false);
|
||||||
@@ -616,6 +699,7 @@ export class World {
|
|||||||
if (!events) return;
|
if (!events) return;
|
||||||
const F = this.field;
|
const F = this.field;
|
||||||
const spawn = regime.operators.spawn;
|
const spawn = regime.operators.spawn;
|
||||||
|
const ops = regime.operators;
|
||||||
const vs = this.program.events.voiceStrain;
|
const vs = this.program.events.voiceStrain;
|
||||||
const splash = this.program.events.splash;
|
const splash = this.program.events.splash;
|
||||||
// The song spawns: how hard a hit births new life where it lands. The
|
// 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 ax = Math.max(0, Math.min(31, (e.x * 32) | 0));
|
||||||
const ay = Math.max(0, Math.min(31, (e.y * 32) | 0));
|
const ay = Math.max(0, Math.min(31, (e.y * 32) | 0));
|
||||||
this.activity[ay * 32 + ax] += 0.5 + 2 * e.strength;
|
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.
|
// The clearest causality: a hit lands as a ring on the ground.
|
||||||
if (e.strength > 0.42) {
|
if (e.strength > 0.42) {
|
||||||
this.pulses.push({ x: e.x, y: e.y, frame, strength: e.strength, voice: e.voice });
|
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) {
|
runStrains(regime) {
|
||||||
const FC = this.FC;
|
const FC = this.FC;
|
||||||
|
const blkRock = this.blkRock;
|
||||||
const fc = FC * FC;
|
const fc = FC * FC;
|
||||||
const m = this.m;
|
const m = this.m;
|
||||||
const blkPop = this.blkPop;
|
const blkPop = this.blkPop;
|
||||||
@@ -723,6 +820,7 @@ 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);
|
||||||
|
|
||||||
// ---- gradients (sign per field × axis) ---------------------
|
// ---- gradients (sign per field × axis) ---------------------
|
||||||
gg[G_FOOD * 2] = Math.sign(gRx);
|
gg[G_FOOD * 2] = Math.sign(gRx);
|
||||||
@@ -760,6 +858,8 @@ export class World {
|
|||||||
actBuf[off + 3 * actFC] = act[3]; // EAT
|
actBuf[off + 3 * actFC] = act[3]; // EAT
|
||||||
actBuf[off + 4 * actFC] = act[4]; // HIT
|
actBuf[off + 4 * actFC] = act[4]; // HIT
|
||||||
actBuf[off + 5 * actFC] = act[5]; // SPAWN
|
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
|
// 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.
|
||||||
@@ -811,6 +911,7 @@ export class World {
|
|||||||
const noise = this.noise;
|
const noise = this.noise;
|
||||||
const noise2 = this.noise2;
|
const noise2 = this.noise2;
|
||||||
const n = this.n;
|
const n = this.n;
|
||||||
|
const rock = this.rock;
|
||||||
|
|
||||||
// ---- 1. block sums ----------------------------------------------------
|
// ---- 1. block sums ----------------------------------------------------
|
||||||
const blkR = this.blkR;
|
const blkR = this.blkR;
|
||||||
@@ -818,14 +919,17 @@ export class World {
|
|||||||
const blkTrail = this.blkTrail;
|
const blkTrail = this.blkTrail;
|
||||||
const blkBody = this.blkBody;
|
const blkBody = this.blkBody;
|
||||||
const blkTotal = this.blkTotal;
|
const blkTotal = this.blkTotal;
|
||||||
|
const blkRock = this.blkRock;
|
||||||
blkR.fill(0);
|
blkR.fill(0);
|
||||||
blkTotal.fill(0);
|
blkTotal.fill(0);
|
||||||
|
blkRock.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];
|
||||||
|
blkRock[b] += rock[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];
|
||||||
@@ -947,7 +1051,7 @@ export class World {
|
|||||||
demand *= sub;
|
demand *= sub;
|
||||||
res[i] = demand > 0 ? Math.max(0, r0 - Math.min(r0, demand)) : r0;
|
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);
|
const society = Math.max(0, 1 - density[i] / societyCeil);
|
||||||
for (let s = 0; s < m; s++) {
|
for (let s = 0; s < m; s++) {
|
||||||
const p = vals[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 ---------------------------------------------------------------
|
// ---- swap ---------------------------------------------------------------
|
||||||
let totalPop = 0;
|
let totalPop = 0;
|
||||||
for (let s = 0; s < m; s++) {
|
for (let s = 0; s < m; s++) {
|
||||||
@@ -1036,6 +1193,7 @@ export class World {
|
|||||||
const coarseOf = this.coarseOf;
|
const coarseOf = this.coarseOf;
|
||||||
const noise = this.noise;
|
const noise = this.noise;
|
||||||
const noise2 = this.noise2;
|
const noise2 = this.noise2;
|
||||||
|
const mask = this.rockMask;
|
||||||
const n = this.n;
|
const n = this.n;
|
||||||
dst.fill(0);
|
dst.fill(0);
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
@@ -1053,8 +1211,12 @@ export class World {
|
|||||||
const mx = (leave * Math.abs(vx)) / w;
|
const mx = (leave * Math.abs(vx)) / w;
|
||||||
const my = (leave * Math.abs(vy)) / w;
|
const my = (leave * Math.abs(vy)) / w;
|
||||||
dst[i] += v - leave;
|
dst[i] += v - leave;
|
||||||
dst[vx > 0.02 ? nRight[i] : vx < -0.02 ? nLeft[i] : i] += mx;
|
// Rock is a wall: mass aimed at a stone cell stays put and
|
||||||
dst[vy > 0.02 ? nDown[i] : vy < -0.02 ? nUp[i] : i] += my;
|
// 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) {
|
if (track) {
|
||||||
const ax = ((i % F) * 32 / F) | 0;
|
const ax = ((i % F) * 32 / F) | 0;
|
||||||
@@ -1066,13 +1228,23 @@ export class World {
|
|||||||
dst[i] += v;
|
dst[i] += v;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diffusion through the four neighbours (conservative).
|
// Diffusion through the OPEN neighbours only: rock reflects the
|
||||||
const d = (src[nUp[i]] + src[nDown[i]] + src[nLeft[i]] + src[nRight[i]] - 4 * v) * diff * sub;
|
// haze instead of drinking it (still conservative overall).
|
||||||
dst[i] += d * 0.5;
|
let open = 0;
|
||||||
dst[nUp[i]] -= d * 0.125;
|
let nb = 0;
|
||||||
dst[nDown[i]] -= d * 0.125;
|
if (!mask[nUp[i]]) { nb += src[nUp[i]]; open++; }
|
||||||
dst[nLeft[i]] -= d * 0.125;
|
if (!mask[nDown[i]]) { nb += src[nDown[i]]; open++; }
|
||||||
dst[nRight[i]] -= d * 0.125;
|
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);
|
const actK = Math.pow(0.5, dt / 1.4);
|
||||||
for (let i = 0; i < act.length; i++) act[i] *= actK;
|
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 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;
|
||||||
const w = well.kernel.w;
|
const w = well.kernel.w;
|
||||||
for (let i = 0; i < idx.length; i++) {
|
for (let i = 0; i < idx.length; i++) {
|
||||||
const cell = idx[i];
|
const cell = idx[i];
|
||||||
|
if (mask[cell]) continue;
|
||||||
res[cell] = Math.min(1.6, res[cell] + add * w[i]);
|
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 bg = hsl(hue + 0.5, 0.40, 0.026 + 0.012 * bright);
|
||||||
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
|
||||||
|
// ground, clearly not alive, clearly not food.
|
||||||
|
const stone = hsl(hue + 0.5, 0.12, 0.44 + 0.06 * bright);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hue,
|
hue,
|
||||||
sat,
|
sat,
|
||||||
bg,
|
bg,
|
||||||
fog,
|
fog,
|
||||||
|
stone,
|
||||||
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.
|
||||||
|
|||||||
@@ -36,18 +36,18 @@ const lerp = (a, b, t) => a + (b - a) * t;
|
|||||||
* speed / growth / deposit / split / spawn
|
* speed / growth / deposit / split / spawn
|
||||||
*/
|
*/
|
||||||
export const REGIMES = {
|
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 },
|
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 },
|
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 },
|
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 },
|
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 },
|
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 },
|
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.
|
// 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
|
* 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,
|
split: 0.85 + 0.35 * density,
|
||||||
spawn: 0.80 + 0.50 * beat,
|
spawn: 0.80 + 0.50 * beat,
|
||||||
peace: 0.85 + 0.30 * tone, // tonal tracks settle easier
|
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),
|
splash: strains.map((s) => SPLASH_BY_ROLE[s.role] ?? 0.5),
|
||||||
voiceStrain,
|
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_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 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.
|
// Gradient fields, two slots each (x, y): food, own strain, kin, foe, well.
|
||||||
export const G_FOOD = 0;
|
export const G_FOOD = 0;
|
||||||
@@ -47,7 +48,9 @@ export const A_HIT = 4;
|
|||||||
export const A_SPAWN = 5;
|
export const A_SPAWN = 5;
|
||||||
export const A_DRIFT = 6; // gradual direction change following a gradient
|
export const A_DRIFT = 6; // gradual direction change following a gradient
|
||||||
export const A_ACCELERATE = 7; // speed up / slow down
|
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 REGS = 6;
|
||||||
|
|
||||||
export const MAX_INSTR = 32;
|
export const MAX_INSTR = 32;
|
||||||
@@ -89,23 +92,23 @@ class Asm {
|
|||||||
|
|
||||||
const ROLES = {
|
const ROLES = {
|
||||||
builder: {
|
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,
|
hungryAt: 0.50, spawnW: 0.30,
|
||||||
},
|
},
|
||||||
grazer: {
|
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,
|
hungryAt: 0.42, spawnW: 0.45,
|
||||||
},
|
},
|
||||||
scout: {
|
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,
|
hungryAt: 0.62, spawnW: 0.60,
|
||||||
},
|
},
|
||||||
sentinel: {
|
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,
|
hungryAt: 0.50, spawnW: 0.25, foeAt: 0.10,
|
||||||
},
|
},
|
||||||
breeder: {
|
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,
|
hungryAt: 0.55, spawnW: 0.85, kinAt: 0.12,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -189,14 +192,25 @@ export function compileStrains(summary, genome, rng) {
|
|||||||
const fed = asm.label();
|
const fed = asm.label();
|
||||||
asm.jump(hungryPatch, fed);
|
asm.jump(hungryPatch, fed);
|
||||||
if (role === 'builder') {
|
if (role === 'builder') {
|
||||||
// The ink-dropper: inscribes, but never quite stops — a slow orbit
|
// The mason: where rock lies underfoot it quarries (rock becomes
|
||||||
// around its ground keeps the colony's edge alive.
|
// carried burden and, ground down, soil); where the ground is
|
||||||
asm.act(A_EXCRETE, 3, 1.35);
|
// 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(4, G_SELF, 0, 0.4);
|
||||||
asm.grad(5, G_SELF, 1, 0.4);
|
asm.grad(5, G_SELF, 1, 0.4);
|
||||||
asm.act(A_DRIFT, 4, 0.8);
|
asm.act(A_DRIFT, 4, 0.8);
|
||||||
asm.act(A_SPAWN, 1, spec.spawnW);
|
asm.act(A_SPAWN, 1, spec.spawnW);
|
||||||
asm.halt();
|
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') {
|
} else if (role === 'grazer') {
|
||||||
// Commutes: follows the food edge, leaves a road behind.
|
// Commutes: follows the food edge, leaves a road behind.
|
||||||
asm.grad(4, G_FOOD, 0, 0.7);
|
asm.grad(4, G_FOOD, 0, 0.7);
|
||||||
@@ -297,6 +311,7 @@ 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;
|
||||||
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];
|
||||||
@@ -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_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_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_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;
|
default: break;
|
||||||
}
|
}
|
||||||
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