diff --git a/docs/DESIGN.md b/docs/DESIGN.md index e0d8ef3..052ee80 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -482,6 +482,11 @@ above, including: doesn't need to fill a rectangle for belts, conveyors and the like. Belts, filters, screw conveyors, shafts, pipes, pumps and outlets (`lineOnly()`) always draw a line, Shift or not, and a diagonal belt drag stays sloped. Walls, grates and gates still Shift-fill. +- **Dev console name discovery and keys (t94, 2026-10-28).** The owner chose close-name + suggestions and a filtered material list because they had not known material names could + contain spaces. Match names with spaces ignored; keep display and internal names, with no + hover hint. Add Tab completion, persistent 50-command history, command-specific help, + output clearing and red errors for easier use. - **The dev console ships in every build (t66, 2026-10-27).** The owner is happy for players to experiment and play their own way, so the console isn't hidden behind a dev build. Turning it on warns first that the save will be marked, and the mark stays with the world; any future diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index 8e9aeb6..dca6272 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -387,10 +387,10 @@ 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. - functions: generateWorld, generateWorldAsync -### src/sim/devconsole.js (114 lines) +### src/sim/devconsole.js (185 lines) Dev commands are pure simulation operations; the browser owns the panel and confirmation. -- functions: resetDevConsole, enableDevConsole, devMaterial, devCommand -- variables: devWorld, devLog, devFree, devSpeed, devPaused, devSteps, DEV_HELP, DEV_MATERIALS +- functions: resetDevConsole, enableDevConsole, devError, devName, devGiveable, devMaterialList, devDistance, devSuggestions, devHistory, devRemember, devRecall, devComplete, devMaterial, devCommand +- variables: devWorld, devLog, devFree, devSpeed, devPaused, devSteps, DEV_COMMANDS, DEV_MATERIALS ### src/sim/save.js (185 lines) Saving and loading a world (serializeWorld / deserializeWorld) and old-save migration. @@ -572,9 +572,10 @@ Hover work is shared by the cursor label and the vein outline. Discovery cards, hover info (I) and the cursor label. - functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel, pipeContentsText -### src/ui/devconsole.js (42 lines) +### src/ui/devconsole.js (71 lines) Bottom panel; command dispatch and state live in sim/devconsole.js. - functions: closeDevConsole, showDevConsole, toggleDevConsole, devPrint, runDevCommand +- variables: devInputHistory, devCompletion ### src/ui/input.js (92 lines) Keyboard and mouse listeners. diff --git a/docs/agent-log.md b/docs/agent-log.md index c4222a0..4199e47 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -6,6 +6,26 @@ open or not verified. Keep each entry short; the git history has the details. +## 2026-10-28: t94 dev console discovery and keyboard ease of use (Codex) + +- Added case/space-insensitive material lookup, three close giveable suggestions and + materials filtering with internal names. Command definitions hold help usage/examples + and keyword candidates. Pure helpers handle cycling completion and capped 50-command + history; UI owns storage (all accesses guarded), keys, output clearing and red errors. + Tab, arrows and Ctrl+L stop before game controls. Simulation tick behaviour unchanged. +- Tests added first: name/filter/help checks failed before implementation. Corrected test + assumptions for the separate giveable Copper ore vein and for the latest submitted + history command. Full python3 test/run_all.py --browser: ALL PASSED (34 headless, + 9 browser suites); console checks: 77 Node, 27 browser. Log: /tmp/t94-all-browser.log. + Browser checks cycling Tab, Up/Down, Ctrl+L, exact bot/control isolation, colours, + clear and history across page reload. Saved and inspected shots/t94-console.png. +- Files: src/sim/devconsole.js, src/ui/devconsole.js, src/styles/devconsole.css, + test/t_dev_console.js, test/t_dev_console.py, docs/DESIGN.md, docs/PROJECT_MAP.md, + this log and only the t94 task (Review). No commit; wrapper owns committing. +- Environment: docs/kandown.md is absent; read kandown work rules. Sandboxed browser + timed out starting a world; the complete passing run used host GPU access. + Owner playtesting remains open. + ## 2026-10-28: t93 round 3 review and tidy (Claude) @@ -62,7 +82,6 @@ open or not verified. Keep each entry short; the git history has the details. /tmp/t93c-red.log, /tmp/t93c-browser-red.log, /tmp/t93c-browser-final.log, /tmp/t93c-final-headless2.log. - ## 2026-10-28: t93 reopened, vacuum-cut faces drain in columns (Codex) - Read AGENTS, the t93 task/log and 807217b; inspected both new owner screenshots. diff --git a/src/sim/devconsole.js b/src/sim/devconsole.js index e953c88..58a3871 100644 --- a/src/sim/devconsole.js +++ b/src/sim/devconsole.js @@ -4,41 +4,112 @@ function resetDevConsole() { devWorld = false; devLog = []; devFree = false; devSpeed = 1; devPaused = false; devSteps = 0; } function enableDevConsole() { devWorld = true; } -const DEV_HELP = [ - 'help — list commands', - 'money | money set — add or set money; negative amounts allowed', - 'debt | debt clear — set or clear claim debt', - 'give [n] — add grains or items (default 1000); display or internal name, unique prefix', - 'bit [tier] — list or set drill tiers (numbered from 0)', - 'bag [tier] — list or set bag tiers (numbered from 0)', - 'unlock — learn all materials and unlock progress rewards', - 'tp [x y] — teleport in grains; no coordinates uses the cursor', - 'free on|off — build without spending money or materials', - 'speed — speed multiplier (0.25, 0.5, 1, 2, 4; clamped to 0.1–8)', - 'pause — toggle dev pause', - 'step [n] — queue 1–10000 ticks while dev paused (default 1)', - 'weather clear|rain — stop current rain or start a one-minute shower', -]; +// Each command defines its usage, example and completion keywords together. +const DEV_COMMANDS = { + help: ['help [command] — list commands or explain one', 'help give'], + materials: ['materials [text] — list giveable names and internal names', 'materials copper'], + clear: ['clear — empty console output', 'clear'], + money: ['money | money set — add or set money; negative amounts allowed', 'money set 500', ['set']], + debt: ['debt | debt clear — set or clear claim debt', 'debt clear', ['clear']], + give: ['give [n] — add grains or items (default 1000); display or internal name, unique prefix', 'give copper ore 500'], + bit: ['bit [tier] — list or set drill tiers (numbered from 0)', 'bit 1'], + bag: ['bag [tier] — list or set bag tiers (numbered from 0)', 'bag 1'], + unlock: ['unlock — learn all materials and unlock progress rewards', 'unlock'], + tp: ['tp [x y] — teleport in grains; no coordinates uses the cursor', 'tp 100 50'], + free: ['free on|off — build without spending money or materials', 'free on', ['on', 'off']], + speed: ['speed — speed multiplier (0.25, 0.5, 1, 2, 4; clamped to 0.1–8)', 'speed 2'], + pause: ['pause — toggle dev pause', 'pause'], + step: ['step [n] — queue 1–10000 ticks while dev paused (default 1)', 'step 10'], + weather: ['weather clear|rain — stop current rain or start a one-minute shower', 'weather rain', ['clear', 'rain']], +}; +function devError(text) { return Object.assign([text], { error: true }); } +function devName(text) { return text.toLowerCase().replace(/\s+/g, ''); } +function devGiveable(id) { return id !== EMPTY && PH[id] !== PH_GAS && (PH[id] !== PH_SOLID || !!YLD[id]); } +function devMaterialList(text = '') { + return Object.entries(DEV_MATERIALS).filter(([key, id]) => devGiveable(id) && (devName(key).includes(devName(text)) || devName(MD[id].name).includes(devName(text)))); +} +// Edit distance is only used on a console error, never in the simulation tick. +function devDistance(a, b) { + const row = Array.from({ length: b.length + 1 }, (_, i) => i); + for (let i = 1; i <= a.length; i++) { + let diagonal = row[0]; row[0] = i; + for (let j = 1; j <= b.length; j++) { + const old = row[j]; + row[j] = Math.min(row[j] + 1, row[j - 1] + 1, diagonal + (a[i - 1] !== b[j - 1])); + diagonal = old; + } + } + return row[b.length]; +} +function devSuggestions(name) { + return devMaterialList().map(([key, id]) => ({ id, distance: Math.min(devDistance(name, devName(key)), devDistance(name, devName(MD[id].name))) })) + .sort((a, b) => a.distance - b.distance).slice(0, 3).map(({ id }) => MD[id].name).join(', '); +} +function devHistory(items) { + items = Array.isArray(items) ? items.filter(s => typeof s === 'string' && s.trim()).slice(-50) : []; + return { items, index: items.length }; +} +function devRemember(history, text) { + if (text.trim()) { history.items.push(text.trim()); if (history.items.length > 50) history.items.shift(); } + history.index = history.items.length; +} +function devRecall(history, direction) { + history.index = Math.max(0, Math.min(history.items.length, history.index + direction)); + return history.items[history.index] || ''; +} +function devComplete(text, previous) { + if (previous && previous.text === text && previous.matches.length) { + const index = (previous.index + 1) % previous.matches.length; + return { matches: previous.matches, index, text: previous.matches[index] }; + } + const input = text.trimStart(), split = input.indexOf(' '); + let matches; + if (split < 0) matches = Object.keys(DEV_COMMANDS).filter(k => k.startsWith(input.toLowerCase())); + else { + const cmd = input.slice(0, split).toLowerCase(), arg = input.slice(split + 1); + if (cmd === 'give') { + const amount = arg.match(/\s+[-+]?\d+(?:\.\d+)?$/), suffix = amount ? amount[0] : ''; + const name = devName(suffix ? arg.slice(0, -suffix.length) : arg); + matches = [...new Set(devMaterialList().filter(([key, id]) => devName(key).startsWith(name) || devName(MD[id].name).startsWith(name)) + .map(([, id]) => 'give ' + MD[id].name + suffix))]; + } else { + const words = cmd === 'help' ? Object.keys(DEV_COMMANDS) : Object.hasOwn(DEV_COMMANDS, cmd) ? DEV_COMMANDS[cmd][2] || [] : []; + matches = words.filter(k => k.startsWith(arg.toLowerCase())).map(k => cmd + ' ' + k); + } + } + return { matches, index: 0, text: matches[0] || text }; +} // Exact internal names win when a display name names a different material. const DEV_MATERIALS = { EMPTY, GRASS, DIRT, LDIRT, SAND, GRAVEL, CLAY, STONE, GRANITE, BASALT, WOOD, LEAVES, COALV, COAL, CUV, CUORE, FEV, FEORE, ASH, SLAG, BRICK, GLASS, COPPER, IRON, STEEL, WATER, MAGMA, MCU, MFE, MST, MGL, STEAM, SMOKE, FIRE, BELTL, BELTR, SHAFT, GATE, HULL, SPRING, RSTONE, RGRAN, RBAS, LCLAY, LWOOD, RBRICK, CULLET, SCRCU, SCRFE, SCRST, BARCU, BARFE, BARST, ACORN, CHARCOAL, SNOW, ICE, SNV, SNORE, TIN, MSN, SCRSN, BARSN, ZNV, ZNORE, ZINC, MZN, SCRZN, BARZN, AGV, AGORE, SILVER, MAG, SCRAG, BARAG, AUV, AUORE, GOLD, MAU, SCRAU, BARAU, BRONZE, MBZ, SCRBZ, BARBZ, BRASS, MBS, SCRBS, BARBS, DOCKW, MSLAG, PIPE, BARGL, CUTST, BARSB, GRINDER, SAWB, PLANK, PRESSB, GREENB, BARBR, RACKB }; function devMaterial(name) { - name = name.toLowerCase(); + name = devName(name); const ids = Object.entries(DEV_MATERIALS); - const internal = ids.find(([key]) => key.toLowerCase() === name); + const internal = ids.find(([key]) => devName(key) === name); if (internal) return internal[1]; - const exact = ids.filter(([, id]) => MD[id].name.toLowerCase() === name); - const matches = [...new Set((exact.length ? exact : ids.filter(([key, id]) => key.toLowerCase().startsWith(name) || MD[id].name.toLowerCase().startsWith(name))).map(([, id]) => id))]; - return matches.length === 1 ? matches[0] : matches.length ? `Ambiguous material: ${matches.map(id => MD[id].name + ' (' + ids.find(([, m]) => m === id)[0] + ')').join(', ')}.` : 'Unknown material. Use a display name or internal name, such as copper ore or CUORE.'; + const exact = ids.filter(([, id]) => devName(MD[id].name) === name); + const matches = [...new Set((exact.length ? exact : ids.filter(([key, id]) => devName(key).startsWith(name) || devName(MD[id].name).startsWith(name))).map(([, id]) => id))]; + return matches.length === 1 ? matches[0] : matches.length ? `Ambiguous material: ${matches.map(id => MD[id].name + ' (' + ids.find(([, m]) => m === id)[0] + ')').join(', ')}.` : 'Unknown material. Did you mean: ' + devSuggestions(name) + '? Use materials to list names.'; } function devCommand(text, cursor) { if (typeof text !== 'string' || !text.trim()) return ['Type help to list commands.']; text = text.trim(); const [cmd, ...args] = text.toLowerCase().split(/\s+/); - const usage = () => ['Usage: ' + DEV_HELP.find(s => s.startsWith(cmd + ' '))]; + const usage = () => devError('Usage: ' + DEV_COMMANDS[cmd][0]); const number = s => s !== undefined && Number.isFinite(Number(s)); - if (cmd === 'help' && !args.length) return DEV_HELP.slice(); - if (!DEV_HELP.some(s => s.startsWith(cmd + ' '))) return ['Unknown command. Type help to list commands.']; - if (!devWorld) return ['Open the dev console and accept the world warning first.']; + if (!Object.hasOwn(DEV_COMMANDS, cmd)) return devError('Unknown command. Type help to list commands.'); + if (cmd === 'help') { + if (!args.length) return Object.values(DEV_COMMANDS).map(def => def[0]); + if (args.length !== 1) return usage(); + if (!Object.hasOwn(DEV_COMMANDS, args[0])) return devError('Unknown command. Type help to list commands.'); + const def = DEV_COMMANDS[args[0]]; + return ['Usage: ' + def[0], 'Example: ' + def[1]]; + } + if (cmd === 'materials') { + const lines = devMaterialList(args.join(' ')).map(([key, id]) => MD[id].name + ' (' + key + ')'); + return lines.length ? lines : ['No matching materials.']; + } + if (cmd === 'clear') return args.length ? usage() : Object.assign([], { clear: true }); + if (!devWorld) return devError('Open the dev console and accept the world warning first.'); let result; switch (cmd) { case 'money': { @@ -56,9 +127,9 @@ function devCommand(text, cursor) { if (number(args[args.length - 1])) n = Number(args.pop()); if (!args.length || !Number.isInteger(n) || n <= 0) return usage(); const m = devMaterial(args.join(' ')); - if (typeof m === 'string') return [m]; - if (m === EMPTY || PH[m] === PH_GAS || (PH[m] === PH_SOLID && !YLD[m])) return [`${MD[m].name} cannot go in the bag.`]; - if (n > 2147483647 - bag[m]) return ['Too many: a material count cannot exceed 2147483647.']; + if (typeof m === 'string') return devError(m); + if (!devGiveable(m)) return devError(`${MD[m].name} cannot go in the bag.`); + if (n > 2147483647 - bag[m]) return devError('Too many: a material count cannot exceed 2147483647.'); const into = PH[m] === PH_SOLID && YLD[m] ? YLD[m] : m; bagAdd(m, n, true); result = `Added ${n} ${MD[into].name}${into !== m ? ` (as ${MD[m].name.toLowerCase()} breaks up)` : ''}.`; break; } @@ -78,7 +149,7 @@ function devCommand(text, cursor) { case 'tp': { if (args.length !== 0 && (args.length !== 2 || !args.every(number))) return usage(); const x = args.length ? Number(args[0]) : cursor && cursor.x, y = args.length ? Number(args[1]) : cursor && cursor.y; - if (!Number.isFinite(x) || !Number.isFinite(y)) return ['No cursor position. Use tp .']; + if (!Number.isFinite(x) || !Number.isFinite(y)) return devError('No cursor position. Use tp .'); bot.x = Math.max(0, Math.min(W - bot.w, x)); bot.y = Math.max(0, Math.min(H - bot.h, y)); bot.vx = bot.vy = 0; fogLook(bot.x + bot.w / 2, bot.y + bot.h / 2); result = `Bot: ${bot.x}, ${bot.y} grains`; break; @@ -95,7 +166,7 @@ function devCommand(text, cursor) { case 'step': { const n = args.length ? Number(args[0]) : 1; if (args.length > 1 || !Number.isInteger(n) || n < 1 || n > 10000 || devSteps + n > 10000) return usage(); - if (!devPaused) return ['Use pause before step.']; + if (!devPaused) return devError('Use pause before step.'); devSteps += n; result = `Queued ${n} ticks.`; break; } case 'weather': diff --git a/src/styles/devconsole.css b/src/styles/devconsole.css index 5eb4135..b20e5ab 100644 --- a/src/styles/devconsole.css +++ b/src/styles/devconsole.css @@ -11,6 +11,7 @@ font: 12px/1.6 var(--font-mono); white-space: pre-wrap; overflow-wrap: anywhere; user-select: text; } +#dev-output .dev-error { color: var(--danger); } #dev-form { display: flex; align-items: center; gap: 10px; } #dev-input { flex: 1; min-width: 0; font-family: var(--font-mono); } .dev-badge { display: inline-block; padding: 1px 6px; border: 1px solid var(--patina-dim); border-radius: 4px; color: var(--patina); font-size: 11px; } diff --git a/src/ui/devconsole.js b/src/ui/devconsole.js index b9089c8..af95512 100644 --- a/src/ui/devconsole.js +++ b/src/ui/devconsole.js @@ -1,4 +1,7 @@ // Bottom panel; command dispatch and state live in sim/devconsole.js. +let devInputHistory = devHistory([]), devCompletion = null; +try { devInputHistory = devHistory(JSON.parse(localStorage.getItem('deepgrain.consoleHistory'))); } catch {} + function closeDevConsole() { $('dev-console').hidden = true; $('dev-input').blur(); @@ -21,22 +24,48 @@ function toggleDevConsole() { } function devPrint(lines) { const log = $('dev-output'); - for (const line of lines) { const row = document.createElement('div'); row.textContent = line; log.appendChild(row); } + for (const line of lines) { + const row = document.createElement('div'); row.textContent = line; + if (lines.error) row.className = 'dev-error'; + log.appendChild(row); + } while (log.children.length > 400) log.firstChild.remove(); log.scrollTop = log.scrollHeight; } function runDevCommand(text) { - devPrint(devCommand(text, GAME.aim)); + const result = devCommand(text, GAME.aim); + if (result.clear) $('dev-output').replaceChildren(); + else devPrint(result); hudDirty = true; lastBagKey = ''; lastBuildKey = ''; renderWallet(); if (!$('m-notebook').hidden) renderNotebook(); } $('dev-close').onclick = closeDevConsole; $('dev-input').onfocus = () => { keys.clear(); GAME.lmb = GAME.rmb = false; GAME.drag = null; GAME.replace = false; }; +$('dev-input').oninput = () => { devCompletion = null; }; $('dev-input').onkeydown = e => { - if (e.key === 'Escape' || e.key === '`') { e.preventDefault(); e.stopPropagation(); closeDevConsole(); } + e.stopPropagation(); + const input = $('dev-input'); + if (e.key === 'Escape' || e.key === '`') { e.preventDefault(); closeDevConsole(); } + else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { + e.preventDefault(); devCompletion = null; + input.value = devRecall(devInputHistory, e.key === 'ArrowUp' ? -1 : 1); + input.setSelectionRange(input.value.length, input.value.length); + } else if (e.key === 'Tab') { + e.preventDefault(); + const cycling = devCompletion && devCompletion.text === input.value; + devCompletion = devComplete(input.value, devCompletion); + input.value = devCompletion.text; + if (!cycling && devCompletion.matches.length > 1) devPrint(['Matches: ' + devCompletion.matches.join(', ')]); + } else if (e.ctrlKey && e.key.toLowerCase() === 'l') { + e.preventDefault(); $('dev-output').replaceChildren(); + } else if (!['Shift', 'Control', 'Alt', 'Meta'].includes(e.key)) devCompletion = null; }; $('dev-form').onsubmit = e => { e.preventDefault(); - const input = $('dev-input'); runDevCommand(input.value); input.value = ''; input.focus(); + const input = $('dev-input'); + devRemember(devInputHistory, input.value); + try { localStorage.setItem('deepgrain.consoleHistory', JSON.stringify(devInputHistory.items)); } catch {} + devCompletion = null; + runDevCommand(input.value); input.value = ''; input.focus(); }; diff --git a/test/t_dev_console.js b/test/t_dev_console.js index 7d76c77..874c8b4 100644 --- a/test/t_dev_console.js +++ b/test/t_dev_console.js @@ -44,7 +44,7 @@ for (const text of ['money', 'money set', 'money Infinity', 'money 1e309', 'mone 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); +check('help lists every command', command('help').length === 15); 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'); @@ -72,4 +72,21 @@ run('alloc(240,160)'); check('a new world starts unmarked without a dev log', run('!devWorld && devLog.length === 0')); check('give turns rock into its rubble and says so', run(`(() => { enableDevConsole(); const before = bag[RBAS]; const out = devCommand('give basalt 50').join(' '); return bag[RBAS] === before + 50 && bag[BASALT] === 0 && /Basalt rubble/.test(out); })()`)); check('give refuses things that cannot be in a bag', run(`(() => { enableDevConsole(); const out = devCommand('give steam 5').join(' '); return /cannot go in the bag/.test(out) && bag[STEAM] === 0; })()`)); + +check('material matching ignores spaces and case', run("devMaterial('CoPpErOrE') === CUORE && devMaterial('copper bar') === BARCU")); +check('unknown material suggests close giveable names', /Did you mean:.*Copper ore/i.test(command('give coperore')[0])); +check('materials lists giveable names with internal names', command('materials').some(s => /Copper ore.*CUORE/.test(s)) && !command('materials').some(s => /STEAM/.test(s))); +check('materials filtering ignores spaces and case', command('materials CoPpErOrE').every(s => /Copper ore/i.test(s)) && command('materials copperore').some(s => /CUORE/.test(s))); +check('help for a command has usage and example', command('help give').some(s => s.startsWith('Usage: give')) && command('help give').some(s => s.startsWith('Example: give copper ore'))); +check('all commands have detailed help', run("Object.keys(DEV_COMMANDS).every(k => devCommand('help ' + k).some(s => s.startsWith('Example:')))")); +check('errors carry explicit status while results do not', command('give no-such-material').error === true && !command('materials').error && command('money nope').error === true); +check('clear requests output clearing without changing world', command('clear').clear === true); +check('command completion cycles and wraps', run("(() => { const a = devComplete('s'), b = devComplete(a.text, a), c = devComplete(b.text, b); return a.matches.length === 2 && a.text === 'speed' && b.text === 'step' && c.text === a.text; })()")); +check('completion keeps spaced materials and amount', run("devComplete('give copperore 25').matches.includes('give Copper ore 25')")); +check('completion supports internal names', run("devComplete('give cuor').text === 'give Copper ore'")); +check('completion cycles ambiguous materials', run("(() => { const a = devComplete('give copper'), b = devComplete(a.text, a); return a.matches.length > 1 && a.text !== b.text; })()")); +check('completion supports keywords and help commands', run("devComplete('free o').matches.length === 2 && devComplete('money s').text === 'money set' && devComplete('debt c').text === 'debt clear' && devComplete('weather r').text === 'weather rain' && devComplete('help gi').text === 'help give'")); +check('completion leaves unmatched input intact', run("devComplete('give zzzzz').text === 'give zzzzz'")); +check('history validates stored data', run("devHistory(null).items.length === 0 && devHistory(['ok', 5, '', ' ']).items.join() === 'ok'")); +check('history caps at 50 and navigates to empty after newest', run("(() => { const h = devHistory([]); for (let i=0; i<55; i++) devRemember(h, 'money ' + i); devRemember(h, ' '); const newest = devRecall(h, -1); for (let i=0;i<60;i++) devRecall(h,-1); const oldest = devRecall(h,-1); for(let i=0;i<60;i++) devRecall(h,1); return h.items.length === 50 && newest === 'money 54' && oldest === 'money 5' && devRecall(h,1) === ''; })()")); done(); diff --git a/test/t_dev_console.py b/test/t_dev_console.py index babf23a..253ec33 100644 --- a/test/t_dev_console.py +++ b/test/t_dev_console.py @@ -59,6 +59,31 @@ with launch() as (browser, page, url): tick = page.evaluate('tick') page.wait_for_timeout(150) check('dev pause freezes simulation ticks', page.evaluate('tick') == tick) + position = page.evaluate("[bot.x, bot.y, bot.lamp, GAME.tool, $('bagpanel').hidden]") + page.fill('#dev-input', 'give copper') + page.keyboard.press('Tab') + first = page.input_value('#dev-input') + page.keyboard.press('Tab') + check('Tab completes spaced material names and cycles matches', first.startswith('give Copper') and first != page.input_value('#dev-input') and 'Matches:' in page.inner_text('#dev-output')) + page.keyboard.press('ArrowUp') + check('Up recalls the newest submitted command', page.input_value('#dev-input') == 'pause') + page.keyboard.press('ArrowDown') + check('Down beyond newest returns an empty input', page.input_value('#dev-input') == '') + page.keyboard.press('Control+l') + check('Ctrl+L clears output and keeps console focus', page.evaluate("$('dev-output').children.length === 0 && document.activeElement === $('dev-input')")) + page.wait_for_timeout(100) + check('Tab, arrows and Ctrl+L leave bot and game controls alone', page.evaluate("[bot.x, bot.y, bot.lamp, GAME.tool, $('bagpanel').hidden]") == position and page.evaluate('keys.size === 0 && !GAME.ctrl')) + command('materials copperore') + command('help give') + command('give coperore') + check('errors are red while results use normal colour', page.evaluate('''() => { + const rows = $('dev-output').children, error = rows[rows.length - 1]; + return error.classList.contains('dev-error') && getComputedStyle(error).color === 'rgb(239, 106, 85)' && getComputedStyle(error).color !== getComputedStyle(rows[0]).color; + }''')) + page.screenshot(path=str(SHOTS / 't94-console.png')) + command('clear') + check('clear command empties output', page.evaluate("$('dev-output').children.length === 0")) + check('submitted history is stored across sessions', page.evaluate("JSON.parse(localStorage.getItem('deepgrain.consoleHistory')).slice(-1)[0] === 'clear'")) command('step 5') page.wait_for_function(f'tick === {tick + 5} && devSteps === 0') page.wait_for_timeout(150) @@ -93,6 +118,9 @@ with launch() as (browser, page, url): await startFromSave(sv); return devWorld && devLog.length === count && devSpeed === 1 && !devPaused && !devFree && $('dev-console').hidden; }''')) + page.reload() + page.wait_for_function("!$('title').hidden") + check('history is restored on a fresh page', page.evaluate("devInputHistory.items.slice(-1)[0] === 'step 5' && devRecall(devInputHistory, -1) === 'step 5'")) check('no browser errors', not errors) browser.close()