# Writing decision cards Guide for anyone (human or model) adding cards to `web/data/cards/`. Every card is one weekly yes/no decision for the ruler of the fictional Republic of Valdoria. A run has 260 of them, in English and French. ## Workflow 1. Read the existing file for your category (`web/data/cards/.json`) to catch the voice and format, and `web/data/characters.json` for the senders. 2. Write new cards in **batches of at most 25** as a JSON array in a scratch file outside the repo. 3. Merge the batch: `node tools/add-cards.mjs `. It rejects duplicate ids and wrong categories, appends the cards, and reformats the file. 4. Validate: `go test ./server`. It checks every field, both languages, effects, conditions, senders, flags and follow-up links, and says which card is wrong. 5. Review the balance: `node tools/card-stats.mjs `. 6. Edit the category file directly to fix whatever the tests or the stats show. Only edit your own category files. Never touch `balance.json`, the code or the tests while adding content. ## The card ```json { "id": "eco_example_01", "category": "economy", "doc": "memo", "sender": "min_finance", "weight": 1, "conditions": { "min": { "debt": 70 }, "max": {}, "flags": [], "notFlags": [], "dayMin": 90, "dayMax": 1500 }, "title": { "en": "…", "fr": "…" }, "body": { "en": "…", "fr": "…" }, "yes": { "label": { "en": "…", "fr": "…" }, "effects": [ … ], "setFlags": [], "followUp": { "id": "eco_next_01", "inDays": 45 } }, "no": { "label": { "en": "…", "fr": "…" }, "effects": [ … ] } } ``` - `doc`: `memo`, `letter`, `petition`, `report` or `telegram`. Telegrams are for urgent news only. - `sender`: an id from `characters.json`. Pick the one who would really write it. - `once` is true by default. `once: false` plus `cooldownDays` makes a card repeatable (only for crises and recurring situations). `followUpOnly: true` keeps a card out of random draws; it appears only through a `followUp`. - `weight` defaults to 1. Use 0.5 for niche cards and 1.5 for central ones, sparingly. - Ids: `__`, unique across all files. Prefixes: `eco soc hlt edu sec def for ind trn hou env jus cul cri`. ### Effects (each option has a list) | Shape | Meaning | |---|---| | `{ "stat": "opinion", "delta": 4 }` | instant shift | | `{ "stat": "opinion", "delta": 4, "days": 30 }` | spread over the days, then fades back | | `{ "stat": "economy", "delta": -2, "bias": true, "days": 365 }` | moves the stat's target for that many days (omit `days` for permanent) | | `{ "sector": "health", "delta": 5, "days": 120 }` | sector quality (also instant or `bias`) | | `{ "oneOff": 0.5 }` | one-time cost in % of GDP (negative = windfall) | | `{ "extraSpending": 0.3 }` | permanent extra spending in % of GDP (negative = savings) | | `{ "revenue": "income", "delta": 3 }` | tax rate in points (`income`, `corporate`, `vat`) | | `{ "revenue": "other", "delta": 0.3 }` | permanent other revenue in % of GDP | Stats: `order stability civilRights opinion economy hardPower softPower unrest armyLoyalty` (0 to 100), plus `growth unemployment inflation` (percent). Sectors: `education health social police army justice industry transport infrastructure housing research culture foreignAffairs environment administration`. **Size guide.** Instant or spread shifts 1 to 6 points. Lasting `bias` effects 1 to 5 points for 180 to 730 days. Sector quality 2 to 6 over 60 to 365 days. `oneOff` 0.1 to 1.5. `extraSpending` 0.1 to 1.0. Tax rates 1 to 4 points. `growth` bias 0.1 to 0.6, `unemployment` 0.1 to 0.5, `inflation` 0.2 to 1.5. Use 2 to 4 effects per option, and mix instant, spread, bias, sector and budget effects. The effects must fit the story: repression raises order and lowers civil rights. **Every option has a real cost and a real benefit.** Saying no is never a free "nothing happens": the status quo has consequences too. ### Conditions and scheduling Spread the cards over the whole term so each year brings new material: - about 30 % available from the start (no `dayMin`), 25 % from day 90 to 400, 25 % from day 400 to 900, 20 % from day 900 to 1500; - 30 to 40 % gated on the state of the country: `min`/`max` on any stat, on `debt` or `deficit` (% of GDP), or on `q:` (sector quality, for "this sector is in trouble" cards); - a few early-only cards with `dayMax`. Flags: `setFlags` on an option records the decision; `conditions.flags` / `notFlags` read it. Every flag a card reads must be set by some card (the tests check). Use flags prefixed with your category. The flags that already exist are `wealth_tax austerity airline_sold crackdown energy_privatised trade_pact emergency_austerity concessions`. ### Follow-up chains About one chain per eight cards: an option has `"followUp": { "id": "...", "inDays": 30 }` pointing at a `followUpOnly` card that continues the story. Branch the story: set a flag in one option and let the follow-up (or a second follow-up card) differ depending on the flag. Chains are 2 to 3 cards long. ## Voice - **Tone**: dry, specific, politically grounded, a little satirical, like a briefing that has seen everything. Concrete places, numbers and institutions in Valdoria. Both sides of every dilemma have a respectable argument. - **Fiction only**: no real countries, people, parties or brands. No slurs. No cartoon villains. - **Length**: body of 2 to 3 sentences (English up to 300 characters, French up to 340): the situation first, then the tension. Title up to 50 characters. Option labels up to 32 characters, led by a verb, clearly different from each other (never "Yes" / "No", never two "Approve"). - **No pronouns for people**: name ministries and organisations, not individuals, and never use a pronoun for the player or anyone else. Address the player as "you" / "vous" sparingly, and in French never with an adjective that agrees with the player. - **Variety**: vary senders, document types and sentence shapes. Do not start every card with "The ministry proposes". No two cards about the same dilemma. - **French**: natural, idiomatic French rather than a translation word for word. Correct accents, « guillemets » with spaces, a space before `? ! : ;`, typographic apostrophe `'` as in the existing cards. Telegram bodies are UPPERCASE without accents and end with `STOP.` in both languages. Keep institution names consistent with the existing cards ("ministère des Finances", "Banque centrale"). ## Balance checks before you finish `node tools/card-stats.mjs ` prints, per answer and for a coin flip (50/50): - one-off cost and extra spending: the coin-flip rows should stay close to zero (about as many windfalls and savings as costs), because a player who answers at random should not bankrupt or enrich the country; - opinion, stability, economy: the coin-flip rows should stay within ±0.5 per card; - the spread of availability and the share of gated cards, per the targets above; - warnings for long bodies, duplicate titles and identical labels.