Files

121 lines
7.3 KiB
Markdown

# 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/<category>.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 <category> <batch.json>`. 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 <category>`.
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: `<prefix>_<slug>_<NN>`, 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>` (sector quality, for "this sector is in trouble" cards);
- two more keys read the player's choices rather than the results: `ratio:<sector>` (the budget divided by the
sector's need, 1 = as needed, 2 = double) and `tax:income` / `tax:corporate` / `tax:vat` (the rate; the start values are 30, 25
and 20). They are for cards that answer extreme choices (`ratio:police` >= 2, `tax:income` >= 42) in the "backlash"
or "windfall" families of M8;
- 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 <category>` 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.