Phase 1 Audit: Categorical Priority Rules + a Bigger Bug It Led To
Generated: June 29, 2026. Covers Phase 1 of ai_revamp_roadmap.md.
The two questions Phase 1 set out to answer
Glitch removal = priority 2000, always
Confirmed dead code in Healthcare - no Healthcare enemy runs a
RemoveGlitchEffect card, so this branch never fires there. In the AI
sector it's live for exactly two enemies: VP of Engineering
(QuantumFirewall.tres, Gain 30 Block + Remove all Glitch) and UX Designer
"Daughter" (DebugMode.tres, Remove 3 Glitch + Draw 1). Both cards are
independently strong even ignoring the Glitch-removal clause - 30 Block is
a full turn's worth of mitigation, and a 1-cost cantrip-with-draw is good
on its own. That means the categorical override rarely costs the AI
anything: the card it's "forced" to play is usually the one it would have
picked anyway. Conclusion: fine as-is. Low blast radius, no patch
needed. Worth revisiting only if a future Glitch-removal card is added
that's weak outside of clearing Glitch - that combination would
reproduce the Phase 0 shape of bug.
Block-scaling damage (Quarterly Growth) = priority -500, always last
Not investigated further this session - deferred, no new evidence
gathered beyond what Phase 0 already showed (it rarely gets played because
the enemy's energy is spent before the sort reaches it). Roadmap text
stands: worth a follow-up sim run to check whether "-500" should become
"low but contests for energy" rather than "guaranteed leftovers."
What this audit actually found: a bigger, related bug
While confirming question 1, I read every CardEffect subclass behind
every AI-sector and Energy-sector enemy card to check whether
GetSequencePriority()'s categorical treatment of Glitch removal had any
siblings. It turned out the more consequential bug wasn't in the AI's
sequencing layer at all - it was one level down, in the effects
themselves.
The pattern: several CardEffect.Execute(PlayerState player, Enemy
enemy, bool playerIsActor) implementations were written assuming only the
player would ever invoke them - either an early if (!playerIsActor)
return; guard, or a if (playerIsActor) { ... } block with no else for
the enemy case. When an enemy plays one of these cards, nothing
happens. No damage, no block, no effect at all - it's a silent no-op,
same failure shape as the Phase 0 Block-priority deadlock, just one layer
deeper (effect resolution instead of AI card-choice sequencing).
Ten effect classes have this shape:
DiscardHandDrawEffect, DrawDiscardEffect, ConditionalDrawEffect,
RandomCardCreateEffect, HighHypePayoffEffect, CardsPlayedSurgeEffect,
HandSizeBlockEffect, RandomEffectCardEffect, CardsPlayedHypeEffect,
RandomDrawEffect. A related but distinct bug, DoubleHypeEffect
(missing else rather than an early return), produced the same outcome
for IPO.tres.
This is not a hypothetical - it produced real dead cards sitting in
real enemy decks, including two complete zero-damage decks and two
bosses whose advertised "finisher" cards never fired:
| Enemy | Dead card(s) | Impact |
|---|---|---|
| Product Manager | 3x GenerativeModel, 2x Pivot | Entire deck had zero damage cards before this fix - same shape as the Maintenance Drone bug from Phase 0 |
| UX Designer ("Daughter") | 2x Firewall (no damage anywhere in deck) | Entire deck had zero damage cards - Firewall itself wasn't guarded, but the deck had no damage source at all |
| The Algorithm (boss) | 3x SingularityEvent | Its 35-damage Hype payoff - the deck's biggest single hit - never landed |
| VP of Engineering (boss) | 2x SingularityEvent, 2x IPO | Same payoff bug, plus a second unrelated dead card (DoubleHypeEffect) in the same deck |
| Attack Vector | 2x SingularityEvent | 14% of deck non-functional |
| Junior Dev ("Son") | 2x APICall | 29% of deck non-functional |
| Model Trainer | 2x GenerativeModel | 14% of deck non-functional |
| Fuel Dispenser | 4x EmergencyGenerator | 29% of deck non-functional, including the card's advertised Block, not just its bonus Surge |
| Repair Relay | 3x EmergencyGenerator | 23% of deck non-functional, on an already damage-thin Defensive deck |
Fix applied
Consistent with the precedent set by Phase 0's Maintenance Drone fix, every
case was resolved by swapping deck contents, not touching shared effect
code - the effect guards are at least partly intentional in places (e.g.
RandomCardCreateEffect adds to the player's hand via
AddCardToHand, regardless of actor, so enabling it for enemies would
hand the player free cards - a different and worse bug). Each dead card
was replaced with a same-cost, thematically-consistent, already-extant
card from the same sector, sized to keep deck counts unchanged:
- Product Manager: 3x GenerativeModel -> 3x NeuralStrike; 2x Pivot -> 2x DDoSAttack
- UX Designer: 2x Firewall -> 2x NeuralStrike
- The Algorithm: 3x SingularityEvent -> 3x GPUBurst
- VP of Engineering: 2x SingularityEvent -> 2x GPUBurst; 2x IPO -> 2x DDoSAttack
- Attack Vector: 2x SingularityEvent folded into existing GPUBurst slot (now 3x GPUBurst)
- Junior Dev: 2x APICall -> 2x NeuralStrike
- Model Trainer: 2x GenerativeModel -> 2x DDoSAttack
- Fuel Dispenser: 4x EmergencyGenerator -> 4x ReinforcedPylon (keeps Surge synergy with GridShield, fits Defensive identity)
- Repair Relay: 3x EmergencyGenerator -> 3x FuelInjection (adds real damage + Surge stacks feeding its existing PowerSurge/SurgePricing payoffs)
Re-running the full-game audit after these fixes:
- Zero-damage-deck audit: 0 enemies with no damage card in their deck (previously 1: Product Manager; UX Designer was already structurally identical and fixed in a prior pass this session).
- Dead-card audit: 0 cards remaining anywhere in the game whose effects are entirely drawn from the 10 confirmed-guarded effect classes.
Both audit scripts are committed as scripts/audit_enemy_damage.py and
scripts/audit_dead_cards.py (promoted from this session's throwaway
scratchpad versions) so future sessions can re-run them in ~1 second
instead of rebuilding them from scratch. Run from game-engine/, e.g.
python3 ../scripts/audit_dead_cards.py.
Phase 1b (follow-up, same session): the remaining 8 effect classes
The guarded-effect pattern looked broader than the 10 classes audited
above based on a grep-level scan. Following up with full reads of all 8:
InsulationEffect, OverchargeEffect, RetainBlockEffect,
NextCardFreeEffect, HomeRemedyEffect, ConditionalSurgeEffect,
InfluenceScalingEffect, EfficiencyFilterEffect.
Result: 6 were false positives. NextCardFreeEffect,
OverchargeEffect, HomeRemedyEffect, InfluenceScalingEffect,
EfficiencyFilterEffect, and ConditionalSurgeEffect all have a proper
if (playerIsActor) {...} else {...} (or equivalent) covering both
actors correctly - the grep that flagged them as "suspicious" was matching
on a partial pattern, the same trap SkipNextDrawEffect fell into earlier
in this same audit. Lesson confirmed twice now: never conclude a guard is
a bug without reading the full Execute().
Only 2 were real: RetainBlockEffect.cs and InsulationEffect.cs,
both if (playerIsActor) { ... } with no else. Neither needed a deck
fix, though, because neither is reachable today: RetainBlockEffect
sets player.BlockRetentionThreshold, a field that exists only on
PlayerState - not Enemy - so even fixing the missing else would need
a new Enemy.cs field first, not a one-line patch. And the only two cards
that use either effect (Insulation.tres, HedgeFund.tres) aren't
referenced in any enemy's Deck array - confirmed via grep -rl
"HedgeFund.tres\|Insulation.tres" resources/characters/ returning no
results. No fix applied; none needed unless a future deck edit
introduces one of these two cards to an enemy. If that happens, add both
class names to the GUARDED set in scripts/audit_dead_cards.py and
decide then whether to add the missing Enemy.cs field or swap the card.