diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 63bb0a0..ff3d427 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -240,6 +240,10 @@ above, including: their price). The top quarter of the underground is unchanged; copper also forms in deep stone and granite, coal in basalt, tin deeper. Lenses grow from lower noise thresholds only: stretching the noise with depth sheared it into diagonal streaks. + No ore is placed where the natural rock temperature would light it (coal) or melt it (metal + ores, within 200°C): deep coal and gold next to the magma burned and ran forever once a + reload woke those areas, and a Huge world dropped to about 10 fps. So deep coal stays in a + cooler mid band, and coal is about 3.2x rather than 5x; total ore about 4.2x. - Waste rock pays: grinding granite and basalt finds about twice the ore, and builders order granite and basalt rubble in bulk. - A Huge size (6144 x 2048); saves are gzip-compressed (a Medium save went from 29 MB to 0.4 MB). diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index 314133c..b56686e 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -324,7 +324,7 @@ Weather: clouds from steam, rain and snow, and the world water count. simTick: one simulation step, in order. - functions: simTick -### src/sim/worldgen.js (322 lines) +### src/sim/worldgen.js (326 lines) World generation: noise, layers, ores, caves, water, trees, and clearing tiny hard specks. - functions: makeNoise, fbm, clearHardSpecks, generateWorld - variables: SPECK_MIN diff --git a/docs/agent-log.md b/docs/agent-log.md index 5899f41..84afa3b 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -4,6 +4,19 @@ 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: t3 fix: deep ore burned and melted after a reload (Claude Code) + +- Owner: a Huge world ran at about 10 fps after travelling between claims, even after a refresh. + Cause: reloading wakes every area, and t3's deep coal (ignites at 430°C, natural rock reaches + that at about 70% depth) and gold next to the magma burned and melted forever: about 2,100 of + 12,288 areas stayed busy, 65 ms per tick. A fresh world hid it because nothing deep was awake. +- `worldgen.js`: an ore is not placed where `AMB[y]` is within 120°C of its ignition or 200°C of + its melting point; a cooler coal band in stone and granite at 45%+ depth replaces most of the + lost coal. Huge after a reload: 335 areas awake, about 5 ms per tick; Medium 104 (126 before + t3). Medium seed 424242 ore is now about 4.2x the original (coal 3.2x, gold 2.8x). +- `test/t_world.js`: no ore in too-hot rock; a reloaded world settles with no fires and under + 10% of areas moving; ore balance ranges updated. Existing saves made with the bug keep it. + ## 2026-10-24: t3: a world that lasts (Claude Code, Codex) - `26d17c2` deeper is richer (`worldgen.js` ore thresholds drop with depth below fy 0.25). diff --git a/src/sim/worldgen.js b/src/sim/worldgen.js index 97a6dc8..a7514b5 100644 --- a/src/sim/worldgen.js +++ b/src/sim/worldgen.js @@ -177,10 +177,12 @@ function generateWorld(seedIn, w, h, onProgress) { } // ores if (m === STONE || m === DIRT || m === GRANITE || m === BASALT || m === CLAY) { + const host = m; // Leave the shallow quarter unchanged; deeper rock has broader, richer lenses. const rich = Math.min(1, Math.max(0, (fy - 0.25) / 0.6)); const c = nCoal(x / 13, y / 9); if ((d > 6 && fy < 0.45 && c > 0.5 - 0.55 * rich && m !== CLAY) || + (fy >= 0.45 && (m === STONE || m === GRANITE) && c > 0.62 - 0.3 * rich) || (m === BASALT && fy > 0.6 && c > 0.58 - 0.4 * rich)) m = COALV; else { const cu = nCu(x / 9, y / 8); @@ -201,6 +203,8 @@ function generateWorld(seedIn, w, h, onProgress) { else if (fy > 0.68 && (m === GRANITE || m === BASALT) && nAu(x / 6, y / 5) > 0.7 - 0.18 * rich) m = AUV; } } + // no ore where the rock is naturally hot enough to light or melt it: it would burn or run forever + if (m !== host && (IGN[m] ? AMB[y] > IGN[m] - 120 : MELT[m] && AMB[y] > MELT[m] - 200)) m = host; } // caves if (d > 9) { diff --git a/test/t_world.js b/test/t_world.js index 75fe17d..fcebce0 100644 --- a/test/t_world.js +++ b/test/t_world.js @@ -170,11 +170,12 @@ const oldOre = [33493, 26508, 77679, 11797, 7723, 3638, 1681]; const oreNames = ['coal', 'copper', 'iron', 'zinc', 'tin', 'silver', 'gold']; oreNames.forEach((name, j) => { const ratio = oreBalance.counts[j] / oldOre[j]; - const lo = j < 2 ? 4.5 : j < 5 ? 4 : 3, hi = j < 2 ? 5.5 : j < 5 ? 5 : 4; + // coal is lower than the rest: seams deep enough to light in the natural heat are left out + const lo = j === 0 ? 3 : j < 5 ? 4 : 2.5, hi = j === 0 ? 4 : j < 5 ? 5 : 3.5; check(`${name} stays within its intended increase`, ratio >= lo && ratio <= hi, ratio.toFixed(2)); }); const totalOre = oreBalance.counts.reduce((a, b) => a + b, 0); -check('total ore is about 4.5 to 5 times the original', totalOre >= 162519 * 4.5 && totalOre <= 162519 * 5, totalOre); +check('total ore is about 4 to 4.5 times the original', totalOre >= 162519 * 4 && totalOre <= 162519 * 4.5, totalOre); check('shallow ore matches the original generator', oreBalance.shallowHash === 706875613, oreBalance.shallowHash); run('generateWorld(99, 4096, 1536, null)'); const marks = run('(()=>{let lakes=0,peaks=0,inLake=false;for(let x=1;x { }; const [withBot, aboveBot] = pour(118), [without] = pour(200); check('sand poured onto the bot falls through it to the ground', withBot === without && aboveBot === 0, `${withBot} grains landed (${without} with the bot away), ${aboveBot} held up above it`); + +// deep ore never sits in rock hot enough to light or melt it: a reloaded world wakes every area, +// and burning coal or running gold down there kept thousands of areas busy forever +run('generateWorld(77, 2048, 1024, null)'); +const hotOre = run('(() => { let n = 0; for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) { const m = mat[y * W + x]; if ([COALV, CUV, FEV, SNV, ZNV, AGV, AUV].includes(m) && (IGN[m] ? AMB[y] > IGN[m] - 120 : AMB[y] > MELT[m] - 200)) n++; } return n; })()'); +check('no ore is generated where the rock is hot enough to light or melt it', hotOre <= 20, hotOre); +run('globalThis.__s = serializeWorld(); deserializeWorld(JSON.parse(JSON.stringify(__s.meta)), __s.buffers); for (let k = 0; k < 900; k++) simTick();'); +const busy = run('(() => { let aw = 0, fl = 0; for (let c = 0; c < NCH; c++) if (awake[c]) aw++; for (let i = 0; i < N; i++) if (mat[i] === FLAME) fl++; return [aw, NCH, fl]; })()'); +check('a reloaded world settles: no fires deep down, few areas still moving', busy[2] === 0 && busy[0] < busy[1] * 0.1, busy); done();