Conditions
A when: block is a recursive tree. It appears on phases and on individual actions, with
identical grammar in both places.
Combinators
when:
all:
- health_above: 40
- any:
- no_blade: true
- enemies_above: 2
all: and any: take lists of condition nodes and nest arbitrarily.
When all: or any: is present on a node, every leaf field on that same node is ignored.
Combinators short-circuit before leaves are read.
# wrong: health_below is silently ignored
when:
health_below: 30
any:
- no_blade: true
- has_trap: false
# right
when:
all:
- health_below: 30
- any:
- no_blade: true
- has_trap: false
This trips people up regularly and produces no error, because both forms are structurally valid YAML.
Numeric conditions
| Condition | True when |
|---|---|
health_below / health_above | Your health percentage |
pips_below / pips_above | Your total pips |
power_pips_above | Your power pips |
shadow_pips_above | Your shadow pips |
round_below / round_above | Current round number |
boss_health_below / boss_health_above | The boss's health percentage |
enemies_above / enemies_below | Live enemy count, exclusive |
enemies_at_least / enemies_at_most | Live enemy count, inclusive |
allies_alive_above / allies_alive_at_least / allies_alive_at_most | Live ally count |
any_ally_health_below | Some ally is below this |
any_enemy_health_below | Some enemy is below this |
all_enemies_health_below | Every enemy is below this |
when:
all:
- pips_above: 3
- enemies_above: 1
_above and _below are strict. _at_least and _at_most are inclusive. With exactly three
enemies, enemies_above: 3 is false and enemies_at_least: 3 is true. Reach for the
inclusive forms when you mean "three or more" — mixing them up is the most common off-by-one
here.
All health conditions are percentages, 0–100.
These fixed-field conditions do filter dead participants. expr: does not — a difference
that matters and is covered in Expressions.
The ally list the runner builds is every live participant on your team, and you are on your own
team. So a solo character satisfies allies_alive_at_least: 1 by itself, allies_alive_at_most: 0
can never be true, and any_ally_health_below: 35 fires on your own health as readily as a
teammate's.
If you mean "somebody other than me is hurt", there is no fixed field for it. Guard the heal
with a target instead — heal: { target: lowest_health_ally } will pick you if you are the most
hurt, which is usually what you wanted anyway.
Every one of these conditions is false when the value cannot be read — no participant, no combat state, a failed memory read. They never error and never default to true. A condition that mysteriously never matches is often a read failure, not a logic mistake.
Boolean and school conditions
has_blade, no_blade, and has_shield accept a boolean or a school string:
when:
no_blade: true # no blade of any school
when:
has_blade: storm # a storm blade specifically
Only the seven wizard schools are understood here: fire, ice, storm, myth, life,
death, balance. The schema also lets you write star, sun, moon, or shadow, but the
condition evaluator does not recognise them and falls back to "a blade of any school" — quietly
widening the check rather than narrowing it. (Action filters like cast: { school: shadow } do
understand all eleven; it is only conditions that are limited.)
no_blade: false is a double negativeno_blade inverts whatever it checks, and the boolean then compares against that inverted
result. So no_blade: false means "there is a blade" — it is has_blade: true written
confusingly. Prefer has_blade: when you mean presence.
Strict boolean conditions
These take only true or false:
boss_presentis_stunnedno_minionhas_minion
is_stunned: true can never matchThe runner passes the round outright when you are stunned, before it evaluates a single phase.
By the time any when: is read, you are not stunned — so is_stunned: true is dead and
is_stunned: false is always true and can be deleted. The same goes for expr: "self.is_stunned == 1".
is_stunned is still meaningful on somebody else: expr: "boss.is_stunned == 1" works.
has_trap/no_trap do not take a bare school stringUnlike has_blade/no_blade/has_shield, has_trap and no_trap have no shorthand for "a
trap of this school" — they accept true/false or the object form below. A bare string like
has_trap: storm is rejected by the schema, so your editor will flag it, but the loader
accepts it and treats it exactly like has_trap: true (any trap, any school). To filter by
school, use the object form: has_trap: { school: storm }.
boss_present is how you split boss and mob plans inside one file:
phases:
boss:
priority: 100
when:
boss_present: true
actions:
- cast: { name: 'Glowbug Squall', enchant: required }
mob:
priority: 50
when:
boss_present: false
actions:
- cast: { name: 'Tempest', enchant: required }
Worth doing whenever the boss has enough health that your farm nuke is a waste on trash.
boss_present, boss_health_below/boss_health_above, and the boss target all ask the NPC's
behaviour template for its mob title and check whether it says "boss". Only if that template
cannot be read do they fall back to the participant's own boss flag, which is unreliable and
misses plenty of real bosses.
The expr: attribute is_boss is different — it reads that unreliable flag directly, with no
template lookup. Prefer boss_present: true or a boss. expression target over
any(enemies).is_boss == 1.
boss_present is also false whenever the enemy list cannot be read at all, so a phase gated on
boss_present: false will not fire during a read failure either. Neither polarity is a safe
"always runs" phase; leave your catch-all ungated.
Target-scoped effect conditions
has_blade, no_blade, has_shield, has_trap, and no_trap also accept an object. This
is the form you need for anything involving sharpened blades or potent traps.
when:
no_blade:
target: p1
school: fire
enchanted: true
protected: false
| Field | Meaning |
|---|---|
target | Any target selector: self, boss, lowest_health, lowest_health_ally, minion, p1, … |
school | Optional school filter, seven base schools only |
enchanted | true = effects from enchanted cards only, false = plain only |
protected | true = protected effects only (Aegis/Indemnity), false = unprotected only |
Those four are the whole list — anything else inside the object is an unknown-field load error.
The enchanted distinction is the whole point. An enchanted blade and a plain blade of the
same school are different effects on the board, and "does p1 have a sharpened fire blade"
is not answerable without it.
protected is a separate axis: it asks whether the effect is shielded from removal, which is
what Aegis and Indemnity do. Sharpen and Potent make an effect bigger, not protected, so a
sharpened blade matches enchanted: true and not protected: true.
Omitting target: does not mean the same thing everywhere. has_blade/no_blade/
has_shield default to yourself; has_trap/no_trap default to the boss, falling back to the
first live enemy when there is no boss. An unrecognised target name lands on those same
defaults rather than failing.
has_trap ask different questionshas_trap: true is true when any live enemy carries a trap. has_trap: {} — the object
form with nothing in it — asks only about the boss. They are not interchangeable, and the
difference bites in multi-enemy fights.
target: p1 in a condition is console-onlyAlias targets resolve through a live client table that only the console populates. On the
desktop app they resolve to nobody, so has_blade: { target: p1 } is permanently false and
no_blade: { target: p1 } is permanently true — meaning a guard written to blade the hitter
once will re-blade it every single round. See
which host is running it.
Layering enchanted and plain
The idiom for stacking both kinds. The enchant: block is not decoration — enchant: required
with no policy to draw on skips the action every round:
enchant:
blade: { prefer: sharpen }
trap: { prefer: potent }
phases:
sharpened_blade:
priority: 100
when:
no_blade:
target: p1
school: fire
enchanted: true
protected: false
actions:
- blade: { school: fire, target: p1, enchant: required }
regular_blade:
priority: 90
when:
no_blade:
target: p1
school: fire
enchanted: false
actions:
- blade: { school: fire, target: p1, enchant: none }
potent_feint:
priority: 80
when:
no_trap:
target: boss
school: death
enchanted: true
protected: false
actions:
- trap: { name: Feint, target: boss, enchant: required }
Sharpened blade first because it is worth more; plain blade only once the sharpened one is down; then the potent feint. Each condition checks the board, so a stripped blade comes back automatically.
The example strategy life_fire.yml does the same thing with action guards inside one phase
instead of three phases — both work, and the guard form keeps priorities from proliferating.
Expression conditions
expr: accepts a small comparison language and sits alongside other leaf fields on the same
node, ANDed with them:
when:
expr: "self.health < 50% && avg(enemies).health > 30%"
Expressions are parsed and checked at load time, not during combat. A malformed
expression, an unknown attribute, or a misplaced % fails strategies validate with a byte
offset.
Full grammar in Expressions.
expr: is a leaf, so the combinator rule applies — an expr: beside an all: on the same
node is ignored. Put it inside the combinator's list:
# wrong
when:
expr: "self.health < 50%"
all:
- pips_above: 3
# right
when:
all:
- expr: "self.health < 50%"
- pips_above: 3
Choosing a form
The fixed-field conditions cover most needs and read better in YAML. Reach for expr: when
you need something they cannot say:
- Comparing two participants
- Aggregating across a group (
avg,any,all) - Boolean flags on a specific participant (
boss.is_stunned == 1) - Combining with
||or!, which the fixed fields have no equivalent for
# fixed fields: clear
when:
all:
- health_below: 40
- enemies_at_least: 2
# expr: expresses something they cannot
when:
expr: "any(allies).health < 30% || boss.health > 80%"