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 7fa1056e09
commit 0a8847594e
7 changed files with 137 additions and 12 deletions
+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();