120 lines
7.3 KiB
JavaScript
120 lines
7.3 KiB
JavaScript
// Reload wakes nearby areas first, without losing distant water, fire or saved state.
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { load, suite, flatWorld, rect, count, ticks } = require('./harness');
|
|
const { run } = load();
|
|
const { check, done } = suite();
|
|
|
|
// Exercise the real hover function headlessly, without starting a browser or GPU.
|
|
run("const localStorage = { getItem: () => '{}', setItem() {} };");
|
|
for (const f of ['ui/state.js', 'ui/hud.js', 'ui/cursor.js', 'ui/info.js'])
|
|
run(fs.readFileSync(path.join(__dirname, '../src', f), 'utf8'));
|
|
flatWorld(run, 256, 256, 200);
|
|
run('globalThis.validationSave = serializeWorld();');
|
|
function refusedBeforeAlloc(label, doctor, message) {
|
|
check(label, run(`(() => {
|
|
const s = { meta: { ...validationSave.meta, cols: { ...validationSave.meta.cols } }, buffers: { ...validationSave.buffers } };
|
|
${doctor}
|
|
const originalAlloc = alloc, originalMat = mat, originalBlocks = blockP;
|
|
const oldW = W, oldH = H, oldMoney = money, oldX = bot.x;
|
|
let allocations = 0, error;
|
|
alloc = () => { allocations++; throw new Error('Allocation reached'); };
|
|
try { deserializeWorld(s.meta, s.buffers); } catch (e) { error = e; }
|
|
finally { alloc = originalAlloc; }
|
|
return error instanceof Error && ${message}.test(error.message) && allocations === 0
|
|
&& W === oldW && H === oldH && mat === originalMat && blockP === originalBlocks
|
|
&& money === oldMoney && bot.x === oldX;
|
|
})()`));
|
|
}
|
|
for (const [w, h] of [[1000000, 1000000], [6145, 1024], [2048, 2049], [0, 1024], [2048, -1], [2048.5, 1024], [2048, NaN]])
|
|
refusedBeforeAlloc(`invalid size ${w} x ${h} is refused before touching the world`, `s.meta.W = ${w}; s.meta.H = ${h};`, '/world size/i');
|
|
for (const key of ['mat', 'temp', 'life', 'flags', 'part', 'blockP', 'blockV', 'bgm', 'fog']) {
|
|
refusedBeforeAlloc(`short ${key} buffer is refused before touching the world`, `s.buffers.${key} = s.buffers.${key}.subarray(1);`, `/${key}/i`);
|
|
refusedBeforeAlloc(`long ${key} buffer is refused before touching the world`, `s.buffers.${key} = new s.buffers.${key}.constructor(s.buffers.${key}.length + 1);`, `/${key}/i`);
|
|
}
|
|
for (const key of ['mat', 'temp', 'life', 'flags', 'part', 'blockP', 'blockV'])
|
|
refusedBeforeAlloc(`missing ${key} buffer is refused before touching the world`, `delete s.buffers.${key};`, `/${key}/i`);
|
|
for (const key of ['surfY', 'dirtY', 'graniteY', 'basaltY', 'magmaY'])
|
|
refusedBeforeAlloc(`short ${key} column is refused before touching the world`, `s.meta.cols.${key} = [];`, `/${key}/i`);
|
|
refusedBeforeAlloc('short slope phases are refused before touching the world', 's.meta.slopePhases = [0];', '/slopePhases/i');
|
|
for (const [label, doctor, clean] of [
|
|
['unknown cell material', 's.buffers.mat[20 * W + 20] = 250;', 'mat[20 * W + 20] === EMPTY'],
|
|
['unknown block code', 's.buffers.blockP[5 * BW + 5] = 250;', 'blockP[5 * BW + 5] === 0'],
|
|
]) {
|
|
check(label + ' loads cleaned and permits hover and removal', run(`(() => {
|
|
const s = serializeWorld(); ${doctor}
|
|
try {
|
|
deserializeWorld(s.meta, s.buffers);
|
|
SET.fog = false; player.lens = false;
|
|
return ${clean} && hoverInfo(20, 20).air && removePieceAt(20, 20) === '';
|
|
} catch (e) { return false; }
|
|
})()`));
|
|
}
|
|
|
|
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'));
|
|
|
|
// Burning is probabilistic: waking does not promise a fuel draw on that exact tick.
|
|
ticks(run, 32);
|
|
check('newly woken fuel resumes burning', run('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();
|