Add the interface, then rebuild the economy around the first playtest
The MVP is playable: a turn screen, a career retrospective, an optional feedback widget, local-storage autosave with file export/import, and a build that flattens everything into one self-contained HTML file. A turn has two phases — the scenario and its options, then what the choice did and, separately, what the day cost regardless. Numbers that move without the player seeing why are most of what makes them meaningless. Locked options are shown greyed with the requirement spelled out rather than hidden, so a player can see the door they cannot open yet. The interface is named entirely by the pack. `pack.display` maps a state path to a label, a format and an order, which is why the header reads "Standing" for a path called `reputation` and why debt disappears while it is zero. The first playtest then found three things, all of them fair. *"Wasn't clear what daily drain was for."* The ledger was labelling changes by the path that moved — "Money −$18" — when the upkeep entry that caused them had a perfectly good name. It now says "Coffee, transit, lunch". The labels were in the data the whole time and never reached the screen. *"Should probably pay rent weekly / get paid every other week… would be good to have day of week and week # shown."* Packs can now declare a calendar, and the engine derives the day name, cycle and index from the turn number before the event is drawn — so content can require a Friday and the header can say "Thursday · Week 2 · Day 12". Upkeep entries take `every` and `offset`, making wages fall on alternate Fridays and rent every Monday. The turn screen carries a diary line — "Wages tomorrow · Rent in 4 days" — computed generically from whatever a pack schedules. *"Always in debt — could never get ahead."* The old economy bled $25 a day regardless of play. It is now roughly break-even at baseline, with a `spare_shift` event that appears *because* you are broke: the way out of a hole should be visible from inside it. Across archetypes a careful player ends around $1,400, an unplanned one treads water, and a careless one sinks into real debt. That retune broke the coverage test, usefully. Random play stopped reaching hard times at all, which made the entire debt branch look dead — it was not, since random play is not a plausible player. Coverage is now the union across five archetypes, including one that is broke *and* well-liked, because the "ask a friend for money" options sit in a state no single-axis strategy reaches. Not covered by any of this: styling and dark mode, which only a human with a browser can check. This machine has none that can render a page. 109 tests. Reasoning in docs/DECISIONS.md §14-21. 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
1d4ac0fdbd
commit
e482f07157
@@ -149,3 +149,115 @@ the game's. Both draw exactly one number per turn from the same generator, so a
|
||||
shared seed correlates the choice index with the event draw and makes whole
|
||||
option combinations structurally unreachable. It presents as a content bug and
|
||||
is not one. Any headless playtest harness written later has to know this.
|
||||
|
||||
## 14. The interface is named by the pack, not by the UI
|
||||
|
||||
`pack.display` maps a state path to `{ label, format, order, hidden,
|
||||
hideWhenZero }`. The header reads it to decide what to show, in what order, and
|
||||
whether a value is money or a bar. Anything the pack does not describe falls
|
||||
back to a name derived from the path, so content is never *required* to supply
|
||||
a label just to be playable.
|
||||
|
||||
This is why the header says "Standing" for a path called `reputation`, and why
|
||||
`debt` disappears while it is zero — both are content's decisions. A Wild West
|
||||
pack renames the whole interface without touching a UI file.
|
||||
|
||||
## 15. A turn has two phases
|
||||
|
||||
Choosing, then outcome. The outcome phase shows what the choice did, as a list
|
||||
of deltas, and separately what the day cost regardless — wages, rent, the loan.
|
||||
Collapsing these into one screen would have made the numbers move without the
|
||||
player seeing why, which is most of what makes them mean anything.
|
||||
|
||||
Locked options are rendered greyed with the requirement spelled out ("Needs
|
||||
Negotiation 15+") rather than hidden. A player should be able to see the door
|
||||
they cannot open yet; content can still opt into hiding one with
|
||||
`whenLocked: 'hide'`.
|
||||
|
||||
The feedback widget lives in the outcome phase, because that is the moment a
|
||||
player has an opinion — they have now seen both the options and the result.
|
||||
|
||||
## 16. Full re-render, with one deliberate exception
|
||||
|
||||
The root element is rebuilt from state on every change. At this size it is
|
||||
instant and it removes a whole class of stale-view bug.
|
||||
|
||||
The exception is the feedback text fields, which update the model without
|
||||
re-rendering: rebuilding the DOM under a cursor throws the player out of the
|
||||
box they are typing in. Their contents are flushed to the log on a timer, when
|
||||
the turn advances, and on `beforeunload`.
|
||||
|
||||
A related bug worth remembering: the flush originally skipped writing when the
|
||||
feedback was empty, which meant *clearing* a rating did not clear it from the
|
||||
log. Withdrawing feedback is itself feedback. The guard is now "has the player
|
||||
touched this", not "is there content".
|
||||
|
||||
## 17. The single-file build is verified by running it
|
||||
|
||||
`tools/build.js` inlines the stylesheet and flattens the module graph into one
|
||||
script. It is a deliberately small bundler that understands only the module
|
||||
syntax this project uses and **throws on anything it does not recognise** — a
|
||||
silent mis-bundle is far worse than a failed build.
|
||||
|
||||
The build is covered by a test that executes the bundled script against the
|
||||
fake DOM and plays a turn through it. A build that merely produces a file
|
||||
proves nothing; a broken bundle is otherwise discovered by whoever was handed
|
||||
the file.
|
||||
|
||||
## 18. What the tests do not cover
|
||||
|
||||
There is no browser on this machine that can render the page — headless Firefox
|
||||
cannot get a framebuffer, and no Chromium is installed. The UI tests drive the
|
||||
real app modules against a minimal fake DOM (`test/fixtures/fake-dom.js`), which
|
||||
covers wiring: imports resolve, handlers fire, screens render the right text,
|
||||
choices reach the engine, saves and the log are written.
|
||||
|
||||
It does not cover layout, CSS, the dark-mode palette, file downloads or file
|
||||
imports. Those need a human with a browser, and should be treated as unverified
|
||||
until someone has clicked them.
|
||||
|
||||
## 19. The calendar is the pack's, derived by the engine
|
||||
|
||||
First playtest: *"wasn't clear what daily drain was for… should probably pay
|
||||
rent weekly / get paid every other week… would be good to have day of week
|
||||
shown and week # instead of just day #"*.
|
||||
|
||||
A pack may declare `calendar: { cycleLength, cycleName, unitNames }`. The
|
||||
engine derives `calendar.index`, `calendar.cycle` and `calendar.name` from the
|
||||
turn number and writes them into engine-owned state *before* the turn's event
|
||||
is drawn — so content can require a Friday, and upkeep can charge rent on a
|
||||
Monday. A pack that declares no calendar has none; the engine imposes no week
|
||||
of its own, and `turnUnit` remains whatever the pack says.
|
||||
|
||||
## 20. Upkeep runs on a schedule
|
||||
|
||||
`every` and `offset` on an upkeep entry make it periodic: wages are
|
||||
`{ every: 14, offset: 4 }` — alternate Fridays, given that turn 1 is a Monday —
|
||||
and rent is `{ every: 7, offset: 7 }`. What was a flat $25/day of unexplained
|
||||
drain is now $18 of small change, a fortnightly paycheque and a weekly rent
|
||||
cheque, which is both easier to plan around and easier to feel.
|
||||
|
||||
Two display consequences, both of which were the real cause of the "unclear
|
||||
drain" complaint. The ledger now labels a change with the upkeep entry that
|
||||
caused it ("Rent −$455") rather than the path it moved ("Money −$455"). And
|
||||
the turn screen carries a diary line — "Wages tomorrow · Rent in 4 days" —
|
||||
computed generically from any pack's scheduled entries, so the player is told
|
||||
what is coming before it happens rather than after.
|
||||
|
||||
## 21. Content coverage is measured across player archetypes
|
||||
|
||||
Playtest: *"always in debt — could never get ahead"*. The economy was retuned so
|
||||
that a careful player climbs, a careless one sinks, and an unplanned one treads
|
||||
water, with a `spare_shift` event that surfaces *because* you are broke — the
|
||||
way out of a hole should be visible from inside it.
|
||||
|
||||
That retune broke the old coverage test, and usefully. Random play stopped
|
||||
reaching hard times at all, which made the whole debt branch look dead. It was
|
||||
not dead; random play is simply not a plausible player.
|
||||
|
||||
Coverage is now the union across five archetypes (`test/fixtures/strategies.js`)
|
||||
— random, thrifty, spendthrift, sociable, and "desperate", which is broke *and*
|
||||
well-liked. That last one exists because the "ask a friend for money" options
|
||||
sit in a state no single-axis strategy ever reaches. The economy's shape is
|
||||
asserted as an ordering between archetypes rather than as absolute figures, so
|
||||
tuning does not churn the tests.
|
||||
|
||||
Reference in New Issue
Block a user