74 lines
6.3 KiB
JavaScript
74 lines
6.3 KiB
JavaScript
// Dev command validation, discovery, build costs, runtime controls and save metadata.
|
|
const { load, suite, flatWorld } = require('./harness');
|
|
const { run } = load();
|
|
const { check, done } = suite();
|
|
flatWorld(run, 240, 160, 110);
|
|
const command = text => run(`devCommand(${JSON.stringify(text)})`);
|
|
check('commands cannot change an unmarked world', command('money 123')[0].includes('warning'));
|
|
run('enableDevConsole(); money = 100;');
|
|
command('MoNeY -25'); command('money set 1234.5');
|
|
check('money adds signed values and sets amounts, ignoring case', run('money === 1234.5'));
|
|
command('money -34.5');
|
|
check('negative money amount subtracts', run('money === 1200'));
|
|
command('debt 500');
|
|
check('debt sets claim amount', run('debt === 500'));
|
|
command('debt clear');
|
|
check('debt clear pays off claim', run('debt === 0'));
|
|
run('seen.fill(0); EV.length = 0;');
|
|
command('give cUoRe 7'); command('give Copper Bar 3'); command('give charc'); command('give sand 30000');
|
|
check('give matches internal names, display names and unique prefixes', run('bag[CUORE] === 7 && bag[BARCU] === 3 && bag[CHARCOAL] === 1000'));
|
|
check('give exceeds bag capacity, counts totals and discovers like a pickup', run("bag[SAND] === 30000 && bagTotal === 31010 && seen[SAND] && EV.some(e => e.t === 'discover' && e.m === SAND && e.how === 'pickup')"));
|
|
check('ambiguous prefix explains candidates', command('give cop')[0].startsWith('Ambiguous material:'));
|
|
check('unknown material is explained', command('give nonexistent')[0].startsWith('Unknown material'));
|
|
check('all internal aliases resolve exactly', run('Object.keys(DEV_MATERIALS).length === NMAT && Object.entries(DEV_MATERIALS).every(([k, v]) => devMaterial(k) === v)'));
|
|
command('give sand 2147483647');
|
|
check('give rejects typed-array overflow', run('bag[SAND] === 30000'));
|
|
command('bit 1e100'); command('bag -5');
|
|
check('tier setters clamp to valid range', run('player.bitTier === BIT_TIERS.length - 1 && player.tankTier === 0'));
|
|
check('tier queries list every tier', command('bit').length === run('BIT_TIERS.length') && command('bag').length === run('TANK_TIERS.length'));
|
|
run('player.lens = player.flask = false;'); command('unlock');
|
|
check('unlock learns materials, rewards and milestones', run('seen.every(Boolean) && player.lens && player.flask && MILESTONES.every(m => ledger[m.id])'));
|
|
command('tp -500 9999');
|
|
check('teleport clamps the entire bot inside the world', run('bot.x === 0 && bot.y === H - bot.h && bot.vx === 0 && bot.vy === 0'));
|
|
run('devCommand("tp", {x: 70, y: 45})');
|
|
check('teleport can use the cursor', run('bot.x === 70 && bot.y === 45'));
|
|
command('speed 1e100'); check('speed clamps high', run('devSpeed === 8'));
|
|
command('speed -1'); check('speed clamps low', run('devSpeed === 0.1'));
|
|
command('step'); check('step requires dev pause', run('devSteps === 0'));
|
|
command('pause'); command('step 5');
|
|
check('paused steps are queued for the frame loop', run('devPaused && devSteps === 5'));
|
|
command('pause'); check('resuming discards queued steps', run('!devPaused && devSteps === 0'));
|
|
const beforeBad = run('JSON.stringify([money, debt, bagTotal, player, devLog, devSpeed, devSteps])');
|
|
for (const text of ['money', 'money set', 'money Infinity', 'money 1e309', 'money 1 2', 'debt -1', 'debt nope', 'give sand -1', 'give sand 1.5', 'bit 1.5', 'bag nope', 'unlock extra', 'tp 1', 'tp NaN 4', 'free maybe', 'speed NaN', 'step 0', 'step 10001', 'pause extra', 'weather snow', 'nonsense']) {
|
|
const output = command(text);
|
|
check('bad arguments produce a helpful error: ' + text, output.length > 0 && !output[0].startsWith('>'));
|
|
}
|
|
check('invalid commands do not mutate or log', beforeBad === run('JSON.stringify([money, debt, bagTotal, player, devLog, devSpeed, devSteps])'));
|
|
check('help lists every command', command('help').length === 13);
|
|
run('money = 17; bag.fill(0); bagTotal = 0; bagAdd(RSTONE, 32, true); bagAdd(LCLAY, 100, true);');
|
|
check('normal build costs are checked', run('!canAfford([[CASH, 100]], 1)'));
|
|
command('free on');
|
|
const buildBag = run('JSON.stringify(Array.from(bag))');
|
|
check('free bypasses affordability', run('canAfford([[CASH, 100000], [BARCU, 99]], 1)'));
|
|
check('free places a wall without taking existing rubble', run("placeBlock(BP_WALL, RSTONE, 10, 10, false) === '' && bag[RSTONE] === 32"));
|
|
check('free places a machine without charging money', run("placeMachineAt('fan', 0, 24, 10) === '' && money === 17"));
|
|
check('free never charges, including blueprint ghosts built later', run('(() => { const m = money; payCost([[CASH, 500]], 3); return money === m; })()'));
|
|
check('free places a mold without taking clay', run("typeof addMold('ingot', 35, 10, LCLAY, false) !== 'string' && bag[LCLAY] === 100"));
|
|
run("ghostBlocks.set(12 * BW + 17, [BP_BELT, 1]); ghostMachines.push({type: 'fan', dir: 0, bx: 20, by: 17}); ghostMolds.push({kind: 'ingot', bx: 9, by: 16, wall: LCLAY}); ghostTick(); ghostTick();");
|
|
check('ghost blocks, machines and molds build free too', run('!ghostBlocks.size && !ghostMachines.length && !ghostMolds.length && money === 17 && ghostMissing === 0'));
|
|
check('all free paths preserve bag contents and totals', buildBag === run('JSON.stringify(Array.from(bag))') && run('bagTotal === 132'));
|
|
command('free off'); run('payCost([[CASH, 2], [RSTONE, 4]], 1)');
|
|
check('normal payment resumes after free off', run('money === 15 && bag[RSTONE] === 28 && bagTotal === 128'));
|
|
command('weather rain');
|
|
check('weather rain starts a shower near the bot', run('weather.shower && rainAt(bot.x) > 0 && weather.shower.until === tick + 3600'));
|
|
command('weather clear');
|
|
check('weather clear stops current shower', run('weather.shower === null && weather.raining.every(v => !v)'));
|
|
run('for (let i = 0; i < 210; i++) devCommand("money 1");');
|
|
check('dev log keeps the last 200 commands', run('devLog.length === 200 && devLog.every(e => e[0] === tick && e[1] === "money 1")'));
|
|
run('devCommand("speed 4"); devCommand("pause"); devCommand("free on"); const devSaved = serializeWorld(); const devLogCopy = JSON.stringify(devLog); deserializeWorld(devSaved.meta, devSaved.buffers);');
|
|
check('dev mark and command log survive save round trip', run('devWorld && devSaved.meta.dev === true && JSON.stringify(devLog) === devLogCopy'));
|
|
check('load resets speed, free building, pause and pending steps', run('devSpeed === 1 && !devFree && !devPaused && devSteps === 0'));
|
|
run('alloc(240,160)');
|
|
check('a new world starts unmarked without a dev log', run('!devWorld && devLog.length === 0'));
|
|
done();
|