121 lines
7.3 KiB
Markdown
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.
|