sts2-bot/docs/research/08-what-actually-wins.md
0xrsydn c00cfe78c3 docs(research): what actually wins in Slay the Spire 2
Two findings, both from measurement rather than the public guides.

1. The public STS2 meta is not usable as evidence. Deck-size targets disagree by
3x across five sources (12-18 to 25-35), and one tier list names Demon Form,
Barricade, Limit Break and Body Slam as STS2 Ironclad cards -- those are Slay the
Spire 1 cards, so that page is an STS1 list retitled. Only Offering and Break
appear on more than one list.

2. Our own run files are far better evidence, and they contain 1052 labelled
decisions. They show the bot is NOT bloating (249 picks from 1087 offers = 23%
per card, ~69% per reward), that 81% of runs die in Act 1, and that 43% die to an
Act 1 boss with `THE_KIN_BOSS` alone killing 9.

The binding constraint is combat defence, not deck composition: those Kin fights
ran 5-10 turns and cost 44-80 HP with block cards in hand the whole time.
2026-09-22 06:09:16 +07:00

7.9 KiB
Raw Permalink Blame History

08 — What actually wins (and why the web meta is not usable)

Status: current. Supersedes the assumption in 06-decision-architecture.md that deck composition is the primary lever.

Question

Before tuning decision quality, establish what actually decides a run in STS2: deck archetype, deck size ("lean weight"), card rarity, or something else.

Finding 1 — the public STS2 "meta" is not usable as evidence

Searched for deck archetypes, tier lists and Act 1 advice. The sources contradict each other badly, and at least one is a copy of a Slay the Spire 1 list relabelled.

Deck size target, by source:

Source Deck size target
metabot.gg 1218
gamestrategyhub 1520
games.gg ~22
slashskill 2530
sts2guides 2535

That is a 3x range. A real meta does not disagree with itself by 3x.

Ironclad S-tier, by source:

Source S-tier
slaythespire2.space Demon Form, Barricade, Dark Embrace, Limit Break, Body Slam, Whirlwind
spireforge Barricade, Cruelty, Dark Embrace
stratgg Offering, Pyre, Battle Trance
metabot.gg Apotheosis, Secret Weapon, Mad Science, Folly, Tank
nat1gaming Offering, Break, Feed

slaythespire2.space lists Demon Form, Barricade, Limit Break, Body Slam and Whirlwind as STS2 Ironclad cards. Those are Slay the Spire 1 cards. That page is an STS1 tier list with the title changed.

Only two cards — Offering and Break — appear on more than one list.

Conclusion: these are AI-generated SEO pages. Do not build a scoring model on them. The one idea they agree on is qualitative and obvious ("commit to one archetype, keep the deck lean"), which we cannot operationalise without numbers.

Finding 2 — our own run history is far better evidence

37 runs on disk, all reachable and machine-readable:

~/Library/Application Support/SlayTheSpire2/steam/<id>/modded/profile1/saves/history/*.run

Each file records killed_by_encounter, win, the full deck, relics, and map_point_history — a per-map-point log that includes every card offered with a was_picked flag, plus per-fight damage_taken, potion_used, rest_site_choices and upgraded_cards.

That is a labelled dataset of our own drafting and combat outcomes.

Outcome distribution

runs=37  wins=0  max_pts=33
act1 deaths: 30/37 (81%)
act2 deaths:  4/37
act3 deaths:  3/37
Killed by Count
THE_KIN_BOSS 9
VANTOM_BOSS 5
CEREMONIAL_BEAST_BOSS 2
Act 1 normals/elites 21

43% of runs die to an Act 1 boss. THE_KIN_BOSS alone kills 9 of 37. That is the single largest lever in the dataset.

The bot is not bloating

total offers=1087  total picks=249  overall pick rate=23%
rewards=323  rewards where a card was taken=249  ->  77% taken, 23% skipped

Descriptive, not a benchmark. The per-card rate (23%) is mechanically diluted by offer size — 280 of 323 rewards offered 3 cards, some offered up to 7 — so it cannot diagnose skip policy on its own. The metric that can is the share of rewards taken (77%) and, below it, cards added per map point. We have no reference for what a good skip rate is in STS2, so this says what the bot does, not whether it is right.

The "deck bloat" hypothesis is not supported: 23% of rewards were skipped, one card was taken per reward at most, and 81% of runs never reach the point where deck composition matters.

Drafting profile (offers / picks)

Card Seen Took Pick rate
PERFECTED_STRIKE 17 17 100%
EVIL_EYE 19 12 63%
POMMEL_STRIKE 26 14 54%
SHRUG_IT_OFF 30 16 53%
ARMAMENTS 39 19 49%
TREMBLE 45 0 0%
RUPTURE 18 0 0%
FIGHT_ME 17 0 0%
SWORD_BOOMERANG 16 0 0%

TREMBLE is the most-offered card in the game and the bot has never once taken it. Worth auditing, but it is not the binding constraint.

Finding 3 — the binding constraint is combat defense, not deck building

Every THE_KIN_BOSS fight, from the run files:

damage taken: [44, 50, 50, 53, 63, 64, 70, 75, 80]
potions used: [3, 3, 2, 1, 3, 3, 2, 3, 2]
turns:        [6, 9, 10, 6, 6, 10, 9, 7, 5]
deck size:    [17, 18, 19, 19, 20, 20, 21, 22, 23]

The bot enters at ~7480 HP and dies. Fights last 510 turns and it takes 4480 damage, i.e. ~1013 per turn, almost entirely unblocked. It spends 13 potions and still loses.

Every one of those decks contained block cards:

DEFEND_IRONCLAD   9/9
BASH              9/9
STRIKE_IRONCLAD   9/9

So the cards are there. The bot does not play them.

Root cause A — the fallback treats a boss's main attack as "chip damage"

facts.py:

THREAT_CHIP = "chip"        # <= 15% of max HP
THREAT_HEAVY = "heavy"      # <= 40% of max HP
HP_HEALTHY = "healthy"      # > 60%

At max_hp = 80 that gives CHIP <= 12, and HEALTHY for hp > 48.

brain.py::_fallback_combat:

must_respect = (
    f.threat in (F.THREAT_HEAVY, F.THREAT_SEVERE, F.THREAT_LETHAL)
    or f.hp_bucket in (F.HP_WOUNDED, F.HP_CRITICAL)
)

So at 74/80 HP, any hit of 12 or less is ignored. Measured, reproduced:

  hp  incoming   threat  hp_bucket   fallback play
  74        12     chip    healthy   Bash (dmg)     <- takes the full 12
  74        13    heavy    healthy   Defend (block)
  60        12     chip    healthy   Bash (dmg)     <- still attacking
  45        12     chip    wounded   Defend (block) <- too late, 29 HP gone

The boss's main attack lands exactly on the CHIP/HEAVY boundary, so the bot attacks and eats it. It only starts blocking once HP drops below 48 — after donating ~29 HP.

The model is myopic: it asks "is this turn's hit big?" when the correct question is "will the total damage over the remaining fight kill me?".

Root cause B — should_defend is asked and then discarded

brain.py::_jev_combat builds this question:

questions["should_defend"] = noul(
    "Given `combat.incoming_threat` and `combat.your_health`, "
    "is preventing damage more valuable than dealing damage this turn?"
)

It appears exactly once in the entire codebase — at the line where it is created. It is never read.

$ grep -rn 'should_defend' *.py
brain.py:278:    questions["should_defend"] = noul(

So it costs latency in every combat turn and does nothing.

Root cause C — the surviving question is damage-biased

Defense is therefore delegated entirely to:

questions["best_play"] = choice(
    "Which single play best advances winning this fight?", options)

"Advances winning" biases toward damage, and a Choice over 58 cards is the question shape measured in 02-system-one-jev.md as diluting with option count.

Measured directly against the Kin state (74/80 HP, 12 incoming, turn 6):

$ python3 -c "... brain._jev_combat(f, jev.JevClient()) ..."
play_card {'card_index': 4, 'target': 'kin'} | jev chose Bash | conf: 0.42
    -> Bash DAMAGE

Jev picks Bash at confidence 0.42. That is below the 0.45 gate, so _jev_combat bails to _fallback_combat, which also picks damage. Both paths agree on the wrong answer.

Conclusion

The bot has no defense policy. In boss fights it attacks until it dies, and the data shows exactly that: 510 turn fights, 1013 unblocked damage per turn, 4480 damage taken, 9 runs ended by one boss.

Priority order for the next work:

  1. Make defense a first-class, code-computed decision. Not a Jev preference. The quantity that matters is total incoming damage over the expected remaining fight, not this turn's hit.
  2. Fix or delete should_defend. A dead question that costs latency is worse than no question.
  3. Only then revisit deck composition. 81% of runs never reach the point where it matters.

Deck size, rarity and archetype are real concerns in Slay the Spire, but they are not what is killing this bot right now.