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
.means phase prefix only. The left side is always a timing trigger (harvest,roll,action.end). Bare verbs have no dot.- Resource amounts are always positional:
g N. Colon notation (g:N) only appears inside=assignments in KV strings where a space would be ambiguous. chooseuses brackets for entity picks, bare words for resource picks. Mixed is allowed:choose g 3 <citizens where name==Knight>.- 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 eventmodify_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.