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
@@ -275,3 +275,94 @@ test('wages and rent appear in the ledger on the days they fall due', async () =
|
||||
dom.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('the status bar says what you are playing, which pack, and who you are', async () => {
|
||||
const dom = await boot();
|
||||
try {
|
||||
const status = text(firstByClass(dom.root, 'statusbar'));
|
||||
assert.match(status, /The Ladder/, 'the game');
|
||||
assert.match(status, /Corporate Ladder/, 'the pack');
|
||||
assert.match(status, /Mail Associate I/, 'the current title');
|
||||
assert.match(status, /\d+\.\d+\.\d+ · /, 'the version and build');
|
||||
} finally {
|
||||
dom.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('the status bar title follows the player up a tier', async () => {
|
||||
const { statusBar } = await import('../src/ui/components.js');
|
||||
const { corporateLadder } = await import('../content/corporateladder/index.js');
|
||||
const dom = installFakeDom();
|
||||
try {
|
||||
const promoted = statusBar(corporateLadder, { stage: 'dispatch' });
|
||||
assert.match(promoted.textContent, /Dispatch Coordinator/);
|
||||
} finally {
|
||||
dom.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('the keyboard alone can play a turn: focus lands where Enter should go', async () => {
|
||||
const dom = await boot();
|
||||
try {
|
||||
// Choosing: the first option a player can actually take.
|
||||
const firstPlayable = enabledOptions(dom.root)[0];
|
||||
assert.equal(dom.document.activeElement, firstPlayable,
|
||||
'the first available option is focused, so Enter takes it');
|
||||
|
||||
dom.document.activeElement.click();
|
||||
|
||||
// Outcome: the button that moves the run on.
|
||||
const next = firstByClass(dom.root, 'button--next');
|
||||
assert.equal(dom.document.activeElement, next,
|
||||
'the next-turn button is focused, so Enter advances');
|
||||
|
||||
dom.document.activeElement.click();
|
||||
assert.ok(firstByClass(dom.root, 'scenario'), 'and we are choosing again');
|
||||
assert.equal(dom.document.activeElement, enabledOptions(dom.root)[0]);
|
||||
} finally {
|
||||
dom.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('locked options are not in the tab order', async () => {
|
||||
const dom = await boot();
|
||||
try {
|
||||
// Disabled buttons are skipped by the browser's own tab handling; the
|
||||
// guarantee this test protects is that locked options stay disabled.
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const locked = optionButtons(dom.root).filter((b) => b.disabled);
|
||||
if (locked.length) {
|
||||
assert.ok(locked.every((b) => b.disabled === true));
|
||||
return;
|
||||
}
|
||||
enabledOptions(dom.root)[0].click();
|
||||
firstByClass(dom.root, 'button--next').click();
|
||||
}
|
||||
assert.fail('no locked option appeared in 40 turns');
|
||||
} finally {
|
||||
dom.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('the ledger reads as English, and leaves bookkeeping out of it', async () => {
|
||||
const { changeList } = await import('../src/ui/components.js');
|
||||
const { corporateLadder: pack } = await import('../content/corporateladder/index.js');
|
||||
const dom = installFakeDom();
|
||||
try {
|
||||
const rendered = changeList(pack, [
|
||||
{ path: 'money', op: 'add', value: -20, from: 520, to: 500, clamped: false },
|
||||
{ path: 'flags.holds_the_envelope', op: 'set', value: true, from: false, to: true, clamped: false },
|
||||
{ path: 'notable', op: 'push', value: 'Kept an envelope.', from: [], to: ['Kept an envelope.'], clamped: false },
|
||||
]);
|
||||
const body = rendered.textContent;
|
||||
|
||||
assert.match(body, /Money/);
|
||||
assert.match(body, /−\$20/);
|
||||
assert.match(body, /The envelope is in your locker\./, 'a flag reads as a sentence');
|
||||
assert.doesNotMatch(body, /now true/, 'not as a boolean');
|
||||
assert.doesNotMatch(body, /Notable/, 'the retrospective\'s own list stays out of the ledger');
|
||||
assert.doesNotMatch(body, /Kept an envelope\./);
|
||||
} finally {
|
||||
dom.restore();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user