257 lines
38 KiB
Markdown
257 lines
38 KiB
Markdown
# 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`):
|
||
```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 860 in total; the figures below add up to 860, not 920): 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)
|
||
- [x] **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.)
|
||
- [x] **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.
|
||
- [x] **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.
|
||
- [x] **M3 Budget ledger**: quarterly pause, sliders, tax levers, deficit and debt projection.
|
||
- [x] **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.
|
||
- [x] **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.
|
||
- [x] **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).
|
||
- [x] **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. Done: 864 cards, 117 of them follow-up-only, about 100 chains. Tools: `tools/add-cards.mjs`, `tools/card-stats.mjs`, `tools/cardfmt.mjs`, guide in `docs/CARD-WRITING.md`.
|
||
- [x] **M8 Balance**: the difficulty is now measured and gated. Passive play is risky, a balanced plan wins, bold play pays up to a point and extremes end in a fall. Tools `tools/balance-check.mjs` (with `--check`), `balance-search.mjs`, `bal-edit.mjs`; guide in `docs/BALANCE.md`.
|
||
- [ ] **M8b Polish (optional)**: animations, sounds (typewriter, stamp, telegraph; off by default), a second scenario, a repo README.
|
||
|
||
## 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.
|
||
- 2026-10-25 (M7): Pool is 864 cards: economy 100, social 73, health 60, education 60, security 70, defense 60, foreign 70, industry 70, transport 60, housing 41, environment 50, justice 60, culture 40, crises 50. 117 cards are follow-up-only (119 links, about one chain per 7 cards, several branch on yes/no), 29 are repeatable (mostly crises), 353 have a stat or quality condition. Availability by year: start 286, year 1 202, year 2 189, year 3 127, year 4+ 60. The plan's old "about 920" was an addition error; the per-category targets add up to 860. Seven writers worked in parallel from `docs/CARD-WRITING.md`; defense and foreign cards only use invented countries (Torvania, Marenia, Belmora, Kessaly, Draveth). Coin-flip averages (yes/no averaged) are near zero for opinion, stability, economy and cost; unrest runs about +0.24 because "no" answers lean toward unrest, which is fine for now (M8). Seven duplicate English titles were retitled.
|
||
- 2026-10-25 (M7): Pool check, 120 random-answer runs (20 seeds x 6 strategies): no empty week, a run sees 205 distinct cards on average (min 39 on the early-ending starve runs, max 261), draws are spread over categories and years, and the endings match M5 (baseline, spendthrift, austerity and police-state random play reach the end; ruin ends in default; starve in revolution or coup). 9 cards were never drawn because of narrow conditions (coup attempt and its two follow-ups, police strike, strike wave, regional fund loan, industry rescue, drought pipeline, guildhall roof); they are rare-situation cards and are kept. `server/cards_test.go` now requires at least 800 cards and a minimum per category.
|
||
- 2026-10-25 (M8): Starting point, measured with the new harness: every strategy except ruin, starve and tyrant survived 100 % of runs, random play scored 58 and a card-reading bot only 61, so decisions barely mattered. Causes: the start state was an equilibrium with nothing pushing back, sector effects were weak, and debt cost only about 10 score points at 150 % of GDP.
|
||
- 2026-10-25 (M8): Engine changes (all tunable in balance.json). Indicator drift (opinion fades, order and stability erode, unrest creeps up, scaled by `driftScale` 1.1); sector needs grow yearly (`needGrowth`); a world business cycle with shocks (`world`, state version 2 with a migration that adds `world: {cycle: 0}`), receipts follow growth (`fiscal.growthSens`); `gain` 3.0 multiplies how much sector quality moves indicators; `excess` and `short` terms reward quality above 70 to 75 and punish neglect; interest premium coefficient 0.0008 (was 0.0003); default at 165 % (was 200, warning at 135); the score leans 50 % on the last year (`score.lastYearShare`); grades S 73, A 64, B 56, C 47, D 38; revolution zone is unrest above 62 and opinion below 28; coup also triggers from army loyalty under 18 alone, and from loyalty under 35 with stability under 50; zone chances grow 0.004 per day (was 0.015 and 0.02), so a warning leaves about a month.
|
||
- 2026-10-25 (M8): Emergency session. A new danger warning (revolution, coup, impeachment, default) opens the budget ledger at once, at most once per `gameOver.emergencyCooldown` (45) days, with a stamped banner. Passive play with a sensible reaction survives about 95 % of runs against 79 % without it, and extremes still fall. `state.risk` gained `emergency` and `lastEmergency`; `restoreState` repairs saves without them.
|
||
- 2026-10-25 (M8): Cards react to the player's choices. New condition keys `ratio:<sector>` and `tax:<id>` (engine, Go validator, guide). 50 new crisis cards: 34 backlash (`cri_bk_*`: police state, army excess or neglect, high and low taxes, debt and rating, starved services, waste) and 16 windfall (`cri_wf_*`: sustained high sector quality). The pool is 914 cards. Windfalls fire about 1.1 times per run for the balanced plan and 2.6 for the 1.5x plan; backlash fires 2 to 3 times per run for police state, tax hike and spendthrift and almost never for the milder versions.
|
||
- 2026-10-25 (M8): Result, 24 seeds each, human-like card answers: random 79 % survive (grade D); planner 100 % and score 61 (A in 17 % of runs); the same plan at 1.5x boldness 100 % with A in 29 %; at 2x 63 % survive; at 3x default every time; ruin, starve, deep cuts, tyrant 0 %; spendthrift 33 %; mild versions of each idea 75 to 100 %. The best budget the search found scores about 63 to 65. `balance-check --check` encodes these bands. Soft spots: S is rare, unrest runs slightly positive in the card pool, blanket austerity is fatal.
|
||
- 2026-10-25 (M8): Newspaper headlines for a global slump or boom. The ledger hint says needs grow yearly. Dev parameter `?untilAlert` stops a fast-forward at the first danger warning. Engine checks cover the world cycle, growing needs, the new condition keys, coup from neglect, the emergency session with its cooldown and the version 1 migration.
|