6.9 KiB
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
- Read the existing file for your category (
web/data/cards/<category>.json) to catch the voice and format, andweb/data/characters.jsonfor the senders. - Write new cards in batches of at most 25 as a JSON array in a scratch file outside the repo.
- 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. - Validate:
go test ./server. It checks every field, both languages, effects, conditions, senders, flags and follow-up links, and says which card is wrong. - Review the balance:
node tools/card-stats.mjs <category>. - 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
{
"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,reportortelegram. Telegrams are for urgent news only.sender: an id fromcharacters.json. Pick the one who would really write it.onceis true by default.once: falsepluscooldownDaysmakes a card repeatable (only for crises and recurring situations).followUpOnly: truekeeps a card out of random draws; it appears only through afollowUp.weightdefaults 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/maxon any stat, ondebtordeficit(% of GDP), or onq:<sector>(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 withSTOP.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.