libstrat
libstrat is a YAML format for describing combat. Instead of hardcoding spell logic in Go or Lua, you write a file that answers:
- Which client or clients run this?
- What happens first in the fight?
- When do I heal, blade, trap, or flee?
- Which target gets hit?
- Which cards get enchanted, and with what?
The combat runner loads the file, and each round it walks your phases in priority order, finds the first one whose conditions hold, and executes the first action in that phase that resolves to a real card.
Why data and not code
Combat decisions are made under time pressure, hundreds of times a session, with no operator watching. A strategy expressed as data can be fully validated when the file loads — unknown fields, bad client aliases, malformed expressions, and impossible enchant chains all become errors before a single round is played.
The same logic written imperatively fails in the middle of a duel, which is the worst possible time to discover a typo.
The loop back from actions to phases is the part worth remembering: a phase that matches but resolves nothing does not end the round — the runner falls through to the next eligible phase.
name:, not its filenameTwo files in your strategy folder that both say name: storm are one strategy — whichever
loads second silently replaces the first. Renaming the file changes nothing; changing name:
changes everything. See File format → name.
The smallest useful file
$schema: https://scm.kebab.sh/strat/config.json
name: pass-only
description: Always passes.
phases:
default:
actions:
- pass
A real one
$schema: https://scm.kebab.sh/strat/config.json
name: storm-hitter
description: Storm hitter that buffs first, then AOE attacks.
school: storm
clients:
- p2
enchant:
auto: true
damage:
prefer: epic
fallback: colossal
targets:
default_enemy: boss
default_ally: self
fallback: pass
phases:
setup:
priority: 50
when:
no_blade: storm
actions:
- blade: { school: storm, target: self }
execute:
priority: 10
actions:
- cast:
type: damage
school: storm
aoe: true
min_pips: 4
enchant: required
- cast: { type: damage, school: storm, target: boss }
- wand_hit: { target: first }
Read top to bottom: this is for p2, it prefers Epic then Colossal for damage enchants, it
targets the boss by default, and it passes if nothing resolves. At priority 50 it blades if it
has no storm blade; at priority 10 it tries an enchanted AOE of at least 4 pips, then any
storm damage spell at the boss, then a wand hit.
Where to go
| Page | Covers |
|---|---|
| Getting started | Writing and loading your first file |
| File format | Every top-level field |
| Phases | Priority, ordering, resolution |
| Conditions | when: in all its forms |
| Expressions | The expr: mini-language |
| Actions | cast, blade, trap, heal, and friends |
| Targeting | Target selectors and client aliases |
| Enchants | Enchant policies and chains |
| Teams | One file, several characters |
| Examples | Annotated complete strategies |
| Troubleshooting | When it loads but does nothing |
Two hosts, one format
The same file is run by two different programs: the libwiz console and the desktop app's
trainer. The format, the validation, and the round-by-round evaluation are identical in both.
Everything around the file is not — which clients a strategy binds to, whether p1/p2
targets work at all, and what the default strategy is called all differ.
Read Getting started → which host is running it before you write anything that names a client alias. It is the single most common source of "this worked yesterday on the other machine".
Relationship to Deimos
The format and much of the evaluation semantics are modelled on Deimos, so that existing strategies port over. Where libstrat deliberately differs, it is almost always in the direction of failing at load time instead of silently evaluating false forever. Those divergences are catalogued in Expressions.