A loaded world starts around the player first, the rest a few areas a tick (t21)

- Areas within 150 grains of the bot start at once; the others 12 a tick, nearest first, and any
  the bot flies toward start straight away. A Huge world is fully started after 17 s.
- Huge world, first 600 ticks: average 15.2 -> 5.5 ms/tick, worst 79.7 -> 16.2.
- New test/t_load.js.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 22:41:57 -04:00
co-authored by Claude Opus 5.5
parent 80420c3453
commit 7a653d94b4
7 changed files with 137 additions and 12 deletions
+6
View File
@@ -358,6 +358,12 @@ above, including:
still sleep. Through burning fuel sinking stays at the old slow pace: furnace beds are tuned to 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. it, and dirt heaped on a burning pile must smother it into charcoal rather than sink through.
- **A loaded world starts around the player first (t21, 2026-10-25).** Loading used to start every
area of the world at once, and a Huge world then took 15 ms a tick for its first 10 seconds,
with ticks up to 80 ms. Now the areas within 150 grains of the bot start at once, the rest 12 a
tick, nearest first (a Huge world is fully started after 17 s), and flying toward areas still
waiting starts them. Nothing is skipped: the same settling happens, spread out.
## 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 -5
View File
@@ -220,10 +220,10 @@ 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 (102 lines) ### src/sim/world.js (104 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, meanSurface, computeAmbient, fillNoise, wakeXY, wakeHeatXY, wakeIdx, wakeHeatIdx - functions: emit, bagCap, discover, alloc, depthU, depthY, meanSurface, 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, powderArea, roofDirty, 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, roofDirty, loadWakeOrder, loadWakePending, loadWakeIndex, 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.
@@ -329,7 +329,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 (27 lines) ### src/sim/tick.js (28 lines)
simTick: one simulation step, in order. simTick: one simulation step, in order.
- functions: simTick - functions: simTick
@@ -342,9 +342,9 @@ World generation helpers: noise and removal of tiny hard rock specks.
World generation: staged terrain, ore, caves and placement, with synchronous and async entry points. World generation: staged terrain, ore, caves and placement, with synchronous and async entry points.
- functions: generateWorld, generateWorldAsync - functions: generateWorld, generateWorldAsync
### src/sim/save.js (141 lines) ### src/sim/save.js (177 lines)
Saving and loading a world (serializeWorld / deserializeWorld) and old-save migration. Saving and loading a world (serializeWorld / deserializeWorld) and old-save migration.
- functions: serializeWorld, deserializeWorld - functions: serializeWorld, deserializeWorld, startLoadWake, wakeLoadArea, wakeLoadNearBot, loadWakeTick
- variables: SAVE_VERSION, TOFF - variables: SAVE_VERSION, TOFF
### src/render/setup.js (200 lines) ### src/render/setup.js (200 lines)
@@ -509,6 +509,7 @@ The frame loop and boot.
- `test/t_econ.js`: Economy: the dock (instant sale), orders, prices, the market dip, money for machines, alloys. - `test/t_econ.js`: Economy: the dock (instant sale), orders, prices, the market dip, money for machines, alloys.
- `test/t_fog.js`: Fog: surface, sight, range, saves and simulation independence. - `test/t_fog.js`: Fog: surface, sight, range, saves and simulation independence.
- `test/t_furnace_repeat.js`: The starter furnaces keep working when fired again and again without cleaning, however the charge goes in: mixed, ore first with coal on top, or blown in through the vent. Each case plays furnace_play.js, which builds the design and fires it like a player. - `test/t_furnace_repeat.js`: The starter furnaces keep working when fired again and again without cleaning, however the charge goes in: mixed, ore first with coal on top, or blown in through the vent. Each case plays furnace_play.js, which builds the design and fires it like a player.
- `test/t_load.js`: Reload wakes nearby areas first, without losing distant water, fire or saved state.
- `test/t_molds.js`: Molds and furnaces stay usable: ingots drop below and keep their stubs, plugs over molds are cleared, spoiled castings can be knocked out, grains never drop through a grate by teleporting, and slag lets the fire breathe. - `test/t_molds.js`: Molds and furnaces stay usable: ingots drop below and keep their stubs, plugs over molds are cleared, spoiled castings can be knocked out, grains never drop through a grate by teleporting, and slag lets the fire breathe.
- `test/t_physics.js`: Sand and other powders, molten metal and gases: heaps, sorting, sinking, setting, rising and fading. Water has its own suite (t_water.js). Tall worlds keep geothermal heat out of the way. - `test/t_physics.js`: Sand and other powders, molten metal and gases: heaps, sorting, sinking, setting, rising and fading. Water has its own suite (t_water.js). Tall worlds keep geothermal heat out of the way.
- `test/t_pipes.js`: Pipes, pumps and outlets move liquids; the Grinder grinds waste rock for trace ore. - `test/t_pipes.js`: Pipes, pumps and outlets move liquids; the Grinder grinds waste rock for trace ore.
+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-25: t21: a loaded world starts around the player first (Claude Code, Codex)
- Owner: "lets speed up the first load speed. load in areas around the player first".
- `src/sim/save.js`: `deserializeWorld` no longer wakes every area; `startLoadWake()` keeps the
load-time heat scan's result per area, wakes those within 150 grains of the bot, and queues the
rest by distance; `loadWakeTick()` (from `simTick`) wakes 12 a tick and, every 8 ticks, any still
waiting within 150 grains of the bot. The queue is not saved.
- Huge world, ms/tick before -> after: ticks 0-600 average 15.2 -> 5.5, max 79.7 -> 16.2, 95th
percentile 21.0 -> 11.0; ticks 600-1800 average 7.3 -> 10.0 (the same work, later). Every area
has started by tick 1024 (17 s).
- `test/t_load.js` (19 checks). `run_all.py --browser` ALL PASSED.
## 2026-10-24: t21 phase 4: duplicates merged, largest files split (Claude Code, Codex) ## 2026-10-24: t21 phase 4: duplicates merged, largest files split (Claude Code, Codex)
- Merged seven real duplicates: wall refunds (`removeWallCell`, walls and molds), tube edges and - Merged seven real duplicates: wall refunds (`removeWallCell`, walls and molds), tube edges and
+43 -7
View File
@@ -117,13 +117,6 @@ function deserializeWorld(meta, buffers) {
seen = new Uint8Array(NMAT); meta.seen.forEach((v, k) => { if (k < NMAT) seen[k] = v; }); seen = new Uint8Array(NMAT); meta.seen.forEach((v, k) => { if (k < NMAT) seen[k] = v; });
ledger = meta.ledger || {}; ledger = meta.ledger || {};
Object.assign(stats, meta.stats || {}); Object.assign(stats, meta.stats || {});
awake.fill(3);
for (let c = 0; c < NCH; c++) {
const cx = c % CW, cy = (c / CW) | 0, x0 = cx << 5, y0 = cy << 5, x1 = Math.min(W, x0 + 32), y1 = Math.min(H, y0 + 32);
let dev = 0;
for (let y = y0; y < y1 && dev < 2; y++) for (let x = x0; x < x1; x++) { const d = Math.abs(temp[y * W + x] - AMB[y]); if (d > dev) dev = d; if (flags[y * W + x] & F_BURN) dev = 99; }
if (dev >= 2) { hot[c] = 6; hotLvl[c] = 2; }
}
powerDirty = true; rebuildPower(); updateRoof(); powerDirty = true; rebuildPower(); updateRoof();
if (!weather.base) weather.base = worldWater(); // older saves: take the water it has now as its starting amount if (!weather.base) weather.base = worldWater(); // older saves: take the water it has now as its starting amount
if (!meta.specks) clearHardSpecks(true); // older saves: tidy away tiny specks of too-hard rock once if (!meta.specks) clearHardSpecks(true); // older saves: tidy away tiny specks of too-hard rock once
@@ -137,5 +130,48 @@ function deserializeWorld(meta, buffers) {
for (const mo of molds) for (const i of moldCellsAt(mo.kind, mo.x, mo.y).walls) fogRevealAround(i % W, (i / W) | 0, 12); for (const mo of molds) for (const i of moldCellsAt(mo.kind, mo.x, mo.y).walls) fogRevealAround(i % W, (i / W) | 0, 12);
} }
fogLook(bot.x + bot.w / 2, bot.y + bot.h / 2); fogLook(bot.x + bot.w / 2, bot.y + bot.h / 2);
startLoadWake();
EV.length = 0; EV.length = 0;
} }
// Keep the load-time heat scan, but start distant areas in order instead of all at once.
function startLoadWake() {
awake.fill(0); hot.fill(0); hotLvl.fill(0);
loadWakePending = new Uint8Array(NCH).fill(1);
for (let c = 0; c < NCH; c++) {
const cx = c % CW, cy = (c / CW) | 0, x0 = cx << 5, y0 = cy << 5, x1 = Math.min(W, x0 + 32), y1 = Math.min(H, y0 + 32);
let dev = 0;
for (let y = y0; y < y1 && dev < 2; y++) for (let x = x0; x < x1; x++) { const d = Math.abs(temp[y * W + x] - AMB[y]); if (d > dev) dev = d; if (flags[y * W + x] & F_BURN) dev = 99; }
if (dev >= 2) loadWakePending[c] = 2;
}
const x = bot.x + bot.w / 2, y = bot.y + bot.h / 2;
const distance = c => (c % CW * 32 + 16 - x) ** 2 + (((c / CW) | 0) * 32 + 16 - y) ** 2;
loadWakeOrder = Uint32Array.from({ length: NCH }, (_, c) => c);
loadWakeOrder.sort((a, b) => distance(a) - distance(b));
loadWakeIndex = 0;
wakeLoadNearBot();
}
function wakeLoadArea(c) {
const pending = loadWakePending[c];
if (!pending) return;
loadWakePending[c] = 0;
awake[c] = 3;
if (pending === 2) { hot[c] = 6; hotLvl[c] = 2; }
}
function wakeLoadNearBot() {
const x = bot.x + bot.w / 2, y = bot.y + bot.h / 2;
const x0 = Math.max(0, Math.floor((x - 150) / 32)), x1 = Math.min(CW - 1, Math.floor((x + 150) / 32));
const y0 = Math.max(0, Math.floor((y - 150) / 32)), y1 = Math.min(CH - 1, Math.floor((y + 150) / 32));
for (let cy = y0; cy <= y1; cy++) for (let cx = x0; cx <= x1; cx++) {
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 <= 150 * 150) wakeLoadArea(cy * CW + cx);
}
}
function loadWakeTick() {
if (tick % 8 === 0) wakeLoadNearBot();
// Twelve per tick finishes a Huge world in 1024 ticks, and a Small one in 171.
const end = Math.min(loadWakeIndex + 12, loadWakeOrder.length);
while (loadWakeIndex < end) wakeLoadArea(loadWakeOrder[loadWakeIndex++]);
if (loadWakeIndex === loadWakeOrder.length) loadWakeOrder = loadWakePending = null;
}
+1
View File
@@ -3,6 +3,7 @@
function simTick() { function simTick() {
tick++; tick++;
tickStamp = (tickStamp % 255) + 1; tickStamp = (tickStamp % 255) + 1;
if (loadWakeOrder) loadWakeTick();
if (tick % 20 === 1) updateDirtyRoof(); if (tick % 20 === 1) updateDirtyRoof();
weatherTick(); weatherTick();
machinesTick(); machinesTick();
+2
View File
@@ -14,6 +14,7 @@ let bgm; // background layer: trees. Liquids, powders and gases pass in front of
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, powderArea, roofDirty; let awake, hot, hotLvl, powderArea, roofDirty;
let loadWakeOrder = null, loadWakePending = null, loadWakeIndex = 0;
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 = [[], [], [], [], []];
@@ -49,6 +50,7 @@ function alloc(w, h) {
surfBase = Math.round(Math.min(h * 0.27, 175)); underD = h - surfBase; shallowD = 0; lapse = 0.36; surfBase = Math.round(Math.min(h * 0.27, 175)); underD = h - surfBase; shallowD = 0; lapse = 0.36;
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);
loadWakeOrder = loadWakePending = null; loadWakeIndex = 0;
awake = new Uint8Array(NCH); hot = new Uint8Array(NCH); hotLvl = new Uint8Array(NCH); awake = new Uint8Array(NCH); hot = new Uint8Array(NCH); hotLvl = new Uint8Array(NCH);
roofDirty = new Uint8Array(CW).fill(1); roofDirty = new Uint8Array(CW).fill(1);
powderArea = new Uint8Array(NCH).fill(1); // unknown areas get one visit before being ruled out powderArea = new Uint8Array(NCH).fill(1); // unknown areas get one visit before being ruled out
+67
View File
@@ -0,0 +1,67 @@
// Reload wakes nearby areas first, without losing distant water, fire or saved state.
const { load, suite, flatWorld, rect, count, ticks } = require('./harness');
const { run } = load();
const { check, done } = suite();
flatWorld(run, 1024, 256, 200);
run(`
bot.x = 510; bot.y = 170;
setCell(192 * W + 16, WOOD, AMB[192], F_BURN); life[192 * W + 16] = FUEL[WOOD];
temp[210 * W + 16] = AMB[210] + 200;
temp[210 * W + 512] = AMB[210] + 200;
`);
rect(run, 48, 160, 49, 199, run('STONE'));
rect(run, 78, 160, 79, 199, run('STONE'));
rect(run, 50, 170, 60, 179, run('WATER'));
run('globalThis.loadSave = serializeWorld(); deserializeWorld(loadSave.meta, loadSave.buffers);');
check('nearby areas wake immediately, with heat', run('awake[6 * CW + 16] > 0 && hot[6 * CW + 16] > 0'));
check('distant areas wait, including their heat', run('awake[6 * CW] === 0 && hot[6 * CW] === 0'));
ticks(run, 1);
check('nearby areas are awake on the first tick while far areas wait', run('awake[6 * CW + 16] > 0 && awake[6 * CW] === 0'));
check('the order contains every area once and goes outward from the saved bot', run(`(() => {
const visited = new Uint8Array(NCH);
let previous = -1;
for (const c of loadWakeOrder) {
const d = (c % CW * 32 + 16 - (bot.x + bot.w / 2)) ** 2 + (((c / CW) | 0) * 32 + 16 - (bot.y + bot.h / 2)) ** 2;
if (visited[c] || d < previous) return false;
visited[c] = 1; previous = d;
}
return visited.every(v => v === 1);
})()`));
check('waiting leaves distant fire and water unchanged', run('life[192 * W + 16] === FUEL[WOOD] && mat[170 * W + 50] === WATER'));
ticks(run, 120);
check('all areas of a small world have had their turn', run('loadWakeOrder === null && loadWakePending === null'));
check('distant burning wood resumes burning after its turn', run('life[192 * W + 16] < FUEL[WOOD] || mat[192 * W + 16] !== WOOD'));
check('distant stored heat resumes flowing', run('temp[210 * W + 16] < AMB[210] + 190'));
ticks(run, 600);
check('distant water settles without losing grains', count(run, run('WATER')) === 110 && count(run, run('WATER'), 50, 196, 77, 199) === 110);
check('the settled water area goes back to sleep', run('awake[6 * CW + 1] === 0 && awake[6 * CW + 2] === 0'));
// Moving to a waiting area must apply its saved heat hint, even at ambient with F_BURN.
run('deserializeWorld(loadSave.meta, loadSave.buffers); bot.x = 14; bot.y = 190;');
check('the destination is still waiting before moving wakes it', run('loadWakePending[6 * CW] === 2 && hot[6 * CW] === 0'));
ticks(run, 8);
check('moving near a waiting area wakes it within eight ticks, including heat', run('loadWakePending[6 * CW] === 0 && hot[6 * CW] > 0 && life[192 * W + 16] < FUEL[WOOD]'));
// Saving midway keeps the actual cells, and loading starts a fresh queue at the new bot.
run('globalThis.midLoadSave = serializeWorld(); deserializeWorld(midLoadSave.meta, midLoadSave.buffers);');
check('saving midway preserves all cell buffers', run(`(() => {
const saved = serializeWorld();
return ['mat', 'temp', 'life', 'flags'].every(key => saved.buffers[key].every((v, i) => v === midLoadSave.buffers[key][i]));
})()`));
check('loading midway starts a new queue around the current bot', run('loadWakeIndex === 0 && loadWakePending[6 * CW] === 0 && loadWakePending[6 * CW + 16] > 0'));
run('alloc(64, 64);');
check('alloc clears a previous load queue', run('loadWakeOrder === null && loadWakePending === null && loadWakeIndex === 0'));
run('deserializeWorld(loadSave.meta, loadSave.buffers); generateWorld(77, 256, 256, null);');
check('generating a new world clears the load queue', run('loadWakeOrder === null && loadWakePending === null && loadWakeIndex === 0'));
// The Huge-world budget is checked with real ticks on quiet terrain.
flatWorld(run, 6144, 2048, 1900);
run('startLoadWake();');
check('a Huge world has 12288 scheduled areas', run('loadWakeOrder.length === 12288'));
ticks(run, 1023);
check('the final areas are still queued at tick 1023', run('loadWakeOrder !== null && loadWakePending.some(v => v > 0)'));
ticks(run, 1);
check('every Huge-world area has had its turn by tick 1024 (17.07 seconds)', run('loadWakeOrder === null && loadWakePending === null && loadWakeIndex === 12288'));
done();