Files
basegame-vcko/docs/effect-strings.md
2026-05-02 23:25:02 -07:00

8.3 KiB

Effect String Syntax

This document covers how effect strings work across the three card tables (citizens, domains, monsters), where the syntax currently diverges, and the proposed unified grammar.


Current state by table

Citizens — special_payout_on_turn / special_payout_off_turn

Card String Meaning
Merchant choose g 2 m 2 Pick one: +2g or +2m
Mercenary exchange s 1 g 2 Pay 1s, gain 2g
Champion exchange g 1 s 4 Pay 1g, gain 4s
Paladin exchange s 1 m 3 Pay 1s, gain 3m
Butcher count owned_worker g 2 Gain 2g per owned Worker citizen

Domains — activation_effect

Card String Meaning
Ancient Tomb action.modify_monster_strength +3 Prompt: add 3 to a monster's strength cost
Pretorius Conclave choose <citizens> Prompt: take any citizen from the board
Cursed Cavern m 4 + concurrent_flip_one_citizen Gain 4m; all players flip a citizen
Darktide Harbour choose <citizens where role==shadow> Prompt: take a shadow citizen
Cloudrider's Camp s 3 + choose <citizens where role==soldier and gold_cost<=2> Gain 3s; prompt: take a soldier citizen worth ≤2g
Wisborg manipulate_resources mode=self_convert pay=g:3 gain=v:3 optional=true Optionally pay 3g to gain 3vp

Domains — passive_effect

Card String Meaning
Jousting Field harvest.gain_per_owned_citizen_name Knight g 1 Harvest phase: gain 1g per Knight owned
Foxgrove Palisade roll.set_one_die target=6 cost=g:2 Roll phase: pay 2g to set a die to 6
The Desert Orchid roll.set_one_die target=1 cost=g_per_owned_role:holy_citizen Roll phase: pay 1g per holy citizen to set a die to 1
Emerald Stronghold effect.add action.emeraldstronghold Flag: ignore + when buying citizens
Pratchett's Plateau effect.add action.pratchettsplateau Flag: domains cost 1g less
Shelley Commons action.end manipulate_resources mode=pay_to_player gain=v:1 pay=g:1 optional=true End of action: optionally pay 1g to a player for 1vp
Cathedral of St Aquila action.end manipulate_resources mode=take_from_player take=g:1 optional=true End of action: optionally take 1g from a player
King Tower action.end manipulate_resources mode=pay_to_player gain=v:1 pay=m:1 optional=true End of action: optionally pay 1m to a player for 1vp
The Orb of Urdr action.end manipulate_resources mode=take_from_player take=m:1 optional=true End of action: optionally take 1m from a player

Monsters — special_reward

Card String Meaning
Goblin Mage choose g 1 m 1 Pick one: +1g or +1m
Goblin Bomber choose g 2 m 2 s 2 Pick one: +2g, +2m, or +2s
Goblin King count area Hills g 1 Gain 1g per Hills monster slain
Skeleton King count area Ruins g 2 Gain 2g per Ruins monster slain
Bane Spider choose g 3 <citizens where name==Knight> Pick one: +3g or take a Knight citizen
Ettercap choose <citizens where gold_cost<=2> Take a citizen worth ≤2g
Spider Queen choose <count area Forest g 2> <citizens + v 1> Pick one: 2g per Forest monster slain, or take a citizen and gain 1vp
Satyr Mage choose g 5 m 5 s 5 Pick one: +5g, +5m, or +5s
Troll count area Valley m 2 Gain 2m per Valley monster slain
Dire Bear choose g 2 m 2 Pick one: +2g or +2m
Orc Warrior choose <citizens where gold_cost<=3> Take a citizen worth ≤3g
Orc Batrider choose <citizens> Take any citizen
Orc Chieftain count area Mountain g 2 Gain 2g per Mountain monster slain

Where the syntax diverges

1. Resource notation — two forms

Citizens and monsters use positional shorthand; domain KV pairs use colon notation:

# positional (citizens, monsters)
choose g 2 m 2
count owned_worker g 2

# colon inside KV values (domain passives)
action.end manipulate_resources mode=pay_to_player gain=v:1 pay=g:1
manipulate_resources mode=self_convert pay=g:3 gain=v:3
roll.set_one_die cost=g:2

2. count — same structure, different second word

The two count patterns are syntactically parallel but semantically distinct. No unification needed beyond being aware they share a parser.

count owned_worker g 2     # count by citizen role owned
count area Hills g 1        # count by monster area slain

3. choose — brackets sometimes, not always

The bracket vs no-bracket distinction does carry real meaning and is worth keeping:

choose g 1 m 1                              # pick one of these resource amounts
choose g 3 <citizens where name==Knight>    # pick a resource amount OR an entity
choose <citizens where gold_cost<=2>        # pick an entity from a filtered set

4. exchange — only exists in citizens

No equivalent pattern in domains or monsters. Could be expressed as a compound but exchange is readable:

exchange s 1 g 2    # pay 1s, receive 2g

5. . is doing three different jobs

harvest.gain_per_owned_citizen_name ...   # dot = phase separator (phase.verb)
roll.set_one_die ...                       # dot = phase separator (phase.verb)
action.end manipulate_resources ...       # dot = phase separator, then space, then verb
action.modify_monster_strength +3        # dot = namespace separator, not timing
effect.add action.emeraldstronghold      # dot = verb separator, then dot = namespace

6. manipulate_resources wrapper verbosity

The mode= value is doing the same work as a first-word verb. The wrapper adds noise:

# current
action.end manipulate_resources mode=pay_to_player gain=v:1 pay=g:1 optional=true

# without the wrapper — same information
action.end pay_to_player g 1 v 1 optional

Proposed unified grammar

Core rules

  1. . means phase prefix only. The left side is always a timing trigger (harvest, roll, action.end). Bare verbs have no dot.
  2. Resource amounts are always positional: g N. Colon notation (g:N) only appears inside = assignments in KV strings where a space would be ambiguous.
  3. choose uses brackets for entity picks, bare words for resource picks. Mixed is allowed: choose g 3 <citizens where name==Knight>.
  4. Compound effects use +. Each leg is a self-contained effect: m 4 + concurrent_flip_one_citizen.

Proposed rewrites

Domain activation:

# before → after
action.modify_monster_strength +3                             → modify_monster_strength 3
m 4 + concurrent_flip_one_citizen                             → no change
manipulate_resources mode=self_convert pay=g:3 gain=v:3 ...  → self_convert g 3 v 3 optional
choose <citizens where role==shadow>                          → no change
s 3 + choose <citizens where role==soldier and gold_cost<=2>  → no change

Domain passive:

# before → after
action.end manipulate_resources mode=pay_to_player gain=v:1 pay=g:1 optional=true  → action.end pay_to_player g 1 v 1 optional
action.end manipulate_resources mode=take_from_player take=g:1 optional=true        → action.end take_from_player g 1 optional
action.end manipulate_resources mode=pay_to_player gain=v:1 pay=m:1 optional=true  → action.end pay_to_player m 1 v 1 optional
action.end manipulate_resources mode=take_from_player take=m:1 optional=true        → action.end take_from_player m 1 optional

harvest.gain_per_owned_citizen_name Knight g 1   → no change
roll.set_one_die target=6 cost=g:2               → no change
effect.add action.emeraldstronghold              → no change

Citizens and monsters: no changes needed — syntax is already consistent within each table and the proposed rules codify what they already do.


What stays as parsed strings vs opaque keys

All effects in the tables above are parsed strings — a new card with different numbers works without any code change.

The only candidates for opaque keys are effects with branching prompt logic unique to a single card that cannot be generalized with different parameters:

  • concurrent_flip_one_citizen (Cursed Cavern) — multi-player concurrent event
  • modify_monster_strength 3 (Ancient Tomb) — board-state mutation prompt

Even these stay as strings under the unified grammar; they just dispatch to named functions rather than an inline parsing branch. The DB string is the key; the function is the implementation.