From cce81e4eca83a495788785d182b7524a1c720d57 Mon Sep 17 00:00:00 2001 From: 44r0n7 <44r0n7+gitea@pm.me> Date: Sat, 24 Oct 2026 22:05:03 -0400 Subject: [PATCH] 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 --- docs/DESIGN.md | 7 ++++++ docs/PROJECT_MAP.md | 12 +++++----- docs/agent-log.md | 12 ++++++++++ src/sim/cells.js | 27 ++++++++++++++++++---- src/sim/tick.js | 1 + src/sim/world.js | 3 ++- src/sim/worldgen.js | 6 ++--- test/t_physics.js | 56 ++++++++++++++++++++++++++++++--------------- 8 files changed, 91 insertions(+), 33 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index f7d0930..eba5679 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -351,6 +351,13 @@ above, including: - World generation settles the land at the old water speed, so every seed keeps making the 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 - **Concrete**, as a stronger wall material from rubble. diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index 6b51346..cb2aa12 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -216,20 +216,20 @@ Starter furnace designs and the milestone list shown in the notebook. - functions: smelterBlueprint - 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. - 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) Exploration only: never changes grains or the random stream. - functions: fogAlloc, fogMark, fogSeen, fogInit, fogLook, fogRevealAround, fogRevealAll - 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). -- functions: setCell, swapCells, move, enter, powder, passable, liquid, gas, simMove -- variables: SORTP, LSTEP, LFILM +- functions: setCell, swapCells, move, enter, powder, passable, liquid, gas, wakeBotPowder, simMove +- variables: SORTP, SORTP_FUEL, LSTEP, LFILM ### src/sim/heat.js (246 lines) 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 - variables: WBAND, WS, weather -### src/sim/tick.js (26 lines) +### src/sim/tick.js (27 lines) simTick: one simulation step, in order. - functions: simTick diff --git a/docs/agent-log.md b/docs/agent-log.md index dd11fd1..e01c576 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -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 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) - `test/t_physics.js` (41 checks): heaps, digging under a heap, ore sorting through ash, sand diff --git a/src/sim/cells.js b/src/sim/cells.js index 3d32027..28d3f03 100644 --- a/src/sim/cells.js +++ b/src/sim/cells.js @@ -59,9 +59,12 @@ function enter(m, ph, j, dx, dy) { } // ---------- 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. -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 // 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. @@ -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 // packs too tight for that, so a mixed furnace charge stays mixed until the fire gets in. 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 if (t === BELTL || t === BELTR || part[b] === P_FILTER) return; 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); } } } +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() { for (let cy = CH - 1; cy >= 0; cy--) { const rowBase = cy * CW; 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; const y0 = cy << 5, y1 = Math.min(H, y0 + 32); for (let y = y1 - 1; y >= y0; y--) { @@ -176,8 +191,10 @@ function simMove() { const dx = ltr ? 1 : -1, xEnd = ltr ? x1 : x0 - 1; for (let x = ltr ? x0 : x1 - 1; x !== xEnd; x += dx) { const i = yw + x, m = mat[i]; - if (m === EMPTY || stamp[i] === tickStamp) continue; + if (m === EMPTY) continue; const ph = PH[m]; + if (ph === PH_POWDER) powderArea[rowBase + cx] = 1; + if (stamp[i] === tickStamp) continue; if (ph === PH_SOLID) continue; const p = part[i]; if (p >= P_TU && p <= P_TR && ph === PH_POWDER) continue; // a screw tube moves its own grains diff --git a/src/sim/tick.js b/src/sim/tick.js index 2d0b164..5942d62 100644 --- a/src/sim/tick.js +++ b/src/sim/tick.js @@ -10,6 +10,7 @@ function simTick() { pulseTick(); entTick(); if (flyers.length) flyTick(); + if (tick % 8 === 0) wakeBotPowder(); simMove(); heatPass(); for (let c = 0; c < NCH; c++) if (awake[c]) awake[c]--; diff --git a/src/sim/world.js b/src/sim/world.js index 1d4ea68..6da0bb5 100644 --- a/src/sim/world.js +++ b/src/sim/world.js @@ -13,7 +13,7 @@ let ents = []; // loose objects (bars) 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 ghostBlocks = new Map(), ghostMolds = []; -let awake, hot, hotLvl; +let awake, hot, hotLvl, powderArea; let tick = 0, tickStamp = 1; let machines = [null], molds = [], nets = [null], pcells = new Set(), powerDirty = true; 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); AMB = new Float32Array(h); 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; beltL = []; beltR = []; tubeLists = [[], [], [], [], []]; brokenMachines = []; ghostMachines = []; ghostBlocked = new Set(); diff --git a/src/sim/worldgen.js b/src/sim/worldgen.js index ecebb30..68425e1 100644 --- a/src/sim/worldgen.js +++ b/src/sim/worldgen.js @@ -303,9 +303,9 @@ function* worldSteps(seedIn, w, h) { } fillNoise(); 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); - 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++) { tick++; tickStamp = (tickStamp % 255) + 1; simMove(); @@ -313,7 +313,7 @@ function* worldSteps(seedIn, w, h) { if (!any) break; 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']; // 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; } diff --git a/test/t_physics.js b/test/t_physics.js index cdb9b98..71e365b 100644 --- a/test/t_physics.js +++ b/test/t_physics.js @@ -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, `${count(run, run('SAND'), 140, 120, 179, 159)} of ${grains('SAND')} in the hole`); asleep('sand after digging'); -// heavy ore works its way down through light ash, slowly, while the pile is being stirred (a -// 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')); +// Supported layers start asleep, just like a pile the bot walks up to. 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')); -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`); -const stir = n => run(`for (let k = 0; k < ${n}; k++) { if (k % 8 === 0) wakeXY(160, 112); simTick(); }`); -stir(600); const ore1 = oreY(run('CUORE')); stir(5400); const ore2 = oreY(run('CUORE')); -check('stirred, heavy ore sinks down through light ash, slowly', ore1 - oreRest < 2 && ore2 - oreRest > 2 && grains('CUORE') === 72 && grains('ASH') === 144, - `ore rows ${oreRest.toFixed(1)} -> ${ore1.toFixed(1)} -> ${ore2.toFixed(1)}`); -ticks(run, 1200); asleep('ore after stirring stops'); -world(); -rect(run, 150, 100, 151, 119, run('STONE')); rect(run, 170, 100, 171, 119, run('STONE')); -rect(run, 152, 112, 169, 119, run('LDIRT')); rect(run, 152, 104, 169, 111, run('SAND')); -const sand0 = oreY(run('SAND')); ticks(run, 6000); -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)}`); -asleep('a pile of similar weights'); +function sortingPile(near, similar = false) { + world(640); + rect(run, 150, 100, 151, 119, run('STONE')); rect(run, 170, 100, 171, 119, run('STONE')); + rect(run, 152, 112, 169, 119, run(similar ? 'LDIRT' : 'ASH')); + rect(run, 152, similar ? 104 : 108, 169, 111, run(similar ? 'SAND' : 'CUORE')); + run(`bot.x = ${near ? 158 : 530}; bot.y = 90; awake.fill(0);`); +} +sortingPile(true); +let sortedAt = 0; +for (let t = 8; t <= 600; t += 8) { + ticks(run, 8); + if (!sortedAt && count(run, run('CUORE'), 152, 116, 169, 119) === 72) sortedAt = t; +} +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 world(); rect(run, 150, 90, 151, 119, run('STONE')); rect(run, 190, 90, 191, 119, run('STONE'));