Files
Country-Game---The-Desk/docs/PLAN.md
T

32 KiB
Raw Blame History

Country Game: "The Desk" — Plan

Shared plan for both of us. The first coding step copies this file to docs/PLAN.md in the repo. From then on, that copy is the one to follow: tick the checkboxes, edit the numbers, and add entries to the decisions log at the bottom.

Context

The goal is a single-player browser game in the style of Suzerain, delivered as a docker-compose service. You rule a fictional country for 5 in-game years. You set the sector budgets (in % of GDP) every quarter and answer a yes/no policy card every week. The country's indicators change a little every in-game day. Debt, unrest and repression feed into each other and can end the run early (revolution, coup or default). If you survive, the game ends on a 5-year review ("bilan"). Everything on screen is shown as paper documents on a wooden desk with a leather pad.

The project folder is empty, so everything below is new code.

Decisions taken

Topic Decision
Backend Go, standard library only (net/http + embed). It serves static files and has a /healthz endpoint. No game logic runs on the server.
Frontend Vanilla JS ES modules + CSS, no bundler and no npm. The only runtime dependency is zero: no framework, no chart library (charts are hand-drawn SVG).
Fonts Special Elite (typewriter text, Apache-2.0, not OFL), IM Fell English (headings, OFL) and Courier Prime (figures, OFL), stored with their licence files in web/fonts/ so the game works offline.
Time 1 day = 10 s at 1× speed (a full run is about 5 h). Speed controls: pause, 1×, 2×, 4×, plus a "slow" setting (60 s/day) in Settings. The game pauses automatically when the weekly card arrives, at the quarterly budget review, and on crisis telegrams.
Saves Auto-save to localStorage every in-game day. 3 manual save slots. Export/import as a plain base64 string (and as a downloadable .sav file).
Language English and French. Every text field is { "en": "...", "fr": "..." }, and there's a language switch in the menu.
Randomness A seeded PRNG (mulberry32) whose state is stored in the save, so reloading a save doesn't re-roll the cards.

About "AppData": a web page can't write to AppData or the macOS/Linux equivalents directly. However, localStorage is already stored on disk inside the browser profile (which on Windows lives under AppData). The .sav file export covers the case of keeping a save file you can see.

Repository layout

country_game/
├─ docker-compose.yml          # one service "game", port 8080
├─ Dockerfile                  # golang:1.23-alpine build stage → gcr.io/distroless/static
├─ go.mod                      # module countrygame, no require block
├─ server/
│  ├─ main.go                  # embed ../web, FileServer, cache headers, /healthz, PORT env
│  └─ content_test.go          # `go test` validates every card/scenario JSON (schema, ids, en+fr present, refs exist)
├─ web/
│  ├─ index.html
│  ├─ css/   desk.css  papers.css  ledger.css  ui.css
│  ├─ fonts/ img/ (small noise/stamp SVGs only)
│  ├─ data/
│  │  ├─ balance.json          # every tunable coefficient (no magic numbers in code)
│  │  ├─ scenarios/default.json (+ index.json listing the scenarios)
│  │  ├─ newspaper.json        # headline templates (EN/FR), done in M2
│  │  ├─ endgame.json          # game-over banners, urgent telegrams, historian titles and verdicts, done in M5
│  │  ├─ characters.json       # ministers / senders (name, seal initials, role), done in M4
│  │  └─ cards/<category>.json # card pools, one file per category, plus cards/index.json (the manifest)
│  └─ js/
│     ├─ main.js               # boot, load data, main loop (requestAnimationFrame + accumulator)
│     ├─ engine/               # PURE: no DOM, no fetch, so it can run headless
│     │  rng.js state.js time.js economy.js sectors.js indicators.js modifiers.js game.js index.js   (done, M1)
│     │  projection.js                                                                                  (done, M3)
│     │  validate.js                                                                                    (done, M6)
│     │  cards.js                                                                                       (done, M4)
│     │  gameover.js score.js                                                                           (done, M5)
│     ├─ ui/                   # DOM only, reads state, dispatches actions
│     │  desk.js dashboard.js newspaper.js calendar.js charts.js inbox.js (stub)   (done, M2)
│     │  ledger.js                                                                 (done, M3)
│     │  decision.js                                                               (done, M4; replaces the inbox stub)
│     │  telegram.js endgame.js                                                    (done, M5)
│     │  menu.js                                                                   (done, M6)
│     ├─ save/  storage.js codec.js                                                (done, M6)
│     └─ i18n/  i18n.js en.json fr.json   # UI strings only; card text lives in cards (done, M2)
├─ tools/
│  └─ engine-check.mjs         # `node tools/engine-check.mjs`: engine regression checks (determinism, pause gating, effects)
└─ docs/PLAN.md

web/dev/sim.html + sim-core.js (the balance runner) live under web/ instead of tools/, because the page needs the Go server to load the engine as ES modules (file:// blocks them). Open http://localhost:8080/dev/sim.html.

Rule: engine/ never touches the DOM. ui/ never changes the state directly; it calls engine actions (answerCard, setBudget, tick). This keeps the balance tools and any later server-side validation possible.

Simulation model

State (one JSON object, this is also the save)

version, seed, rngState, day (0…1825), speed, lang, countryName, scenarioId, economy{}, sectors{}, indicators{}, modifiers[], flags{}, cardHistory[], pendingCard, followUps[], history{t[], series{}} (one sample per week), pending{weeklyCard, review}, over, ref{}, log[]

pending holds the pauses waiting on the player; advanceDay refuses to run while either is set, so a reload mid-decision comes back to the same decision. ref freezes the scenario's start values: every indicator target is start value + weighted deviations from the start state, so the starting country is an exact equilibrium and a hands-off run stays flat apart from noise.

Economy (in % of GDP unless stated)

  • gdp (absolute value, in a fictional currency) and growth (annual %, applied daily).
  • Revenue: three tax levers, adjustable at the quarterly review: income tax, corporate tax and VAT. Each one turns into revenue in % of GDP, with diminishing returns, and pushes growth or opinion in the opposite direction.
  • Spending = the sum of sector allocations + debt interest.
  • deficit = spending − revenue, which is added to debt every day (1/365 per day).
  • Interest rate = base rate + a risk premium that rises with debt/GDP (convex curve).
  • Debt bands (thresholds tunable in balance.json):
    • < 60 %: no effect.
    • 60–90 %: small stability drain, interest starts to climb.
    • 90–150 %: industry quality decays, growth −, unrest +.
    • 150 %: daily chance of a "bond market panic" event.

    • 200 %: game over by sovereign default.

  • Also tracked: unemployment and inflation (simple functions of growth, industry and deficit).

Sectors (budget % of GDP, set every quarter)

Education, Health, Social welfare & pensions, Police, Army, Justice, Industry & energy, Public transport, Infrastructure, Housing, Research, Culture & media, Foreign affairs, Environment, Administration (15 sectors).

Each sector has:

  • alloc: the % of GDP the player gave it.
  • need: the baseline % from the scenario.
  • quality (0–100), which moves toward a target every day: target = f(alloc/need), a saturating curve, so overspending gives diminishing returns.

The time constant differs per sector. Police reacts in about 2 weeks, Health in about 3 months, Education in about 1 year, Infrastructure in about 2 years. This is what makes the game strategic: you pay now and see the effect later.

Indicators (0–100)

Order, Stability, Civil Rights, Public Opinion (0 = angry, 100 = ecstatic), Economy, Hard Power, Soft Power. Two hidden or semi-visible gauges also exist: Unrest and Army Loyalty.

Every day, each indicator moves toward target = Σ(weight × sector quality) + economy terms + active modifiers. It moves by a fraction of the gap, plus small noise. The weight matrix lives in balance.json. Some examples:

  • Police raises Order and lowers Civil Rights (strongly if over-funded).
  • Army raises Hard Power and Army Loyalty.
  • Culture and Foreign affairs raise Soft Power.
  • Social welfare, Health and Housing raise Opinion and Stability.
  • High unemployment or inflation lowers Opinion.

Unrest goes up when Opinion and Stability are low, when unemployment is high and when debt is above 90 %. It goes down with Order, but heavy police use lowers Civil Rights and Soft Power and feeds future unrest. This is the debt → unrest → police → instability spiral you described.

Card effects (seen after each day)

An effect can be:

  • instant: {stat, delta} (stat is an indicator or growth/unemployment/inflation);
  • spread over time: {stat, delta, days}, applied as delta/days every day. The value then drifts back toward its target, so this is a temporary bump;
  • bias: {stat, delta, bias: true, days?} offsets the stat's target for days days (permanent if omitted). Use it for lasting policy effects;
  • budget: {revenue: "income"|"corporate"|"vat", delta} changes a tax rate in rate points; {revenue: "other", delta} and {extraSpending: delta} are permanent changes in % of GDP; {oneOff: pct} is a one-time cost in % of GDP (negative = windfall);
  • sector quality: {sector, delta[, days][, bias]};
  • flags set or cleared;
  • follow-up: {cardId, inDays}.

The daily tick applies every active modifier, so the consequences of a choice unfold day after day on the dashboard.

Game over (checked daily, risk-based rather than hard cut-offs)

  • Revolution: Unrest > 70 and Opinion < 20 → a daily chance of 2 % × days spent in the zone (the days fade by 1 per day outside it).
  • Military coup: Army Loyalty < 35 and Stability < 35 → a daily chance of 1.5 % × days in the zone. The army only turns on a failing government: neglecting it alone is not enough.
  • Sovereign default: debt > 200 % of GDP, immediately.
  • Impeachment / no-confidence: Civil Rights < 10 and Opinion < 25 for 60 days in a row.
  • Warnings: entering any of these zones (or debt reaching 160 %) queues an urgent telegram that stops time until the player acknowledges it. Thresholds live in balance.json under gameOver; the first four were lowered from the original draft (85/15 and 20/30) because the model never got close to them.
  • The dashboard's "Hold on power" line shows Secure, Shaky (close to a zone) or Critical (inside one).

Each game over has its own illustrated final document, for example a newspaper front page announcing "GOVERNMENT FALLS".

End-of-term review (day 1826)

A weighted score over the indicators averaged across the whole term (not just final values), plus bonuses or penalties for final debt, average growth and average unemployment (balance.score). Grades: S 75, A 66, B 58, C 50, D 40, F below. A game that ends early scores at most 35 and always gets an F. The historian's title is the first entry of endgame.json whose conditions match the term's averages (order of the list is the priority; the last entry has no conditions), or the one title tied to the way the player lost. The final screen is a newspaper front page with the grade, title and verdict, charts of opinion, stability, economy and debt over the term, and the five decisions with the greatest effect (optionImpact in engine/score.js). A hands-off term scores 57 (C, "The Caretaker").

Pronouns. The game opens by asking how the press should refer to the player: He/Him, She/Her, They/Them (Il/Lui, Elle/Elle, Iel/Ielle in French). It only changes text that refers to the player, which is the historian's verdict and title. Texts use tokens: {sub} {Sub} {obj} {pos} {is} {has} {was} in English, {sub} {Sub} {tonic} {e} in French ({e} is the agreement ending: nothing, e, ·e). French titles come in three forms (masculine, feminine, inclusive with a middle dot). Everywhere else (cards, newspaper, telegrams) text avoids pronouns for the player. tp() and tt() in i18n.js fill the tokens, and server/endgame_test.go rejects unknown tokens.

Time and the quarterly cycle

  • The main loop uses requestAnimationFrame with a time accumulator. A new day starts when the accumulator reaches msPerDay / speed.
  • Every Monday a card is drawn and the game pauses. You can't resume until you answer it (stamp APPROVED or REJECTED).
  • Every 13 weeks the budget ledger opens and the game pauses. You can change the sector allocations and the three tax levers, with a live projection of the deficit and of debt over the next 4 quarters. The rest of the time the ledger is read-only.
  • Crisis events can arrive in the middle of a week as telegrams. They either need an answer or are information only.

Card system

  • How many: 5 years × 52 weeks = 260 weekly draws. The goal is a pool of at least 800 base cards, plus follow-up and crisis cards, so that each run sees less than a third of the pool.
  • Schema (web/data/cards/<category>.json):
{
  "id": "eco_tax_wealthy_01",
  "category": "economy",
  "doc": "memo",                       // memo | telegram | petition | report | letter
  "sender": "min_finance",
  "weight": 1.0,
  "once": true,
  "conditions": { "min": {"debt": 60}, "max": {}, "flags": [], "notFlags": [], "dayMin": 0, "dayMax": 1825 },
  "title": { "en": "Tax the wealthy?", "fr": "Taxer les plus riches ?" },
  "body":  { "en": "...", "fr": "..." },
  "yes": { "label": {"en":"Sign the decree","fr":"Signer le décret"},
           "effects": [ {"revenue":"income","delta":3},
                        {"stat":"opinion","delta":4,"days":30},
                        {"stat":"economy","delta":-3,"days":90} ],
           "setFlags": ["wealth_tax"], "followUp": {"id":"eco_capital_flight_01","inDays":45} },
  "no":  { "label": {"en":"Shelve it","fr":"Classer le dossier"}, "effects": [ ... ] }
}
  • How a card is drawn (engine/cards.js): start from all cards whose conditions are met and that haven't been used. Weight each one by weight × a category boost (a sector below quality 40 makes its category up to 2× likelier) × a novelty factor (0.4 per card of the same category among the last 3). Follow-ups that are due take priority over random draws. With no eligible card the week is simply empty and the game is not blocked.
  • Fields beyond the schema above: once defaults to true; once: false makes a card repeatable with cooldownDays (default 180); followUpOnly: true keeps a card out of random draws so it only appears through a followUp. Condition stats are any indicator, growth/unemployment/inflation, debt and deficit (% of GDP) and q:<sector> (sector quality). Options can also clearFlags. Every pending card is saved in state.pendingCard, so a reload shows the same card.
  • Effect size guide used for the first 64 cards: instant or spread shifts of 1–6 points on indicators, lasting bias effects of 1–5 over 180–730 days, sector quality shifts of 2–6 over 60–365 days, one-off costs of 0.1–1.5 % of GDP (negative = windfall), extraSpending of 0.1–1.0 % of GDP, tax rate changes of 2–4 points. Every option has a cost; there is no free choice.
  • Target count per category (about 920 in total): economy 100, social 70, health 60, education 60, police/security 70, army/defense 60, foreign/diplomacy 70, industry/energy 70, transport/infrastructure 60, housing 40, environment 50, justice/civil rights 60, culture/media 40, crises 50.
  • How the content gets written: Sonnet writes it in batches of about 40 cards per category file, in English and French. After each batch, go test ./server checks the schema, that ids are unique, that both languages are present, that follow-up ids exist, and that effect stats are valid.

Desk UI (Suzerain-like)

  • Background: a wood texture made in CSS (layered repeating-linear-gradient plus an inline SVG feTurbulence grain), with a darker vignette.
  • Leather pad: a rounded rectangle with a stitched border (dashed outline), its own grain and an inner shadow.
  • Everything is a paper on the desk, each with a slight random rotation, a drop shadow and a paper texture:
    • Ministerial dashboard (top left): a typed report with the 7 indicators as ink gauges and SVG sparklines drawn on graph paper.
    • Calendar / clock (top right): a desk calendar with the date and the speed buttons as brass toggles.
    • Daily newspaper (side): 1–3 generated headlines driven by state changes and events.
    • Decision folder (centre): the weekly card as a memo, telegram or petition with a sender seal. You answer with two rubber stamps (APPROVED / REJECTED) and a stamp animation.
    • Budget ledger: a green-ruled accounting book with one row per sector, a slider and an ink figure. It opens as a two-page spread.
    • Telegrams: crisis alerts that slide in on yellow strips.
  • Main menu: a closed folder labelled "TOP SECRET". It has New game (country name + scenario), Continue, Load slot, Import, Settings (language, slow mode, sound off) and Credits.
  • Accessibility: real <button> elements, keyboard shortcuts (Space = pause, Y/N = stamp), and a minimum font size on the papers.

Save system

  • Where: localStorage keys cg.autosave, cg.slot.1 to cg.slot.3 and cg.settings. Each save holds { meta, state }; meta (country, day, scenario, saved-at, how it ended) lets the menu list saves without reading the whole state. Every access is wrapped in try/catch, with an in-memory fallback: the game keeps running if storage is missing, blocked or full, and the Settings screen then warns that saves will not survive the visit.
  • Autosave: written after every in-game day and every player action, and when the tab is hidden or closed, so a reload comes back to the same decision. A finished game (any ending) deletes the autosave, so "Continue" only appears for games still in progress. Games started with dev URL parameters never write the autosave.
  • Slots and menu: the folder-style menu has Continue, New game (country name, scenario if there is more than one, pronouns), Load game, Import, Settings (language, slow time) and Credits. While playing, the Menu button on the calendar (or Escape) opens a pause menu with Resume, Save game, Load game, Export, Import, Settings and Main menu. Overwriting a slot, deleting one and loading over a running game all need a second click ("Sure?").
  • Export string: "CG1:" + CRC32 of the JSON (8 hex digits) + ":" + base64(UTF-8 JSON), about 40,000 characters for a full term, so no compression. The CRC only detects corrupted or truncated strings; it is not protection against editing, as you chose. Whitespace is ignored so wrapped strings still import. The menu offers Copy (with a fallback if the clipboard is blocked), Download .sav, and Import by paste or file.
  • Loading is checked: codec.js verifies the format and checksum and migrates old versions (MIGRATIONS[n] turns version n into n+1; saves from a newer version are refused). Then engine/validate.js (restoreState) checks every number is finite and in range, every id is known (sectors, indicators, taxes, modifiers, history series), and repairs references to cards that no longer exist (an unknown pending card is dropped, unknown follow-ups removed). A damaged or tampered save is refused with a message; it never reaches the UI.
  • Settings: language (also switchable in-game from the pause menu) and slow time (1 day = 60 s instead of 10). They are stored separately from games.

Docker

  • Dockerfile: a multi-stage build. The Go build stage runs CGO_ENABLED=0 go build, and the web/ files are embedded into the binary. The final image is distroless/static, about 10 MB.
  • docker-compose.yml: the game service, with ports: ["8080:8080"], restart: unless-stopped and a healthcheck on /healthz.

Milestones (tick as we go)

  • M0 Skeleton: go.mod, server/main.go, Dockerfile, compose, empty desk page served; copy this plan to docs/PLAN.md. (Verified as a local binary; docker compose up --build still to be run once Docker Desktop is started.)
  • M1 Engine core: rng, state, time loop, sectors, indicators, economy/debt, modifiers; balance.json; web/dev/sim.html runs headless years per strategy; server/content_test.go validates balance.json and the scenario.
  • M2 Desk UI: wood and leather, papers, dashboard with sparklines, calendar and speed controls, newspaper. The centre folder is a stub (see decisions log) until M3/M4.
  • M3 Budget ledger: quarterly pause, sliders, tax levers, deficit and debt projection.
  • M4 Cards: draw engine, decision folder and stamps, follow-ups; first 60 cards (EN/FR) across all categories; content_test.go. 64 cards written (62 draw at random, 2 follow-up only), 14 categories, 20 senders.
  • M5 Crises and endings: unrest and army loyalty, crisis telegrams, the 4 game overs, the 5-year review with historian titles. Also the pronoun question at the start of a game.
  • M6 Saves and menu: auto-save, slots, export/import string and file, main menu, settings, language switch. Sound is not in the settings yet because the game has no sounds (M8, optional).
  • M7 Content production: grow the card pool to at least 800 cards in batches by category, plus 50 crisis cards and the follow-up chains.
  • M8 Balance and polish: sim.html runs 200 games per strategy (random, austerity, spendthrift, police state, balanced). Targets: random play fails about 40–60 % of runs, balanced play survives. Then animations, optional sounds, and a second scenario.

Verification

  • docker compose up --build → open http://localhost:8080. Check that the menu loads, a new game starts, days advance at 10 s, and 4× speed works.
  • go test ./server passes (content validation).
  • web/dev/sim.html: 200 runs per strategy finish without exceptions, and the distribution of outcomes is printed. Check that debt > 90 % visibly increases unrest, and that unrest leads to a game over in the spendthrift strategy.
  • Manual: answer a card and watch the effects build up over the following days. Open the quarterly ledger on day 91. Export a string, change one character and check that the import is rejected; import an untouched string and check it gives the same state. Switch to French in the middle of a game.
  • Check in Chrome and Firefox; the layout must work at a 1280×720 minimum.

Open points (to settle as we go)

  • Starting scenario figures: a proposal is in web/data/scenarios/default.json ("Republic of Valdoria": GDP 480, debt 58 %, growth 2 %, unemployment 6.5 %, inflation 2.5 %, spending 34.3 % of GDP, revenue about 35.7 %). Waiting for your review.
  • Whether elections happen during the term (for example a mid-term vote in year 3 that can end the run). Not included yet.
  • Sound effects (typewriter, stamp, telegraph). Optional, in M8.

Decisions log

  • 2026-10-25: Go stdlib + vanilla JS; 10 s/day default with pause/speeds; plain base64 export; EN+FR.
  • 2026-10-25: go:embed can't reference ../web, so the embed lives in web/embed.go (package web, exported Files) and server/main.go imports countrygame/web. web/data, web/fonts and web/img hold a README.txt placeholder because embed rejects empty folders; delete them once real files exist.
  • 2026-10-25: The binary has a -healthcheck flag (the distroless image has no curl), used by the compose healthcheck.
  • 2026-10-25 (M1): The sim page moved to web/dev/ (see layout note). History is sampled weekly, not daily, to keep saves small. Modifiers come in two kinds, shift (moves the current value) and bias (moves the target); see Card effects. Tax-rate effects are in rate points, so the schema example changed from 0.6 to 3.
  • 2026-10-25 (M1 balance, from web/dev/sim.html, seed 1, 5 years): baseline stays flat. Spendthrift (all budgets x1.4) ends with debt 132 %, stability 44, unrest 49 but no game over yet. Austerity (x0.75) reaches debt 11 % with opinion 41 and unrest 35, which looks too safe. Police state: civil rights 23, order 72, soft power 33. Game-over thresholds (M5) and tuning (M8) still to do; the austerity result is the first thing to revisit.
  • 2026-10-25 (M2): The desk is three papers on the leather pad (dashboard, centre folder, calendar + newspaper). Layout checked at 1280×720 in Edge, English and French. A language button (EN/FR) sat on the calendar so the translations could be checked early; in M6 it moved into Settings.
  • 2026-10-25 (M2): The centre folder is a stub. It only shows the pending pause with an "Acknowledge" button; M3 puts the budget ledger there and M4 the decision cards. At the start of a game the first pause is the day-0 budget review.
  • 2026-10-25 (M2): The game starts a new run on every page load (autosave comes in M6) at speed 1×, and resumes at that speed after each acknowledged pause. Keys: Space = pause/resume, 1/2/3 = 1×/2×/4×. Special Elite is Apache-2.0 rather than OFL, so its licence file ships in web/fonts/ with the others.
  • 2026-10-25 (M2): The newspaper picks headlines from the 4-week movement of each indicator plus a few "level" rules (unrest, opinion, debt, stability, civil rights, army loyalty, inflation, deficit). It uses a hash of the week, never the game RNG. Dev URL params in main.js: ?lang=fr, ?seed=N, ?alloc=K (scale all budgets), ?skip=DAYS (fast-forward), ?dev (exposes window.__game).
  • 2026-10-25 (M3): The ledger is a two-page book that covers the centre and right columns while a review is pending; the dashboard stays visible on the left. The left page sets the 15 sector budgets, the right page the three tax rates, the Treasury summary and a four-quarter projection. Nothing is applied until "Sign and file"; "Reset" restores the values the review opened with. The weekly briefing comes after the review when both fall on the same day (day 91, 182, ...).
  • 2026-10-25 (M3): engine/projection.js (previewBudget) copies the game, applies the draft and runs the real daily tick 4 quarters ahead with noise switched off, so the projection uses the same rules as the game (about 7 ms per call). It assumes no new policies, so cards will move the real numbers. The ledger sliders stop at 2x a sector's need (limits.sliderMaxRatio); the engine still accepts up to 3x (limits.allocMaxRatio). A red tick on each slider marks the sector's need (budgets) or the revenue-maximising rate (taxes), and the ▲/▼ next to each budget compares the sector's quality now with the quality it is heading to at that budget.
  • 2026-10-25 (M3): UI tested through the browser's DevTools protocol (slider input, live preview, reset, sign, day-91 reopening); no repo script for it because it depends on a local Edge/Chrome. tools/engine-check.mjs now also covers previewBudget.
  • 2026-10-25 (M4): The centre folder now shows the weekly card as a paper document (memo, telegram, petition, report or letter, each with its own look), the sender's seal, and two stamp buttons showing the option text. Pressing one (or Y / N) plays a 0.75 s stamp animation and then applies the answer. The old "Acknowledge" stub is gone. Cards are drawn when the weekly pause starts, not when the folder is shown.
  • 2026-10-25 (M4): Bug fixed, present since M2: the first animation frame could carry a timestamp earlier than the clock's start, giving a negative elapsed time that pre-loaded a whole day, so every game began one day early. consumeClock now ignores negative steps (regression test in tools/engine-check.mjs).
  • 2026-10-25 (M4): 64 cards: economy 8, social 5, health 4, education 4, security 5, defense 5, foreign 5, industry 5, transport 4, housing 3, environment 4, justice 4, culture 3, crises 5 (bond panic at debt >= 100 %, outbreak, earthquake, general strike at unrest >= 50, coup plot at army loyalty <= 45; all repeatable with cooldowns). Three follow-up chains: wealth tax -> capital flight (45 days), crackdown -> rights report (30 days), energy privatisation -> energy strike (60 days). A random run answers about 62 cards, then the pool is empty and the remaining weeks pass with no card; M7 fixes that by growing the pool to 800+. Card bodies avoid pronouns for people and name ministries instead, so French needs no gender agreement.
  • 2026-10-25 (M4): server/cards_test.go validates every card file against the manifest, scenario sectors, characters.json and balance.json: unknown or misspelled fields, stats, sectors and flags, missing English or French text, dangling or unreachable follow-ups, flags used in conditions but never set, and a minimum of 60 cards. Checked by deliberately breaking cards. sim-core.js now draws and answers cards (random, always-yes and always-no strategies added). With cards, 5-year averages over 6 seeds: baseline debt 58 %, spendthrift 140 % with unrest 55, austerity 15 % with opinion 39. Dev URL option ?card=ID shows a given card first.
  • 2026-10-25 (M5): Pronoun question added at the start of every game (see the Pronouns paragraph above). It is a pending pause like the others (pending.profile), shown first in the folder; the budget ledger waits for it. Dev URL option ?pronouns=he|she|they skips it. The M6 new-game screen can take over the question; the engine side (setPronouns, state.pronouns) stays.
  • 2026-10-25 (M5): pending now has four flags (profile, weeklyCard, review, alert) and advanceDay refuses to run while any is true (isWaiting). The previews in the ledger clear all of them. Game over is checked every day after the economy moves, before the weekly and quarterly pauses.
  • 2026-10-25 (M5): Reachability, from web/dev/sim.html over 12 seeds: do-nothing, tax hikes, austerity, spendthrift and police-state runs all reach the end of the term. ruin (budgets x2, taxes cut) ends in default around day 1050, tyrant (police x3, army x2, justice and social x0.3) in impeachment around day 860, starve (budgets x0.25, taxes up) in revolution or coup around day 300. Army neglect alone survives. The thresholds are a first pass; tune them in M8 together with the rest of the balance (austerity is still too safe).
  • 2026-10-25 (M5): The "Play again" button reloads the page, since there is no menu yet; M6 replaces it. Dev tool: ?alloc=K&skip=DAYS fast-forwards a game at a budget multiple, which is the quickest way to see each end screen.
  • 2026-10-25 (M6): The game now opens on the main menu. The New game screen asks for the country name and the pronouns (He/Him, She/Her, They/Them; Il/Lui, Elle/Elle, Iel/Ielle), so the in-folder pronoun question from M5 only appears for games started with dev parameters. "Play again" on the end screen returns to the menu. Dev URL options still start a game at once; ?menu forces the menu even with them.
  • 2026-10-25 (M6): The language button left the calendar (replaced by a Menu button); language lives in Settings, in the main menu and the pause menu, and is remembered. With no saved choice the browser language decides (French if it starts with fr). Time does not run while the menu is open. Space, 1-3 and Escape work in game; the menu has its own Escape (back, or resume).
  • 2026-10-25 (M6): The decision panel's keyboard listener leaked every time the panels were rebuilt (language change since M4); it is now removed when the panels are rebuilt. On load, restoreState also keeps the alert pause flag consistent with the alert queue.
  • 2026-10-25 (M6): New tests. tools/engine-check.mjs covers the codec (round trip, whitespace, CRC reference value, UTF-8, every kind of damage, versions and migrations), 23 kinds of invalid or tampered saves, repair of stale references, a restored game continuing identically, and storage (slots, settings, missing, throwing and full storage). server/ui_content_test.go checks that en.json and fr.json have identical keys and placeholders and that the scenario index matches the files. The menu was exercised in a real browser: new game, continue after reload with the same waiting card, slot save and overwrite, export, import (valid, tampered, garbage, empty), settings, load and delete with confirmation, and the game-over cleanup.