Skip to main content

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.

Combinators shadow sibling leaves

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

ConditionTrue when
health_below / health_aboveYour health percentage
pips_below / pips_aboveYour total pips
power_pips_aboveYour power pips
shadow_pips_aboveYour shadow pips
round_below / round_aboveCurrent round number
boss_health_below / boss_health_aboveThe boss's health percentage
enemies_above / enemies_belowLive enemy count, exclusive
enemies_at_least / enemies_at_mostLive enemy count, inclusive
allies_alive_above / allies_alive_at_least / allies_alive_at_mostLive ally count
any_ally_health_belowSome ally is below this
any_enemy_health_belowSome enemy is below this
all_enemies_health_belowEvery 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.

"Allies" includes you

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 negative

no_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_present
  • is_stunned
  • no_minion
  • has_minion
is_stunned: true can never match

The 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 string

Unlike 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.

How "boss" is decided

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
FieldMeaning
targetAny target selector: self, boss, lowest_health, lowest_health_ally, minion, p1, …
schoolOptional school filter, seven base schools only
enchantedtrue = effects from enchanted cards only, false = plain only
protectedtrue = 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.

The bare-boolean and object forms of has_trap ask different questions

has_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-only

Alias 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%"