Piles near the bot sort, and 20 times faster; burning fuel keeps the slow pace (t21)

- SORTP 0.003 -> 0.06: ore sinks through 8 rows of ash in about 7 s near the bot.
- wakeBotPowder(): every 8 ticks, areas with loose grains within 100 grains of the bot get a
  sorting chance; far away, resting piles still sleep.
- Sinking through burning fuel stays at 0.003, so buried burning piles still char and furnace
  beds behave as tuned. World generation settles at the old rate: seeds are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 22:05:03 -04:00
co-authored by Claude Opus 5.5
parent 9be04471ee
commit cce81e4eca
8 changed files with 91 additions and 33 deletions
+7
View File
@@ -351,6 +351,13 @@ above, including:
- World generation settles the land at the old water speed, so every seed keeps making the - World generation settles the land at the old water speed, so every seed keeps making the
same world. same world.
- **Piles near the player sort, and quickly (t21, 2026-10-24).** A grain at least twice as heavy as
the powder under it sinks through it at 20 times the old pace (ore through ash: 4 rows to the
bottom in about 7 s). Before, a resting pile went to sleep and never sorted. Now areas with loose
grains within about 100 grains of the bot get a sorting chance every 8 ticks; far away, piles
still sleep. Through burning fuel sinking stays at the old slow pace: furnace beds are tuned to
it, and dirt heaped on a burning pile must smother it into charcoal rather than sink through.
## Ideas not built yet ## Ideas not built yet
- **Concrete**, as a stronger wall material from rubble. - **Concrete**, as a stronger wall material from rubble.
+6 -6
View File
@@ -216,20 +216,20 @@ Starter furnace designs and the milestone list shown in the notebook.
- functions: smelterBlueprint - functions: smelterBlueprint
- variables: STARTER_DESIGNS, MILESTONES - variables: STARTER_DESIGNS, MILESTONES
### src/sim/world.js (97 lines) ### src/sim/world.js (98 lines)
World state: the per-cell typed arrays, alloc(), ambient temperature by depth, waking. World state: the per-cell typed arrays, alloc(), ambient temperature by depth, waking.
- functions: emit, bagCap, discover, alloc, depthU, depthY, computeAmbient, fillNoise, wakeXY, wakeHeatXY, wakeIdx, wakeHeatIdx - functions: emit, bagCap, discover, alloc, depthU, depthY, computeAmbient, fillNoise, wakeXY, wakeHeatXY, wakeIdx, wakeHeatIdx
- variables: W, H, N, CW, CH, NCH, mat, temp, life, flags, stamp, part, mach, digp, noiseB, airS, netId, isPow, occ, surfY, dirtY, graniteY, basaltY, magmaY, roofY, AMB, worldSeed, surfBase, underD, shallowD, lapse, BW, BH, NB, blockP, blockV, ents, bgm, flyers, ghostBlocks, ghostMolds, awake, hot, hotLvl, tick, tickStamp, machines, molds, nets, pcells, powerDirty, beltL, beltR, tubeLists, brokenMachines, ghostMachines, ghostBlocked, EV, bot, bag, bagTotal, player, seen, ledger, stats - variables: W, H, N, CW, CH, NCH, mat, temp, life, flags, stamp, part, mach, digp, noiseB, airS, netId, isPow, occ, surfY, dirtY, graniteY, basaltY, magmaY, roofY, AMB, worldSeed, surfBase, underD, shallowD, lapse, BW, BH, NB, blockP, blockV, ents, bgm, flyers, ghostBlocks, ghostMolds, awake, hot, hotLvl, powderArea, tick, tickStamp, machines, molds, nets, pcells, powerDirty, beltL, beltR, tubeLists, brokenMachines, ghostMachines, ghostBlocked, EV, bot, bag, bagTotal, player, seen, ledger, stats
### src/sim/fog.js (52 lines) ### src/sim/fog.js (52 lines)
Exploration only: never changes grains or the random stream. Exploration only: never changes grains or the random stream.
- functions: fogAlloc, fogMark, fogSeen, fogInit, fogLook, fogRevealAround, fogRevealAll - functions: fogAlloc, fogMark, fogSeen, fogInit, fogLook, fogRevealAround, fogRevealAll
- variables: FOG, FOG_SENSE, FOG_SENSE_FADE, fog, FW, FH - variables: FOG, FOG_SENSE, FOG_SENSE_FADE, fog, FW, FH
### src/sim/cells.js (189 lines) ### src/sim/cells.js (206 lines)
Cell helpers (setCell, move, enter) and the falling-sand movement of powders, liquids and gases (simMove). Cell helpers (setCell, move, enter) and the falling-sand movement of powders, liquids and gases (simMove).
- functions: setCell, swapCells, move, enter, powder, passable, liquid, gas, simMove - functions: setCell, swapCells, move, enter, powder, passable, liquid, gas, wakeBotPowder, simMove
- variables: SORTP, LSTEP, LFILM - variables: SORTP, SORTP_FUEL, LSTEP, LFILM
### src/sim/heat.js (246 lines) ### src/sim/heat.js (246 lines)
Heat: conduction, melting and freezing, reactions, fire and air, steel. Heat: conduction, melting and freezing, reactions, fire and air, steel.
@@ -325,7 +325,7 @@ Weather: clouds from steam, rain and snow, and the world water count.
- functions: wrnd, resetWeather, rainGap, worldWater, absorbSteam, spawnRain, rainAt, weatherTick - functions: wrnd, resetWeather, rainGap, worldWater, absorbSteam, spawnRain, rainAt, weatherTick
- variables: WBAND, WS, weather - variables: WBAND, WS, weather
### src/sim/tick.js (26 lines) ### src/sim/tick.js (27 lines)
simTick: one simulation step, in order. simTick: one simulation step, in order.
- functions: simTick - functions: simTick
+12
View File
@@ -4,6 +4,18 @@ What agents have done, newest first. Add an entry for every piece of work: the d
note or request it was for, what changed, the files touched, the tests run, and anything left note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details. open or not verified. Keep each entry short; the git history has the details.
## 2026-10-24: t21: piles near the bot sort quickly (Claude Code, Codex)
- Owner: "a pile should sort when the player is nearby. the sorting shouldnt take long anyway."
- `src/sim/cells.js`: `SORTP` 0.003 -> 0.06, `SORTP_FUEL` 0.003 for sinking through burning fuel
(at 0.06 dirt sank through a buried burning wood pile and `t_world`'s charcoal check failed);
`wakeBotPowder()` every 8 ticks from `simTick` wakes areas with loose grains (`powderArea`
hint, kept by `simMove`) within 100 grains of the bot. `worldSteps` settles at 0.003: seeds
unchanged.
- Cost on a Medium dig site: 0.108 -> 0.134 ms/tick. `t_physics` sorting checks: near the bot ore
sorts in 432 ticks then sleeps; far away it stays; sand on dirt never sorts.
- Tests: `run_all.py --browser` ALL PASSED.
## 2026-10-24: t21 phase 2: physics test pass for sand, molten metal and gases (Claude Code, Codex) ## 2026-10-24: t21 phase 2: physics test pass for sand, molten metal and gases (Claude Code, Codex)
- `test/t_physics.js` (41 checks): heaps, digging under a heap, ore sorting through ash, sand - `test/t_physics.js` (41 checks): heaps, digging under a heap, ore sorting through ash, sand
+22 -5
View File
@@ -59,9 +59,12 @@ function enter(m, ph, j, dx, dy) {
} }
// ---------- Movement ---------- // ---------- Movement ----------
// Heavy grains settle through a loose pile of much lighter ones, slowly. A sorting // Heavy grains settle through a loose pile of much lighter ones. A sorting
// step keeps its area awake for a while so a pile finishes sorting even at rest. // step keeps its area awake for a while so a pile finishes sorting even at rest.
let SORTP = 0.003; // Through burning fuel it stays slow: a furnace bed is tuned to it, and dirt heaped on a burning
// pile has to smother it into charcoal, not sink through.
let SORTP = 0.06;
const SORTP_FUEL = 0.003;
// Liquids step up to LSTEP cells toward a drop in one tick (fewer the thicker they are), and a top-layer grain looks LFILM // Liquids step up to LSTEP cells toward a drop in one tick (fewer the thicker they are), and a top-layer grain looks LFILM
// times as far for one (a two-times range strands the far end of a 200-grain-wide ledge). // times as far for one (a two-times range strands the far end of a 200-grain-wide ledge).
// World generation settles with both at 1, so every seed keeps making the same world. // World generation settles with both at 1, so every seed keeps making the same world.
@@ -73,7 +76,7 @@ function powder(i, x, y, m) {
// a grain at least twice as dense works its way down through a loose pile. Unlit fuel // a grain at least twice as dense works its way down through a loose pile. Unlit fuel
// packs too tight for that, so a mixed furnace charge stays mixed until the fire gets in. // packs too tight for that, so a mixed furnace charge stays mixed until the fire gets in.
const t = mat[b]; const t = mat[b];
if (PH[t] === PH_POWDER && DENS[m] >= DENS[t] * 2 && (!FUEL[t] || (flags[b] & F_BURN)) && stamp[b] !== tickStamp && !occ[b] && part[b] !== P_GRATE && part[b] !== P_FILTER && rnd() < SORTP) { move(i, b, x, y, x, y + 1); awake[(y >> 5) * CW + (x >> 5)] = 40; return; } if (PH[t] === PH_POWDER && DENS[m] >= DENS[t] * 2 && (!FUEL[t] || (flags[b] & F_BURN)) && stamp[b] !== tickStamp && !occ[b] && part[b] !== P_GRATE && part[b] !== P_FILTER && rnd() < (FUEL[t] ? SORTP_FUEL : SORTP)) { move(i, b, x, y, x, y + 1); awake[(y >> 5) * CW + (x >> 5)] = 40; return; }
// a grain lying right on a belt or filter is held by it: the surface carries it, it does not slide off the edge // a grain lying right on a belt or filter is held by it: the surface carries it, it does not slide off the edge
if (t === BELTL || t === BELTR || part[b] === P_FILTER) return; if (t === BELTL || t === BELTR || part[b] === P_FILTER) return;
if (rnd() < STICK[m]) { if (rnd() < STICK[m]) {
@@ -158,11 +161,23 @@ function gas(i, x, y, m) {
if (nx >= 0 && nx < W && mat[i + d] === EMPTY && passable(m, PH_GAS, i + d, d, 0)) { move(i, i + d, x, y, nx, y); } if (nx >= 0 && nx < W && mat[i + d] === EMPTY && passable(m, PH_GAS, i + d, d, 0)) { move(i, i + d, x, y, nx, y); }
} }
} }
function wakeBotPowder() {
const x = bot.x + bot.w / 2, y = bot.y + bot.h / 2;
const x0 = Math.max(0, Math.floor((x - 100) / 32)), x1 = Math.min(CW - 1, Math.floor((x + 100) / 32));
const y0 = Math.max(0, Math.floor((y - 100) / 32)), y1 = Math.min(CH - 1, Math.floor((y + 100) / 32));
for (let cy = y0; cy <= y1; cy++) for (let cx = x0; cx <= x1; cx++) {
const c = cy * CW + cx;
if (!powderArea[c] || awake[c]) continue;
const dx = Math.max(cx * 32 - x, 0, x - (cx * 32 + 31));
const dy = Math.max(cy * 32 - y, 0, y - (cy * 32 + 31));
if (dx * dx + dy * dy <= 10000) awake[c] = 1;
}
}
function simMove() { function simMove() {
for (let cy = CH - 1; cy >= 0; cy--) { for (let cy = CH - 1; cy >= 0; cy--) {
const rowBase = cy * CW; const rowBase = cy * CW;
let any = false; let any = false;
for (let cx = 0; cx < CW; cx++) if (awake[rowBase + cx]) { any = true; break; } for (let cx = 0; cx < CW; cx++) if (awake[rowBase + cx]) { any = true; powderArea[rowBase + cx] = 0; }
if (!any) continue; if (!any) continue;
const y0 = cy << 5, y1 = Math.min(H, y0 + 32); const y0 = cy << 5, y1 = Math.min(H, y0 + 32);
for (let y = y1 - 1; y >= y0; y--) { for (let y = y1 - 1; y >= y0; y--) {
@@ -176,8 +191,10 @@ function simMove() {
const dx = ltr ? 1 : -1, xEnd = ltr ? x1 : x0 - 1; const dx = ltr ? 1 : -1, xEnd = ltr ? x1 : x0 - 1;
for (let x = ltr ? x0 : x1 - 1; x !== xEnd; x += dx) { for (let x = ltr ? x0 : x1 - 1; x !== xEnd; x += dx) {
const i = yw + x, m = mat[i]; const i = yw + x, m = mat[i];
if (m === EMPTY || stamp[i] === tickStamp) continue; if (m === EMPTY) continue;
const ph = PH[m]; const ph = PH[m];
if (ph === PH_POWDER) powderArea[rowBase + cx] = 1;
if (stamp[i] === tickStamp) continue;
if (ph === PH_SOLID) continue; if (ph === PH_SOLID) continue;
const p = part[i]; const p = part[i];
if (p >= P_TU && p <= P_TR && ph === PH_POWDER) continue; // a screw tube moves its own grains if (p >= P_TU && p <= P_TR && ph === PH_POWDER) continue; // a screw tube moves its own grains
+1
View File
@@ -10,6 +10,7 @@ function simTick() {
pulseTick(); pulseTick();
entTick(); entTick();
if (flyers.length) flyTick(); if (flyers.length) flyTick();
if (tick % 8 === 0) wakeBotPowder();
simMove(); simMove();
heatPass(); heatPass();
for (let c = 0; c < NCH; c++) if (awake[c]) awake[c]--; for (let c = 0; c < NCH; c++) if (awake[c]) awake[c]--;
+2 -1
View File
@@ -13,7 +13,7 @@ let ents = []; // loose objects (bars)
let bgm; // background layer: trees. Liquids, powders and gases pass in front of it. let bgm; // background layer: trees. Liquids, powders and gases pass in front of it.
let flyers = []; // grains in flight, blown out of the bot let flyers = []; // grains in flight, blown out of the bot
let ghostBlocks = new Map(), ghostMolds = []; let ghostBlocks = new Map(), ghostMolds = [];
let awake, hot, hotLvl; let awake, hot, hotLvl, powderArea;
let tick = 0, tickStamp = 1; let tick = 0, tickStamp = 1;
let machines = [null], molds = [], nets = [null], pcells = new Set(), powerDirty = true; let machines = [null], molds = [], nets = [null], pcells = new Set(), powerDirty = true;
let beltL = [], beltR = [], tubeLists = [[], [], [], [], []]; let beltL = [], beltR = [], tubeLists = [[], [], [], [], []];
@@ -50,6 +50,7 @@ function alloc(w, h) {
surfY = new Int16Array(w); roofY = new Int16Array(w); dirtY = new Int16Array(w); graniteY = new Int16Array(w); basaltY = new Int16Array(w); magmaY = new Int16Array(w); surfY = new Int16Array(w); roofY = new Int16Array(w); dirtY = new Int16Array(w); graniteY = new Int16Array(w); basaltY = new Int16Array(w); magmaY = new Int16Array(w);
AMB = new Float32Array(h); AMB = new Float32Array(h);
awake = new Uint8Array(NCH); hot = new Uint8Array(NCH); hotLvl = new Uint8Array(NCH); awake = new Uint8Array(NCH); hot = new Uint8Array(NCH); hotLvl = new Uint8Array(NCH);
powderArea = new Uint8Array(NCH).fill(1); // unknown areas get one visit before being ruled out
machines = [null]; molds = []; nets = [null]; pcells = new Set(); powerDirty = true; machines = [null]; molds = []; nets = [null]; pcells = new Set(); powerDirty = true;
beltL = []; beltR = []; tubeLists = [[], [], [], [], []]; brokenMachines = []; beltL = []; beltR = []; tubeLists = [[], [], [], [], []]; brokenMachines = [];
ghostMachines = []; ghostBlocked = new Set(); ghostMachines = []; ghostBlocked = new Set();
+3 -3
View File
@@ -303,9 +303,9 @@ function* worldSteps(seedIn, w, h) {
} }
fillNoise(); fillNoise();
yield [0.75, 'Settling loose ground']; yield [0.75, 'Settling loose ground'];
// settle loose material, with water at the speed it had when the seeds were made // settle loose material at the water and sorting speeds the seeds were made with
awake.fill(3); awake.fill(3);
const lstep = LSTEP, lfilm = LFILM; LSTEP = LFILM = 1; const lstep = LSTEP, lfilm = LFILM, sortp = SORTP; LSTEP = LFILM = 1; SORTP = 0.003;
for (let k = 0; k < 260; k++) { for (let k = 0; k < 260; k++) {
tick++; tickStamp = (tickStamp % 255) + 1; tick++; tickStamp = (tickStamp % 255) + 1;
simMove(); simMove();
@@ -313,7 +313,7 @@ function* worldSteps(seedIn, w, h) {
if (!any) break; if (!any) break;
if ((k & 31) === 0) yield [0.75 + 0.24 * k / 260, 'Settling loose ground']; if ((k & 31) === 0) yield [0.75 + 0.24 * k / 260, 'Settling loose ground'];
} }
LSTEP = lstep; LFILM = lfilm; LSTEP = lstep; LFILM = lfilm; SORTP = sortp;
yield [0.99, 'Finishing']; yield [0.99, 'Finishing'];
// re-ambient after settling, then start clean // re-ambient after settling, then start clean
for (let i = 0; i < N; i++) { const m = mat[i]; if (m !== MAGMA) temp[i] = AMB[(i / w) | 0]; life[i] = PH[m] === PH_LIQUID && FRZ[m] ? 255 : 0; } for (let i = 0; i < N; i++) { const m = mat[i]; if (m !== MAGMA) temp[i] = AMB[(i / w) | 0]; life[i] = PH[m] === PH_LIQUID && FRZ[m] ? 255 : 0; }
+38 -18
View File
@@ -24,25 +24,45 @@ ticks(run, 1800);
check('digging under a heap makes it fall into the hole', grains('SAND') === 400 && count(run, run('SAND'), 140, 120, 179, 159) > 300, check('digging under a heap makes it fall into the hole', grains('SAND') === 400 && count(run, run('SAND'), 140, 120, 179, 159) > 300,
`${count(run, run('SAND'), 140, 120, 179, 159)} of ${grains('SAND')} in the hole`); `${count(run, run('SAND'), 140, 120, 179, 159)} of ${grains('SAND')} in the hole`);
asleep('sand after digging'); asleep('sand after digging');
// heavy ore works its way down through light ash, slowly, while the pile is being stirred (a // Supported layers start asleep, just like a pile the bot walks up to.
// working furnace bed is always awake); a pile left at rest goes to sleep and stays as it is
world();
rect(run, 150, 100, 151, 119, run('STONE')); rect(run, 170, 100, 171, 119, run('STONE'));
rect(run, 152, 112, 169, 119, run('ASH')); rect(run, 152, 108, 169, 111, run('CUORE'));
const oreY = (m) => run(`(() => { let s = 0, n = 0; for (let i = 0; i < N; i++) if (mat[i] === ${m}) { s += (i / W) | 0; n++; } return s / n; })()`); const oreY = (m) => run(`(() => { let s = 0, n = 0; for (let i = 0; i < N; i++) if (mat[i] === ${m}) { s += (i / W) | 0; n++; } return s / n; })()`);
const ore0 = oreY(run('CUORE')); ticks(run, 3600); const oreRest = oreY(run('CUORE')); function sortingPile(near, similar = false) {
check('a resting pile of ore on ash goes to sleep as it is', Math.abs(oreRest - ore0) < 0.5 && awake() === 0, `ore rows ${ore0.toFixed(1)} -> ${oreRest.toFixed(1)}, ${awake()} awake`); world(640);
const stir = n => run(`for (let k = 0; k < ${n}; k++) { if (k % 8 === 0) wakeXY(160, 112); simTick(); }`); rect(run, 150, 100, 151, 119, run('STONE')); rect(run, 170, 100, 171, 119, run('STONE'));
stir(600); const ore1 = oreY(run('CUORE')); stir(5400); const ore2 = oreY(run('CUORE')); rect(run, 152, 112, 169, 119, run(similar ? 'LDIRT' : 'ASH'));
check('stirred, heavy ore sinks down through light ash, slowly', ore1 - oreRest < 2 && ore2 - oreRest > 2 && grains('CUORE') === 72 && grains('ASH') === 144, rect(run, 152, similar ? 104 : 108, 169, 111, run(similar ? 'SAND' : 'CUORE'));
`ore rows ${oreRest.toFixed(1)} -> ${ore1.toFixed(1)} -> ${ore2.toFixed(1)}`); run(`bot.x = ${near ? 158 : 530}; bot.y = 90; awake.fill(0);`);
ticks(run, 1200); asleep('ore after stirring stops'); }
world(); sortingPile(true);
rect(run, 150, 100, 151, 119, run('STONE')); rect(run, 170, 100, 171, 119, run('STONE')); let sortedAt = 0;
rect(run, 152, 112, 169, 119, run('LDIRT')); rect(run, 152, 104, 169, 111, run('SAND')); for (let t = 8; t <= 600; t += 8) {
const sand0 = oreY(run('SAND')); ticks(run, 6000); ticks(run, 8);
check('sand on dirt (close in weight) stays where it is', Math.abs(oreY(run('SAND')) - sand0) < 0.5, `sand rows ${sand0.toFixed(1)} -> ${oreY(run('SAND')).toFixed(1)}`); if (!sortedAt && count(run, run('CUORE'), 152, 116, 169, 119) === 72) sortedAt = t;
asleep('a pile of similar weights'); }
check('near the bot, resting ore sorts to the bottom of ash within 600 ticks', sortedAt > 0 && grains('CUORE') === 72 && grains('ASH') === 144,
`all 72 ore at bottom after ${sortedAt || '>600'} ticks, ${grains('CUORE')} ore + ${grains('ASH')} ash`);
ticks(run, 600); asleep('sorted ore near the bot');
sortingPile(false);
const ore0 = oreY(run('CUORE')); ticks(run, 1200);
check('far from the bot, resting ore on ash stays put and sleeps', oreY(run('CUORE')) === ore0 && grains('CUORE') === 72 && grains('ASH') === 144 && awake() === 0,
`ore rows ${ore0} -> ${oreY(run('CUORE'))}, ${awake()} awake`);
// Visiting an old sleeping pile must work, too (not only freshly placed grains).
run('bot.x = 158;'); ticks(run, 600);
check('approaching a sleeping pile starts sorting', count(run, run('CUORE'), 152, 116, 169, 119) === 72,
`${count(run, run('CUORE'), 152, 116, 169, 119)}/72 ore at bottom`);
for (const near of [true, false]) {
sortingPile(near, true);
const sand0 = oreY(run('SAND')); ticks(run, 6000);
check(`sand on dirt (close in weight) stays put and sleeps ${near ? 'near' : 'far from'} the bot`, oreY(run('SAND')) === sand0 && grains('SAND') === 144 && grains('LDIRT') === 144 && awake() === 0,
`sand rows ${sand0} -> ${oreY(run('SAND'))}, ${awake()} awake`);
}
world(); ticks(run, 8); run('wakeBotPowder();');
check('nearby areas without powder are skipped after one visit', awake() === 0,
`${awake()} awake`);
rect(run, 152, 116, 169, 119, run('SAND')); ticks(run, 16);
run('wakeBotPowder();');
check('processed areas remember powder added after an empty visit', awake() > 0,
`${awake()} awake`);
// sand dropped into a pond sinks, and the water ends up on top // sand dropped into a pond sinks, and the water ends up on top
world(); world();
rect(run, 150, 90, 151, 119, run('STONE')); rect(run, 190, 90, 191, 119, run('STONE')); rect(run, 150, 90, 151, 119, run('STONE')); rect(run, 190, 90, 191, 119, run('STONE'));