Show the build, play from the keyboard, and clean up the ledger
Five things from the second playtest. A status line at the top of every screen: the game, the pack, your current title, and the version plus the commit it came from. `tools/stamp.js` writes the commit into src/build-info.js before serving and before building, so a distributed dist/theladder.html states exactly which commit produced it. An uncommitted working tree gets a `+` suffix — "4cab9fc+" is honest in a way a bare SHA would not be. The whole game is now playable on Enter. Every screen marks one control and the app focuses it after each render: the first *available* option while choosing, the next-turn button once the result is in, "keep going" on the retrospective. Tab and Shift+Tab reach the other options. Locked options needed no special handling — the browser's tab order already skips disabled controls, so they stay visible without being in the way. "What is notable? under What changed?" — two fixes. `notable` is the retrospective's own list of moments; a display rule can now say `inLedger: false` and it is left out, because "Notable — changed" tells nobody anything. Flags are the opposite and stay, but as sentences the pack supplies rather than as booleans: "The envelope is in your locker." instead of "the envelope: now true". Exported logs live in logs/, gitignored, and `npm run analyze` reads the newest one there when given no argument. A log records what a real person did turn by turn, including whatever they typed into the feedback box — committing one once would put it in the history for good. Version set to 0.3.0 on the reasoning that the engine was 0.1 and the interface 0.2; say if you want a different scheme and I will renumber. 119 tests. Reasoning in docs/DECISIONS.md §25-28. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VMSFHyVPitUoosW5wyEADj
This commit is contained in:
co-authored by
Claude Opus 5
parent
4cab9fc41e
commit
76072edf4e
+27
-3
@@ -10,7 +10,29 @@
|
||||
// It also surfaces the two things worth more than any rating — the gates
|
||||
// players kept bouncing off, and anything they said they wanted to do.
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { readFile, readdir, stat } from 'node:fs/promises';
|
||||
import { resolve, dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const logsDir = resolve(root, 'logs');
|
||||
|
||||
/** The most recently modified log in logs/, when none was named. */
|
||||
async function newestLog() {
|
||||
let names;
|
||||
try {
|
||||
names = (await readdir(logsDir)).filter((name) => name.endsWith('.json'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (!names.length) return null;
|
||||
|
||||
const timed = await Promise.all(names.map(async (name) => {
|
||||
const path = join(logsDir, name);
|
||||
return { path, at: (await stat(path)).mtimeMs };
|
||||
}));
|
||||
return timed.sort((a, b) => b.at - a.at)[0].path;
|
||||
}
|
||||
|
||||
const bar = (fraction, width = 24) =>
|
||||
'█'.repeat(Math.max(1, Math.round(fraction * width))).padEnd(width, '·');
|
||||
@@ -147,12 +169,14 @@ function reportFeedback(entries) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const path = process.argv[2];
|
||||
const path = process.argv[2] ?? await newestLog();
|
||||
if (!path) {
|
||||
console.error('usage: node tools/analyze-log.js <exported-feedback-log.json>');
|
||||
console.error('usage: npm run analyze -- <exported-feedback-log.json>');
|
||||
console.error(' or: drop the export into logs/ and run npm run analyze');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
if (!process.argv[2]) console.log(`reading ${path.replace(`${root}/`, '')}\n`);
|
||||
|
||||
const log = JSON.parse(await readFile(path, 'utf8'));
|
||||
const entries = log.entries ?? [];
|
||||
|
||||
Reference in New Issue
Block a user