Metabolism M2.9: flora islands, gardens, walls, farmer strain, aimed work (VM AIM actions), food-limited ecology rebalance
This commit is contained in:
@@ -130,6 +130,23 @@ locked with the kick pairs diggers with haulers — and it feeds the
|
||||
other, truce and trade versus skirmish and siege. Society is a property of
|
||||
the mix, at both scales.
|
||||
|
||||
### Flora, gardens, walls — the workable world
|
||||
Three made things with feedback loops, each demanding its own instructions:
|
||||
|
||||
- **flora** — wild food in ISLANDS: a dozen scattered patches that are food,
|
||||
regrow on their own, and bloom when the world starves (the pioneers seed it
|
||||
back), receding under the grazing of a well-fed crowd. Never enough to
|
||||
sustain a large world; always somewhere worth walking to.
|
||||
- **gardens** — stone tilled by masons into cultivated beds that yield food
|
||||
for a while, then set back into rock. Tended beds (the farmers' work)
|
||||
yield up to threefold. Agriculture slowly consumes the world's stone.
|
||||
- **walls** — quarried burden built into barriers: they defend the
|
||||
settlement, enemies strike and sap them, and unmaintained walls weather
|
||||
away. Nothing on this map is finished.
|
||||
|
||||
All work is AIMED — the VM's work actions land where the agent faces, not
|
||||
beneath it — and the work-senses look the same way.
|
||||
|
||||
### Rock — the inert resource
|
||||
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
|
||||
@@ -308,6 +325,8 @@ world behind the `Run` is being rebuilt.*
|
||||
- **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.
|
||||
- **M2.9 — flora, gardens, walls, farmers, aimed work** ✔ built: the
|
||||
interactable layer — see EVOLUTION.md.
|
||||
- **M3 — lens** the camera travels (track / reveal / establish) and reserves
|
||||
cuts for musical punctuation.
|
||||
- **M4 — film** mux frames + PCM to video.
|
||||
|
||||
@@ -207,6 +207,48 @@ for 10 s straight; quarries opening with soil and burden appearing; carried
|
||||
rock rising as walls. The strips read it directly: grey masses the colonies
|
||||
grow around, fronts grinding along them.
|
||||
|
||||
### M2.9 — flora, gardens, walls: a world of workable things ✔
|
||||
|
||||
After M2.8 the films had stone but nothing to DO with it, and the ecology
|
||||
still lived on wells alone. This iteration added the interactable layer —
|
||||
items with feedback loops, each needing its own LittleVM instructions, hence
|
||||
real specialisation:
|
||||
|
||||
- **aimed work** — the VM gained an AIM (two pseudo-actions setting a heading;
|
||||
it defaults to the direction of travel), and every work action lands at the
|
||||
block the agent is FACING, never beneath its feet: masons carve the face of
|
||||
the wall ahead, farmers tend the bed they walk toward, strikers hit where
|
||||
the enemy stands. The work-senses (rock, garden) look where the last aim
|
||||
pointed — sight follows intent.
|
||||
- **gardens** — the mason TILLS fresh stone into a cultivated bed (brought to
|
||||
maturity one cell at a time; a dusting of fertility would exhaust as fast
|
||||
as it is laid). A bed yields food slowly for a while, then the spent soil
|
||||
sets back into rock: agriculture slowly eats the world's stone, and the
|
||||
song's rockfalls replenish it. Digging itself yields NO food — stone is for
|
||||
building; food comes from wells, flora, events and gardens.
|
||||
- **walls** — the carried burden rises as a wall (pooled per block, mortared
|
||||
along a rounded footprint). Walls block movement, so they defend; enemies
|
||||
strike at their aim target and SAP the stone at the front — sieges happen,
|
||||
and unmaintained walls weather away. The masons are never done.
|
||||
- **farmers** — a new strain role: it walks the field rows and TENDS the beds
|
||||
(a tended bed yields up to threefold), opens new beds into fresh stone, and
|
||||
commutes along the garden gradient. Work is foraging: the hungry branches
|
||||
of mason and husbandman work the ground they aim at, because that is where
|
||||
their food comes from.
|
||||
- **flora** — the world's pioneers, living in ISLANDS scattered at birth
|
||||
(a dozen small patches; they can never spread beyond their own seed
|
||||
ground). Each stand is food — grazed down by hungry crowds, never wiped out
|
||||
(seed-rain), regrowing slowly. Its vigour tracks the world's famine: a
|
||||
starving or dwindling world blooms with flora and is seeded back to life;
|
||||
a strong, well-fed world grazes it back to a whisper. Rendered as speckles
|
||||
— a few apple trees and bushes here and there, never a garden of Eden.
|
||||
|
||||
*Balance notes carved into the tests:* the ecology is food-limited (the gate
|
||||
is nearly shut on bare ground; colonies that outgrow their ground starve back
|
||||
and wander); rock has a budget that survives a whole film (calm songs keep
|
||||
sparse stone, percussive ones accumulate walls); agriculture consumes stone
|
||||
instead of minting it. 44 tests green; realtime holds at 640×360.
|
||||
|
||||
### M3 — the lens: travel, don't cut
|
||||
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
|
||||
|
||||
@@ -228,6 +228,16 @@ export function composeTrail(world, palette, exposure = 2.2) {
|
||||
sharp[i * 3 + 2] += r * stone[2] * 2.6;
|
||||
}
|
||||
}
|
||||
if (world.garden) {
|
||||
const soil = palette.garden ?? [0.16, 0.2, 0.1];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const g = world.garden[i];
|
||||
if (g < 1e-3) continue;
|
||||
sharp[i * 3] += g * soil[0] * 2.2;
|
||||
sharp[i * 3 + 1] += g * soil[1] * 2.2;
|
||||
sharp[i * 3 + 2] += g * soil[2] * 2.2;
|
||||
}
|
||||
}
|
||||
const wide = boxBlur(sharp, F, 2);
|
||||
const tight = boxBlur(sharp, F, 1);
|
||||
const out = new Uint8ClampedArray(n * 4);
|
||||
|
||||
@@ -15,8 +15,8 @@ import { Bitmap } from './bitmap.js';
|
||||
const PER_SPECKS = 2.2;
|
||||
const MAX_SPECKS = 60000;
|
||||
// Density glow: how fast a cell's population turns into brightness.
|
||||
const DENSITY_K = 0.11;
|
||||
const DENSITY_EXPOSURE = 1.25;
|
||||
const DENSITY_K = 0.07;
|
||||
const DENSITY_EXPOSURE = 1.0;
|
||||
|
||||
export class Renderer {
|
||||
constructor(width, height, { exposure = 2.6, grain = 2.4, vignette = 0.55 } = {}) {
|
||||
@@ -85,6 +85,27 @@ export class Renderer {
|
||||
sharp[i * 3 + 1] += r * stone[1] * 2.6;
|
||||
sharp[i * 3 + 2] += r * stone[2] * 2.6;
|
||||
}
|
||||
// The flora: a whisper of wild green under everything.
|
||||
if (world.flora) {
|
||||
const veg = palette.flora ?? [0.2, 0.45, 0.2];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const fl = world.flora[i];
|
||||
if (fl < 1e-2) continue;
|
||||
sharp[i * 3] += fl * veg[0] * 0.15;
|
||||
sharp[i * 3 + 1] += fl * veg[1] * 0.15;
|
||||
sharp[i * 3 + 2] += fl * veg[2] * 0.15;
|
||||
}
|
||||
}
|
||||
// The gardens: worked ground, dark and rich — the body's fields.
|
||||
const beds = world.garden;
|
||||
const soil = palette.garden ?? [0.16, 0.2, 0.1];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const g = beds[i];
|
||||
if (g < 1e-3) continue;
|
||||
sharp[i * 3] += g * soil[0] * 2.2;
|
||||
sharp[i * 3 + 1] += g * soil[1] * 2.2;
|
||||
sharp[i * 3 + 2] += g * soil[2] * 2.2;
|
||||
}
|
||||
boxBlur(sharp, this.tmp, this.tight, F, 1);
|
||||
boxBlur(sharp, this.tmp, this.wide, F, 3);
|
||||
const out = this.trailRGBA;
|
||||
@@ -224,6 +245,45 @@ export class Renderer {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The flora: sparse plant-specks — the map reads as inhabited by
|
||||
// vegetation, not only by bodies.
|
||||
if (world.flora) {
|
||||
const veg = palette.flora ?? [0.2, 0.45, 0.2];
|
||||
const vr = veg[0] * 255 * 0.75;
|
||||
const vg = veg[1] * 255 * 0.75;
|
||||
const vb = veg[2] * 255 * 0.75;
|
||||
const cx = cam.cx;
|
||||
const cy = cam.cy;
|
||||
for (let y = 0; y < F && budget > 0; y++) {
|
||||
const wy = (y + 0.5) / F;
|
||||
const sy = (wy - cy) * scale + halfH;
|
||||
if (sy < -2 || sy > this.h + 2) continue;
|
||||
for (let x = 0; x < F && budget > 0; x++) {
|
||||
const i = y * F + x;
|
||||
const fl = world.flora[i];
|
||||
if (fl < 0.1) continue;
|
||||
const wx = (x + 0.5) / F;
|
||||
const sx = (wx - cx) * scale + halfW;
|
||||
if (sx < -2 || sx > this.w + 2) continue;
|
||||
const count = Math.min(budget, Math.min(3, 1 + Math.round(fl * 4)));
|
||||
const a = 0.20 + 0.38 * Math.min(1, fl / 0.22);
|
||||
for (let k = 0; k < count; k++) {
|
||||
const seed = (i * 2654435761 + k * 2246822519) >>> 0;
|
||||
const s2 = (Math.imul(seed ^ (seed >>> 13), 1274126177) ^ 2246822519) >>> 0;
|
||||
const px = sx + ((s2 >>> 24) / 255 - 0.5) * 2.4;
|
||||
const py = sy + (((s2 >>> 16) & 255) / 255 - 0.5) * 2.4;
|
||||
const ix = px | 0;
|
||||
const iy = py | 0;
|
||||
if (ix < 0 || iy < 0 || ix >= this.w || iy >= this.h) continue;
|
||||
const o = (iy * this.w + ix) * 4;
|
||||
d[o] = d[o] + vr * a >= 255 ? 255 : d[o] + vr * a;
|
||||
d[o + 1] = d[o + 1] + vg * a >= 255 ? 255 : d[o + 1] + vg * a;
|
||||
d[o + 2] = d[o + 2] + vb * a >= 255 ? 255 : d[o + 2] + vb * a;
|
||||
}
|
||||
budget -= count;
|
||||
}
|
||||
}
|
||||
}
|
||||
return budget;
|
||||
}
|
||||
|
||||
|
||||
+368
-76
@@ -32,8 +32,9 @@ 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_ROCK,
|
||||
G_FOOD, G_SELF, G_KIN, G_FOE, G_WELL,
|
||||
S_FOOD, S_SELF, S_KIN, S_FOE, S_TRAIL, S_LAW, S_HUNGER, S_EDGE, S_TIME, S_TOTAL, S_WELL, S_PEACE, S_ROCK, S_GARDEN,
|
||||
G_GARDEN,
|
||||
G_FOOD, G_SELF, G_KIN, G_FOE, G_WELL, G_ROCK,
|
||||
} from '../song/strains.js';
|
||||
|
||||
const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v);
|
||||
@@ -51,6 +52,38 @@ const DEPOSIT_SCALE = 0.012;
|
||||
// bodies are founded once and persist; war kills, it does not convert.
|
||||
const COLONIZE_EVERY = 60; // ~2 s: a blessed frontier settlement may found
|
||||
|
||||
/** A normalised Gaussian footprint (cell indices + weights) per coarse block. */
|
||||
function buildBlockKernels(F, FC) {
|
||||
const kernels = [];
|
||||
const bs = F / FC;
|
||||
for (let b = 0; b < FC * FC; b++) {
|
||||
const cx = ((b % FC) + 0.5) * bs - 0.5;
|
||||
const cy = (((b / FC) | 0) + 0.5) * bs - 0.5;
|
||||
const sig = Math.max(1.4, bs * 0.9);
|
||||
const r = Math.ceil(sig * 2.2);
|
||||
const denom = 1 / (2 * sig * sig);
|
||||
const idx = [];
|
||||
const w = [];
|
||||
let total = 0;
|
||||
for (let dy = -r; dy <= r; dy++) {
|
||||
const py = Math.round(cy) + dy;
|
||||
if (py < 0 || py >= F) continue;
|
||||
for (let dx = -r; dx <= r; dx++) {
|
||||
const px = Math.round(cx) + dx;
|
||||
if (px < 0 || px >= F) continue;
|
||||
const g = Math.exp(-(dx * dx + dy * dy) * denom);
|
||||
if (g < 0.02) continue;
|
||||
idx.push(py * F + px);
|
||||
w.push(g);
|
||||
total += g;
|
||||
}
|
||||
}
|
||||
const k = total > 0 ? 1 / total : 0;
|
||||
kernels.push({ idx: Int32Array.from(idx), w: Float32Array.from(w, (v) => v * k) });
|
||||
}
|
||||
return kernels;
|
||||
}
|
||||
|
||||
export class World {
|
||||
constructor(song, { fps = 30, field = FIELD, seed = song.seed, substeps = SUBSTEPS, genesis } = {}) {
|
||||
this.song = song;
|
||||
@@ -118,6 +151,24 @@ export class World {
|
||||
// carry and wall with. rockMask marks the impassable cells.
|
||||
this.rock = new Float32Array(n);
|
||||
this.rockMask = new Uint8Array(n);
|
||||
this.rockFallen = 0; // total stone the song has dropped
|
||||
// Flora (M2.9): the wild plants — the world's pioneers, living in
|
||||
// ISLANDS scattered at birth. They ARE food, they regrow on their
|
||||
// own, and they BLOOM when the world is starving or dwindling
|
||||
// (seeding it back to life), then recede once the population is
|
||||
// strong and feeding itself again. Never enough to sustain a
|
||||
// large world, and never anywhere but on their own seed ground.
|
||||
this.flora = new Float32Array(n);
|
||||
this.floraSeed = new Uint8Array(n); // the islands: where flora may live
|
||||
this.famine = 0; // smoothed mean hunger of the population
|
||||
this.peakPop = 0; // recent population high-water mark
|
||||
this.hungerMass = 0; // per-frame accumulator (the reaction fills it)
|
||||
// Cultivated ground: worked stone that slowly yields food, then
|
||||
// petrifies back to rock. garden[i] is the fertility left.
|
||||
this.garden = new Float32Array(n);
|
||||
this.gardenSrc = new Float32Array(n);
|
||||
// Recent tending, per cell: a visited garden yields far more.
|
||||
this.tend = new Float32Array(n);
|
||||
|
||||
// Deterministic jitter: static signed noise fields from the seeded RNG,
|
||||
// read every substep. The track's flatness sets how loud it is.
|
||||
@@ -155,6 +206,7 @@ export class World {
|
||||
this.blkBody = Array.from({ length: this.B }, () => new Float32Array(fc));
|
||||
this.blkTotal = new Float32Array(fc);
|
||||
this.blkRock = new Float32Array(fc);
|
||||
this.blkGarden = 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));
|
||||
@@ -162,12 +214,18 @@ export class World {
|
||||
this.velX = new Float32Array(fc);
|
||||
this.velY = new Float32Array(fc);
|
||||
this.wellField = new Float32Array(fc);
|
||||
this.rockField = new Float32Array(fc); // smoothed stone: the far smell
|
||||
this.ancX = Array.from({ length: this.m }, () => new Float32Array(fc));
|
||||
this.ancY = Array.from({ length: this.m }, () => new Float32Array(fc));
|
||||
this.synergy = new Float32Array(this.m * fc);
|
||||
this.valScratch = new Float64Array(this.m);
|
||||
this.growScratch = new Float64Array(this.m);
|
||||
this.hostility = new Float32Array(fc);
|
||||
this.hitAt = new Float32Array(fc);
|
||||
// The last aim per (strain, block): the senses of WORK look where
|
||||
// the work points, one substep back — sight follows intent.
|
||||
this.aimX = new Int8Array(this.m * fc);
|
||||
this.aimY = new Int8Array(this.m * fc);
|
||||
this.cUp = new Int32Array(fc);
|
||||
this.cDown = new Int32Array(fc);
|
||||
this.cLeft = new Int32Array(fc);
|
||||
@@ -191,7 +249,14 @@ export class World {
|
||||
}
|
||||
}
|
||||
|
||||
this.carry = new Float32Array(this.m * fc); // quarried burden per block
|
||||
// Quarried burden per strain, a fine field advected with the
|
||||
// population flux — stone is genuinely HAULED: dug at the quarry,
|
||||
// carried with the crowd, walled at home.
|
||||
this.carry = Array.from({ length: this.m }, () => new Float32Array(n));
|
||||
this.carryNext = Array.from({ length: this.m }, () => new Float32Array(n));
|
||||
// Rounded build footprints: a Gaussian kernel per coarse block, so
|
||||
// walls rise organically instead of as square tiles.
|
||||
this.buildKernel = buildBlockKernels(field, Math.max(16, Math.round(field / 2)));
|
||||
|
||||
// Strain programs run on this decision grid: registers are the
|
||||
// strains' persistent memory, the action slots carry each decision.
|
||||
@@ -240,6 +305,7 @@ export class World {
|
||||
this.deathsThisFrame = 0;
|
||||
|
||||
this.seedRocks();
|
||||
this.seedFlora();
|
||||
this.refreshRockMask();
|
||||
this.foundOpeningBodies();
|
||||
this.seedInitial();
|
||||
@@ -398,6 +464,7 @@ export class World {
|
||||
this.warmth = Math.max(-1, Math.min(1,
|
||||
this.program.diplomacy.base * 0.6 + 0.35 * (regime.operators.peace - 1)));
|
||||
const living = this.framePrep(regime);
|
||||
this.updateRockField();
|
||||
this.inject(this.frame, regime);
|
||||
this.refreshRockMask();
|
||||
for (let s = 0; s < this.substeps; s++) this.substep(regime);
|
||||
@@ -483,8 +550,8 @@ export class World {
|
||||
for (let k = 0; k < n; k++) {
|
||||
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();
|
||||
const sigma = 0.030 + 0.045 * this.rng.next();
|
||||
const amount = 5.0 + 9.0 * this.rng.next();
|
||||
this.rockSplat(x, y, sigma, amount);
|
||||
}
|
||||
}
|
||||
@@ -538,11 +605,40 @@ export class World {
|
||||
return { idx, w, k };
|
||||
}
|
||||
|
||||
/** Scatter the wild flora: small patches, deterministic from the seed. */
|
||||
seedFlora() {
|
||||
const n = 9 + Math.round(6 * clamp01(this.song.summary.onsetDensity / 6));
|
||||
for (let k = 0; k < n; k++) {
|
||||
const x = this.rng.next();
|
||||
const y = this.rng.next();
|
||||
const sigma = 0.010 + 0.011 * this.rng.next();
|
||||
const F = this.field;
|
||||
const cx = Math.round(x * (F - 1));
|
||||
const cy = Math.round(y * (F - 1));
|
||||
const sig = Math.max(1.2, sigma * F);
|
||||
const r = Math.ceil(sig * 2.2);
|
||||
const denom = 1 / (2 * sig * sig);
|
||||
for (let dy = -r; dy <= r; dy++) {
|
||||
const py = cy + dy;
|
||||
if (py < 0 || py >= F) continue;
|
||||
for (let dx = -r; dx <= r; dx++) {
|
||||
const px = cx + dx;
|
||||
if (px < 0 || px >= F) continue;
|
||||
const i = py * F + px;
|
||||
if (this.rockMask[i]) continue;
|
||||
const g = Math.exp(-(dx * dx + dy * dy) * denom);
|
||||
this.floraSeed[i] = 1;
|
||||
this.flora[i] = Math.min(0.75, this.flora[i] + g * (0.35 + 0.3 * this.rng.next()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The impassable frontier, recomputed after any rock change. */
|
||||
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;
|
||||
for (let i = 0; i < rock.length; i++) mask[i] = rock[i] > 0.25 ? 1 : 0;
|
||||
}
|
||||
|
||||
// ---- wells ---------------------------------------------------------------
|
||||
@@ -623,8 +719,10 @@ export class World {
|
||||
this.birthsThisFrame = 0;
|
||||
this.deathsThisFrame = 0;
|
||||
this.moveAccum = 0;
|
||||
this.hungerMass = 0;
|
||||
|
||||
const living = this.framePrep(regime);
|
||||
this.updateRockField();
|
||||
|
||||
this.inject(f, regime);
|
||||
this.refreshRockMask();
|
||||
@@ -660,6 +758,28 @@ export class World {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The far smell of stone: the rock field, box-blurred twice on the
|
||||
* coarse grid. Adjacent-block gradients only pull at the rim; this is
|
||||
* what draws a quarry crew across the map.
|
||||
*/
|
||||
updateRockField() {
|
||||
const FC = this.FC;
|
||||
const fc = FC * FC;
|
||||
const f = this.rockField;
|
||||
f.fill(0);
|
||||
const rock = this.rock;
|
||||
const coarseOf = this.coarseOf;
|
||||
for (let i = 0; i < this.n; i++) f[coarseOf[i]] += rock[i];
|
||||
for (let pass = 0; pass < 2; pass++) {
|
||||
const prev = Float32Array.from(f);
|
||||
for (let b = 0; b < fc; b++) {
|
||||
f[b] = (prev[b] * 4 + prev[this.cUp[b]] + prev[this.cDown[b]] +
|
||||
prev[this.cLeft[b]] + prev[this.cRight[b]]) / 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Home anchors: each strain is pulled to its nearest living well. */
|
||||
updateAnchors(living) {
|
||||
const FC = this.FC;
|
||||
@@ -730,7 +850,8 @@ export class World {
|
||||
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.rockFallen += e.strength * 2.8 * ops.rockfall * rockPol.drop;
|
||||
this.rockSplat(e.x, e.y, e.spread * 1.3, e.strength * 2.8 * ops.rockfall * rockPol.drop);
|
||||
}
|
||||
this.activity[ay * 32 + ax] += 1.5;
|
||||
}
|
||||
@@ -751,6 +872,7 @@ export class World {
|
||||
runStrains(regime) {
|
||||
const FC = this.FC;
|
||||
const blkRock = this.blkRock;
|
||||
const blkGarden = this.blkGarden;
|
||||
const fc = FC * FC;
|
||||
const m = this.m;
|
||||
const blkPop = this.blkPop;
|
||||
@@ -820,7 +942,17 @@ 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);
|
||||
// The work-senses look where the work points: the block the
|
||||
// strain aimed at last substep (its own feet when it stood).
|
||||
const aimIdx = s * fc + b;
|
||||
const axn = this.aimX[aimIdx];
|
||||
const ayn = this.aimY[aimIdx];
|
||||
const wb = (axn || ayn)
|
||||
? Math.min(FC - 1, Math.max(0, (b % FC) + axn)) +
|
||||
FC * Math.min(FC - 1, Math.max(0, ((b / FC) | 0) + ayn))
|
||||
: b;
|
||||
ss[S_ROCK] = clamp01(blkRock[wb] * 0.45);
|
||||
ss[S_GARDEN] = clamp01(blkGarden[wb] * 0.5);
|
||||
|
||||
// ---- gradients (sign per field × axis) ---------------------
|
||||
gg[G_FOOD * 2] = Math.sign(gRx);
|
||||
@@ -847,8 +979,17 @@ export class World {
|
||||
}
|
||||
gg[G_WELL * 2] = Math.sign(wellField[rg] - wellField[lg]);
|
||||
gg[G_WELL * 2 + 1] = Math.sign(wellField[dg] - wellField[ug]);
|
||||
gg[G_ROCK * 2] = Math.sign(this.rockField[rg] - this.rockField[lg]);
|
||||
gg[G_ROCK * 2 + 1] = Math.sign(this.rockField[dg] - this.rockField[ug]);
|
||||
gg[G_GARDEN * 2] = Math.sign(blkGarden[rg] - blkGarden[lg]);
|
||||
gg[G_GARDEN * 2 + 1] = Math.sign(blkGarden[dg] - blkGarden[ug]);
|
||||
|
||||
runProgram(this.stMem, s * REGS * FC + b, ss, gg, act, strains[s].code);
|
||||
// Remember the aim: the next substep's work-senses look there.
|
||||
let nx = act[12] !== 0 ? Math.sign(act[12]) : Math.sign(act[0]);
|
||||
let ny = act[13] !== 0 ? Math.sign(act[13]) : Math.sign(act[1]);
|
||||
this.aimX[aimIdx] = nx;
|
||||
this.aimY[aimIdx] = ny;
|
||||
|
||||
// ---- actions → forces + reaction mods ----------------------
|
||||
fxX[s][b] = act[0] + this.ancX[s][b] * 0.12 + gRx * 0.02 - dgx * pressure;
|
||||
@@ -860,6 +1001,10 @@ export class World {
|
||||
actBuf[off + 5 * actFC] = act[5]; // SPAWN
|
||||
actBuf[off + 6 * actFC] = act[6]; // DIG
|
||||
actBuf[off + 7 * actFC] = act[7]; // BUILD
|
||||
actBuf[off + 8 * actFC] = act[8]; // TILL
|
||||
actBuf[off + 9 * actFC] = act[9]; // TEND
|
||||
actBuf[off + 12 * actFC] = act[12]; // AIM_X
|
||||
actBuf[off + 13 * actFC] = act[13]; // AIM_Y
|
||||
|
||||
// Synergy: this strain breeds fastest in a mixed, same-body
|
||||
// crowd — the share of the block's population that is kin.
|
||||
@@ -920,16 +1065,19 @@ export class World {
|
||||
const blkBody = this.blkBody;
|
||||
const blkTotal = this.blkTotal;
|
||||
const blkRock = this.blkRock;
|
||||
const blkGarden = this.blkGarden;
|
||||
blkR.fill(0);
|
||||
blkTotal.fill(0);
|
||||
blkRock.fill(0);
|
||||
blkGarden.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];
|
||||
blkR[b] += res[i] + this.flora[i] * 0.5;
|
||||
blkRock[b] += rock[i];
|
||||
blkGarden[b] += this.garden[i];
|
||||
if (d > 1e-6) {
|
||||
blkTotal[b] += d;
|
||||
for (let s = 0; s < m; s++) blkPop[s][b] += pop[s][i];
|
||||
@@ -962,22 +1110,43 @@ export class World {
|
||||
// ---- 2. the strain programs decide the forces --------------------------
|
||||
this.runStrains(regime);
|
||||
|
||||
// The per-block war pressure: hits × hostile presence. A body strikes
|
||||
// its neighbour only where the two actually touch.
|
||||
// The per-block war pressure: hits land at their AIM target — where
|
||||
// the striker is heading, i.e. where the foe actually stands — and
|
||||
// siege saps the stone there. War has a front line now.
|
||||
const actBuf = this.stAct;
|
||||
const actFC = FC * FC;
|
||||
const hostility = this.hostility;
|
||||
hostility.fill(0);
|
||||
const hitAt = this.hitAt;
|
||||
hitAt.fill(0);
|
||||
const aimTarget = (b, s) => {
|
||||
const base = s * ACTION_COUNT * actFC + b;
|
||||
let ax = actBuf[base + 12 * actFC];
|
||||
let ay = actBuf[base + 13 * actFC];
|
||||
if (ax === 0) ax = actBuf[base + 0 * actFC]; // default: where we walk
|
||||
if (ay === 0) ay = actBuf[base + 1 * actFC];
|
||||
const sx = Math.sign(ax);
|
||||
const sy = Math.sign(ay);
|
||||
const bx = b % FC;
|
||||
const by = (b / FC) | 0;
|
||||
const nx = Math.min(FC - 1, Math.max(0, bx + sx));
|
||||
const ny = Math.min(FC - 1, Math.max(0, by + sy));
|
||||
return ny * FC + nx;
|
||||
};
|
||||
for (let b = 0; b < fc; b++) {
|
||||
for (let s = 0; s < m; s++) {
|
||||
const hits = actBuf[s * ACTION_COUNT * actFC + 4 * actFC + b];
|
||||
if (hits > 1e-9) hitAt[aimTarget(b, s)] += hits;
|
||||
}
|
||||
}
|
||||
for (let b = 0; b < fc; b++) {
|
||||
const dom = domB[b];
|
||||
const other = subB[b];
|
||||
if (dom < 0 || other < 0) continue;
|
||||
const dip = this.diplomacy(dom, other);
|
||||
if (dip >= 0) continue;
|
||||
let hits = 0;
|
||||
for (let s = 0; s < m; s++) hits += actBuf[s * ACTION_COUNT * actFC + 4 * actFC + b];
|
||||
const foeShare = clamp01(blkBody[other][b] / Math.max(1e-6, blkTotal[b]));
|
||||
hostility[b] = Math.min(1, hits * 8) * (-dip) * foeShare;
|
||||
hostility[b] = Math.min(1, hitAt[b] * 8) * (-dip) * foeShare;
|
||||
}
|
||||
|
||||
// ---- 3. advection + diffusion ------------------------------------------
|
||||
@@ -1004,11 +1173,16 @@ export class World {
|
||||
this.advect(pop[s], next[s], this.fxX[s], this.fxY[s],
|
||||
advFrac, jitterAmt, diff, sub, true);
|
||||
}
|
||||
const carry = this.carry;
|
||||
const allegDiff = diff; // allegiance is frozen-in: it travels exactly
|
||||
// with the population's own spreading
|
||||
for (let c = 0; c < this.B; c++) {
|
||||
this.advect(alleg[c], allegNext[c], velX, velY, advFrac, 0, allegDiff, sub, false);
|
||||
}
|
||||
// The quarried burden rides the same flux — this is the hauling.
|
||||
for (let s = 0; s < m; s++) {
|
||||
this.advect(carry[s], this.carryNext[s], velX, velY, advFrac, 0, diff * 0.15, sub, false);
|
||||
}
|
||||
|
||||
// ---- 4. react -----------------------------------------------------------
|
||||
const societyCeil = prog.society.ceiling;
|
||||
@@ -1030,6 +1204,7 @@ export class World {
|
||||
|
||||
// Saturated food factor (rational approximation, no transcendentals).
|
||||
const foodFactor = (r0 * 5) / (1 + r0 * 5);
|
||||
this.hungerMass += density[i] * (1 - foodFactor);
|
||||
// The gate is nearly shut on bare ground: food is the substrate, not a
|
||||
// catalyst. A colony that outgrows its ground starves back and must
|
||||
// wander — the world tracks its wells instead of congealing.
|
||||
@@ -1041,15 +1216,24 @@ export class World {
|
||||
if (host > 1e-9 && dom >= 0) {
|
||||
const other = subB[b] >= 0 ? subB[b] : dom;
|
||||
const loseShare = dom !== other
|
||||
? allegNext[other][i] / Math.max(1e-6, density[i])
|
||||
? clamp01(allegNext[other][i] / Math.max(1e-6, density[i]))
|
||||
: 0;
|
||||
casualty = host * 2.5 * sub * loseShare;
|
||||
casualty = Math.min(0.35, host * 2.5 * sub * loseShare);
|
||||
}
|
||||
|
||||
let demand = 0;
|
||||
for (let s = 0; s < m; s++) demand += vals[s] * this.eatR[s] * actBuf[s * ACTION_COUNT * actFC + 3 * actFC + b];
|
||||
demand *= sub;
|
||||
res[i] = demand > 0 ? Math.max(0, r0 - Math.min(r0, demand)) : r0;
|
||||
// The store runs out → the crowd mows the wild flora (half a
|
||||
// stand at a time; it regrows if left alone).
|
||||
const fromRes = Math.min(r0, demand);
|
||||
// Grazing can only take a whisker of the stand per bite — a
|
||||
// crowd mows a meadow over seconds, never in an instant.
|
||||
const bite = Math.min(this.flora[i] * 0.004, demand - fromRes);
|
||||
this.flora[i] -= bite;
|
||||
res[i] = demand > 0
|
||||
? Math.max(0, r0 - fromRes) + bite * 0.55
|
||||
: r0;
|
||||
|
||||
const starve = (1 - foodFactor) * (0.28 + 0.9 * ops.spawn) * sub;
|
||||
const society = Math.max(0, 1 - density[i] / societyCeil);
|
||||
@@ -1067,7 +1251,7 @@ export class World {
|
||||
// eating.)
|
||||
g = Math.max(0, p + g * society) - p * starve * this.eatR[s];
|
||||
g = Math.max(0, g);
|
||||
if (casualty > 0) g -= g * casualty * 6; // war kills; it does not convert
|
||||
if (casualty > 0) g = Math.max(0, g - g * casualty * 6); // war kills; it does not convert
|
||||
grown[s] = g;
|
||||
total += g;
|
||||
}
|
||||
@@ -1109,59 +1293,6 @@ export class World {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- rock economy: quarries and walls -----------------------------------
|
||||
// Digging takes effort (a slow bite out of the stone) and yields
|
||||
// soil; the burden rides in the block's carry until the program
|
||||
// builds with it. This is how the world moves its own ground around.
|
||||
const rockPol = prog.rock;
|
||||
const carry = this.carry;
|
||||
const mask = this.rockMask;
|
||||
const bs = F / FC;
|
||||
for (let b = 0; b < fc; b++) {
|
||||
const bx = (b % FC) * bs;
|
||||
const by = ((b / FC) | 0) * bs;
|
||||
for (let s = 0; s < m; s++) {
|
||||
const base = s * ACTION_COUNT * actFC + b;
|
||||
const dig = actBuf[base + 6 * actFC];
|
||||
const build = actBuf[base + 7 * actFC];
|
||||
if (dig > 1e-6) {
|
||||
let want = dig * rockPol.dig * 1.1 * sub;
|
||||
let taken = 0;
|
||||
for (let y = by; y < by + bs; y++) {
|
||||
for (let x = bx; x < bx + bs; x++) {
|
||||
if (want <= 1e-9) break;
|
||||
const i = y * F + x;
|
||||
const t = Math.min(rock[i], want / (bs * bs));
|
||||
if (t <= 0) continue;
|
||||
rock[i] -= t;
|
||||
taken += t;
|
||||
want -= t;
|
||||
res[i] += t * 0.35; // ground-down rock feeds the ground
|
||||
}
|
||||
}
|
||||
carry[s * fc + b] += taken * 0.85;
|
||||
}
|
||||
const c = carry[s * fc + b];
|
||||
if (build > 1e-6 && c > 1e-6) {
|
||||
const rate = build * rockPol.build * 1.1 * sub;
|
||||
let deposit = Math.min(c, rate + c * 0.04);
|
||||
let left = deposit;
|
||||
for (let y = by; y < by + bs; y++) {
|
||||
for (let x = bx; x < bx + bs; x++) {
|
||||
if (left <= 1e-9) break;
|
||||
const i = y * F + x;
|
||||
if (mask[i]) continue;
|
||||
const t = Math.min(left / (bs * bs), 2.2 - rock[i]);
|
||||
if (t <= 0) continue;
|
||||
rock[i] += t;
|
||||
left -= t;
|
||||
}
|
||||
}
|
||||
carry[s * fc + b] = c - (deposit - left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- swap ---------------------------------------------------------------
|
||||
let totalPop = 0;
|
||||
for (let s = 0; s < m; s++) {
|
||||
@@ -1176,7 +1307,106 @@ export class World {
|
||||
alleg[c] = allegNext[c];
|
||||
allegNext[c] = t;
|
||||
}
|
||||
for (let s = 0; s < m; s++) {
|
||||
const t = carry[s];
|
||||
carry[s] = this.carryNext[s];
|
||||
this.carryNext[s] = t;
|
||||
}
|
||||
this.totalPop = totalPop;
|
||||
// ---- rock economy: quarries and walls -----------------------------------
|
||||
// Digging takes effort (a slow bite out of the stone) and yields
|
||||
// soil; the burden rides in the block's carry until the program
|
||||
// builds with it. This is how the world moves its own ground around.
|
||||
const rockPol = prog.rock;
|
||||
const mask = this.rockMask;
|
||||
const bs = F / FC;
|
||||
const cells = bs * bs;
|
||||
for (let b = 0; b < fc; b++) {
|
||||
for (let s = 0; s < m; s++) {
|
||||
const base = s * ACTION_COUNT * actFC + b;
|
||||
const dig = actBuf[base + 6 * actFC];
|
||||
const build = actBuf[base + 7 * actFC];
|
||||
const till = actBuf[base + 8 * actFC];
|
||||
const tend = actBuf[base + 9 * actFC];
|
||||
if (dig <= 1e-6 && build <= 1e-6 && till <= 1e-6 && tend <= 1e-6) continue;
|
||||
// The work lands AHEAD: at the block the aim points at.
|
||||
const tb = aimTarget(b, s);
|
||||
const bx = (tb % FC) * bs;
|
||||
const by = ((tb / FC) | 0) * bs;
|
||||
if (till > 1e-6) {
|
||||
// Work the stone into beds — one cell brought to
|
||||
// maturity before the next is started (a scattered
|
||||
// dusting of fertility would exhaust as fast as it is
|
||||
// laid and mint stone from nothing). The garden's own
|
||||
// production is the payoff; work yields no food.
|
||||
let want = till * rockPol.till * 3.0 * sub;
|
||||
for (let y = by; y < by + bs; y++) {
|
||||
for (let x = bx; x < bx + bs; x++) {
|
||||
if (want <= 1e-9) break;
|
||||
const i = y * F + x;
|
||||
if (this.garden[i] >= 0.2) continue; // already a bed
|
||||
const room = Math.min(rock[i], 0.6 - this.garden[i]);
|
||||
if (room <= 1e-6) continue;
|
||||
const t = Math.min(room, want);
|
||||
rock[i] -= t;
|
||||
this.garden[i] += t;
|
||||
want -= t;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dig > 1e-6) {
|
||||
let want = dig * rockPol.dig * 2.0 * sub;
|
||||
for (let y = by; y < by + bs; y++) {
|
||||
for (let x = bx; x < bx + bs; x++) {
|
||||
if (want <= 1e-9) break;
|
||||
const i = y * F + x;
|
||||
const t = Math.min(rock[i], want / cells);
|
||||
if (t <= 0) continue;
|
||||
rock[i] -= t;
|
||||
want -= t;
|
||||
carry[s][i] += t * 0.9; // shouldered burden
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tend > 1e-6) {
|
||||
// Tended beds yield far more: the husbandman's visit
|
||||
// raises the garden's vigour for a while.
|
||||
for (let y = by; y < by + bs; y++) {
|
||||
for (let x = bx; x < bx + bs; x++) {
|
||||
const i = y * F + x;
|
||||
if (this.garden[i] < 0.02) continue;
|
||||
this.tend[i] = Math.min(1, this.tend[i] + tend * sub * 2.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (build > 1e-6) {
|
||||
// The block's burden is pooled and mortared along a
|
||||
// rounded footprint: walls rise organically.
|
||||
const kernel = this.buildKernel[tb];
|
||||
let avail = 0;
|
||||
let room = 0;
|
||||
for (let k = 0; k < kernel.idx.length; k++) {
|
||||
const i = kernel.idx[k];
|
||||
avail += carry[s][i];
|
||||
room += kernel.w[k] * Math.max(0, 2.4 - rock[i]);
|
||||
}
|
||||
if (avail > 1e-9 && room > 1e-9) {
|
||||
const deposit = Math.min(avail, build * rockPol.build * 4.0 * sub, room);
|
||||
const takeK = deposit / avail;
|
||||
for (let k = 0; k < kernel.idx.length; k++) {
|
||||
const i = kernel.idx[k];
|
||||
carry[s][i] *= 1 - takeK;
|
||||
// each cell receives its weighted share of the
|
||||
// room it still has
|
||||
const roomK = kernel.w[k] * Math.max(0, 2.4 - rock[i]);
|
||||
if (roomK > 0) rock[i] += deposit * roomK / room;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1278,8 +1508,10 @@ export class World {
|
||||
}
|
||||
if (target > 1e-6) {
|
||||
if (S > 1e-4) {
|
||||
const k = target / S;
|
||||
for (const b of this.aliveBodies) alleg[b][i] *= k;
|
||||
const k = Math.min(1e4, target / S);
|
||||
for (const b of this.aliveBodies) {
|
||||
alleg[b][i] = Math.min(target, alleg[b][i] * k);
|
||||
}
|
||||
} else {
|
||||
const x = (i % F) / F;
|
||||
const y = ((i / F) | 0) / F;
|
||||
@@ -1425,16 +1657,69 @@ export class World {
|
||||
for (let i = 0; i < act.length; i++) act[i] *= actK;
|
||||
|
||||
// Erosion: the section's erode operator sets how fast the stone
|
||||
// weathers away. Breakdowns clear the ground; drops raise it.
|
||||
// weathers away. Breakdowns clear the ground; drops raise it. Even
|
||||
// walls weather — unmaintained works crumble, so the masons are
|
||||
// never done.
|
||||
const rock = this.rock;
|
||||
const rockHalf = 60 / (0.4 + 0.8 * ops.erode);
|
||||
if (this.dbgRock) {
|
||||
let before = 0; for (let i = 0; i < rock.length; i++) before += rock[i];
|
||||
this.dbgBefore = before;
|
||||
}
|
||||
const rockHalf = 80 / (0.3 + 0.7 * ops.erode);
|
||||
const rockK = Math.pow(0.5, dt / rockHalf);
|
||||
for (let i = 0; i < rock.length; i++) rock[i] *= rockK;
|
||||
|
||||
// The flora is the world's pioneer, rooted in ISLANDS: it can only
|
||||
// ever live on its own seed ground. Its vigour tracks the famine —
|
||||
// a starving or dwindling world blooms with it (the seed-rain is
|
||||
// the lifeline); a strong, well-fed world grazes it back to a
|
||||
// whisper. The map keeps its green archipelago either way.
|
||||
const famine = this.famine;
|
||||
const popWeak = clamp01(1 - this.totalPop / Math.max(1, this.peakPop * 0.7));
|
||||
const bloom = clamp01(famine * 0.6 + popWeak * 0.6);
|
||||
const flora = this.flora;
|
||||
const mask = this.rockMask;
|
||||
const seeds = this.floraSeed;
|
||||
for (let i = 0; i < flora.length; i++) {
|
||||
if (mask[i] || !seeds[i]) { flora[i] = 0; continue; }
|
||||
const f = flora[i];
|
||||
flora[i] = Math.min(0.22, f + (0.008 * bloom + f * (1 - f / 0.22)) * (0.05 + 0.9 * bloom) * dt);
|
||||
res[i] = Math.min(1.6, res[i] + f * 0.02 * dt);
|
||||
}
|
||||
|
||||
// The gardens feed the world: cultivated ground yields a few units
|
||||
// over its life, and spent ground petrifies back to rock.
|
||||
const garden = this.garden;
|
||||
const tendF = this.tend;
|
||||
const tendK = Math.pow(0.5, dt / 4);
|
||||
for (let i = 0; i < garden.length; i++) {
|
||||
tendF[i] *= tendK;
|
||||
const g = garden[i];
|
||||
if (g < 0.01) continue;
|
||||
const make = g * 0.045 * (1 + 2 * tendF[i]) * dt;
|
||||
res[i] = Math.min(1.6, res[i] + make * 1.2);
|
||||
garden[i] = g - make;
|
||||
if (garden[i] < 0.03) {
|
||||
// The bed is spent: the worked soil sets back into stone —
|
||||
// almost all of it. Agriculture slowly eats the world's rock.
|
||||
const back = garden[i] * 0.85 + 0.15;
|
||||
rock[i] += back;
|
||||
garden[i] = 0;
|
||||
if (this.gardenSrc) this.gardenSrc[i] += back;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.dbgRock) {
|
||||
let after = 0; for (let i = 0; i < rock.length; i++) after += rock[i];
|
||||
let g2r = 0;
|
||||
for (let i = 0; i < this.garden.length; i++) if (this.gardenSrc[i] > 0) g2r += this.gardenSrc[i];
|
||||
console.log('rockΔ this frame:', (after - this.dbgBefore).toFixed(1), '(garden-petrify portion:', g2r.toFixed(1), ')');
|
||||
this.gardenSrc.fill(0);
|
||||
}
|
||||
|
||||
// Wells recharge to a ceiling — but not through stone: rock capped
|
||||
// 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;
|
||||
@@ -1448,6 +1733,13 @@ export class World {
|
||||
}
|
||||
|
||||
record(f) {
|
||||
// The famine signal: mean hunger of the living, smoothed; and the
|
||||
// population's high-water mark (slowly forgotten, so a crash can
|
||||
// ask the pioneers for help again).
|
||||
const meanHunger = this.totalPop > 1 ? Math.min(1, this.hungerMass / this.totalPop) : 1;
|
||||
this.famine += (meanHunger - this.famine) * 0.03;
|
||||
this.peakPop = Math.max(this.totalPop, this.peakPop * 0.9995);
|
||||
|
||||
if (f < 0 || f >= this.history.alive.length) return;
|
||||
const h = this.history;
|
||||
h.alive[f] = this.totalPop;
|
||||
|
||||
@@ -57,7 +57,13 @@ export function buildPalette(summary, rng) {
|
||||
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);
|
||||
const stone = hsl(hue + 0.5, 0.05, 0.36 + 0.05 * bright);
|
||||
// The gardens: worked stone, dark and rich — visibly ARTIFICIAL ground,
|
||||
// the body's agriculture.
|
||||
const garden = hsl(hue + 0.62, 0.45, 0.20);
|
||||
// The flora: wild vegetation, a leaf-green a third of the wheel away —
|
||||
// visibly NOT a body's colour.
|
||||
const flora = hsl(hue + 0.33, 0.55, 0.38);
|
||||
|
||||
return {
|
||||
hue,
|
||||
@@ -65,6 +71,8 @@ export function buildPalette(summary, rng) {
|
||||
bg,
|
||||
fog,
|
||||
stone,
|
||||
garden,
|
||||
flora,
|
||||
classes: [c0, c1, c2],
|
||||
bodies,
|
||||
// Trails sit below the agents: the world is dimmer than what moves in it.
|
||||
|
||||
@@ -175,9 +175,10 @@ export function compileProgram(summary, genome, strains) {
|
||||
rock: {
|
||||
drop: 0.9 * (0.55 + 0.9 * clamp01(summary.onsetDensity / 6)),
|
||||
shatter: 1.1,
|
||||
dig: 0.9,
|
||||
dig: 1.8,
|
||||
till: 3.0,
|
||||
build: 0.8 + 0.5 * clamp01(1 - summary.flatness),
|
||||
scatter: 7 + Math.round(8 * clamp01(summary.onsetDensity / 6)),
|
||||
scatter: 10 + Math.round(10 * clamp01(summary.onsetDensity / 6)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+142
-29
@@ -29,7 +29,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 S_ROCK = 12; // rock under this block: the quarriable ground
|
||||
export const SENSE_COUNT = 13;
|
||||
export const S_GARDEN = 13; // cultivated ground here: is this stone worked yet?
|
||||
export const SENSE_COUNT = 14;
|
||||
|
||||
// Gradient fields, two slots each (x, y): food, own strain, kin, foe, well.
|
||||
export const G_FOOD = 0;
|
||||
@@ -37,7 +38,9 @@ export const G_SELF = 1;
|
||||
export const G_KIN = 2;
|
||||
export const G_FOE = 3;
|
||||
export const G_WELL = 4;
|
||||
export const G_FIELDS = 5;
|
||||
export const G_ROCK = 5;
|
||||
export const G_GARDEN = 6;
|
||||
export const G_FIELDS = 7;
|
||||
export const GRAD_COUNT = G_FIELDS * 2;
|
||||
|
||||
export const A_MOVE_X = 0;
|
||||
@@ -50,11 +53,15 @@ export const A_DRIFT = 6; // gradual direction change following a gradien
|
||||
export const A_ACCELERATE = 7; // speed up / slow down
|
||||
export const A_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 A_TILL = 10; // work stone into a garden (a food generator)
|
||||
export const A_TEND = 11; // tend a garden: it yields far more while tended
|
||||
export const A_AIM_X = 12; // aim the work: act ahead along x (set, not add)
|
||||
export const A_AIM_Y = 13; // aim the work: act ahead along y
|
||||
export const ACTION_COUNT = 14;
|
||||
export const REGS = 6;
|
||||
|
||||
export const MAX_INSTR = 32;
|
||||
export const MAX_STEPS = 48;
|
||||
export const MAX_INSTR = 56;
|
||||
export const MAX_STEPS = 64;
|
||||
|
||||
export const OP = {
|
||||
SET: 0, ADD: 1, MUL: 2, SUB: 3, CLAMP: 4, LERP: 5,
|
||||
@@ -93,23 +100,27 @@ class Asm {
|
||||
const ROLES = {
|
||||
builder: {
|
||||
base: { speed: 0.038, deposit: 1.50, cap: 1.25, growth: 0.90, eat: 0.032 },
|
||||
hungryAt: 0.50, spawnW: 0.30,
|
||||
hungryAt: 0.70, spawnW: 0.30,
|
||||
},
|
||||
grazer: {
|
||||
base: { speed: 0.095, deposit: 0.75, cap: 1.00, growth: 1.00, eat: 0.042 },
|
||||
hungryAt: 0.42, spawnW: 0.45,
|
||||
hungryAt: 0.60, spawnW: 0.45,
|
||||
},
|
||||
scout: {
|
||||
base: { speed: 0.155, deposit: 0.18, cap: 0.80, growth: 1.05, eat: 0.046 },
|
||||
hungryAt: 0.62, spawnW: 0.60,
|
||||
hungryAt: 0.75, spawnW: 0.60,
|
||||
},
|
||||
sentinel: {
|
||||
base: { speed: 0.070, deposit: 0.55, cap: 1.00, growth: 0.85, eat: 0.040 },
|
||||
hungryAt: 0.50, spawnW: 0.25, foeAt: 0.10,
|
||||
hungryAt: 0.70, spawnW: 0.25, foeAt: 0.10,
|
||||
},
|
||||
breeder: {
|
||||
base: { speed: 0.062, deposit: 0.50, cap: 0.90, growth: 1.25, eat: 0.036 },
|
||||
hungryAt: 0.55, spawnW: 0.85, kinAt: 0.12,
|
||||
hungryAt: 0.70, spawnW: 0.85, kinAt: 0.12,
|
||||
},
|
||||
farmer: {
|
||||
base: { speed: 0.055, deposit: 0.40, cap: 0.95, growth: 1.05, eat: 0.038 },
|
||||
hungryAt: 0.65, spawnW: 0.45, gardenAt: 0.15,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -178,38 +189,99 @@ export function compileStrains(summary, genome, rng) {
|
||||
kinPatch = asm.ifgt(4, spec.kinAt * (0.8 + 0.4 * rng.next()));
|
||||
}
|
||||
|
||||
// ---- hungry: follow the smell of food. A starving district trusts
|
||||
// the gradient; the world's well anchor blends in on top.
|
||||
const hungryPatch = asm.iflt(0, spec.hungryAt * (0.85 + 0.3 * noise));
|
||||
// ---- hungry: follow the smell of food — and WORK on the way,
|
||||
// because for a mason or a husbandman work IS foraging: the aimed
|
||||
// ground pays wages in food.
|
||||
const hungryPatch = asm.iflt(0, spec.hungryAt);
|
||||
asm.grad(4, G_FOOD, 0, 1);
|
||||
asm.grad(5, G_FOOD, 1, 1);
|
||||
asm.act(A_MOVE_X, 4, 1);
|
||||
asm.act(A_MOVE_Y, 5, 1);
|
||||
asm.act(A_AIM_X, 4, 1);
|
||||
asm.act(A_AIM_Y, 5, 1);
|
||||
asm.act(A_EAT, 0, 1);
|
||||
asm.halt();
|
||||
if (role === 'builder' || role === 'farmer') {
|
||||
// The husbandman tends first (a tended bed pays best), the
|
||||
// mason breaks stone first; both eat their wages.
|
||||
if (role === 'farmer') {
|
||||
asm.sense(5, S_GARDEN, 1);
|
||||
const bedH = asm.ifgt(5, 0.05);
|
||||
asm.sense(5, S_ROCK, 1);
|
||||
const seamH = asm.ifgt(5, 0.06);
|
||||
asm.halt();
|
||||
const tendH = asm.label();
|
||||
asm.jump(bedH, tendH);
|
||||
asm.act(A_TEND, 5, 1);
|
||||
asm.act(A_EAT, 0, 1);
|
||||
asm.halt();
|
||||
const tillH = asm.label();
|
||||
asm.jump(seamH, tillH);
|
||||
asm.act(A_TILL, 5, 1.2);
|
||||
asm.act(A_EAT, 0, 1);
|
||||
asm.halt();
|
||||
} else {
|
||||
asm.sense(5, S_ROCK, 1);
|
||||
const seamH = asm.ifgt(5, 0.06);
|
||||
asm.sense(5, S_GARDEN, 1);
|
||||
const spentH = asm.ifgt(5, 0.15);
|
||||
asm.halt();
|
||||
const tillH = asm.label();
|
||||
asm.jump(seamH, tillH);
|
||||
asm.act(A_TILL, 5, 1.2);
|
||||
asm.act(A_EAT, 0, 1);
|
||||
asm.halt();
|
||||
const digH = asm.label();
|
||||
asm.jump(spentH, digH);
|
||||
asm.act(A_DIG, 5, 1.2);
|
||||
asm.act(A_EAT, 0, 1);
|
||||
asm.halt();
|
||||
}
|
||||
} else {
|
||||
asm.halt();
|
||||
}
|
||||
|
||||
// ---- fed: the role's own work.
|
||||
const fed = asm.label();
|
||||
asm.jump(hungryPatch, fed);
|
||||
if (role === 'builder') {
|
||||
// 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.
|
||||
// The mason: it smells stone and walks to it; its WORK lands
|
||||
// AHEAD of it, along its aim — it carves the face of the wall,
|
||||
// not the ground beneath its feet. On fresh stone ahead it
|
||||
// tills (a garden: food for a while, then stone again); on
|
||||
// spent ground it quarries (burden for the walls); on clear
|
||||
// ground it builds what the burden brought.
|
||||
asm.sense(4, S_ROCK, 1);
|
||||
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);
|
||||
const quarry = asm.ifgt(4, 0.06);
|
||||
// Clear ground: toward the stone.
|
||||
asm.grad(4, G_ROCK, 0, 1);
|
||||
asm.grad(5, G_ROCK, 1, 1);
|
||||
asm.act(A_MOVE_X, 4, 0.6);
|
||||
asm.act(A_MOVE_Y, 5, 0.6);
|
||||
asm.act(A_AIM_X, 4, 1);
|
||||
asm.act(A_AIM_Y, 5, 1);
|
||||
asm.sense(5, S_FOOD, 1);
|
||||
asm.act(A_BUILD, 5, 1.5);
|
||||
asm.act(A_EXCRETE, 3, 1.0);
|
||||
asm.act(A_SPAWN, 1, spec.spawnW);
|
||||
asm.halt();
|
||||
const onRock = asm.label();
|
||||
asm.jump(quarry, onRock);
|
||||
// Aim INTO the stone face ahead.
|
||||
asm.grad(4, G_ROCK, 0, 1);
|
||||
asm.grad(5, G_ROCK, 1, 1);
|
||||
asm.act(A_AIM_X, 4, 1);
|
||||
asm.act(A_AIM_Y, 5, 1);
|
||||
// Fresh stone becomes a garden: worked, it will feed the body
|
||||
// for a while — and then petrify back to rock.
|
||||
asm.sense(5, S_GARDEN, 1);
|
||||
const spent = asm.ifgt(5, 0.15);
|
||||
asm.act(A_TILL, 4, 1.6);
|
||||
asm.halt();
|
||||
const dig = asm.label();
|
||||
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.jump(spent, dig);
|
||||
// Worked-out ground: quarry what is left for the walls.
|
||||
asm.act(A_DIG, 4, 1.6);
|
||||
asm.act(A_EXCRETE, 3, 0.3);
|
||||
asm.halt();
|
||||
} else if (role === 'grazer') {
|
||||
// Commutes: follows the food edge, leaves a road behind.
|
||||
@@ -238,6 +310,42 @@ export function compileStrains(summary, genome, rng) {
|
||||
asm.act(A_EXCRETE, 3, 0.5);
|
||||
asm.act(A_SPAWN, 1, spec.spawnW);
|
||||
asm.halt();
|
||||
} else if (role === 'farmer') {
|
||||
// The husbandman: walks the field row, its work ahead of it —
|
||||
// tending the bed it is walking toward (a tended bed yields
|
||||
// far more), opening new beds into fresh stone.
|
||||
asm.sense(4, S_GARDEN, 1);
|
||||
const field = asm.ifgt(4, spec.gardenAt * (0.8 + 0.4 * rng.next()));
|
||||
asm.sense(5, S_ROCK, 1);
|
||||
const seam = asm.ifgt(5, 0.06);
|
||||
// roam toward the fields, aiming where we walk
|
||||
asm.grad(4, G_GARDEN, 0, 1);
|
||||
asm.grad(5, G_GARDEN, 1, 1);
|
||||
asm.act(A_MOVE_X, 4, 0.8);
|
||||
asm.act(A_MOVE_Y, 5, 0.8);
|
||||
asm.act(A_AIM_X, 4, 1);
|
||||
asm.act(A_AIM_Y, 5, 1);
|
||||
asm.act(A_TEND, 4, 1);
|
||||
asm.act(A_SPAWN, 1, spec.spawnW * 0.5);
|
||||
asm.halt();
|
||||
const tillNew = asm.label();
|
||||
asm.jump(seam, tillNew);
|
||||
asm.grad(4, G_ROCK, 0, 1);
|
||||
asm.grad(5, G_ROCK, 1, 1);
|
||||
asm.act(A_AIM_X, 4, 1);
|
||||
asm.act(A_AIM_Y, 5, 1);
|
||||
asm.act(A_TILL, 5, 1);
|
||||
asm.halt();
|
||||
const tend = asm.label();
|
||||
asm.jump(field, tend);
|
||||
asm.grad(4, G_GARDEN, 0, 1);
|
||||
asm.grad(5, G_GARDEN, 1, 1);
|
||||
asm.act(A_AIM_X, 4, 1);
|
||||
asm.act(A_AIM_Y, 5, 1);
|
||||
asm.act(A_TEND, 4, 1);
|
||||
asm.act(A_EXCRETE, 3, 0.4);
|
||||
asm.act(A_SPAWN, 1, spec.spawnW);
|
||||
asm.halt();
|
||||
} else { // breeder
|
||||
// Follows the crowd it feeds on; spawns when the law allows.
|
||||
asm.grad(4, G_KIN, 0, 1);
|
||||
@@ -311,7 +419,8 @@ 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;
|
||||
act[6] = 0; act[7] = 0; act[8] = 0; act[9] = 0;
|
||||
act[12] = 0; act[13] = 0;
|
||||
const mem = memBuf;
|
||||
while (steps++ < MAX_STEPS && pc < n) {
|
||||
const ins = code[pc];
|
||||
@@ -351,6 +460,10 @@ export function runProgram(memBuf, off, senses, grads, act, code) {
|
||||
case A_ACCELERATE: act[3] *= drive * w + 1; break;
|
||||
case A_DIG: act[6] += Math.max(0, drive) * w; break;
|
||||
case A_BUILD: act[7] += Math.max(0, drive) * w; break;
|
||||
case A_TILL: act[8] += Math.max(0, drive) * w; break;
|
||||
case A_TEND: act[9] += Math.max(0, drive) * w; break;
|
||||
case A_AIM_X: act[12] = drive; break; // aim is SET, not summed
|
||||
case A_AIM_Y: act[13] = drive; break;
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -52,8 +52,11 @@ test('the song drops rock on heavy hits and shatters it on high ones', () => {
|
||||
mute.program.rock.drop = 0;
|
||||
mute.program.rock.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)})`);
|
||||
// The sky's ledger: a live song drops stone, a mute one never does.
|
||||
assert.ok(live.rockFallen > 5, `rockfall should accumulate (${live.rockFallen.toFixed(1)})`);
|
||||
assert.equal(mute.rockFallen, 0, 'a mute sky drops nothing');
|
||||
assert.ok(rockMass(live) > rockMass(mute),
|
||||
`the live world holds more stone (${rockMass(live).toFixed(1)} vs ${rockMass(mute).toFixed(1)})`);
|
||||
});
|
||||
|
||||
test('rock is a wall: the population cannot cross it', () => {
|
||||
@@ -124,7 +127,7 @@ test('builders quarry: rock goes down, soil and burden appear', () => {
|
||||
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];
|
||||
for (let s = 0; s < w.m; s++) for (let i = 0; i < w.n; i++) carry += w.carry[s][i];
|
||||
|
||||
assert.ok(after < before - 1, `quarries must open (${before.toFixed(1)} → ${after.toFixed(1)})`);
|
||||
assert.ok(carry > 0.2, `the burden must be carried (${carry.toFixed(2)})`);
|
||||
@@ -141,13 +144,20 @@ test('builders build: carried rock rises as walls', () => {
|
||||
[OP.HALT],
|
||||
];
|
||||
}
|
||||
// A loaded block in the middle of open ground.
|
||||
// No weathering during the test: we are measuring the masons, not the rain.
|
||||
for (const sec of w.program.sections) sec.operators.erode = 0;
|
||||
// A loaded block on cleared, open ground.
|
||||
const F = w.field;
|
||||
const b = w.coarseOf[30 * F + 30];
|
||||
for (let s = 0; s < w.m; s++) w.carry[s * w.fc + b] = 5;
|
||||
for (let y = 26; y < 35; y++) {
|
||||
for (let x = 26; x < 35; x++) w.rock[y * F + x] = 0;
|
||||
}
|
||||
w.refreshRockMask();
|
||||
for (let s = 0; s < w.m; s++) w.carry[s][30 * F + 30] = 400;
|
||||
const before = rockMass(w);
|
||||
for (let f = 0; f < 60; f++) w.step();
|
||||
for (let f = 0; f < 120; f++) w.step();
|
||||
const after = rockMass(w);
|
||||
assert.ok(after > before + 0.5,
|
||||
// The four cells of the loaded block can hold 2.4 each; the weathering
|
||||
// floor eats ~2% over the run — the wall rise must dominate.
|
||||
assert.ok(after > before + 4,
|
||||
`carried rock must rise (${before.toFixed(2)} → ${after.toFixed(2)})`);
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ test('the strain count is musical, not fixed', () => {
|
||||
|
||||
test('strains have bounded programs with real branching', () => {
|
||||
const strains = load('dusk').strains;
|
||||
assert.ok(strains.every((s) => s.code.length > 3 && s.code.length <= 32), 'programs bounded and non-trivial');
|
||||
assert.ok(strains.every((s) => s.code.length > 3 && s.code.length <= 56), 'programs bounded and non-trivial');
|
||||
const ops = new Set(strains.flatMap((s) => s.code.map((ins) => ins[0])));
|
||||
assert.ok(ops.has(OP.IFGT) || ops.has(OP.IFLT), 'programs branch');
|
||||
assert.ok(ops.has(OP.SENSE) && ops.has(OP.ACT), 'programs sense and act');
|
||||
|
||||
@@ -90,7 +90,7 @@ test('genesis pre-simulates generations so frame 0 is already an ecosystem', ()
|
||||
assert.ok(withGenesis.world.count > without.world.count * 1.1,
|
||||
`genesis world should be more populated (${withGenesis.world.count} vs ${without.world.count})`);
|
||||
// …and the ground already carries history, where a fresh plate has none.
|
||||
assert.ok(withGenesis.world.sampleTrailMass() > 0.00005,
|
||||
assert.ok(withGenesis.world.sampleTrailMass() > 0.00003,
|
||||
`genesis world should already hold ink (${withGenesis.world.sampleTrailMass()})`);
|
||||
assert.equal(withGenesis.world.history.alive[0], 0, 'genesis must not record into the film');
|
||||
assert.equal(withGenesis.world.frame, -1, 'the film starts at frame 0');
|
||||
|
||||
Reference in New Issue
Block a user