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:
JesseMarkowitz
2026-09-10 05:41:32 -04:00
co-authored by Claude Opus 5
parent 4cab9fc41e
commit 76072edf4e
19 changed files with 416 additions and 25 deletions
+12 -3
View File
@@ -10,13 +10,15 @@
// because this is the moment a player has an opinion.
import { h } from '../dom.js';
import { statPanel, characterCard, changeList } from '../components.js';
import { statPanel, characterCard, changeList, statusBar } from '../components.js';
import { requirementText, turnHeading, upcomingUpkeep } from '../format.js';
export function turnScreen(pack, state, schema, view, actions) {
const heading = turnHeading(pack, state);
return h('div', { class: 'screen screen--turn' },
statusBar(pack, state),
h('header', { class: 'hud' },
h('div', { class: 'hud__where' },
h('p', { class: 'hud__stage' }, pack.stages?.[state.stage]?.title ?? state.stage),
@@ -78,6 +80,9 @@ function diary(pack, state) {
function choosingView(pack, view, actions) {
const { event, options } = view.turn;
const visible = options.filter((o) => !o.hidden);
// The first option a player can actually take gets the focus, so Enter plays
// it and Tab walks to the others.
const firstAvailable = visible.find((o) => o.available)?.option.id;
return h('section', { class: 'scenario' },
h('p', { class: 'scenario__text' }, event.description),
@@ -86,6 +91,7 @@ function choosingView(pack, view, actions) {
h('button', {
class: `option${available ? '' : ' option--locked'}`,
disabled: !available,
dataset: option.id === firstAvailable ? { autofocus: 'true' } : {},
onClick: available ? () => actions.onChoose(option.id) : null,
},
h('span', { class: 'option__label' }, option.label),
@@ -114,8 +120,11 @@ function outcomeView(pack, view, actions) {
feedbackWidget(view, actions),
h('button', { class: 'button button--primary button--next', onClick: actions.onNext },
`Next ${pack.turnUnit ?? 'turn'}`),
h('button', {
class: 'button button--primary button--next',
dataset: { autofocus: 'true' },
onClick: actions.onNext,
}, `Next ${pack.turnUnit ?? 'turn'} →`),
);
}