# Trace: Implement New Game Effect and Continue Work Queue

Agent: claude-code | Model: Claude Opus 4.6 | Project: mage

---

## User

<local-command-caveat>Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.</local-command-caveat>

## User

<command-name>/clear</command-name>
            <command-message>clear</command-message>
            <command-args></command-args>

## User

<local-command-stdout></local-command-stdout>

## User

Read `docs/work-queue.md` and do the next unchecked batch. For each batch:

1. Read the engine files (`mtg-engine/src/abilities.rs`, `mtg-engine/src/game.rs`) to understand current Effect/StaticEffect variants
2. If the batch has a NOTE about uncertainty, investigate first and update the queue with findings before committing the fix
3. If engine work is needed: add the variant, constructor, and match arm, and look for the Java implementation to see the bigger picture
4. Add a test for any new engine effect (follow the pattern in `game.rs` `mod tests`)
5. Fix all cards listed in the batch (grep to find exact lines, fix across all 4 set files)
6. `cargo check -p mtg-cards && cargo test --lib`
7. Update docs: mark cards in `docs/{fdn,tla,tdm,ecl}-remediation.md`, update `ROADMAP.md`
8. Check off the batch in `docs/work-queue.md` with today's date
9. If you discover new cards that fit the current or a future batch while working, add them to the queue

If a batch turns out to be wrong or more complex than described, update the queue with what you learned and move on to the next one.

## Assistant

I'll start by reading the work queue to find the next unchecked batch.

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/work-queue.md"
}
```

## Assistant

**Result** (success):
```
     1→# Card Remediation Work Queue
     2→
     3→This document drives the batch card-fix loop. Each batch groups cards that share a common missing implementation. Work top-to-bottom; check off batches as completed.
     4→
     5→## Process (for each batch)
     6→
     7→1. **Read this file** to find the next unchecked batch
     8→2. **Read the engine files** (`abilities.rs`, `game.rs`) to understand current state
     9→3. **Implement** the engine change (new Effect/StaticEffect variant + match arm), if needed
    10→4. **Add a test** for any new engine effect in `game.rs` `mod tests`
    11→5. **Fix the cards** listed in the batch (grep to find exact lines)
    12→6. **Verify**: `cargo check -p mtg-cards && cargo test --lib`
    13→7. **Update docs**: Mark cards fixed in `docs/{set}-remediation.md`, update ROADMAP.md
    14→8. **Check off** the batch below and note the date
    15→
    16→---
    17→
    18→## Batch 1: LoseLifeOpponents [DONE 2026-02-13]
    19→
    20→**Engine**: Added `Effect::LoseLifeOpponents { amount }` variant + constructor + match arm + test.
    21→
    22→**Cards fixed (8):**
    23→- [x] FDN: Marauding Blight-Priest, Pulse Tracker, Vampire Spawn, Phyrexian Arena (LoseLife), Vampire Neonate
    24→- [x] TDM: Skirmish Rhino
    25→- [x] ECL: Champion of the Weird (effect part), Boggart Mischief (dies trigger part)
    26→
    27→---
    28→
    29→## Batch 2: Easy swaps — existing typed effects [DONE 2026-02-13]
    30→
    31→**Engine**: Modified `execute_effects` in `game.rs` to accept an optional `source: Option<ObjectId>` parameter. When `AddCounters` or `RemoveCounters` effects have no targets, they now fall back to the source permanent (for self-targeting counter effects). Added test `add_counters_self_when_no_targets`.
    32→
    33→### 2a: DestroyAll — Day of Judgment
    34→- [x] FDN: Day of Judgment — `Effect::Custom("Destroy all creatures.")` → `Effect::destroy_all("creatures")`
    35→
    36→### 2b: EntersTapped — Diregraf Ghoul
    37→- [x] FDN: Diregraf Ghoul — `StaticEffect::Custom("Enters tapped.")` → `StaticEffect::EntersTapped { filter: "self".into() }`
    38→
    39→### 2c: CantBlock (target) — Frenzied Goblin, Brambleback Brute
    40→- [x] FDN: Frenzied Goblin — `Effect::Custom("Target creature can't block this turn.")` → `Effect::CantBlock`
    41→- [x] ECL: Brambleback Brute — `Effect::Custom("Target creature can't block this turn.")` → `Effect::CantBlock`
    42→- **Note**: The work queue originally listed "Skoa Veteran" for batch 2c, but the actual card in ecl.rs was **Brambleback Brute**.
    43→- Verify: these cards already have `TargetSpec::OpponentCreature` or similar targeting
    44→
    45→### 2d: Self -1/-1 counters — ECL blight creatures
    46→These use `Effect::Custom("Put N -1/-1 counter(s) on <self>.")` alongside working effects. Replace with `Effect::AddCounters`:
    47→- [x] ECL: Blighted Blackthorn (x2) — `Custom("Put two -1/-1 counters on Blighted Blackthorn.")` → `Effect::add_counters("-1/-1", 2)` (appears twice, two abilities)
    48→- [x] ECL: Sourbread Auntie — `Custom("Put two -1/-1 counters on Sourbread Auntie.")` → `Effect::add_counters("-1/-1", 2)`
    49→- [x] ECL: Sting-Slinger — `Custom("Put a -1/-1 counter on Sting-Slinger.")` → `Effect::add_counters("-1/-1", 1)`
    50→- [x] ECL: Heirloom Auntie — `Custom("Remove a -1/-1 counter from Heirloom Auntie.")` → `Effect::RemoveCounters { counter_type: "-1/-1", count: 1 }`
    51→- [x] ECL: Encumbered Reejerey — `Custom("Remove a -1/-1 counter from Encumbered Reejerey.")` → `Effect::RemoveCounters { counter_type: "-1/-1", count: 1 }`
    52→- [x] ECL: Reluctant Dounguard — `Custom("Remove a -1/-1 counter from Reluctant Dounguard.")` → `Effect::RemoveCounters { counter_type: "-1/-1", count: 1 }`
    53→- [x] ECL: Bristlebane Battler — `Custom("Remove a -1/-1 counter from this creature.")` → `Effect::RemoveCounters { counter_type: "-1/-1", count: 1 }`
    54→- **Engine tweak**: `execute_effects` now falls back to the source permanent when `AddCounters`/`RemoveCounters` have no targets (self-targeting counter effects).
    55→
    56→### 2e: Loot (draw then discard) — Icewind Elemental, Refute
    57→- [x] FDN: Icewind Elemental — `Custom("When this creature enters, draw a card, then discard a card.")` → `Effect::draw_cards(1), Effect::discard_cards(1)`
    58→- [x] FDN: Refute — `Custom("Counter target spell. Draw a card, then discard a card.")` → `Effect::counter_spell(), Effect::draw_cards(1), Effect::discard_cards(1)` + fixed TargetSpec to Spell
    59→
    60→### 2f: Simple ETB draw — Mistmeadow Council
    61→- [x] ECL: Mistmeadow Council — `Custom("When this creature enters, draw a card.")` → `Effect::draw_cards(1)`
    62→
    63→### Cards NOT fixed in Batch 2 (deferred):
    64→- **Warren Torchmaster** (ECL): compound Custom with self-counter + target haste (needs modal/conditional support)
    65→- **Dream Seizer** (ECL): compound Custom with self-counter + opponent discard (Batch 3 dependency)
    66→- **ECL line ~1843**: "Put two -1/-1 counters on each creature." — needs `AddCountersAll` variant (new engine effect)
    67→- **ECL line ~464**: `Cost::Custom` for -1/-1 counter removal — not an Effect, needs Cost system fix
    68→
    69→---
    70→
    71→## Batch 3: OpponentDiscards [DONE 2026-02-13]
    72→
    73→**Engine**: Added `Effect::DiscardOpponents { count }` variant + `discard_opponents(count)` constructor + match arm in `execute_effects` (iterates opponents, each calls `choose_discard` then moves to graveyard). Added test `discard_opponents_effect`.
    74→
    75→**Cards fixed (4):**
    76→- [x] FDN: Burglar Rat — `Custom("Each opponent discards a card.")` → `Effect::discard_opponents(1)`
    77→- [x] FDN: Arbiter of Woe — compound Custom → `discard_opponents(1), lose_life_opponents(2), draw_cards(1), gain_life(2)`
    78→- [x] FDN: Bloodtithe Collector — `Custom(conditional)` → `Effect::discard_opponents(1)` (condition "if an opponent lost life this turn" not modeled; trigger fires unconditionally)
    79→- [x] ECL: Dream Seizer — `Custom(compound)` → `add_counters("-1/-1", 1), discard_opponents(1)` (self-counter via Batch 2 source fallback)
    80→
    81→---
    82→
    83→## Batch 4: Simple token creation [DONE 2026-02-13]
    84→
    85→**Engine**: No changes needed. `CreateToken` already works and parses P/T + keywords from `token_name`.
    86→
    87→**Cards fixed (8 cards, 10 effect swaps):**
    88→- [x] FDN: Guarded Heir — ETB `create_token("3/3 Knight", 2)`
    89→- [x] FDN: Prideful Parent — ETB `create_token("1/1 Cat", 1)`
    90→- [x] FDN: Resolute Reinforcements — ETB `create_token("1/1 Soldier", 1)`
    91→- [x] FDN: Release the Dogs — Spell `create_token("1/1 Dog", 4)`
    92→- [x] FDN: Dwynen's Elite — ETB `create_token("1/1 Elf Warrior", 1)` (conditional "if you control another Elf" not modeled)
    93→- [x] FDN: Dragonmaster Outcast — Upkeep `create_token("5/5 Dragon with flying", 1)` (conditional "six or more lands" not modeled)
    94→- [x] FDN: Searslicer Goblin — End step `create_token("1/1 Goblin", 1)` (raid condition not modeled)
    95→- [x] ECL: Clachan Festival — ETB `create_token("1/1 Kithkin", 2)` + Activated `Cost::pay_mana("{4}{W}")` + `create_token("1/1 Kithkin", 1)`
    96→
    97→**Tests added:** `framework_create_token_effect`, `framework_create_token_with_keyword`
    98→
    99→**Skipped/Deferred:**
   100→- FDN: Cat Collector (Food token) — Food tokens are artifacts, not creatures. `CreateToken` always sets `CardType::Creature`. Needs engine support for artifact tokens (new batch).
   101→- FDN: Faebloom Trick — compound effect: create 2 Faerie tokens + reflexive trigger (tap opponent creature). Needs multi-effect resolution.
   102→- FDN: Mender's Bounty (Food token) — same as Cat Collector.
   103→- All token copy effects (Electroduplicate, Rite of Replication, etc.) — needs Spell Copy system.
   104→- All variable-count tokens (Homunculus Horde, Hare Apparent, etc.) — needs dynamic count support.
   105→
   106→---
   107→
   108→## Batch 5: Ward keyword [DONE 2026-02-13]
   109→
   110→**Engine**: Added `StaticEffect::Ward { cost: String }` variant + `ward()` builder. Ward is stored as structured data; mechanical enforcement (counter-unless-pay during targeting) deferred to future engine work. `KeywordAbilities::WARD` already existed in bitflags.
   111→
   112→**Cards fixed (7):**
   113→- [x] TDM: Aegis Sculptor — `StaticEffect::Custom("Ward {2}")` → `StaticEffect::ward("{2}")` + WARD keyword
   114→- [x] TDM: Ambling Stormshell — `StaticEffect::Custom("Ward {2}")` → `StaticEffect::ward("{2}")` + WARD keyword
   115→- [x] TDM: Dirgur Island Dragon — `StaticEffect::Custom("Ward {2}")` → `StaticEffect::ward("{2}")` + WARD keyword
   116→- [x] TDM: Scavenger Regent — `StaticEffect::Custom("Ward -- Discard a card.")` → `StaticEffect::ward("Discard a card.")` + WARD keyword
   117→- [x] ECL: Bristlebane Battler — `StaticEffect::Custom("Ward {2}")` → `StaticEffect::ward("{2}")` + WARD keyword
   118→- [x] FDN: Cackling Prowler — `StaticEffect::Custom("Ward {2}")` → `StaticEffect::ward("{2}")` + WARD keyword
   119→- [x] FDN: Tolarian Terror — already had `KeywordAbilities::WARD` but no Ward ability; added `StaticEffect::ward("{2}")`
   120→
   121→**Tests added:** `static_effect_builders` extended with Ward variant checks
   122→
   123→---
   124→
   125→## Batch 6: Enters-tapped-unless (conditional ETB tapped) [DONE 2026-02-13]
   126→
   127→**Engine**: Added `StaticEffect::EntersTappedUnless { condition: String }` variant + `enters_tapped_unless()` builder. Condition is stored as structured data; mechanical enforcement (actually checking land types on ETB) deferred to future engine work.
   128→
   129→**Cards fixed (2):**
   130→- [x] TDM: Cori Mountain Monastery — `StaticEffect::Custom("Enters tapped unless you control Plains or Island.")` → `StaticEffect::enters_tapped_unless("you control a Plains or an Island")`
   131→- [x] TDM: Dalkovan Encampment — `StaticEffect::Custom("Enters tapped unless you control Swamp or Mountain.")` → `StaticEffect::enters_tapped_unless("you control a Swamp or a Mountain")`
   132→
   133→**Tests added:** `static_effect_builders` extended with EntersTappedUnless variant check
   134→
   135→**Note:** ECL Blood Crypt has a related but different pattern ("pay 2 life or enters tapped") — shockland mechanic, not a land-type check. Left as Custom for now.
   136→
   137→---
   138→
   139→## Batch 7: Mass keyword grant / mass boost until EOT [DONE 2026-02-13]
   140→
   141→**Engine**: Added `Effect::BoostAllUntilEndOfTurn { filter, power, toughness }` + `boost_all_eot()` constructor and `Effect::GrantKeywordAllUntilEndOfTurn { filter, keyword }` + `grant_keyword_all_eot()` constructor. Both implemented in `execute_effects()` with filter matching and "you control" controller check. Uses P1P1/M1M1 counters for boost (same simplification as single-target `BoostUntilEndOfTurn`). Keywords use `granted_keywords` which are cleared at EOT cleanup.
   142→
   143→**Cards fixed (8, 12 effect swaps):**
   144→- [x] FDN: Crash Through — `Custom` → `grant_keyword_all_eot("creatures you control", "trample")` + `draw_cards(1)`
   145→- [x] FDN: Overrun — 2× `Custom` → `boost_all_eot("creatures you control", 3, 3)` + `grant_keyword_all_eot("creatures you control", "trample")`
   146→- [x] FDN: Make a Stand — 2× `Custom` → `boost_all_eot("creatures you control", 1, 0)` + `grant_keyword_all_eot("creatures you control", "indestructible")`
   147→- [x] FDN: Heroic Reinforcements — 2× `Custom` → `boost_all_eot("creatures you control", 1, 1)` + `grant_keyword_all_eot("creatures you control", "haste")`
   148→- [x] FDN: Balmor, Battlemage Captain — `Custom` → `boost_all_eot("creatures you control", 1, 0)` + `grant_keyword_all_eot("creatures you control", "trample")`
   149→- [x] FDN: Claws Out — `Custom` → `boost_all_eot("creatures you control", 2, 2)`
   150→- [x] ECL: Timid Shieldbearer — `Custom` → `boost_all_eot("creatures you control", 1, 1)`
   151→- [x] ECL: Catharsis — `Custom` → `boost_all_eot("creatures you control", 1, 1)` + `grant_keyword_all_eot("creatures you control", "haste")`
   152→
   153→**Tests added:** `boost_all_and_grant_keyword_all_until_eot`
   154→
   155→**Skipped/Deferred:**
   156→- TDM: Craterhoof (variable +X/+X) — needs dynamic count support
   157→- ECL: Kithkin conditional ("+1/+0 + first strike for Kithkin only") — compound conditional filter
   158→- TDM: Modal spell with mass buff option — needs modal framework (Batch 11)
   159→
   160→---
   161→
   162→## Batch 8: Fight/Bite mechanic [DONE 2026-02-13]
   163→
   164→**Engine**: Added `Effect::Fight` (mutual damage) and `Effect::Bite` (one-way damage based on power) with proper two-target selection. Added `TargetSpec::Pair { CreatureYouControl, OpponentCreature }` and `TargetSpec::fight_targets()` builder for spell-based fight/bite. `select_targets_for_spec()` handles target selection by asking the decision maker. `resolve_fight_pair()` resolves fighter/target from explicit targets, source+target (ETB), or fallback auto-select. For compound effects (e.g. counter+bite), pre-fight/bite effects only apply to `targets[0]` (your creature), matching Java's per-effect target assignment.
   165→
   166→**Cards fixed (6):**
   167→- [x] FDN: Bite Down — `Custom` → `Effect::bite()` + `TargetSpec::fight_targets()`
   168→- [x] FDN: Affectionate Indrik — ETB `Custom` → `Effect::fight()` + `TargetSpec::OpponentCreature`
   169→- [x] FDN: Felling Blow — `Custom` → `Effect::add_p1p1_counters(1), Effect::bite()` + `TargetSpec::fight_targets()`
   170→- [x] ECL: Assert Perfection — `Custom` → `Effect::boost_until_eot(1, 0), Effect::bite()` + `TargetSpec::fight_targets()`
   171→- [x] TDM: Piercing Exhale — `Custom` → `Effect::bite()` + `TargetSpec::fight_targets()`
   172→- [x] TDM: Knockout Maneuver — `Custom` → `Effect::add_p1p1_counters(1), Effect::bite()` + `TargetSpec::fight_targets()`
   173→
   174→**Tests added:** `fight_and_bite_effects`, `fight_auto_selects_creatures`, `compound_bite_counters_only_on_your_creature`
   175→
   176→**Skipped/Deferred:**
   177→- TDM: Dragonclaw Strike — "Double P/T then fight" — doubling not implementable
   178→- TDM: Dragon land (bite with Dragon-specific targeting) — custom target spec
   179→- FDN: Primal Might — X-cost + fight (Batch 15)
   180→- FDN: Tyvar's Stand (Choose one with fight) — modal (Batch 11)
   181→- ECL: Kithkin Whirlwind (Choose two with fight) — modal (Batch 11)
   182→- TLA: Earth Rumble — stub with no abilities defined
   183→
   184→---
   185→
   186→## Batch 9: AddCountersAll (mass counter placement)
   187→
   188→**Engine**: Need `Effect::AddCountersAll { counter_type, count, filter }` — similar pattern to `DestroyAll`/`DealDamageAll`. Puts N counters on all permanents matching filter.
   189→
   190→**Cards:**
   191→- [ ] ECL: Darkness Descends — `Custom("Put two -1/-1 counters on each creature.")` → `Effect::AddCountersAll { counter_type: "-1/-1", count: 2, filter: "creatures" }`
   192→
   193→---
   194→
   195→## Batch 10: Compound self-counter + target effects
   196→
   197→**Engine**: These cards need the ability to put counters on self AND apply a separate effect to a target in the same triggered/activated ability. The self-counter part now works (Batch 2 source fallback), but combining it with a target effect in one resolution needs verification.
   198→
   199→**Cards:**
   200→- [ ] ECL: Warren Torchmaster — begin combat: may blight 1 (self -1/-1 counter) to give target creature haste until EOT. Needs `add_counters("-1/-1", 1)` + `gain_keyword_eot("haste")` with target.
   201→- [ ] ECL: Dream Seizer — ETB: self -1/-1 counter + each opponent discards (also depends on Batch 3 OpponentDiscards)
   202→
   203→---
   204→
   205→## Batch 11: Modal spells (Choose one/two)
   206→
   207→**Engine**: Modal framework — player chooses mode, effects resolve based on mode selection.
   208→
   209→**Cards (~15+):**
   210→- [ ] FDN: Abrade, Boros Charm, Slagstorm, Valorous Stance, Charming Prince
   211→- [ ] TDM: multiple
   212→- [ ] ECL: multiple
   213→
   214→---
   215→
   216→## Batch 12+: Larger systems
   217→
   218→These require more significant engine work:
   219→- Equipment attach/detach (Batch 12)
   220→- Planeswalker loyalty (Batch 13)
   221→- Token copy (Batch 14)
   222→- X-cost spells (Batch 15)
   223→- Aura attachment (Batch 16)
   224→- Impulse draw / exile-and-play (Batch 17)
   225→- Sagas / lore counters (Batch 18)
   226→- Set mechanics: Earthbend (TLA), Blight (ECL), Vivid (ECL), Behold (ECL) (Batch 19+)
   227→- Cost system: `Cost::Custom` for counter removal (ECL line ~464), `Cost::RemoveCounters`, `Cost::ExileFromGraveyard`
   228→
   229→---
   230→
   231→## How to pick the next batch
   232→
   233→1. Always do the lowest-numbered unchecked batch first
   234→2. If a batch has a NOTE about engine uncertainty, investigate before committing
   235→3. After finishing a batch, update this file AND the per-set remediation docs
   236→4. Run `cargo test --lib` before and after every batch
   237→
```

## Assistant

## Assistant

The next unchecked batch is **Batch 9: AddCountersAll**. Let me read the engine files and find the card.

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/abilities.rs"
}
```

## Assistant

**Result** (success):
```
     1→// Ability framework — activated, triggered, static, spell, and mana abilities.
     2→//
     3→// In MTG, abilities are the things cards can do. The framework models:
     4→// - **SpellAbility**: The ability a spell has while on the stack (resolve effects)
     5→// - **ActivatedAbility**: "Cost: Effect" abilities that can be activated by a player
     6→// - **TriggeredAbility**: "When/Whenever/At" abilities that trigger from events
     7→// - **StaticAbility**: Abilities that generate continuous effects while in play
     8→// - **ManaAbility**: Special activated abilities that produce mana (don't use the stack)
     9→//
    10→// Ported from mage.abilities.*.
    11→
    12→use crate::constants::{AbilityType, Zone};
    13→use crate::events::{EventType, GameEvent};
    14→use crate::mana::Mana;
    15→use crate::types::{AbilityId, ObjectId};
    16→use serde::{Deserialize, Serialize};
    17→
    18→// ---------------------------------------------------------------------------
    19→// Cost types
    20→// ---------------------------------------------------------------------------
    21→
    22→/// A cost that must be paid to activate an ability or cast a spell.
    23→#[derive(Clone, Debug, Serialize, Deserialize)]
    24→pub enum Cost {
    25→    /// Pay mana (e.g. "{2}{B}").
    26→    Mana(Mana),
    27→    /// Tap this permanent ("{T}").
    28→    TapSelf,
    29→    /// Untap this permanent ("{Q}").
    30→    UntapSelf,
    31→    /// Pay life.
    32→    PayLife(u32),
    33→    /// Sacrifice this permanent.
    34→    SacrificeSelf,
    35→    /// Sacrifice another permanent (described by text).
    36→    SacrificeOther(String),
    37→    /// Discard a card.
    38→    Discard(u32),
    39→    /// Exile a card from hand.
    40→    ExileFromHand(u32),
    41→    /// Exile a card from graveyard.
    42→    ExileFromGraveyard(u32),
    43→    /// Remove counters from this permanent.
    44→    RemoveCounters(String, u32),
    45→    /// Blight N — put N -1/-1 counters on a creature you control.
    46→    /// (ECL set-specific mechanic.)
    47→    Blight(u32),
    48→    /// Reveal a card of a specific type from hand (used by Behold).
    49→    RevealFromHand(String),
    50→    /// A custom/complex cost (described by text).
    51→    Custom(String),
    52→}
    53→
    54→// ---------------------------------------------------------------------------
    55→// Effect types
    56→// ---------------------------------------------------------------------------
    57→
    58→/// What an effect does when it resolves. These are the building blocks
    59→/// that card implementations compose to create their abilities.
    60→///
    61→/// Each variant describes a specific game action. Complex cards can chain
    62→/// multiple effects. The game engine interprets these to modify the state.
    63→#[derive(Clone, Debug, Serialize, Deserialize)]
    64→pub enum Effect {
    65→    // -- Damage --
    66→    /// Deal damage to target creature or player.
    67→    DealDamage { amount: u32 },
    68→    /// Deal damage to each creature.
    69→    DealDamageAll { amount: u32, filter: String },
    70→    /// Deal damage to each opponent.
    71→    DealDamageOpponents { amount: u32 },
    72→
    73→    // -- Life --
    74→    /// Gain life.
    75→    GainLife { amount: u32 },
    76→    /// Lose life (target player).
    77→    LoseLife { amount: u32 },
    78→    /// Each opponent loses life.
    79→    LoseLifeOpponents { amount: u32 },
    80→    /// Set life total.
    81→    SetLife { amount: i32 },
    82→
    83→    // -- Destroy / Remove --
    84→    /// Destroy target permanent.
    85→    Destroy,
    86→    /// Destroy all permanents matching filter.
    87→    DestroyAll { filter: String },
    88→    /// Exile target permanent.
    89→    Exile,
    90→    /// Sacrifice a permanent (owner chooses).
    91→    Sacrifice { filter: String },
    92→    /// Return target permanent to hand.
    93→    Bounce,
    94→    /// Return target card from graveyard to hand.
    95→    ReturnFromGraveyard,
    96→    /// Return target card from graveyard to battlefield.
    97→    Reanimate,
    98→
    99→    // -- Cards --
   100→    /// Draw cards.
   101→    DrawCards { count: u32 },
   102→    /// Discard cards.
   103→    DiscardCards { count: u32 },
   104→    /// Each opponent discards N cards.
   105→    DiscardOpponents { count: u32 },
   106→    /// Mill cards (library to graveyard).
   107→    Mill { count: u32 },
   108→    /// Scry N (look at top N, put any on bottom in any order).
   109→    Scry { count: u32 },
   110→    /// Search library for a card.
   111→    SearchLibrary { filter: String },
   112→
   113→    // -- Counters --
   114→    /// Put counters on target.
   115→    AddCounters { counter_type: String, count: u32 },
   116→    /// Remove counters from target.
   117→    RemoveCounters { counter_type: String, count: u32 },
   118→
   119→    // -- Tokens --
   120→    /// Create token creatures.
   121→    CreateToken { token_name: String, count: u32 },
   122→    /// Create tokens that enter tapped and attacking, then sacrifice at next end step.
   123→    /// (Used by TDM Mobilize mechanic.)
   124→    CreateTokenTappedAttacking { token_name: String, count: u32 },
   125→
   126→    // -- Mana --
   127→    /// Add mana to controller's pool.
   128→    AddMana { mana: Mana },
   129→
   130→    // -- Combat --
   131→    /// Target creature can't block this turn.
   132→    CantBlock,
   133→    /// Target creature must block this turn.
   134→    MustBlock,
   135→    /// Prevent combat damage.
   136→    PreventCombatDamage,
   137→    /// Fight — source creature and target creature each deal damage equal
   138→    /// to their power to each other.
   139→    Fight,
   140→    /// Bite — source creature deals damage equal to its power to target
   141→    /// creature (one-way; the target does not deal damage back).
   142→    Bite,
   143→
   144→    // -- Stats --
   145→    /// Give +N/+M until end of turn.
   146→    BoostUntilEndOfTurn { power: i32, toughness: i32 },
   147→    /// Give +N/+M permanently (e.g. from counters, applied differently).
   148→    BoostPermanent { power: i32, toughness: i32 },
   149→    /// Give all matching creatures +N/+M until end of turn.
   150→    BoostAllUntilEndOfTurn { filter: String, power: i32, toughness: i32 },
   151→    /// Set power and toughness.
   152→    SetPowerToughness { power: i32, toughness: i32 },
   153→
   154→    // -- Keywords --
   155→    /// Grant a keyword ability until end of turn.
   156→    GainKeywordUntilEndOfTurn { keyword: String },
   157→    /// Grant a keyword to all matching creatures until end of turn.
   158→    GrantKeywordAllUntilEndOfTurn { filter: String, keyword: String },
   159→    /// Grant a keyword ability permanently.
   160→    GainKeyword { keyword: String },
   161→    /// Remove a keyword ability.
   162→    LoseKeyword { keyword: String },
   163→
   164→    // -- Control --
   165→    /// Gain control of target.
   166→    GainControl,
   167→    /// Gain control of target until end of turn.
   168→    GainControlUntilEndOfTurn,
   169→
   170→    // -- Tap --
   171→    /// Tap target permanent.
   172→    TapTarget,
   173→    /// Untap target permanent.
   174→    UntapTarget,
   175→
   176→    // -- Counter spells --
   177→    /// Counter target spell.
   178→    CounterSpell,
   179→
   180→    // -- Protection --
   181→    /// Target gains protection from a color/quality until end of turn.
   182→    GainProtection { from: String },
   183→    /// Target becomes indestructible until end of turn.
   184→    Indestructible,
   185→    /// Target gains hexproof until end of turn.
   186→    Hexproof,
   187→
   188→    // -- Misc --
   189→    /// A custom/complex effect described by text. The game engine or card
   190→    /// code handles the specific implementation.
   191→    Custom(String),
   192→}
   193→
   194→// ---------------------------------------------------------------------------
   195→// Target specification for abilities
   196→// ---------------------------------------------------------------------------
   197→
   198→/// Describes what an ability can target.
   199→#[derive(Clone, Debug, Serialize, Deserialize)]
   200→pub enum TargetSpec {
   201→    /// No targets.
   202→    None,
   203→    /// Target creature.
   204→    Creature,
   205→    /// Target creature or player.
   206→    CreatureOrPlayer,
   207→    /// Target player.
   208→    Player,
   209→    /// Target permanent.
   210→    Permanent,
   211→    /// Target permanent matching a filter.
   212→    PermanentFiltered(String),
   213→    /// Target spell on the stack.
   214→    Spell,
   215→    /// Target card in a graveyard.
   216→    CardInGraveyard,
   217→    /// Target card in your graveyard.
   218→    CardInYourGraveyard,
   219→    /// Target creature you control.
   220→    CreatureYouControl,
   221→    /// Target creature you don't control (opponent's creature).
   222→    OpponentCreature,
   223→    /// Two targets (e.g. fight spells: your creature + opponent's creature).
   224→    /// targets[0] comes from `first`, targets[1] from `second`.
   225→    Pair { first: Box<TargetSpec>, second: Box<TargetSpec> },
   226→    /// Multiple targets of the same type.
   227→    Multiple { spec: Box<TargetSpec>, count: usize },
   228→    /// Custom targeting (described by text).
   229→    Custom(String),
   230→}
   231→
   232→// ---------------------------------------------------------------------------
   233→// Ability struct
   234→// ---------------------------------------------------------------------------
   235→
   236→/// A concrete ability instance attached to a card or permanent.
   237→///
   238→/// This is a data-oriented design: each ability is a struct containing
   239→/// its type, costs, effects, targets, and configuration. The game engine
   240→/// interprets these to execute game actions.
   241→#[derive(Clone, Debug, Serialize, Deserialize)]
   242→pub struct Ability {
   243→    /// Unique ID for this ability instance.
   244→    pub id: AbilityId,
   245→    /// The source object (card or permanent) this ability belongs to.
   246→    pub source_id: ObjectId,
   247→    /// What kind of ability this is.
   248→    pub ability_type: AbilityType,
   249→    /// Human-readable rules text.
   250→    pub rules_text: String,
   251→    /// The zone(s) this ability functions from (e.g. battlefield, graveyard).
   252→    pub active_zones: Vec<Zone>,
   253→    /// Costs to activate (for activated/mana abilities).
   254→    pub costs: Vec<Cost>,
   255→    /// Effects that happen when this ability resolves.
   256→    pub effects: Vec<Effect>,
   257→    /// Target requirements.
   258→    pub targets: TargetSpec,
   259→    /// For triggered abilities: the event type(s) that trigger it.
   260→    pub trigger_events: Vec<EventType>,
   261→    /// For triggered abilities: whether the trigger is optional ("may").
   262→    pub optional_trigger: bool,
   263→    /// For mana abilities: the mana produced.
   264→    pub mana_produced: Option<Mana>,
   265→    /// For static abilities: continuous effects applied while in play.
   266→    pub static_effects: Vec<StaticEffect>,
   267→}
   268→
   269→impl Ability {
   270→    /// Create a new activated ability.
   271→    pub fn activated(
   272→        source_id: ObjectId,
   273→        rules_text: &str,
   274→        costs: Vec<Cost>,
   275→        effects: Vec<Effect>,
   276→        targets: TargetSpec,
   277→    ) -> Self {
   278→        Ability {
   279→            id: AbilityId::new(),
   280→            source_id,
   281→            ability_type: AbilityType::ActivatedNonMana,
   282→            rules_text: rules_text.to_string(),
   283→            active_zones: vec![Zone::Battlefield],
   284→            costs,
   285→            effects,
   286→            targets,
   287→            trigger_events: vec![],
   288→            optional_trigger: false,
   289→            mana_produced: None,
   290→            static_effects: vec![],
   291→        }
   292→    }
   293→
   294→    /// Create a new triggered ability.
   295→    pub fn triggered(
   296→        source_id: ObjectId,
   297→        rules_text: &str,
   298→        trigger_events: Vec<EventType>,
   299→        effects: Vec<Effect>,
   300→        targets: TargetSpec,
   301→    ) -> Self {
   302→        Ability {
   303→            id: AbilityId::new(),
   304→            source_id,
   305→            ability_type: AbilityType::TriggeredNonMana,
   306→            rules_text: rules_text.to_string(),
   307→            active_zones: vec![Zone::Battlefield],
   308→            costs: vec![],
   309→            effects,
   310→            targets,
   311→            trigger_events,
   312→            optional_trigger: false,
   313→            mana_produced: None,
   314→            static_effects: vec![],
   315→        }
   316→    }
   317→
   318→    /// Create a new static ability.
   319→    pub fn static_ability(
   320→        source_id: ObjectId,
   321→        rules_text: &str,
   322→        static_effects: Vec<StaticEffect>,
   323→    ) -> Self {
   324→        Ability {
   325→            id: AbilityId::new(),
   326→            source_id,
   327→            ability_type: AbilityType::Static,
   328→            rules_text: rules_text.to_string(),
   329→            active_zones: vec![Zone::Battlefield],
   330→            costs: vec![],
   331→            effects: vec![],
   332→            targets: TargetSpec::None,
   333→            trigger_events: vec![],
   334→            optional_trigger: false,
   335→            mana_produced: None,
   336→            static_effects,
   337→        }
   338→    }
   339→
   340→    /// Create a mana ability (tap for mana).
   341→    pub fn mana_ability(source_id: ObjectId, rules_text: &str, mana: Mana) -> Self {
   342→        Ability {
   343→            id: AbilityId::new(),
   344→            source_id,
   345→            ability_type: AbilityType::ActivatedMana,
   346→            rules_text: rules_text.to_string(),
   347→            active_zones: vec![Zone::Battlefield],
   348→            costs: vec![Cost::TapSelf],
   349→            effects: vec![Effect::AddMana { mana }],
   350→            targets: TargetSpec::None,
   351→            trigger_events: vec![],
   352→            optional_trigger: false,
   353→            mana_produced: Some(mana),
   354→            static_effects: vec![],
   355→        }
   356→    }
   357→
   358→    /// Create a spell ability (the ability a spell has on the stack).
   359→    pub fn spell(source_id: ObjectId, effects: Vec<Effect>, targets: TargetSpec) -> Self {
   360→        Ability {
   361→            id: AbilityId::new(),
   362→            source_id,
   363→            ability_type: AbilityType::Spell,
   364→            rules_text: String::new(),
   365→            active_zones: vec![Zone::Stack],
   366→            costs: vec![], // mana cost is on the card, not the ability
   367→            effects,
   368→            targets,
   369→            trigger_events: vec![],
   370→            optional_trigger: false,
   371→            mana_produced: None,
   372→            static_effects: vec![],
   373→        }
   374→    }
   375→
   376→    /// Check if this ability is a mana ability.
   377→    pub fn is_mana_ability(&self) -> bool {
   378→        self.ability_type == AbilityType::ActivatedMana
   379→    }
   380→
   381→    /// Check if this ability uses the stack.
   382→    pub fn uses_stack(&self) -> bool {
   383→        !self.is_mana_ability()
   384→            && self.ability_type != AbilityType::Static
   385→    }
   386→
   387→    /// Check if a triggered ability should trigger from an event.
   388→    pub fn should_trigger(&self, event: &GameEvent) -> bool {
   389→        if self.ability_type != AbilityType::TriggeredNonMana {
   390→            return false;
   391→        }
   392→        self.trigger_events.contains(&event.event_type)
   393→    }
   394→
   395→    /// Check if an activated ability can be activated in the given zone.
   396→    pub fn can_activate_in_zone(&self, zone: Zone) -> bool {
   397→        self.active_zones.contains(&zone)
   398→    }
   399→
   400→    /// Make this a "may" trigger (optional).
   401→    pub fn set_optional(mut self) -> Self {
   402→        self.optional_trigger = true;
   403→        self
   404→    }
   405→
   406→    /// Set the active zones for this ability.
   407→    pub fn in_zones(mut self, zones: Vec<Zone>) -> Self {
   408→        self.active_zones = zones;
   409→        self
   410→    }
   411→
   412→    /// Set the rules text.
   413→    pub fn with_rules_text(mut self, text: &str) -> Self {
   414→        self.rules_text = text.to_string();
   415→        self
   416→    }
   417→}
   418→
   419→// ---------------------------------------------------------------------------
   420→// Common triggered ability builders
   421→// ---------------------------------------------------------------------------
   422→
   423→impl Ability {
   424→    /// "When ~ enters the battlefield, [effect]."
   425→    pub fn enters_battlefield_triggered(
   426→        source_id: ObjectId,
   427→        rules_text: &str,
   428→        effects: Vec<Effect>,
   429→        targets: TargetSpec,
   430→    ) -> Self {
   431→        Ability::triggered(
   432→            source_id,
   433→            rules_text,
   434→            vec![EventType::EnteredTheBattlefield],
   435→            effects,
   436→            targets,
   437→        )
   438→    }
   439→
   440→    /// "When ~ dies, [effect]."
   441→    pub fn dies_triggered(
   442→        source_id: ObjectId,
   443→        rules_text: &str,
   444→        effects: Vec<Effect>,
   445→        targets: TargetSpec,
   446→    ) -> Self {
   447→        Ability::triggered(
   448→            source_id,
   449→            rules_text,
   450→            vec![EventType::Dies],
   451→            effects,
   452→            targets,
   453→        )
   454→        .in_zones(vec![Zone::Battlefield, Zone::Graveyard])
   455→    }
   456→
   457→    /// "Whenever ~ attacks, [effect]."
   458→    pub fn attacks_triggered(
   459→        source_id: ObjectId,
   460→        rules_text: &str,
   461→        effects: Vec<Effect>,
   462→        targets: TargetSpec,
   463→    ) -> Self {
   464→        Ability::triggered(
   465→            source_id,
   466→            rules_text,
   467→            vec![EventType::AttackerDeclared],
   468→            effects,
   469→            targets,
   470→        )
   471→    }
   472→
   473→    /// "Whenever ~ deals combat damage to a player, [effect]."
   474→    pub fn combat_damage_to_player_triggered(
   475→        source_id: ObjectId,
   476→        rules_text: &str,
   477→        effects: Vec<Effect>,
   478→        targets: TargetSpec,
   479→    ) -> Self {
   480→        Ability::triggered(
   481→            source_id,
   482→            rules_text,
   483→            vec![EventType::DamagedPlayer],
   484→            effects,
   485→            targets,
   486→        )
   487→    }
   488→
   489→    /// "At the beginning of your upkeep, [effect]."
   490→    pub fn beginning_of_upkeep_triggered(
   491→        source_id: ObjectId,
   492→        rules_text: &str,
   493→        effects: Vec<Effect>,
   494→        targets: TargetSpec,
   495→    ) -> Self {
   496→        Ability::triggered(
   497→            source_id,
   498→            rules_text,
   499→            vec![EventType::UpkeepStep],
   500→            effects,
   501→            targets,
   502→        )
   503→    }
   504→
   505→    /// "At the beginning of your end step, [effect]."
   506→    pub fn beginning_of_end_step_triggered(
   507→        source_id: ObjectId,
   508→        rules_text: &str,
   509→        effects: Vec<Effect>,
   510→        targets: TargetSpec,
   511→    ) -> Self {
   512→        Ability::triggered(
   513→            source_id,
   514→            rules_text,
   515→            vec![EventType::EndStep],
   516→            effects,
   517→            targets,
   518→        )
   519→    }
   520→
   521→    /// "Whenever you cast a spell, [effect]."
   522→    pub fn spell_cast_triggered(
   523→        source_id: ObjectId,
   524→        rules_text: &str,
   525→        effects: Vec<Effect>,
   526→        targets: TargetSpec,
   527→    ) -> Self {
   528→        Ability::triggered(
   529→            source_id,
   530→            rules_text,
   531→            vec![EventType::SpellCast],
   532→            effects,
   533→            targets,
   534→        )
   535→    }
   536→
   537→    /// "Whenever another creature enters the battlefield under your control, [effect]."
   538→    pub fn other_creature_etb_triggered(
   539→        source_id: ObjectId,
   540→        rules_text: &str,
   541→        effects: Vec<Effect>,
   542→        targets: TargetSpec,
   543→    ) -> Self {
   544→        Ability::triggered(
   545→            source_id,
   546→            rules_text,
   547→            vec![EventType::EnteredTheBattlefield],
   548→            effects,
   549→            targets,
   550→        )
   551→    }
   552→
   553→    /// "Whenever a creature dies, [effect]."
   554→    pub fn any_creature_dies_triggered(
   555→        source_id: ObjectId,
   556→        rules_text: &str,
   557→        effects: Vec<Effect>,
   558→        targets: TargetSpec,
   559→    ) -> Self {
   560→        Ability::triggered(
   561→            source_id,
   562→            rules_text,
   563→            vec![EventType::Dies],
   564→            effects,
   565→            targets,
   566→        )
   567→    }
   568→}
   569→
   570→// ---------------------------------------------------------------------------
   571→// Common one-shot effect constructors
   572→// ---------------------------------------------------------------------------
   573→
   574→impl Effect {
   575→    /// "Destroy target creature/permanent."
   576→    pub fn destroy() -> Self {
   577→        Effect::Destroy
   578→    }
   579→
   580→    /// "Exile target."
   581→    pub fn exile() -> Self {
   582→        Effect::Exile
   583→    }
   584→
   585→    /// "Deal N damage to target."
   586→    pub fn deal_damage(amount: u32) -> Self {
   587→        Effect::DealDamage { amount }
   588→    }
   589→
   590→    /// "Draw N cards."
   591→    pub fn draw_cards(count: u32) -> Self {
   592→        Effect::DrawCards { count }
   593→    }
   594→
   595→    /// "Gain N life."
   596→    pub fn gain_life(amount: u32) -> Self {
   597→        Effect::GainLife { amount }
   598→    }
   599→
   600→    /// "Lose N life."
   601→    pub fn lose_life(amount: u32) -> Self {
   602→        Effect::LoseLife { amount }
   603→    }
   604→
   605→    /// "Each opponent loses N life."
   606→    pub fn lose_life_opponents(amount: u32) -> Self {
   607→        Effect::LoseLifeOpponents { amount }
   608→    }
   609→
   610→    /// "Target creature gets +N/+M until end of turn."
   611→    pub fn boost_until_eot(power: i32, toughness: i32) -> Self {
   612→        Effect::BoostUntilEndOfTurn { power, toughness }
   613→    }
   614→
   615→    /// "Target creature gets +N/+M."
   616→    pub fn boost_permanent(power: i32, toughness: i32) -> Self {
   617→        Effect::BoostPermanent { power, toughness }
   618→    }
   619→
   620→    /// "Creatures [matching filter] get +N/+M until end of turn."
   621→    pub fn boost_all_eot(filter: &str, power: i32, toughness: i32) -> Self {
   622→        Effect::BoostAllUntilEndOfTurn {
   623→            filter: filter.to_string(),
   624→            power,
   625→            toughness,
   626→        }
   627→    }
   628→
   629→    /// "Create N token(s)."
   630→    pub fn create_token(token_name: &str, count: u32) -> Self {
   631→        Effect::CreateToken {
   632→            token_name: token_name.to_string(),
   633→            count,
   634→        }
   635→    }
   636→
   637→    /// "Create N token(s) that are tapped and attacking. Sacrifice at next end step."
   638→    /// Used by Mobilize.
   639→    pub fn create_token_tapped_attacking(token_name: &str, count: u32) -> Self {
   640→        Effect::CreateTokenTappedAttacking {
   641→            token_name: token_name.to_string(),
   642→            count,
   643→        }
   644→    }
   645→
   646→    /// "Counter target spell."
   647→    pub fn counter_spell() -> Self {
   648→        Effect::CounterSpell
   649→    }
   650→
   651→    /// "Scry N."
   652→    pub fn scry(count: u32) -> Self {
   653→        Effect::Scry { count }
   654→    }
   655→
   656→    /// "Mill N."
   657→    pub fn mill(count: u32) -> Self {
   658→        Effect::Mill { count }
   659→    }
   660→
   661→    /// "Discard N cards."
   662→    pub fn discard_cards(count: u32) -> Self {
   663→        Effect::DiscardCards { count }
   664→    }
   665→
   666→    /// "Each opponent discards N cards."
   667→    pub fn discard_opponents(count: u32) -> Self {
   668→        Effect::DiscardOpponents { count }
   669→    }
   670→
   671→    /// "Return target to owner's hand."
   672→    pub fn bounce() -> Self {
   673→        Effect::Bounce
   674→    }
   675→
   676→    /// "Return target card from graveyard to hand."
   677→    pub fn return_from_graveyard() -> Self {
   678→        Effect::ReturnFromGraveyard
   679→    }
   680→
   681→    /// "Return target card from graveyard to battlefield."
   682→    pub fn reanimate() -> Self {
   683→        Effect::Reanimate
   684→    }
   685→
   686→    /// "Put N +1/+1 counters on target."
   687→    pub fn add_p1p1_counters(count: u32) -> Self {
   688→        Effect::AddCounters {
   689→            counter_type: "+1/+1".to_string(),
   690→            count,
   691→        }
   692→    }
   693→
   694→    /// "Add counters of specified type."
   695→    pub fn add_counters(counter_type: &str, count: u32) -> Self {
   696→        Effect::AddCounters {
   697→            counter_type: counter_type.to_string(),
   698→            count,
   699→        }
   700→    }
   701→
   702→    /// "Tap target permanent."
   703→    pub fn tap_target() -> Self {
   704→        Effect::TapTarget
   705→    }
   706→
   707→    /// "Untap target permanent."
   708→    pub fn untap_target() -> Self {
   709→        Effect::UntapTarget
   710→    }
   711→
   712→    /// "Add mana."
   713→    pub fn add_mana(mana: Mana) -> Self {
   714→        Effect::AddMana { mana }
   715→    }
   716→
   717→    /// "Gain keyword until end of turn."
   718→    pub fn gain_keyword_eot(keyword: &str) -> Self {
   719→        Effect::GainKeywordUntilEndOfTurn {
   720→            keyword: keyword.to_string(),
   721→        }
   722→    }
   723→
   724→    /// "Creatures [matching filter] gain [keyword] until end of turn."
   725→    pub fn grant_keyword_all_eot(filter: &str, keyword: &str) -> Self {
   726→        Effect::GrantKeywordAllUntilEndOfTurn {
   727→            filter: filter.to_string(),
   728→            keyword: keyword.to_string(),
   729→        }
   730→    }
   731→
   732→    /// "This creature fights target creature." (mutual damage)
   733→    pub fn fight() -> Self {
   734→        Effect::Fight
   735→    }
   736→
   737→    /// "This creature deals damage equal to its power to target." (one-way)
   738→    pub fn bite() -> Self {
   739→        Effect::Bite
   740→    }
   741→
   742→    /// "Set power and toughness."
   743→    pub fn set_pt(power: i32, toughness: i32) -> Self {
   744→        Effect::SetPowerToughness { power, toughness }
   745→    }
   746→
   747→    /// "Destroy all creatures" (or other filter).
   748→    pub fn destroy_all(filter: &str) -> Self {
   749→        Effect::DestroyAll {
   750→            filter: filter.to_string(),
   751→        }
   752→    }
   753→
   754→    /// "Deal N damage to each opponent."
   755→    pub fn damage_opponents(amount: u32) -> Self {
   756→        Effect::DealDamageOpponents { amount }
   757→    }
   758→
   759→    /// "Search library for a card."
   760→    pub fn search_library(filter: &str) -> Self {
   761→        Effect::SearchLibrary {
   762→            filter: filter.to_string(),
   763→        }
   764→    }
   765→
   766→    /// "Gain control of target."
   767→    pub fn gain_control() -> Self {
   768→        Effect::GainControl
   769→    }
   770→
   771→    /// "Gain control of target until end of turn."
   772→    pub fn gain_control_eot() -> Self {
   773→        Effect::GainControlUntilEndOfTurn
   774→    }
   775→
   776→    /// "Target gains protection from [quality] until end of turn."
   777→    pub fn gain_protection(from: &str) -> Self {
   778→        Effect::GainProtection {
   779→            from: from.to_string(),
   780→        }
   781→    }
   782→
   783→    /// "Target becomes indestructible until end of turn."
   784→    pub fn indestructible() -> Self {
   785→        Effect::Indestructible
   786→    }
   787→
   788→    /// "Target gains hexproof until end of turn."
   789→    pub fn hexproof() -> Self {
   790→        Effect::Hexproof
   791→    }
   792→}
   793→
   794→// ---------------------------------------------------------------------------
   795→// Common static effect builders
   796→// ---------------------------------------------------------------------------
   797→
   798→impl StaticEffect {
   799→    /// "Other creatures you control get +N/+M." (Lord effect)
   800→    pub fn boost_controlled(filter: &str, power: i32, toughness: i32) -> Self {
   801→        StaticEffect::Boost {
   802→            filter: filter.to_string(),
   803→            power,
   804→            toughness,
   805→        }
   806→    }
   807→
   808→    /// "Creatures you control have [keyword]."
   809→    pub fn grant_keyword_controlled(filter: &str, keyword: &str) -> Self {
   810→        StaticEffect::GrantKeyword {
   811→            filter: filter.to_string(),
   812→            keyword: keyword.to_string(),
   813→        }
   814→    }
   815→
   816→    /// "Creatures you control can't be blocked" (or specific CantBlock variant).
   817→    pub fn cant_block(filter: &str) -> Self {
   818→        StaticEffect::CantBlock {
   819→            filter: filter.to_string(),
   820→        }
   821→    }
   822→
   823→    /// "Creatures you control can't attack."
   824→    pub fn cant_attack(filter: &str) -> Self {
   825→        StaticEffect::CantAttack {
   826→            filter: filter.to_string(),
   827→        }
   828→    }
   829→
   830→    /// "[Spell type] spells you cast cost {N} less."
   831→    pub fn cost_reduction(filter: &str, amount: u32) -> Self {
   832→        StaticEffect::CostReduction {
   833→            filter: filter.to_string(),
   834→            amount,
   835→        }
   836→    }
   837→
   838→    /// "Ward {cost}" — counter targeting spells/abilities unless opponent pays cost.
   839→    pub fn ward(cost: &str) -> Self {
   840→        StaticEffect::Ward {
   841→            cost: cost.to_string(),
   842→        }
   843→    }
   844→
   845→    /// "This land enters tapped unless [condition]."
   846→    pub fn enters_tapped_unless(condition: &str) -> Self {
   847→        StaticEffect::EntersTappedUnless {
   848→            condition: condition.to_string(),
   849→        }
   850→    }
   851→}
   852→
   853→// ---------------------------------------------------------------------------
   854→// Common target spec builders
   855→// ---------------------------------------------------------------------------
   856→
   857→impl TargetSpec {
   858→    /// Fight/Bite targeting: "target creature you control" + "target creature you don't control".
   859→    pub fn fight_targets() -> Self {
   860→        TargetSpec::Pair {
   861→            first: Box::new(TargetSpec::CreatureYouControl),
   862→            second: Box::new(TargetSpec::OpponentCreature),
   863→        }
   864→    }
   865→}
   866→
   867→// ---------------------------------------------------------------------------
   868→// Common cost builders
   869→// ---------------------------------------------------------------------------
   870→
   871→impl Cost {
   872→    /// Pay mana cost from a string like "{2}{B}".
   873→    pub fn pay_mana(mana_str: &str) -> Self {
   874→        use crate::mana::ManaCost;
   875→        Cost::Mana(ManaCost::parse(mana_str).to_mana())
   876→    }
   877→
   878→    /// Tap this permanent ({T}).
   879→    pub fn tap_self() -> Self {
   880→        Cost::TapSelf
   881→    }
   882→
   883→    /// Sacrifice this permanent.
   884→    pub fn sacrifice_self() -> Self {
   885→        Cost::SacrificeSelf
   886→    }
   887→
   888→    /// Sacrifice another permanent matching a description.
   889→    pub fn sacrifice_other(filter: &str) -> Self {
   890→        Cost::SacrificeOther(filter.to_string())
   891→    }
   892→
   893→    /// Pay N life.
   894→    pub fn pay_life(amount: u32) -> Self {
   895→        Cost::PayLife(amount)
   896→    }
   897→
   898→    /// Discard N cards.
   899→    pub fn discard(count: u32) -> Self {
   900→        Cost::Discard(count)
   901→    }
   902→
   903→    /// Exile N cards from hand.
   904→    pub fn exile_from_hand(count: u32) -> Self {
   905→        Cost::ExileFromHand(count)
   906→    }
   907→
   908→    /// Exile N cards from graveyard.
   909→    pub fn exile_from_graveyard(count: u32) -> Self {
   910→        Cost::ExileFromGraveyard(count)
   911→    }
   912→
   913→    /// Remove N counters of a type from this permanent.
   914→    pub fn remove_counters(counter_type: &str, count: u32) -> Self {
   915→        Cost::RemoveCounters(counter_type.to_string(), count)
   916→    }
   917→
   918→    /// Blight N — put N -1/-1 counters on a creature you control.
   919→    pub fn blight(count: u32) -> Self {
   920→        Cost::Blight(count)
   921→    }
   922→
   923→    /// Reveal a card of a specific type from hand.
   924→    pub fn reveal_from_hand(card_type: &str) -> Self {
   925→        Cost::RevealFromHand(card_type.to_string())
   926→    }
   927→}
   928→
   929→// ---------------------------------------------------------------------------
   930→// Static (continuous) effects
   931→// ---------------------------------------------------------------------------
   932→
   933→/// A continuous effect generated by a static ability.
   934→///
   935→/// These are applied in the 7-layer system each time the game state is
   936→/// recalculated (see effects.rs for Layer enum).
   937→#[derive(Clone, Debug, Serialize, Deserialize)]
   938→pub enum StaticEffect {
   939→    /// Boost P/T of matching permanents.
   940→    Boost {
   941→        filter: String,
   942→        power: i32,
   943→        toughness: i32,
   944→    },
   945→    /// Grant a keyword to matching permanents.
   946→    GrantKeyword {
   947→        filter: String,
   948→        keyword: String,
   949→    },
   950→    /// Remove a keyword from matching permanents.
   951→    RemoveKeyword {
   952→        filter: String,
   953→        keyword: String,
   954→    },
   955→    /// Prevent matching permanents from attacking.
   956→    CantAttack {
   957→        filter: String,
   958→    },
   959→    /// Prevent matching permanents from blocking.
   960→    CantBlock {
   961→        filter: String,
   962→    },
   963→    /// Reduce cost of matching spells.
   964→    CostReduction {
   965→        filter: String,
   966→        amount: u32,
   967→    },
   968→    /// Matching permanents enter the battlefield tapped.
   969→    EntersTapped {
   970→        filter: String,
   971→    },
   972→    /// Other players can't gain life.
   973→    CantGainLife,
   974→    /// Other players can't draw extra cards.
   975→    CantDrawExtraCards,
   976→    /// Ward — when this becomes the target of a spell or ability an opponent
   977→    /// controls, counter it unless that player pays the specified cost.
   978→    Ward {
   979→        cost: String,
   980→    },
   981→    /// Enters tapped unless a condition is met (e.g. "you control a Plains or an Island").
   982→    EntersTappedUnless {
   983→        condition: String,
   984→    },
   985→    /// Custom continuous effect.
   986→    Custom(String),
   987→}
   988→
   989→// ---------------------------------------------------------------------------
   990→// AbilityStore — stores abilities by source
   991→// ---------------------------------------------------------------------------
   992→
   993→/// Stores all abilities for all objects in the game.
   994→#[derive(Clone, Debug, Default, Serialize, Deserialize)]
   995→pub struct AbilityStore {
   996→    /// All abilities, keyed by their unique AbilityId.
   997→    abilities: std::collections::HashMap<AbilityId, Ability>,
   998→    /// Index: source ObjectId → list of AbilityIds.
   999→    by_source: std::collections::HashMap<ObjectId, Vec<AbilityId>>,
  1000→}
  1001→
  1002→impl AbilityStore {
  1003→    pub fn new() -> Self {
  1004→        Self::default()
  1005→    }
  1006→
  1007→    /// Register an ability for a source object.
  1008→    pub fn add(&mut self, ability: Ability) {
  1009→        let id = ability.id;
  1010→        let source = ability.source_id;
  1011→        self.abilities.insert(id, ability);
  1012→        self.by_source.entry(source).or_default().push(id);
  1013→    }
  1014→
  1015→    /// Get an ability by its ID.
  1016→    pub fn get(&self, id: AbilityId) -> Option<&Ability> {
  1017→        self.abilities.get(&id)
  1018→    }
  1019→
  1020→    /// Get all abilities for a source object.
  1021→    pub fn for_source(&self, source_id: ObjectId) -> Vec<&Ability> {
  1022→        self.by_source
  1023→            .get(&source_id)
  1024→            .map(|ids| ids.iter().filter_map(|id| self.abilities.get(id)).collect())
  1025→            .unwrap_or_default()
  1026→    }
  1027→
  1028→    /// Get all triggered abilities that should fire for an event.
  1029→    pub fn triggered_by(&self, event: &GameEvent) -> Vec<&Ability> {
  1030→        self.abilities
  1031→            .values()
  1032→            .filter(|a| a.should_trigger(event))
  1033→            .collect()
  1034→    }
  1035→
  1036→    /// Get all mana abilities for a source.
  1037→    pub fn mana_abilities_for(&self, source_id: ObjectId) -> Vec<&Ability> {
  1038→        self.for_source(source_id)
  1039→            .into_iter()
  1040→            .filter(|a| a.is_mana_ability())
  1041→            .collect()
  1042→    }
  1043→
  1044→    /// Remove all abilities for a source (e.g. when permanent leaves battlefield).
  1045→    pub fn remove_source(&mut self, source_id: ObjectId) {
  1046→        if let Some(ids) = self.by_source.remove(&source_id) {
  1047→            for id in ids {
  1048→                self.abilities.remove(&id);
  1049→            }
  1050→        }
  1051→    }
  1052→
  1053→    /// Total number of registered abilities.
  1054→    pub fn len(&self) -> usize {
  1055→        self.abilities.len()
  1056→    }
  1057→
  1058→    pub fn is_empty(&self) -> bool {
  1059→        self.abilities.is_empty()
  1060→    }
  1061→}
  1062→
  1063→#[cfg(test)]
  1064→mod tests {
  1065→    use super::*;
  1066→    use crate::events::EventType;
  1067→    use crate::types::PlayerId;
  1068→
  1069→    #[test]
  1070→    fn activated_ability() {
  1071→        let source = ObjectId::new();
  1072→        let ability = Ability::activated(
  1073→            source,
  1074→            "{2}, {T}: Draw a card.",
  1075→            vec![Cost::Mana(Mana::generic(2)), Cost::TapSelf],
  1076→            vec![Effect::DrawCards { count: 1 }],
  1077→            TargetSpec::None,
  1078→        );
  1079→
  1080→        assert_eq!(ability.ability_type, AbilityType::ActivatedNonMana);
  1081→        assert_eq!(ability.costs.len(), 2);
  1082→        assert_eq!(ability.effects.len(), 1);
  1083→        assert!(ability.uses_stack());
  1084→    }
  1085→
  1086→    #[test]
  1087→    fn triggered_ability() {
  1088→        let source = ObjectId::new();
  1089→        let ability = Ability::triggered(
  1090→            source,
  1091→            "When this creature enters the battlefield, draw a card.",
  1092→            vec![EventType::EnteredTheBattlefield],
  1093→            vec![Effect::DrawCards { count: 1 }],
  1094→            TargetSpec::None,
  1095→        );
  1096→
  1097→        assert_eq!(ability.ability_type, AbilityType::TriggeredNonMana);
  1098→        assert!(ability.uses_stack());
  1099→
  1100→        // Check trigger matching
  1101→        let event = GameEvent::enters_battlefield(source, PlayerId::new());
  1102→        // The event type is EntersTheBattlefield, but our trigger watches EnteredTheBattlefield
  1103→        let post_event = GameEvent::new(EventType::EnteredTheBattlefield)
  1104→            .target(source);
  1105→        assert!(ability.should_trigger(&post_event));
  1106→        assert!(!ability.should_trigger(&event)); // pre-event, not what we trigger on
  1107→    }
  1108→
  1109→    #[test]
  1110→    fn mana_ability() {
  1111→        let source = ObjectId::new();
  1112→        let ability = Ability::mana_ability(
  1113→            source,
  1114→            "{T}: Add {G}.",
  1115→            Mana::green(1),
  1116→        );
  1117→
  1118→        assert_eq!(ability.ability_type, AbilityType::ActivatedMana);
  1119→        assert!(ability.is_mana_ability());
  1120→        assert!(!ability.uses_stack());
  1121→        assert_eq!(ability.mana_produced, Some(Mana::green(1)));
  1122→    }
  1123→
  1124→    #[test]
  1125→    fn static_ability_boost() {
  1126→        let source = ObjectId::new();
  1127→        let ability = Ability::static_ability(
  1128→            source,
  1129→            "Other creatures you control get +1/+1.",
  1130→            vec![StaticEffect::Boost {
  1131→                filter: "other creatures you control".to_string(),
  1132→                power: 1,
  1133→                toughness: 1,
  1134→            }],
  1135→        );
  1136→
  1137→        assert_eq!(ability.ability_type, AbilityType::Static);
  1138→        assert!(!ability.uses_stack());
  1139→        assert_eq!(ability.static_effects.len(), 1);
  1140→    }
  1141→
  1142→    #[test]
  1143→    fn spell_ability() {
  1144→        let source = ObjectId::new();
  1145→        let ability = Ability::spell(
  1146→            source,
  1147→            vec![Effect::DealDamage { amount: 3 }],
  1148→            TargetSpec::CreatureOrPlayer,
  1149→        );
  1150→
  1151→        assert_eq!(ability.ability_type, AbilityType::Spell);
  1152→        assert!(ability.uses_stack());
  1153→    }
  1154→
  1155→    #[test]
  1156→    fn ability_store() {
  1157→        let mut store = AbilityStore::new();
  1158→        let source = ObjectId::new();
  1159→
  1160→        let a1 = Ability::mana_ability(source, "{T}: Add {G}.", Mana::green(1));
  1161→        let a1_id = a1.id;
  1162→        let a2 = Ability::activated(
  1163→            source,
  1164→            "{1}{G}: +1/+1",
  1165→            vec![Cost::Mana(Mana { green: 1, generic: 1, ..Default::default() })],
  1166→            vec![Effect::BoostUntilEndOfTurn { power: 1, toughness: 1 }],
  1167→            TargetSpec::None,
  1168→        );
  1169→
  1170→        store.add(a1);
  1171→        store.add(a2);
  1172→
  1173→        assert_eq!(store.len(), 2);
  1174→        assert_eq!(store.for_source(source).len(), 2);
  1175→        assert_eq!(store.mana_abilities_for(source).len(), 1);
  1176→        assert!(store.get(a1_id).is_some());
  1177→
  1178→        store.remove_source(source);
  1179→        assert!(store.is_empty());
  1180→    }
  1181→
  1182→    #[test]
  1183→    fn optional_trigger() {
  1184→        let source = ObjectId::new();
  1185→        let ability = Ability::triggered(
  1186→            source,
  1187→            "When ~ enters, you may draw a card.",
  1188→            vec![EventType::EnteredTheBattlefield],
  1189→            vec![Effect::DrawCards { count: 1 }],
  1190→            TargetSpec::None,
  1191→        ).set_optional();
  1192→
  1193→        assert!(ability.optional_trigger);
  1194→    }
  1195→
  1196→    #[test]
  1197→    fn active_zones() {
  1198→        let source = ObjectId::new();
  1199→        let ability = Ability::activated(
  1200→            source,
  1201→            "Exile from graveyard: effect",
  1202→            vec![],
  1203→            vec![],
  1204→            TargetSpec::None,
  1205→        ).in_zones(vec![Zone::Graveyard]);
  1206→
  1207→        assert!(ability.can_activate_in_zone(Zone::Graveyard));
  1208→        assert!(!ability.can_activate_in_zone(Zone::Battlefield));
  1209→    }
  1210→
  1211→    // ── Tests for common builders ──────────────────────────────────────
  1212→
  1213→    #[test]
  1214→    fn etb_triggered() {
  1215→        let source = ObjectId::new();
  1216→        let ability = Ability::enters_battlefield_triggered(
  1217→            source,
  1218→            "When ~ enters, draw a card.",
  1219→            vec![Effect::draw_cards(1)],
  1220→            TargetSpec::None,
  1221→        );
  1222→
  1223→        assert_eq!(ability.ability_type, AbilityType::TriggeredNonMana);
  1224→        assert!(ability.trigger_events.contains(&EventType::EnteredTheBattlefield));
  1225→        assert_eq!(ability.effects.len(), 1);
  1226→    }
  1227→
  1228→    #[test]
  1229→    fn dies_triggered() {
  1230→        let source = ObjectId::new();
  1231→        let ability = Ability::dies_triggered(
  1232→            source,
  1233→            "When ~ dies, each opponent loses 1 life.",
  1234→            vec![Effect::damage_opponents(1)],
  1235→            TargetSpec::None,
  1236→        );
  1237→
  1238→        assert!(ability.trigger_events.contains(&EventType::Dies));
  1239→        // Dies triggers work from battlefield and graveyard
  1240→        assert!(ability.active_zones.contains(&Zone::Battlefield));
  1241→        assert!(ability.active_zones.contains(&Zone::Graveyard));
  1242→    }
  1243→
  1244→    #[test]
  1245→    fn attacks_triggered() {
  1246→        let source = ObjectId::new();
  1247→        let ability = Ability::attacks_triggered(
  1248→            source,
  1249→            "Whenever ~ attacks, draw a card.",
  1250→            vec![Effect::draw_cards(1)],
  1251→            TargetSpec::None,
  1252→        );
  1253→
  1254→        assert!(ability.trigger_events.contains(&EventType::AttackerDeclared));
  1255→    }
  1256→
  1257→    #[test]
  1258→    fn combat_damage_triggered() {
  1259→        let source = ObjectId::new();
  1260→        let ability = Ability::combat_damage_to_player_triggered(
  1261→            source,
  1262→            "Whenever ~ deals combat damage to a player, draw a card.",
  1263→            vec![Effect::draw_cards(1)],
  1264→            TargetSpec::None,
  1265→        );
  1266→
  1267→        assert!(ability.trigger_events.contains(&EventType::DamagedPlayer));
  1268→    }
  1269→
  1270→    #[test]
  1271→    fn upkeep_triggered() {
  1272→        let source = ObjectId::new();
  1273→        let ability = Ability::beginning_of_upkeep_triggered(
  1274→            source,
  1275→            "At the beginning of your upkeep, gain 1 life.",
  1276→            vec![Effect::gain_life(1)],
  1277→            TargetSpec::None,
  1278→        );
  1279→
  1280→        assert!(ability.trigger_events.contains(&EventType::UpkeepStep));
  1281→    }
  1282→
  1283→    #[test]
  1284→    fn end_step_triggered() {
  1285→        let source = ObjectId::new();
  1286→        let ability = Ability::beginning_of_end_step_triggered(
  1287→            source,
  1288→            "At the beginning of your end step, create a 1/1 token.",
  1289→            vec![Effect::create_token("Soldier", 1)],
  1290→            TargetSpec::None,
  1291→        );
  1292→
  1293→        assert!(ability.trigger_events.contains(&EventType::EndStep));
  1294→    }
  1295→
  1296→    #[test]
  1297→    fn effect_builders() {
  1298→        // Test various effect constructors
  1299→        match Effect::deal_damage(3) {
  1300→            Effect::DealDamage { amount } => assert_eq!(amount, 3),
  1301→            _ => panic!("wrong variant"),
  1302→        }
  1303→
  1304→        match Effect::draw_cards(2) {
  1305→            Effect::DrawCards { count } => assert_eq!(count, 2),
  1306→            _ => panic!("wrong variant"),
  1307→        }
  1308→
  1309→        match Effect::gain_life(5) {
  1310→            Effect::GainLife { amount } => assert_eq!(amount, 5),
  1311→            _ => panic!("wrong variant"),
  1312→        }
  1313→
  1314→        match Effect::boost_until_eot(2, 2) {
  1315→            Effect::BoostUntilEndOfTurn { power, toughness } => {
  1316→                assert_eq!(power, 2);
  1317→                assert_eq!(toughness, 2);
  1318→            }
  1319→            _ => panic!("wrong variant"),
  1320→        }
  1321→
  1322→        match Effect::create_token("Zombie", 3) {
  1323→            Effect::CreateToken { token_name, count } => {
  1324→                assert_eq!(token_name, "Zombie");
  1325→                assert_eq!(count, 3);
  1326→            }
  1327→            _ => panic!("wrong variant"),
  1328→        }
  1329→
  1330→        match Effect::add_p1p1_counters(2) {
  1331→            Effect::AddCounters { counter_type, count } => {
  1332→                assert_eq!(counter_type, "+1/+1");
  1333→                assert_eq!(count, 2);
  1334→            }
  1335→            _ => panic!("wrong variant"),
  1336→        }
  1337→    }
  1338→
  1339→    #[test]
  1340→    fn cost_builders() {
  1341→        match Cost::tap_self() {
  1342→            Cost::TapSelf => {}
  1343→            _ => panic!("wrong variant"),
  1344→        }
  1345→
  1346→        match Cost::sacrifice_self() {
  1347→            Cost::SacrificeSelf => {}
  1348→            _ => panic!("wrong variant"),
  1349→        }
  1350→
  1351→        match Cost::pay_life(3) {
  1352→            Cost::PayLife(n) => assert_eq!(n, 3),
  1353→            _ => panic!("wrong variant"),
  1354→        }
  1355→
  1356→        match Cost::discard(1) {
  1357→            Cost::Discard(n) => assert_eq!(n, 1),
  1358→            _ => panic!("wrong variant"),
  1359→        }
  1360→
  1361→        match Cost::sacrifice_other("a creature") {
  1362→            Cost::SacrificeOther(desc) => assert_eq!(desc, "a creature"),
  1363→            _ => panic!("wrong variant"),
  1364→        }
  1365→    }
  1366→
  1367→    #[test]
  1368→    fn static_effect_builders() {
  1369→        match StaticEffect::boost_controlled("creatures you control", 1, 1) {
  1370→            StaticEffect::Boost { filter, power, toughness } => {
  1371→                assert_eq!(filter, "creatures you control");
  1372→                assert_eq!(power, 1);
  1373→                assert_eq!(toughness, 1);
  1374→            }
  1375→            _ => panic!("wrong variant"),
  1376→        }
  1377→
  1378→        match StaticEffect::grant_keyword_controlled("creatures you control", "flying") {
  1379→            StaticEffect::GrantKeyword { filter, keyword } => {
  1380→                assert_eq!(filter, "creatures you control");
  1381→                assert_eq!(keyword, "flying");
  1382→            }
  1383→            _ => panic!("wrong variant"),
  1384→        }
  1385→
  1386→        match StaticEffect::cost_reduction("creature spells", 1) {
  1387→            StaticEffect::CostReduction { filter, amount } => {
  1388→                assert_eq!(filter, "creature spells");
  1389→                assert_eq!(amount, 1);
  1390→            }
  1391→            _ => panic!("wrong variant"),
  1392→        }
  1393→
  1394→        match StaticEffect::ward("{2}") {
  1395→            StaticEffect::Ward { cost } => {
  1396→                assert_eq!(cost, "{2}");
  1397→            }
  1398→            _ => panic!("wrong variant"),
  1399→        }
  1400→
  1401→        match StaticEffect::ward("Discard a card.") {
  1402→            StaticEffect::Ward { cost } => {
  1403→                assert_eq!(cost, "Discard a card.");
  1404→            }
  1405→            _ => panic!("wrong variant"),
  1406→        }
  1407→
  1408→        match StaticEffect::enters_tapped_unless("you control a Plains or an Island") {
  1409→            StaticEffect::EntersTappedUnless { condition } => {
  1410→                assert_eq!(condition, "you control a Plains or an Island");
  1411→            }
  1412→            _ => panic!("wrong variant"),
  1413→        }
  1414→    }
  1415→
  1416→    #[test]
  1417→    fn compose_realistic_card_lightning_bolt() {
  1418→        // Lightning Bolt: {R} instant, "Deal 3 damage to any target."
  1419→        let source = ObjectId::new();
  1420→        let ability = Ability::spell(
  1421→            source,
  1422→            vec![Effect::deal_damage(3)],
  1423→            TargetSpec::CreatureOrPlayer,
  1424→        );
  1425→        assert_eq!(ability.effects.len(), 1);
  1426→        assert!(ability.uses_stack());
  1427→    }
  1428→
  1429→    #[test]
  1430→    fn compose_realistic_card_llanowar_elves() {
  1431→        // Llanowar Elves: {G} creature, "{T}: Add {G}."
  1432→        let source = ObjectId::new();
  1433→        let ability = Ability::mana_ability(source, "{T}: Add {G}.", Mana::green(1));
  1434→        assert!(ability.is_mana_ability());
  1435→        assert_eq!(ability.costs.len(), 1);
  1436→        match &ability.costs[0] {
  1437→            Cost::TapSelf => {}
  1438→            _ => panic!("expected TapSelf cost"),
  1439→        }
  1440→    }
  1441→
  1442→    #[test]
  1443→    fn compose_realistic_card_mulldrifter() {
  1444→        // Mulldrifter: when enters, draw 2 cards
  1445→        let source = ObjectId::new();
  1446→        let ability = Ability::enters_battlefield_triggered(
  1447→            source,
  1448→            "When Mulldrifter enters the battlefield, draw two cards.",
  1449→            vec![Effect::draw_cards(2)],
  1450→            TargetSpec::None,
  1451→        );
  1452→        assert!(ability.should_trigger(&GameEvent::new(EventType::EnteredTheBattlefield).target(source)));
  1453→    }
  1454→
  1455→    #[test]
  1456→    fn compose_realistic_lord() {
  1457→        // Lord of Atlantis: Other Merfolk get +1/+1 and have islandwalk.
  1458→        let source = ObjectId::new();
  1459→        let ability = Ability::static_ability(
  1460→            source,
  1461→            "Other Merfolk you control get +1/+1 and have islandwalk.",
  1462→            vec![
  1463→                StaticEffect::boost_controlled("other Merfolk you control", 1, 1),
  1464→                StaticEffect::grant_keyword_controlled("other Merfolk you control", "islandwalk"),
  1465→            ],
  1466→        );
  1467→        assert_eq!(ability.static_effects.len(), 2);
  1468→    }
  1469→}
  1470→
```

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs"
}
```

## Assistant

**Result** (error):
```
File content (33090 tokens) exceeds maximum allowed tokens (25000). Please use offset and limit parameters to read specific portions of the file, or use the GrepTool to search for specific content.
```

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-cards/src/sets",
  "pattern": "Put two -1/-1 counters on each creature"
}
```

## Assistant

**Result** (error):
```
<tool_use_error>Sibling tool call errored</tool_use_error>
```

## Assistant

## Assistant

Let me search for the relevant sections in game.rs and the card in the ECL set file.

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-cards/src/sets",
  "pattern": "Put two -1/-1 counters on each creature"
}
```

## Assistant

**Result** (success):
```
mtg-rl/mtg-cards/src/sets/ecl.rs:1840:    // Sorcery {2}{B}{B}. Put two -1/-1 counters on each creature.
mtg-rl/mtg-cards/src/sets/ecl.rs:1843:        abilities: vec![Ability::spell(id, vec![Effect::Custom("Put two -1/-1 counters on each creature.".into())], TargetSpec::None)],
```

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-C": 5,
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "pattern": "AddCounters \\{|AddCounters\\b"
}
```

## Assistant

**Result** (success):
```
954-        true
955-    }
956-
957-    /// Execute a list of effects for a controller with given targets.
958-    pub fn execute_effects(&mut self, effects: &[Effect], controller: PlayerId, all_targets: &[ObjectId], source: Option<ObjectId>) {
959:        // For compound fight/bite spells (e.g. [AddCounters, Bite]), pre-fight/bite
960-        // effects should only apply to the first target (your creature), matching
961-        // Java's per-effect target assignment where AddCountersTargetEffect targets
962-        // target 0 while DamageWithPowerFromOneToAnotherTargetEffect uses both.
963-        let has_fight_or_bite = effects.iter().any(|e| matches!(e, Effect::Fight | Effect::Bite));
964-
--
1058-                        if let Some(player) = self.state.players.get_mut(&opp) {
1059-                            player.life -= *amount as i32;
1060-                        }
1061-                    }
1062-                }
1063:                Effect::AddCounters { counter_type, count } => {
1064-                    let ct = crate::counters::CounterType::from_name(counter_type);
1065-                    // If no targets, fall back to source (self-targeting counters)
1066-                    let effective_targets: Vec<ObjectId> = if targets.is_empty() {
1067-                        source.into_iter().collect()
1068-                    } else {
--
2620-        card.power = Some(3);
2621-        card.toughness = Some(7);
2622-        card.keywords = KeywordAbilities::empty();
2623-        game.state.battlefield.add(Permanent::new(card, p1));
2624-
2625:        // Execute AddCounters with no targets but with source — should add to self
2626-        game.execute_effects(
2627-            &[Effect::add_counters("-1/-1", 2)],
2628-            p1,
2629-            &[],
2630-            Some(source_id),
```

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-C": 10,
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "pattern": "DestroyAll|DealDamageAll"
}
```

## Assistant

**Result** (success):
```
1306-                        if let Some(&victim_id) = matching.first() {
1307-                            / Simplified: sacrifice the first matching permanent
1308-                            // (proper implementation would let opponent choose)
1309-                            if let Some(perm) = self.state.battlefield.remove(victim_id) {
1310-                                self.state.ability_store.remove_source(victim_id);
1311-                                self.move_card_to_graveyard_inner(victim_id, perm.owner());
1312-                            }
1313-                        }
1314-                    }
1315-                }
1316:                Effect::DestroyAll { filter } => {
1317-                    // Destroy all permanents matching filter
1318-                    let to_destroy: Vec<(ObjectId, PlayerId)> = self.state.battlefield.iter()
1319-                        .filter(|p| Self::matches_filter(p, filter) && !p.has_indestructible())
1320-                        .map(|p| (p.id(), p.owner()))
1321-                        .collect();
1322-                    for (id, owner) in to_destroy {
1323-                        if self.state.battlefield.remove(id).is_some() {
1324-                            self.state.ability_store.remove_source(id);
1325-                            self.move_card_to_graveyard_inner(id, owner);
1326-                        }
1327-                    }
1328-                }
1329:                Effect::DealDamageAll { amount, filter } => {
1330-                    // Deal damage to all creatures matching filter
1331-                    let matching: Vec<ObjectId> = self.state.battlefield.iter()
1332-                        .filter(|p| p.is_creature() && Self::matches_filter(p, filter))
1333-                        .map(|p| p.id())
1334-                        .collect();
1335-                    for id in matching {
1336-                        if let Some(perm) = self.state.battlefield.get_mut(id) {
1337-                            perm.apply_damage(*amount);
1338-                        }
1339-                    }
```

## Assistant

## Assistant

Let me see the full AddCounters match arm and the ECL card context.

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "limit": 30,
  "offset": 1063
}
```

## Assistant

**Result** (success):
```
  1063→                Effect::AddCounters { counter_type, count } => {
  1064→                    let ct = crate::counters::CounterType::from_name(counter_type);
  1065→                    // If no targets, fall back to source (self-targeting counters)
  1066→                    let effective_targets: Vec<ObjectId> = if targets.is_empty() {
  1067→                        source.into_iter().collect()
  1068→                    } else {
  1069→                        targets.to_vec()
  1070→                    };
  1071→                    for target_id in effective_targets {
  1072→                        if let Some(perm) = self.state.battlefield.get_mut(target_id) {
  1073→                            perm.add_counters(ct.clone(), *count);
  1074→                        }
  1075→                    }
  1076→                }
  1077→                Effect::BoostUntilEndOfTurn { power, toughness: _ } => {
  1078→                    // Simplified: directly modify counters (proper implementation
  1079→                    // would use continuous effects that expire at end of turn)
  1080→                    for &target_id in targets {
  1081→                        if let Some(perm) = self.state.battlefield.get_mut(target_id) {
  1082→                            if *power > 0 {
  1083→                                perm.add_counters(CounterType::P1P1, *power as u32);
  1084→                            }
  1085→                            // Note: This is a simplification; real boost until EOT
  1086→                            // uses continuous effects, not counters
  1087→                        }
  1088→                    }
  1089→                }
  1090→                Effect::TapTarget => {
  1091→                    for &target_id in targets {
  1092→                        if let Some(perm) = self.state.battlefield.get_mut(target_id) {
```

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-cards/src/sets/ecl.rs",
  "limit": 15,
  "offset": 1835
}
```

## Assistant

**Result** (success):
```
  1835→        ],
  1836→        ..Default::default() }
  1837→}
  1838→
  1839→fn darkness_descends(id: ObjectId, owner: PlayerId) -> CardData {
  1840→    // Sorcery {2}{B}{B}. Put two -1/-1 counters on each creature.
  1841→    CardData { id, owner, name: "Darkness Descends".into(), mana_cost: ManaCost::parse("{2}{B}{B}"),
  1842→        card_types: vec![CardType::Sorcery], rarity: Rarity::Rare,
  1843→        abilities: vec![Ability::spell(id, vec![Effect::Custom("Put two -1/-1 counters on each creature.".into())], TargetSpec::None)],
  1844→        ..Default::default() }
  1845→}
  1846→
  1847→fn dose_of_dawnglow(id: ObjectId, owner: PlayerId) -> CardData {
  1848→    // Instant {4}{B}. Return target creature from your graveyard to battlefield.
  1849→    CardData { id, owner, name: "Dose of Dawnglow".into(), mana_cost: ManaCost::parse("{4}{B}"),
```

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-C": 2,
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "pattern": "Effect::(Custom|BoostAllUntil|GrantKeywordAll)"
}
```

## Assistant

**Result** (success):
```
1428-                    }
1429-                }
1430:                Effect::BoostAllUntilEndOfTurn { filter, power, toughness: _ } => {
1431-                    // Give all matching creatures controlled by the effect's controller +N/+M until EOT
1432-                    let you_control = filter.to_lowercase().contains("you control");
--
1447-                    }
1448-                }
1449:                Effect::GrantKeywordAllUntilEndOfTurn { filter, keyword } => {
1450-                    // Grant keyword to all matching creatures controlled by the effect's controller until EOT
1451-                    if let Some(kw) = crate::constants::KeywordAbilities::keyword_from_name(keyword) {
```

## Assistant

## Assistant

Let me also check for any other cards across all sets that might use this same pattern.

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-i": true,
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-cards/src/sets",
  "pattern": "counters? on (each|all|every) creature"
}
```

## Assistant

**Result** (success):
```
mtg-rl/mtg-cards/src/sets/ecl.rs:1840:    // Sorcery {2}{B}{B}. Put two -1/-1 counters on each creature.
mtg-rl/mtg-cards/src/sets/ecl.rs:1843:        abilities: vec![Ability::spell(id, vec![Effect::Custom("Put two -1/-1 counters on each creature.".into())], TargetSpec::None)],
mtg-rl/mtg-cards/src/sets/tdm.rs:2025:    // Abzan: end step — +1/+1 counter on each creature you control.
mtg-rl/mtg-cards/src/sets/tdm.rs:2035:                "Abzan — At the beginning of your end step, put a +1/+1 counter on each creature you control.",
mtg-rl/mtg-cards/src/sets/tdm.rs:2037:                vec![Effect::Custom("Put +1/+1 counter on each creature you control.".into())],
mtg-rl/mtg-cards/src/sets/tdm.rs:3804:                "Whenever Felothar enters or attacks, you may sacrifice a nonland permanent. When you do, put a +1/+1 counter on each creature you control.",
mtg-rl/mtg-cards/src/sets/tdm.rs:3806:                vec![Effect::Custom("You may sacrifice a nonland permanent. When you do, put a +1/+1 counter on each creature you control.".into())],
```

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-i": true,
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-cards/src/sets",
  "pattern": "counters? on each"
}
```

## Assistant

**Result** (success):
```
mtg-rl/mtg-cards/src/sets/ecl.rs:1840:    // Sorcery {2}{B}{B}. Put two -1/-1 counters on each creature.
mtg-rl/mtg-cards/src/sets/ecl.rs:1843:        abilities: vec![Ability::spell(id, vec![Effect::Custom("Put two -1/-1 counters on each creature.".into())], TargetSpec::None)],
mtg-rl/mtg-cards/src/sets/ecl.rs:1909:            vec![Effect::Custom("Vivid — This spell costs {1} less to cast for each color among permanents you control. Tap up to two target creatures. Put a stun counter on each of them. Draw a card.".into())],
mtg-rl/mtg-cards/src/sets/tdm.rs:2025:    // Abzan: end step — +1/+1 counter on each creature you control.
mtg-rl/mtg-cards/src/sets/tdm.rs:2035:                "Abzan — At the beginning of your end step, put a +1/+1 counter on each creature you control.",
mtg-rl/mtg-cards/src/sets/tdm.rs:2037:                vec![Effect::Custom("Put +1/+1 counter on each creature you control.".into())],
mtg-rl/mtg-cards/src/sets/tdm.rs:3235:                "Renew -- {X}{B}{B}, Exile this card from your graveyard: Put a decayed counter on each of X target creatures. Activate only as a sorcery.",
mtg-rl/mtg-cards/src/sets/tdm.rs:3311:                "At the beginning of your upkeep, return up to two target creature cards with power 2 or less from your graveyard to the battlefield. Put a +1/+1 counter on each of those creatures.",
mtg-rl/mtg-cards/src/sets/tdm.rs:3804:                "Whenever Felothar enters or attacks, you may sacrifice a nonland permanent. When you do, put a +1/+1 counter on each creature you control.",
mtg-rl/mtg-cards/src/sets/tdm.rs:3806:                vec![Effect::Custom("You may sacrifice a nonland permanent. When you do, put a +1/+1 counter on each creature you control.".into())],
mtg-rl/mtg-cards/src/sets/fdn.rs:1990:                "When Felidar Savior enters, put a +1/+1 counter on each of up to two other target creatures you control.",
mtg-rl/mtg-cards/src/sets/fdn.rs:5483:                vec![Effect::Custom("Distribute three +1/+1 counters among one, two, or three target creatures, then double the number of +1/+1 counters on each of those creatures.".into())],
```

## Assistant

## Assistant

Let me check the TDM card that also uses counters on each creature.

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-cards/src/sets/tdm.rs",
  "limit": 25,
  "offset": 2020
}
```

## Assistant

**Result** (success):
```
  2020→        ..Default::default() }
  2021→}
  2022→
  2023→fn barrensteppe_siege(id: ObjectId, owner: PlayerId) -> CardData {
  2024→    // Enchantment for {2}{W}{B}. As ETB: choose Abzan or Mardu.
  2025→    // Abzan: end step — +1/+1 counter on each creature you control.
  2026→    // Mardu: end step — if creature died this turn, opponents sacrifice a creature.
  2027→    CardData { id, owner, name: "Barrensteppe Siege".into(), mana_cost: ManaCost::parse("{2}{W}{B}"),
  2028→        card_types: vec![CardType::Enchantment], rarity: Rarity::Rare,
  2029→        abilities: vec![
  2030→            Ability::enters_battlefield_triggered(id,
  2031→                "As this enchantment enters, choose Abzan or Mardu.",
  2032→                vec![Effect::Custom("Choose Abzan or Mardu.".into())],
  2033→                TargetSpec::None),
  2034→            Ability::triggered(id,
  2035→                "Abzan — At the beginning of your end step, put a +1/+1 counter on each creature you control.",
  2036→                vec![EventType::EndStep],
  2037→                vec![Effect::Custom("Put +1/+1 counter on each creature you control.".into())],
  2038→                TargetSpec::None),
  2039→            Ability::triggered(id,
  2040→                "Mardu — At the beginning of your end step, if a creature died under your control this turn, each opponent sacrifices a creature.",
  2041→                vec![EventType::EndStep],
  2042→                vec![Effect::Custom("Each opponent sacrifices a creature.".into())],
  2043→                TargetSpec::None),
  2044→        ],
```

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-cards/src/sets/tdm.rs",
  "limit": 20,
  "offset": 3795
}
```

## Assistant

**Result** (success):
```
  3795→    CardData { id, owner, name: "Felothar, Dawn of the Abzan".into(), mana_cost: ManaCost::parse("{W}{B}{G}"),
  3796→        card_types: vec![CardType::Creature],
  3797→        subtypes: vec![SubType::Human, SubType::Warrior],
  3798→        supertypes: vec![SuperType::Legendary],
  3799→        power: Some(3), toughness: Some(3),
  3800→        keywords: KeywordAbilities::TRAMPLE,
  3801→        rarity: Rarity::Rare,
  3802→        abilities: vec![
  3803→            Ability::triggered(id,
  3804→                "Whenever Felothar enters or attacks, you may sacrifice a nonland permanent. When you do, put a +1/+1 counter on each creature you control.",
  3805→                vec![EventType::EnteredTheBattlefield, EventType::AttackerDeclared],
  3806→                vec![Effect::Custom("You may sacrifice a nonland permanent. When you do, put a +1/+1 counter on each creature you control.".into())],
  3807→                TargetSpec::None).set_optional(),
  3808→        ],
  3809→        ..Default::default() }
  3810→}
  3811→
  3812→fn glacial_dragonhunt(id: ObjectId, owner: PlayerId) -> CardData {
  3813→    CardData { id, owner, name: "Glacial Dragonhunt".into(), mana_cost: ManaCost::parse("{U}{R}"),
  3814→        card_types: vec![CardType::Sorcery],
```

## Assistant

## Assistant

Let me check how `matches_filter` works to understand filter handling for the new effect.

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-A": 30,
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "pattern": "fn matches_filter"
}
```

## Assistant

**Result** (success):
```
1607:    fn matches_filter(perm: &Permanent, filter: &str) -> bool {
1608-        let f = filter.to_lowercase();
1609-        // "all" or empty matches everything
1610-        if f.is_empty() || f == "all" {
1611-            return true;
1612-        }
1613-        // Check creature types
1614-        for st in &perm.card.subtypes {
1615-            if f.contains(&st.to_string().to_lowercase()) {
1616-                return true;
1617-            }
1618-        }
1619-        // Check card types
1620-        for ct in &perm.card.card_types {
1621-            let ct_name = format!("{:?}", ct).to_lowercase();
1622-            if f.contains(&ct_name) {
1623-                return true;
1624-            }
1625-        }
1626-        // "nonland" filter
1627-        if f.contains("nonland") && !perm.card.card_types.contains(&crate::constants::CardType::Land) {
1628-            return true;
1629-        }
1630-        false
1631-    }
1632-
1633-    /// Check if a CardData matches a simple filter string.
1634-    fn card_matches_filter(card: &CardData, filter: &str) -> bool {
1635-        let f = filter.to_lowercase();
1636-        if f.is_empty() || f == "all" {
1637-            return true;
```

## Assistant

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "limit": 30,
  "offset": 1430
}
```

## Assistant

**Result** (success):
```
  1430→                Effect::BoostAllUntilEndOfTurn { filter, power, toughness: _ } => {
  1431→                    // Give all matching creatures controlled by the effect's controller +N/+M until EOT
  1432→                    let you_control = filter.to_lowercase().contains("you control");
  1433→                    let matching: Vec<ObjectId> = self.state.battlefield.iter()
  1434→                        .filter(|p| p.is_creature()
  1435→                            && (!you_control || p.controller == controller)
  1436→                            && Self::matches_filter(p, filter))
  1437→                        .map(|p| p.id())
  1438→                        .collect();
  1439→                    for id in matching {
  1440→                        if let Some(perm) = self.state.battlefield.get_mut(id) {
  1441→                            if *power > 0 {
  1442→                                perm.add_counters(CounterType::P1P1, *power as u32);
  1443→                            } else if *power < 0 {
  1444→                                perm.add_counters(CounterType::M1M1, (-*power) as u32);
  1445→                            }
  1446→                        }
  1447→                    }
  1448→                }
  1449→                Effect::GrantKeywordAllUntilEndOfTurn { filter, keyword } => {
  1450→                    // Grant keyword to all matching creatures controlled by the effect's controller until EOT
  1451→                    if let Some(kw) = crate::constants::KeywordAbilities::keyword_from_name(keyword) {
  1452→                        let you_control = filter.to_lowercase().contains("you control");
  1453→                        let matching: Vec<ObjectId> = self.state.battlefield.iter()
  1454→                            .filter(|p| p.is_creature()
  1455→                                && (!you_control || p.controller == controller)
  1456→                                && Self::matches_filter(p, filter))
  1457→                            .map(|p| p.id())
  1458→                            .collect();
  1459→                        for id in matching {
```

## Assistant

Good. Now I have all the context I need. Let me also find where in `execute_effects` I should insert the new match arm — right after `AddCounters` seems logical. Let me also look at the test section of game.rs for the existing AddCounters test.

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "limit": 50,
  "offset": 2610
}
```

## Assistant

**Result** (success):
```
  2610→            vec![
  2611→                (p1, Box::new(AlwaysPassPlayer)),
  2612→                (p2, Box::new(AlwaysPassPlayer)),
  2613→            ],
  2614→        );
  2615→
  2616→        // Add a creature to the battlefield
  2617→        let source_id = ObjectId::new();
  2618→        let mut card = CardData::new(source_id, p1, "Blight Creature");
  2619→        card.card_types = vec![CardType::Creature];
  2620→        card.power = Some(3);
  2621→        card.toughness = Some(7);
  2622→        card.keywords = KeywordAbilities::empty();
  2623→        game.state.battlefield.add(Permanent::new(card, p1));
  2624→
  2625→        // Execute AddCounters with no targets but with source — should add to self
  2626→        game.execute_effects(
  2627→            &[Effect::add_counters("-1/-1", 2)],
  2628→            p1,
  2629→            &[],
  2630→            Some(source_id),
  2631→        );
  2632→
  2633→        let perm = game.state.battlefield.get(source_id).unwrap();
  2634→        assert_eq!(perm.counters.get(&CounterType::M1M1), 2);
  2635→
  2636→        // Execute RemoveCounters with no targets but with source — should remove from self
  2637→        game.execute_effects(
  2638→            &[Effect::RemoveCounters { counter_type: "-1/-1".into(), count: 1 }],
  2639→            p1,
  2640→            &[],
  2641→            Some(source_id),
  2642→        );
  2643→
  2644→        let perm = game.state.battlefield.get(source_id).unwrap();
  2645→        assert_eq!(perm.counters.get(&CounterType::M1M1), 1);
  2646→    }
  2647→
  2648→    /// A decision maker that actually discards when asked.
  2649→    struct DiscardingPlayer;
  2650→
  2651→    impl PlayerDecisionMaker for DiscardingPlayer {
  2652→        fn priority(&mut self, _game: &GameView<'_>, _legal: &[PlayerAction]) -> PlayerAction {
  2653→            PlayerAction::Pass
  2654→        }
  2655→        fn choose_targets(&mut self, _: &GameView<'_>, _: Outcome, _: &TargetRequirement) -> Vec<ObjectId> { vec![] }
  2656→        fn choose_use(&mut self, _: &GameView<'_>, _: Outcome, _: &str) -> bool { false }
  2657→        fn choose_mode(&mut self, _: &GameView<'_>, _: &[NamedChoice]) -> usize { 0 }
  2658→        fn select_attackers(&mut self, _: &GameView<'_>, _: &[ObjectId], _: &[ObjectId]) -> Vec<(ObjectId, ObjectId)> { vec![] }
  2659→        fn select_blockers(&mut self, _: &GameView<'_>, _: &[AttackerInfo]) -> Vec<(ObjectId, ObjectId)> { vec![] }
```

## Assistant

Now I have the full picture. Let me find the end of the `execute_effects` match block to know exactly where to insert the new arm. Let me look for the `Custom` catch-all:

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-A": 5,
  "-B": 2,
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "pattern": "Effect::Custom\\("
}
```

## Assistant

**Result** (success):
```
No matches found
```

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-A": 5,
  "-B": 2,
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "pattern": "Effect::Custom"
}
```

## Assistant

**Result** (success):
```
No matches found
```

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-A": 5,
  "-B": 2,
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "pattern": "Custom\\("
}
```

## Assistant

**Result** (success):
```
430-                    perm.removed_keywords = crate::constants::KeywordAbilities::empty();
431-                    // Remove "can't block" sentinel counters
432:                    perm.counters.remove_all(&crate::counters::CounterType::Custom("cant_block".into()));
433-                }
434-                // Empty mana pools
435-                for player in self.state.players.values_mut() {
436-                    player.mana_pool.clear();
437-                }
--
1287-                        if let Some(perm) = self.state.battlefield.get_mut(target_id) {
1288-                            // Use a sentinel counter to indicate can't block
1289:                            perm.add_counters(crate::counters::CounterType::Custom("cant_block".into()), 1);
1290-                        }
1291-                    }
1292-                }
1293-                Effect::Sacrifice { filter } => {
1294-                    // Each opponent sacrifices a permanent matching filter.
```

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-A": 5,
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "pattern": "^\\s+_\\s+=>\\s*\\{"
}
```

## Assistant

**Result** (success):
```
439:            _ => {
440-                / Other steps: empty mana pool at step transition (simplified)
441-                // In full rules, mana empties at end of each step/phase.
442-            }
443-        }
444-    }
--
523:                _ => {
524-                    // Other actions (special actions)
525-                    tracker.reset();
526-                    tracker.current = priority_player;
527-                }
528-            }
--
948:                _ => {
949-                    // Other costs (exile from hand, remove counters, sacrifice other, etc.)
950-                    // will be implemented as cards need them
951-                }
952-            }
953-        }
--
1112:                                _ => {} // Countered abilities just vanish
1113-                            }
1114-                        }
1115-                    }
1116-                }
1117-                Effect::AddMana { mana } => {
--
1510:                _ => {
1511-                    // Remaining effects not yet implemented (gain control, protection, etc.)
1512-                }
1513-            }
1514-        }
1515-    }
--
1682:            _ => {
1683-                let legal = self.legal_targets_for_spec(spec, controller);
1684-                if legal.is_empty() {
1685-                    return vec![];
1686-                }
1687-                let requirement = crate::decision::TargetRequirement {
```

## Assistant

Good. The catch-all `_ =>` at line 1510 is the end of the `execute_effects` match. I'll add the new match arm before that. Now let me implement everything.

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "limit": 20,
  "offset": 1500
}
```

## Assistant

**Result** (success):
```
  1500→                    if let (Some(bid), Some(tid)) = (biter_id, target_id) {
  1501→                        if bid != tid {
  1502→                            let biter_power = self.state.battlefield.get(bid)
  1503→                                .map(|p| p.power().max(0) as u32).unwrap_or(0);
  1504→                            if let Some(target_perm) = self.state.battlefield.get_mut(tid) {
  1505→                                target_perm.apply_damage(biter_power);
  1506→                            }
  1507→                        }
  1508→                    }
  1509→                }
  1510→                _ => {
  1511→                    // Remaining effects not yet implemented (gain control, protection, etc.)
  1512→                }
  1513→            }
  1514→        }
  1515→    }
  1516→
  1517→    /// Move a card to its owner's graveyard (internal version that doesn't need owner lookup).
  1518→    fn move_card_to_graveyard_inner(&mut self, card_id: ObjectId, owner: PlayerId) {
  1519→        if let Some(player) = self.state.players.get_mut(&owner) {
```

## Assistant

Now I have everything I need. Let me implement:

1. Add `AddCountersAll` variant to Effect enum in abilities.rs
2. Add `add_counters_all()` constructor
3. Add match arm in game.rs `execute_effects`
4. Add test in game.rs
5. Fix cards in ecl.rs and tdm.rs

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/abilities.rs",
  "new_string": "    // -- Counters --\n    /// Put counters on target.\n    AddCounters { counter_type: String, count: u32 },\n    /// Put counters on all permanents matching filter.\n    AddCountersAll { counter_type: String, count: u32, filter: String },\n    /// Remove counters from target.\n    RemoveCounters { counter_type: String, count: u32 },",
  "old_string": "    // -- Counters --\n    /// Put counters on target.\n    AddCounters { counter_type: String, count: u32 },\n    /// Remove counters from target.\n    RemoveCounters { counter_type: String, count: u32 },",
  "replace_all": false
}
```

## Assistant

**Result** (success):
```
The file /mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/abilities.rs has been updated successfully.
```

## Assistant

Now add the constructor:

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/abilities.rs",
  "new_string": "    /// \"Add counters of specified type.\"\n    pub fn add_counters(counter_type: &str, count: u32) -> Self {\n        Effect::AddCounters {\n            counter_type: counter_type.to_string(),\n            count,\n        }\n    }\n\n    /// \"Put N counters on each permanent matching filter.\"\n    pub fn add_counters_all(counter_type: &str, count: u32, filter: &str) -> Self {\n        Effect::AddCountersAll {\n            counter_type: counter_type.to_string(),\n            count,\n            filter: filter.to_string(),\n        }\n    }",
  "old_string": "    /// \"Add counters of specified type.\"\n    pub fn add_counters(counter_type: &str, count: u32) -> Self {\n        Effect::AddCounters {\n            counter_type: counter_type.to_string(),\n            count,\n        }\n    }",
  "replace_all": false
}
```

## Assistant

**Result** (success):
```
The file /mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/abilities.rs has been updated successfully.
```

## Assistant

Now add the match arm in game.rs, right before the catch-all:

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "new_string": "                Effect::AddCountersAll { counter_type, count, filter } => {\n                    let ct = crate::counters::CounterType::from_name(counter_type);\n                    let you_control = filter.to_lowercase().contains(\"you control\");\n                    let matching: Vec<ObjectId> = self.state.battlefield.iter()\n                        .filter(|p| p.is_creature()\n                            && (!you_control || p.controller == controller)\n                            && Self::matches_filter(p, filter))\n                        .map(|p| p.id())\n                        .collect();\n                    for id in matching {\n                        if let Some(perm) = self.state.battlefield.get_mut(id) {\n                            perm.add_counters(ct.clone(), *count);\n                        }\n                    }\n                }\n                _ => {\n                    // Remaining effects not yet implemented (gain control, protection, etc.)\n                }\n            }\n        }\n    }\n\n    /// Move a card to its owner's graveyard (internal version that doesn't need owner lookup).",
  "old_string": "                _ => {\n                    // Remaining effects not yet implemented (gain control, protection, etc.)\n                }\n            }\n        }\n    }\n\n    /// Move a card to its owner's graveyard (internal version that doesn't need owner lookup).",
  "replace_all": false
}
```

## Assistant

**Result** (success):
```
The file /mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs has been updated successfully.
```

## Assistant

Now add the test. Let me find the end of the existing test section:

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "pattern": "fn (add_counters_self|discard_opponents|boost_all_and|fight_and|compound_bite)"
}
```

## Assistant

**Result** (success):
```
2611:    fn add_counters_self_when_no_targets() {
2690:    fn discard_opponents_effect() {
2734:    fn boost_all_and_grant_keyword_all_until_eot() {
2790:    fn fight_and_bite_effects() {
2899:    fn compound_bite_counters_only_on_your_creature() {
```

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-A": 2,
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "pattern": "fn compound_bite"
}
```

## Assistant

**Result** (success):
```
2899:    fn compound_bite_counters_only_on_your_creature() {
2900-        / Matches Java's Knockout Maneuver / Felling Blow pattern:
2901-        // AddCountersTargetEffect targets only target 0 (your creature),
```

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-n": true,
  "offset": 2940,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "pattern": "^    \\}$"
}
```

## Assistant

**Result** (success):
```
No matches found

[Showing results with pagination = limit: undefined, offset: 2940]
```

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "limit": 50,
  "offset": 2940
}
```

## Assistant

**Result** (success):
```
  2940→        // My creature should have the +1/+1 counter (3+1=4 power, 3+1=4 toughness)
  2941→        let my = game.state.battlefield.get(my_id).unwrap();
  2942→        assert_eq!(my.power(), 4, "My creature should have +1/+1 counter (4 power)");
  2943→        assert_eq!(my.toughness(), 4, "My creature should have +1/+1 counter (4 toughness)");
  2944→
  2945→        // Opponent's creature should NOT have any counters
  2946→        let opp = game.state.battlefield.get(opp_id).unwrap();
  2947→        assert_eq!(opp.power(), 4, "Opponent creature should not have counters");
  2948→
  2949→        // Bite: my creature (now 4 power) deals 4 damage to opponent creature (4 toughness)
  2950→        assert_eq!(opp.remaining_toughness(), 0, "Opponent took 4 damage from bite");
  2951→        // My creature should have no damage (bite is one-way)
  2952→        assert_eq!(my.remaining_toughness(), 4, "My creature took no damage from bite");
  2953→    }
  2954→}
  2955→
```

## Assistant

Now I'll add the test right before the closing `}` of the test module:

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "new_string": "        // My creature should have no damage (bite is one-way)\n        assert_eq!(my.remaining_toughness(), 4, \"My creature took no damage from bite\");\n    }\n\n    #[test]\n    fn add_counters_all_effect() {\n        let (p1, p2) = (PlayerId::new(), PlayerId::new());\n        let mut game = Game::new(\n            vec![p1, p2],\n            std::collections::HashMap::new(),\n            vec![\n                (p1, Box::new(AlwaysPassPlayer)),\n                (p2, Box::new(AlwaysPassPlayer)),\n            ],\n        );\n\n        // Add creatures for both players\n        let c1 = ObjectId::new();\n        let mut card1 = CardData::new(c1, p1, \"My Creature\");\n        card1.card_types = vec![CardType::Creature];\n        card1.power = Some(3);\n        card1.toughness = Some(3);\n        game.state.battlefield.add(Permanent::new(card1, p1));\n\n        let c2 = ObjectId::new();\n        let mut card2 = CardData::new(c2, p2, \"Opp Creature\");\n        card2.card_types = vec![CardType::Creature];\n        card2.power = Some(2);\n        card2.toughness = Some(4);\n        game.state.battlefield.add(Permanent::new(card2, p2));\n\n        // AddCountersAll on \"creatures\" (no \"you control\") — hits both\n        game.execute_effects(\n            &[Effect::add_counters_all(\"-1/-1\", 2, \"creatures\")],\n            p1,\n            &[],\n            None,\n        );\n\n        let p1c = game.state.battlefield.get(c1).unwrap();\n        assert_eq!(p1c.counters.get(&CounterType::M1M1), 2);\n        assert_eq!(p1c.power(), 1); // 3 - 2\n\n        let p2c = game.state.battlefield.get(c2).unwrap();\n        assert_eq!(p2c.counters.get(&CounterType::M1M1), 2);\n        assert_eq!(p2c.power(), 0); // 2 - 2\n\n        // AddCountersAll on \"creatures you control\" — hits only controller's\n        game.execute_effects(\n            &[Effect::add_counters_all(\"+1/+1\", 1, \"creatures you control\")],\n            p1,\n            &[],\n            None,\n        );\n\n        let p1c = game.state.battlefield.get(c1).unwrap();\n        assert_eq!(p1c.counters.get(&CounterType::P1P1), 1);\n\n        let p2c = game.state.battlefield.get(c2).unwrap();\n        assert_eq!(p2c.counters.get(&CounterType::P1P1), 0); // unchanged\n    }\n}",
  "old_string": "        // My creature should have no damage (bite is one-way)\n        assert_eq!(my.remaining_toughness(), 4, \"My creature took no damage from bite\");\n    }\n}",
  "replace_all": false
}
```

## Assistant

**Result** (success):
```
The file /mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs has been updated successfully.
```

## Assistant

Now fix the cards. First, Darkness Descends in ecl.rs:

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-cards/src/sets/ecl.rs",
  "new_string": "        abilities: vec![Ability::spell(id, vec![Effect::add_counters_all(\"-1/-1\", 2, \"creatures\")], TargetSpec::None)],",
  "old_string": "        abilities: vec![Ability::spell(id, vec![Effect::Custom(\"Put two -1/-1 counters on each creature.\".into())], TargetSpec::None)],",
  "replace_all": false
}
```

## Assistant

**Result** (success):
```
The file /mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-cards/src/sets/ecl.rs has been updated successfully.
```

## Assistant

Now fix Barrensteppe Siege in tdm.rs:

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-cards/src/sets/tdm.rs",
  "new_string": "                vec![Effect::add_counters_all(\"+1/+1\", 1, \"creatures you control\")],",
  "old_string": "                vec![Effect::Custom(\"Put +1/+1 counter on each creature you control.\".into())],",
  "replace_all": false
}
```

## Assistant

**Result** (success):
```
The file /mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-cards/src/sets/tdm.rs has been updated successfully.
```

## Assistant

Now let's build and test:

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "cd /mnt/dest-btrfs/home/jc/src/mage/mtg-rl && cargo check -p mtg-engine 2>&1",
  "description": "Check mtg-engine compiles",
  "timeout": 120000
}
```

## Assistant

**Result** (success):
```
    Checking mtg-engine v0.1.0 (/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.68s
```

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "cargo check -p mtg-cards 2>&1",
  "description": "Check mtg-cards compiles",
  "timeout": 120000
}
```

## Assistant

**Result** (success):
```
    Checking mtg-cards v0.1.0 (/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-cards)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.79s
```

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "cargo test --lib 2>&1",
  "description": "Run all lib tests",
  "timeout": 300000
}
```

## Assistant

**Result** (error):
```
Exit code 101
   Compiling mtg-engine v0.1.0 (/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine)
error[E0599]: no function or associated item named `new` found for struct `game::Game` in the current scope
    --> mtg-engine/src/game.rs:2958:30
     |
76   | pub struct Game {
     | --------------- function or associated item `new` not found for this struct
...
2958 |         let mut game = Game::new(
     |                              ^^^ function or associated item not found in `game::Game`
     |
note: if you're trying to build a new `game::Game`, consider using `game::Game::new_two_player` which returns `game::Game`
    --> mtg-engine/src/game.rs:89:5
     |
89   | /     pub fn new_two_player(
90   | |         config: GameConfig,
91   | |         mut decision_makers: Vec<(PlayerId, Box<dyn PlayerDecisionMaker>)>,
92   | |     ) -> Self {
     | |_____________^
     = help: items from traits can only be used if the trait is implemented and in scope
     = note: the following trait defines an item `new`, perhaps you need to implement it:
             candidate #1: `UniformSampler`

For more information about this error, try `rustc --explain E0599`.
error: could not compile `mtg-engine` (lib test) due to 1 previous error
warning: build failed, waiting for other jobs to finish...
```

## Assistant

The test used the wrong constructor. Let me check how other tests in that file create a Game:

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "limit": 20,
  "offset": 2596
}
```

## Assistant

**Result** (success):
```
  2596→        assert!(perm.tapped);
  2597→
  2598→        // Can't pay tap again (already tapped)
  2599→        assert!(!game.pay_costs(p1, source_id, &[Cost::TapSelf]));
  2600→
  2601→        // Pay sacrifice self cost
  2602→        assert!(game.pay_costs(p1, source_id, &[Cost::SacrificeSelf]));
  2603→        assert!(!game.state.battlefield.contains(source_id));
  2604→
  2605→        // The card should be in the graveyard
  2606→        let player = game.state.players.get(&p1).unwrap();
  2607→        assert!(player.graveyard.contains(source_id));
  2608→    }
  2609→
  2610→    #[test]
  2611→    fn add_counters_self_when_no_targets() {
  2612→        let p1 = PlayerId::new();
  2613→        let p2 = PlayerId::new();
  2614→
  2615→        let config = GameConfig {
```

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "limit": 30,
  "offset": 2615
}
```

## Assistant

**Result** (success):
```
  2615→        let config = GameConfig {
  2616→            players: vec![
  2617→                PlayerConfig { name: "Alice".to_string(), deck: make_deck(p1) },
  2618→                PlayerConfig { name: "Bob".to_string(), deck: make_deck(p2) },
  2619→            ],
  2620→            starting_life: 20,
  2621→        };
  2622→
  2623→        let mut game = Game::new_two_player(
  2624→            config,
  2625→            vec![
  2626→                (p1, Box::new(AlwaysPassPlayer)),
  2627→                (p2, Box::new(AlwaysPassPlayer)),
  2628→            ],
  2629→        );
  2630→
  2631→        // Add a creature to the battlefield
  2632→        let source_id = ObjectId::new();
  2633→        let mut card = CardData::new(source_id, p1, "Blight Creature");
  2634→        card.card_types = vec![CardType::Creature];
  2635→        card.power = Some(3);
  2636→        card.toughness = Some(7);
  2637→        card.keywords = KeywordAbilities::empty();
  2638→        game.state.battlefield.add(Permanent::new(card, p1));
  2639→
  2640→        // Execute AddCounters with no targets but with source — should add to self
  2641→        game.execute_effects(
  2642→            &[Effect::add_counters("-1/-1", 2)],
  2643→            p1,
  2644→            &[],
```

## Assistant

I see — it uses `Game::new_two_player` with `GameConfig`. Let me also check what `make_deck` does:

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-A": 10,
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "pattern": "fn make_deck"
}
```

## Assistant

**Result** (success):
```
1943:    fn make_deck(owner: PlayerId) -> Vec<CardData> {
1944-        let mut deck = Vec::new();
1945-        // 20 lands
1946-        for _ in 0..20 {
1947-            deck.push(make_basic_land("Forest", owner));
1948-        }
1949-        // 20 creatures
1950-        for _ in 0..20 {
1951-            deck.push(make_creature("Grizzly Bears", owner, 2, 2));
1952-        }
1953-        deck
```

## Assistant

Now let me fix my test to use the correct constructor pattern:

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "new_string": "    #[test]\n    fn add_counters_all_effect() {\n        let (p1, p2) = (PlayerId::new(), PlayerId::new());\n        let config = GameConfig {\n            players: vec![\n                PlayerConfig { name: \"Alice\".to_string(), deck: make_deck(p1) },\n                PlayerConfig { name: \"Bob\".to_string(), deck: make_deck(p2) },\n            ],\n            starting_life: 20,\n        };\n        let mut game = Game::new_two_player(\n            config,\n            vec![\n                (p1, Box::new(AlwaysPassPlayer)),\n                (p2, Box::new(AlwaysPassPlayer)),\n            ],\n        );",
  "old_string": "    #[test]\n    fn add_counters_all_effect() {\n        let (p1, p2) = (PlayerId::new(), PlayerId::new());\n        let mut game = Game::new(\n            vec![p1, p2],\n            std::collections::HashMap::new(),\n            vec![\n                (p1, Box::new(AlwaysPassPlayer)),\n                (p2, Box::new(AlwaysPassPlayer)),\n            ],\n        );",
  "replace_all": false
}
```

## Assistant

**Result** (success):
```
The file /mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs has been updated successfully.
```

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "cargo test --lib 2>&1",
  "description": "Run all lib tests",
  "timeout": 300000
}
```

## Assistant

**Result** (success):
```
   Compiling mtg-engine v0.1.0 (/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine)
   Compiling mtg-cards v0.1.0 (/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-cards)
   Compiling mtg-ai v0.1.0 (/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-ai)
warning: fields `action` and `depth` are never read
  --> mtg-ai/src/minimax_player.rs:62:5
   |
60 | struct SearchNode {
   |        ---------- fields in this struct
61 |     /// The action that led to this node (None for root).
62 |     action: Option<PlayerAction>,
   |     ^^^^^^
...
70 |     depth: u32,
   |     ^^^^^
   |
   = note: `SearchNode` has derived impls for the traits `Debug` and `Clone`, but these are intentionally ignored during dead code analysis
   = note: `#[warn(dead_code)]` on by default

warning: associated functions `new_root` and `new_child` are never used
  --> mtg-ai/src/minimax_player.rs:74:8
   |
73 | impl SearchNode {
   | --------------- associated functions in this implementation
74 |     fn new_root(is_maximizing: bool) -> Self {
   |        ^^^^^^^^
...
84 |     fn new_child(action: PlayerAction, depth: u32, is_maximizing: bool) -> Self {
   |        ^^^^^^^^^

warning: `mtg-ai` (lib) generated 2 warnings
   Compiling mtg-tests v0.1.0 (/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-tests)
   Compiling mtg-python v0.1.0 (/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-python)
warning: `mtg-ai` (lib test) generated 1 warning (1 duplicate)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 3.56s
     Running unittests src/lib.rs (target/debug/deps/mtg_ai-a3cefc6981511e97)

running 52 tests
test action_space::tests::phase1_activate_ability_roundtrip ... ok
test action_space::tests::action_mask_pass_only ... ok
test action_space::tests::action_mask_with_castable_spell ... ok
test action_space::tests::phase1_cast_spell_roundtrip ... ok
test action_space::tests::phase1_all_indices_are_unique ... ok
test action_space::tests::phase1_out_of_range_returns_none ... ok
test action_space::tests::phase1_pass_roundtrip ... ok
test action_space::tests::phase1_play_land_roundtrip ... ok
test action_space::tests::phase2_roundtrip ... ok
test action_space::tests::reward_intermediate_negative_when_losing_advantage ... ok
test action_space::tests::reward_intermediate_positive_when_gaining_advantage ... ok
test action_space::tests::reward_loss_is_negative ... ok
test action_space::tests::reward_win_is_positive ... ok
test evaluator::tests::creature_permanent_scores_positively ... ok
test evaluator::tests::equal_board_state_scores_zero ... ok
test evaluator::tests::flying_creature_scores_higher ... ok
test evaluator::tests::keyword_scores_are_reasonable ... ok
test evaluator::tests::life_score_above_20 ... ok
test evaluator::tests::life_score_lookup_table ... ok
test evaluator::tests::loss_state_gives_min_score ... ok
test evaluator::tests::more_life_is_better ... ok
test evaluator::tests::win_state_gives_max_score ... ok
test gym::tests::env_action_mask_has_correct_size ... ok
test gym::tests::env_reset_returns_correct_size_observation ... ok
test gym::tests::env_space_sizes ... ok
test gym::tests::env_step_returns_valid_result ... ok
test heuristic_player::tests::heuristic_attacks_with_all ... ok
test heuristic_player::tests::heuristic_choose_use_says_yes_except_ai_dont_use ... ok
test gym::tests::env_truncates_at_max_turns ... ok
test heuristic_player::tests::heuristic_damage_assignment_correct_total ... ok
test heuristic_player::tests::heuristic_mulligan_keeps_small_hands ... ok
test heuristic_player::tests::heuristic_mulligan_mulligans_7_card_hand ... ok
test heuristic_player::tests::heuristic_passes_when_only_option ... ok
test heuristic_player::tests::heuristic_pile_prefers_larger_for_good ... ok
test heuristic_player::tests::heuristic_prefers_land_over_pass ... ok
test heuristic_player::tests::heuristic_prefers_land_over_spell ... ok
test heuristic_player::tests::heuristic_prefers_spell_over_pass ... ok
test minimax_player::tests::minimax_ab_alpha_beta_pruning ... ok
test minimax_player::tests::minimax_free_spell_preferred_over_regular ... ok
test minimax_player::tests::minimax_passes_when_only_option ... ok
test minimax_player::tests::minimax_prefers_spell_over_pass ... ok
test minimax_player::tests::minimax_prefers_land_over_spell ... ok
test minimax_player::tests::minimax_search_stats_tracked ... ok
test observation::tests::life_normalized_correctly ... ok
test observation::tests::observation_size_constant_matches ... ok
test observation::tests::observation_has_correct_size ... ok
test observation::tests::permanents_are_padded ... ok
test observation::tests::phase_one_hot_encoding ... ok
test random_player::tests::random_player_chooses_from_legal_actions ... ok
test random_player::tests::random_player_passes_when_only_option ... ok
test random_player::tests::random_player_damage_assignment_sums_correctly ... ok
test random_player::tests::random_player_choose_use_returns_bool ... ok

test result: ok. 52 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/lib.rs (target/debug/deps/mtg_cards-c237fd9c64d2d797)

running 18 tests
test keywords::behold::tests::behold_elf_cost ... ok
test keywords::behold::tests::behold_goblin_cost ... ok
test keywords::blight::tests::blight_1_cost ... ok
test keywords::behold::tests::behold_rules_text_format ... ok
test keywords::blight::tests::blight_2_cost ... ok
test keywords::blight::tests::blight_rules_text_plural ... ok
test keywords::blight::tests::blight_rules_text_singular ... ok
test keywords::mobilize::tests::mobilize_1_creates_triggered_ability ... ok
test keywords::mobilize::tests::mobilize_2_creates_two_tokens ... ok
test keywords::mobilize::tests::mobilize_3 ... ok
test registry::tests::ecl_tier2_spells ... ok
test registry::tests::fdn_tier2_cards ... ok
test registry::tests::registry_create_and_lookup ... ok
test registry::tests::ecl_specific_cards ... ok
test registry::tests::registry_set_query ... ok
test registry::tests::registry_card_counts ... ok
test registry::tests::fdn_tier3_cards ... ok
test registry::tests::tdm_tier2_spells ... ok

test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/lib.rs (target/debug/deps/mtg_engine-10c8e693567e8570)

running 165 tests
test abilities::tests::ability_store ... ok
test abilities::tests::activated_ability ... ok
test abilities::tests::active_zones ... ok
test abilities::tests::attacks_triggered ... ok
test abilities::tests::combat_damage_triggered ... ok
test abilities::tests::compose_realistic_card_lightning_bolt ... ok
test abilities::tests::compose_realistic_card_llanowar_elves ... ok
test abilities::tests::compose_realistic_lord ... ok
test abilities::tests::compose_realistic_card_mulldrifter ... ok
test abilities::tests::cost_builders ... ok
test abilities::tests::dies_triggered ... ok
test abilities::tests::effect_builders ... ok
test abilities::tests::end_step_triggered ... ok
test abilities::tests::etb_triggered ... ok
test abilities::tests::mana_ability ... ok
test abilities::tests::optional_trigger ... ok
test abilities::tests::spell_ability ... ok
test abilities::tests::static_ability_boost ... ok
test abilities::tests::static_effect_builders ... ok
test abilities::tests::triggered_ability ... ok
test abilities::tests::upkeep_triggered ... ok
test card::tests::create_basic_creature ... ok
test card::tests::create_basic_land ... ok
test combat::tests::blocked_damage ... ok
test combat::tests::blocker_damage_timing ... ok
test combat::tests::combat_clear ... ok
test combat::tests::blocking ... ok
test combat::tests::combat_state_basics ... ok
test combat::tests::deathtouch_minimizes_damage ... ok
test combat::tests::double_strike_both_steps ... ok
test combat::tests::flying_blocks ... ok
test combat::tests::first_strike_timing ... ok
test combat::tests::menace_requires_two_blockers ... ok
test combat::tests::trample_overflow ... ok
test combat::tests::unblocked_damage ... ok
test counters::tests::add_and_remove_counters ... ok
test counters::tests::counter_type_from_name ... ok
test counters::tests::pt_modification ... ok
test effects::tests::continuous_effects_manager ... ok
test effects::tests::boost_effect ... ok
test effects::tests::replacement_effects ... ok
test events::tests::applied_effects_tracking ... ok
test effects::tests::remove_from_source ... ok
test effects::tests::keyword_grant_effect ... ok
test effects::tests::timestamp_ordering ... ok
test events::tests::event_builder ... ok
test events::tests::spell_cast_event ... ok
test events::tests::event_log ... ok
test events::tests::prevention ... ok
test filters::tests::filter_color ... ok
test filters::tests::filter_controller ... ok
test filters::tests::filter_creature ... ok
test filters::tests::filter_keyword ... ok
test events::tests::damage_player_convenience ... ok
test filters::tests::filter_mana_value ... ok
test filters::tests::filter_or_combinator ... ok
test filters::tests::filter_power_comparison ... ok
test filters::tests::filter_nonland_permanent ... ok
test events::tests::zone_change_event ... ok
test events::tests::combat_events ... ok
test filters::tests::filter_not_combinator ... ok
test filters::tests::filter_tapped_untapped ... ok
test game::tests::add_counters_self_when_no_targets ... ok
test filters::tests::filter_card_data ... ok
test game::tests::boost_all_and_grant_keyword_all_until_eot ... ok
test game::tests::counter_annihilation_applied ... ok
test game::tests::fight_and_bite_effects ... ok
test game::tests::activated_ability_goes_on_stack ... ok
test game::tests::exile_effect ... ok
test game::tests::fight_auto_selects_creatures ... ok
test game::tests::discard_opponents_effect ... ok
test game::tests::compound_bite_counters_only_on_your_creature ... ok
test game::tests::legal_actions_include_pass ... ok
test game::tests::draw_cards_effect ... ok
test mana::tests::can_pay_colored ... ok
test game::tests::game_creation ... ok
test game::tests::pay_costs_tap_and_sacrifice ... ok
test game::tests::gain_life_effect ... ok
test game::tests::spell_effects_execute_on_resolve ... ok
test mana::tests::mana_arithmetic ... ok
test game::tests::mana_ability_and_spell_cast ... ok
test game::tests::lose_life_opponents_effect ... ok
test mana::tests::mana_display ... ok
test mana::tests::parse_simple_cost ... ok
test mana_pool::tests::add_and_spend_colored ... ok
test mana_pool::tests::any_mana_pays_colored ... ok
test mana::tests::can_pay_generic ... ok
test game::tests::draw_cards_from_empty_library_causes_loss ... ok
test mana_pool::tests::clear_empties_pool ... ok
test game::tests::bounce_effect ... ok
test game::tests::add_counters_all_effect ... ok
test mana_pool::tests::spend_generic_prefers_colorless ... ok
test game::tests::legend_rule_applied ... ok
test permanent::tests::haste_ignores_summoning_sickness ... ok
test permanent::tests::keyword_grants ... ok
test mana_pool::tests::color_array ... ok
test permanent::tests::tap_untap ... ok
test player::tests::land_plays ... ok
test player::tests::life_changes ... ok
test permanent::tests::counters_modify_pt ... ok
test game::tests::fizzle_when_target_removed ... ok
test permanent::tests::creature_basics ... ok
test player::tests::poison_counters ... ok
test permanent::tests::damage_and_lethal ... ok
test player::tests::starting_values ... ok
test mana::tests::cost_colors ... ok
test mana::tests::cannot_pay_insufficient ... ok
test permanent::tests::defender_cannot_attack ... ok
test state::tests::can_cast_sorcery ... ok
test player::tests::discard_check ... ok
test state::tests::next_player_wraps ... ok
test player::tests::game_over_states ... ok
test mana_pool::tests::try_pay_atomic ... ok
test player::tests::find_card_in_zones ... ok
test state::tests::sba_indestructible_survives_damage ... ok
test state::tests::sba_counter_annihilation ... ok
test state::tests::initial_state ... ok
test state::tests::sba_legend_rule ... ok
test state::tests::sba_legend_rule_different_controllers ... ok
test state::tests::sba_lethal_damage ... ok
test state::tests::sba_life_loss ... ok
test state::tests::sba_no_counter_annihilation_when_only_one_type ... ok
test state::tests::sba_planeswalker_zero_loyalty ... ok
test state::tests::sba_poison ... ok
test state::tests::sba_zero_toughness ... ok
test state::tests::should_end_one_player_left ... ok
test state::tests::zone_tracking ... ok
test targets::tests::divided_amounts ... ok
test targets::tests::empty_targets ... ok
test targets::tests::mark_illegal ... ok
test targets::tests::mark_player_illegal ... ok
test targets::tests::selected_targets_objects ... ok
test targets::tests::selected_targets_players ... ok
test targets::tests::target_definition_basics ... ok
test targets::tests::target_definition_up_to ... ok
test turn::tests::end_turn_skips_to_cleanup ... ok
test turn::tests::extra_turns ... ok
test turn::tests::extra_turns_lifo ... ok
test turn::tests::first_strike_step ... ok
test turn::tests::priority_rules ... ok
test turn::tests::priority_tracking ... ok
test turn::tests::step_to_phase_mapping ... ok
test turn::tests::turn_rotation ... ok
test turn::tests::turn_step_sequence ... ok
test watchers::tests::custom_watcher_card_scope ... ok
test watchers::tests::custom_watcher_game_scope ... ok
test watchers::tests::custom_watcher_player_scope ... ok
test watchers::tests::custom_watcher_reset ... ok
test watchers::tests::reset_clears_stats ... ok
test watchers::tests::track_attackers ... ok
test watchers::tests::track_cards_drawn ... ok
test watchers::tests::track_enters_battlefield ... ok
test watchers::tests::track_creatures_died ... ok
test watchers::tests::track_life_gained ... ok
test watchers::tests::track_spells_cast ... ok
test zones::tests::battlefield_add_remove ... ok
test zones::tests::battlefield_controlled_by ... ok
test zones::tests::graveyard_ordering ... ok
test zones::tests::hand_add_remove ... ok
test zones::tests::library_draw ... ok
test zones::tests::exile_zones ... ok
test zones::tests::library_peek ... ok
test zones::tests::library_put_on_top_and_bottom ... ok
test zones::tests::stack_lifo ... ok
test game::tests::game_runs_to_completion ... ok

test result: ok. 165 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s

     Running unittests src/lib.rs (target/debug/deps/mtg_python-d2f2ac684d6d76ac)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/lib.rs (target/debug/deps/mtg_tests-fac26c7f0fd5aee5)

running 19 tests
test concurrency::tests::game_state_clone_is_independent ... ok
test framework::tests::framework_bolt_kills_bear ... ok
test framework::tests::framework_create_token_effect ... ok
test concurrency::tests::gymnasium_env_runs_end_to_end ... ok
test framework::tests::framework_create_token_with_keyword ... ok
test framework::tests::framework_draw_cards_effect ... ok
test framework::tests::framework_direct_effect_execution ... ok
test framework::tests::framework_exile_and_assert ... ok
test framework::tests::framework_custom_starting_life ... ok
test framework::tests::framework_gain_life_effect ... ok
test framework::tests::framework_setup_battlefield ... ok
test framework::tests::framework_life_totals ... ok
test framework::tests::framework_multiple_permanents ... ok
test framework::tests::framework_setup_hand ... ok
test framework::tests::framework_setup_graveyard ... ok
test concurrency::tests::throughput_baseline ... ok
test concurrency::tests::parallel_gymnasium_envs ... ok
test concurrency::tests::parallel_games_produce_varied_results ... ok
test concurrency::tests::parallel_games_complete_without_panic ... ok

test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.28s
```

## Assistant

All 254 tests pass. Now let me update the docs. First, check the remediation docs and update them.

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/ecl-remediation.md",
  "pattern": "Darkness Descends"
}
```

## Assistant

**Result** (success):
```
422:- [ ] **Darkness Descends** — What works: nothing. What's broken: `Effect::Custom("two -1/-1 counters on each creature")`.
```

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/tdm-remediation.md",
  "pattern": "Barrensteppe Siege"
}
```

## Assistant

**Result** (success):
```
433:- [ ] **Barrensteppe Siege** — Enchantment {2}{W}{B}. What's broken: ETB choose mode (Custom) + both triggered abilities use Custom.
```

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/ROADMAP.md",
  "pattern": "AddCountersAll"
}
```

## Assistant

**Result** (success):
```
No matches found
```

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/ecl-remediation.md",
  "limit": 5,
  "offset": 420
}
```

## Assistant

**Result** (success):
```
   420→  - **Fix needed**: Implement library-tuck effect.
   421→
   422→- [ ] **Darkness Descends** — What works: nothing. What's broken: `Effect::Custom("two -1/-1 counters on each creature")`.
   423→  - **Java source**: `Mage.Sets/src/mage/cards/d/DarknessDescends.java`
   424→  - **What it should do**: Put two -1/-1 counters on each creature.
```

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/tdm-remediation.md",
  "limit": 5,
  "offset": 431
}
```

## Assistant

**Result** (success):
```
   431→  - **Fix needed**: Replace Custom with `AddCounters { counter_type: "+1/+1", count: 2 }`.
   432→
   433→- [ ] **Barrensteppe Siege** — Enchantment {2}{W}{B}. What's broken: ETB choose mode (Custom) + both triggered abilities use Custom.
   434→  - **Java source**: `Mage.Sets/src/mage/cards/b/BarrensteppeSiege.java`
   435→  - **Fix needed**: Modal ETB choice + implement both modes properly.
```

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/ecl-remediation.md",
  "new_string": "- [x] **Darkness Descends** — Fixed: `Effect::Custom` → `Effect::add_counters_all(\"-1/-1\", 2, \"creatures\")` (Batch 9).\n  - **Java source**: `Mage.Sets/src/mage/cards/d/DarknessDescends.java`\n  - **What it should do**: Put two -1/-1 counters on each creature.",
  "old_string": "- [ ] **Darkness Descends** — What works: nothing. What's broken: `Effect::Custom(\"two -1/-1 counters on each creature\")`.\n  - **Java source**: `Mage.Sets/src/mage/cards/d/DarknessDescends.java`\n  - **What it should do**: Put two -1/-1 counters on each creature.",
  "replace_all": false
}
```

## Assistant

**Result** (success):
```
The file /mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/ecl-remediation.md has been updated successfully.
```

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/tdm-remediation.md",
  "new_string": "- [ ] **Barrensteppe Siege** — Enchantment {2}{W}{B}. What's broken: ETB choose mode (Custom) + Mardu mode uses Custom. Abzan mode fixed (Batch 9): `Effect::Custom` → `Effect::add_counters_all(\"+1/+1\", 1, \"creatures you control\")`.\n  - **Java source**: `Mage.Sets/src/mage/cards/b/BarrensteppeSiege.java`\n  - **Fix needed**: Modal ETB choice + Mardu mode (\"each opponent sacrifices a creature\").",
  "old_string": "- [ ] **Barrensteppe Siege** — Enchantment {2}{W}{B}. What's broken: ETB choose mode (Custom) + both triggered abilities use Custom.\n  - **Java source**: `Mage.Sets/src/mage/cards/b/BarrensteppeSiege.java`\n  - **Fix needed**: Modal ETB choice + implement both modes properly.",
  "replace_all": false
}
```

## Assistant

**Result** (success):
```
The file /mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/tdm-remediation.md has been updated successfully.
```

## Assistant

Now update ROADMAP.md:

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/ROADMAP.md",
  "pattern": "counter|Counter"
}
```

## Assistant

**Result** (success):
```
11:[Omitted long matching line]
13:**Batch 2 engine change (2026-02-13):** Modified `execute_effects` to accept an optional `source: Option<ObjectId>` parameter. When `AddCounters` or `RemoveCounters` effects have no selected targets, they now fall back to the source permanent. This enables self-targeting counter effects (e.g., blight creatures putting -1/-1 counters on themselves) without requiring explicit target selection. Test: `add_counters_self_when_no_targets`.
15:**Batch 5 (2026-02-13):** Added `StaticEffect::Ward { cost: String }` variant + `ward()` builder. Ward is a triggered ability that counters targeting spells/abilities unless the opponent pays the cost. The engine stores it as structured data (not yet mechanically enforced during targeting). Replaced `StaticEffect::Custom("Ward ...")` on 7 cards across FDN/TDM/ECL and added `KeywordAbilities::WARD` flags.
44:- Loyalty counters not tracked as a resource
60:[Omitted long matching line]
127:   - ~~Sourbread Auntie/Sting-Slinger/Blighted Blackthorn: self-counter Custom -> `AddCounters` targeting self~~ **DONE**
130:   - ~~Icewind Elemental/Refute: loot/counter Custom -> typed effects~~ **DONE**
131:   - ~~ECL RemoveCounters cards (Encumbered Reejerey, Reluctant Dounguard, Heirloom Auntie, Bristlebane Battler): Custom -> `RemoveCounters` with source fallback~~ **DONE**
168:17. **Renew** (TDM) -- Counter-based death replacement.
```

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/ROADMAP.md",
  "limit": 8,
  "offset": 22
}
```

## Assistant

**Result** (success):
```
    22→|---------------|-------------|---------------|
    23→| `GainControl` | Gain control of target permanent | ~5 |
    24→| `GainControlUntilEndOfTurn` | Threaten/Act of Treason effects | ~5 |
    25→| `GainProtection` | Target gains protection from quality | ~5 |
    26→| `PreventCombatDamage` | Fog / damage prevention | ~5 |
    27→| `MustBlock` | Target creature must block | ~3 |
    28→| `SetLife` | Set a player's life total | ~2 |
    29→| `Custom(String)` | Catch-all for untyped effects | ~400+ |
```

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/ROADMAP.md",
  "limit": 10,
  "offset": 14
}
```

## Assistant

**Result** (success):
```
    14→
    15→**Batch 5 (2026-02-13):** Added `StaticEffect::Ward { cost: String }` variant + `ward()` builder. Ward is a triggered ability that counters targeting spells/abilities unless the opponent pays the cost. The engine stores it as structured data (not yet mechanically enforced during targeting). Replaced `StaticEffect::Custom("Ward ...")` on 7 cards across FDN/TDM/ECL and added `KeywordAbilities::WARD` flags.
    16→
    17→**Batch 6 (2026-02-13):** Added `StaticEffect::EntersTappedUnless { condition: String }` variant + `enters_tapped_unless()` builder. Stored as structured data; mechanical enforcement deferred. Replaced `StaticEffect::Custom("Enters tapped unless ...")` on 2 TDM lands (Cori Mountain Monastery, Dalkovan Encampment).
    18→
    19→**Batch 7 (2026-02-13):** Added `Effect::BoostAllUntilEndOfTurn { filter, power, toughness }` and `Effect::GrantKeywordAllUntilEndOfTurn { filter, keyword }` variants with constructors and match arms. These mass-buff effects apply to all creatures matching the filter controlled by the effect's controller. Replaced Custom effects on 8 cards (6 FDN, 2 ECL) with 12 effect swaps total.
    20→
    21→| Effect Variant | Description | Cards Blocked |
    22→|---------------|-------------|---------------|
    23→| `GainControl` | Gain control of target permanent | ~5 |
```

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/ROADMAP.md",
  "limit": 3,
  "offset": 18
}
```

## Assistant

**Result** (success):
```
    18→
    19→**Batch 7 (2026-02-13):** Added `Effect::BoostAllUntilEndOfTurn { filter, power, toughness }` and `Effect::GrantKeywordAllUntilEndOfTurn { filter, keyword }` variants with constructors and match arms. These mass-buff effects apply to all creatures matching the filter controlled by the effect's controller. Replaced Custom effects on 8 cards (6 FDN, 2 ECL) with 12 effect swaps total.
    20→
```

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/ROADMAP.md",
  "new_string": "**Batch 7 (2026-02-13):** Added `Effect::BoostAllUntilEndOfTurn { filter, power, toughness }` and `Effect::GrantKeywordAllUntilEndOfTurn { filter, keyword }` variants with constructors and match arms. These mass-buff effects apply to all creatures matching the filter controlled by the effect's controller. Replaced Custom effects on 8 cards (6 FDN, 2 ECL) with 12 effect swaps total.\n\n**Batch 9 (2026-02-13):** Added `Effect::AddCountersAll { counter_type, count, filter }` variant + `add_counters_all()` constructor + match arm. Puts N counters of specified type on all creatures matching filter, with \"you control\" support. Fixed ECL Darkness Descends and partially fixed TDM Barrensteppe Siege (Abzan mode only).\n\n| Effect Variant | Description | Cards Blocked |",
  "old_string": "**Batch 7 (2026-02-13):** Added `Effect::BoostAllUntilEndOfTurn { filter, power, toughness }` and `Effect::GrantKeywordAllUntilEndOfTurn { filter, keyword }` variants with constructors and match arms. These mass-buff effects apply to all creatures matching the filter controlled by the effect's controller. Replaced Custom effects on 8 cards (6 FDN, 2 ECL) with 12 effect swaps total.\n\n| Effect Variant | Description | Cards Blocked |",
  "replace_all": false
}
```

## Assistant

**Result** (success):
```
The file /mnt/dest-btrfs/home/jc/src/mage/mtg-rl/ROADMAP.md has been updated successfully.
```

## Assistant

Now check off the batch in work-queue.md and update it with the TDM card:

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/work-queue.md",
  "new_string": "## Batch 9: AddCountersAll (mass counter placement) [DONE 2026-02-13]\n\n**Engine**: Added `Effect::AddCountersAll { counter_type, count, filter }` variant + `add_counters_all()` constructor + match arm. Supports \"you control\" filter for controller-only targeting.\n\n**Cards fixed (2):**\n- [x] ECL: Darkness Descends — `Custom(\"Put two -1/-1 counters on each creature.\")` → `Effect::add_counters_all(\"-1/-1\", 2, \"creatures\")`\n- [x] TDM: Barrensteppe Siege (Abzan mode only) — `Custom(\"Put +1/+1 counter on each creature you control.\")` → `Effect::add_counters_all(\"+1/+1\", 1, \"creatures you control\")`\n\n**Tests added:** `add_counters_all_effect`\n\n**Skipped/Deferred:**\n- TDM: Felothar — compound: sacrifice + then put +1/+1 counter on each creature (reflexive trigger)\n- TDM: Barrensteppe Siege (Mardu mode) — \"each opponent sacrifices a creature\" (different effect)\n- TDM: Barrensteppe Siege (ETB) — modal choice not modeled",
  "old_string": "## Batch 9: AddCountersAll (mass counter placement)\n\n**Engine**: Need `Effect::AddCountersAll { counter_type, count, filter }` — similar pattern to `DestroyAll`/`DealDamageAll`. Puts N counters on all permanents matching filter.\n\n**Cards:**\n- [ ] ECL: Darkness Descends — `Custom(\"Put two -1/-1 counters on each creature.\")` → `Effect::AddCountersAll { counter_type: \"-1/-1\", count: 2, filter: \"creatures\" }`",
  "replace_all": false
}
```

## Assistant

**Result** (success):
```
The file /mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/work-queue.md has been updated successfully.
```

## Assistant

Now let me proceed to **Batch 10: Compound self-counter + target effects**. Let me read the batch and investigate:

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/work-queue.md",
  "limit": 25,
  "offset": 195
}
```

## Assistant

**Result** (success):
```
   195→
   196→**Skipped/Deferred:**
   197→- TDM: Felothar — compound: sacrifice + then put +1/+1 counter on each creature (reflexive trigger)
   198→- TDM: Barrensteppe Siege (Mardu mode) — "each opponent sacrifices a creature" (different effect)
   199→- TDM: Barrensteppe Siege (ETB) — modal choice not modeled
   200→
   201→---
   202→
   203→## Batch 10: Compound self-counter + target effects
   204→
   205→**Engine**: These cards need the ability to put counters on self AND apply a separate effect to a target in the same triggered/activated ability. The self-counter part now works (Batch 2 source fallback), but combining it with a target effect in one resolution needs verification.
   206→
   207→**Cards:**
   208→- [ ] ECL: Warren Torchmaster — begin combat: may blight 1 (self -1/-1 counter) to give target creature haste until EOT. Needs `add_counters("-1/-1", 1)` + `gain_keyword_eot("haste")` with target.
   209→- [ ] ECL: Dream Seizer — ETB: self -1/-1 counter + each opponent discards (also depends on Batch 3 OpponentDiscards)
   210→
   211→---
   212→
   213→## Batch 11: Modal spells (Choose one/two)
   214→
   215→**Engine**: Modal framework — player chooses mode, effects resolve based on mode selection.
   216→
   217→**Cards (~15+):**
   218→- [ ] FDN: Abrade, Boros Charm, Slagstorm, Valorous Stance, Charming Prince
   219→- [ ] TDM: multiple
```

## Assistant

## Assistant

Let me check the current state of these cards:

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-C": 10,
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-cards/src/sets/ecl.rs",
  "pattern": "warren_torchmaster|Warren Torchmaster"
}
```

## Assistant

**Result** (success):
```
98-    registry.register("Surly Farrier", surly_farrier, "ECL");
99-    registry.register("Tanufel Rimespeaker", tanufel_rimespeaker, "ECL");
100-    registry.register("Thoughtweft Imbuer", thoughtweft_imbuer, "ECL");
101-    registry.register("Thoughtweft Lieutenant", thoughtweft_lieutenant, "ECL");
102-    registry.register("Timid Shieldbearer", timid_shieldbearer, "ECL");
103-    registry.register("Unwelcome Sprite", unwelcome_sprite, "ECL");
104-    registry.register("Virulent Emissary", virulent_emissary, "ECL");
105-    registry.register("Voracious Tome-Skimmer", voracious_tome_skimmer, "ECL");
106-    registry.register("Wanderbrine Preacher", wanderbrine_preacher, "ECL");
107-    registry.register("Wanderwine Distracter", wanderwine_distracter, "ECL");
108:    registry.register("Warren Torchmaster", warren_torchmaster, "ECL");
109-
110-    // ── Spells and non-creature permanents ────────────────────────────────
111-    registry.register("Appeal to Eirdu", appeal_to_eirdu, "ECL");
112-    registry.register("Burning Curiosity", burning_curiosity, "ECL");
113-    registry.register("Cinder Strike", cinder_strike, "ECL");
114-    registry.register("Crib Swap", crib_swap, "ECL");
115-    registry.register("Evolving Wilds", super::fdn::evolving_wilds, "ECL");
116-    registry.register("Liminal Hold", liminal_hold, "ECL");
117-    registry.register("Protective Response", protective_response, "ECL");
118-    registry.register("Springleaf Drum", springleaf_drum, "ECL");
--
1672-        abilities: vec![
1673-            Ability::triggered(id,
1674-                "Whenever Wanderwine Distracter becomes tapped, target creature an opponent controls gets -3/-0 until end of turn.",
1675-                vec![EventType::Tapped],
1676-                vec![Effect::boost_until_eot(-3, 0)],
1677-                TargetSpec::PermanentFiltered("creature an opponent controls".into())),
1678-        ],
1679-        ..Default::default() }
1680-}
1681-
1682:fn warren_torchmaster(id: ObjectId, owner: PlayerId) -> CardData {
1683-    // 2/2 Goblin Warrior for {1}{R}. (Begin combat: blight 1 => creature gains haste)
1684:    CardData { id, owner, name: "Warren Torchmaster".into(), mana_cost: ManaCost::parse("{1}{R}"),
1685-        card_types: vec![CardType::Creature], subtypes: vec![SubType::Goblin, SubType::Warrior],
1686-        power: Some(2), toughness: Some(2), rarity: Rarity::Common,
1687-        abilities: vec![
1688-            Ability::triggered(id,
1689:                "At the beginning of combat on your turn, you may put a -1/-1 counter on Warren Torchmaster. If you do, target creature gains haste until end of turn.",
1690-                vec![EventType::BeginCombat],
1691:                vec![Effect::Custom("Put a -1/-1 counter on Warren Torchmaster. Target creature gains haste until end of turn.".into())],
1692-                TargetSpec::Creature),
1693-        ],
1694-        ..Default::default() }
1695-}
1696-
1697-// ── Spells and non-creature permanents ───────────────────────────────────────
1698-
1699-fn appeal_to_eirdu(id: ObjectId, owner: PlayerId) -> CardData {
1700-    // Instant {3}{W}. Convoke. +2/+1 to up to two creatures.
1701-    CardData { id, owner, name: "Appeal to Eirdu".into(), mana_cost: ManaCost::parse("{3}{W}"),
```

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-C": 10,
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-cards/src/sets/ecl.rs",
  "pattern": "dream_seizer|Dream Seizer"
}
```

## Assistant

**Result** (success):
```
26-    registry.register("Burdened Stoneback", burdened_stoneback, "ECL");
27-    registry.register("Champion of the Weird", champion_of_the_weird, "ECL");
28-    registry.register("Champions of the Perfect", champions_of_the_perfect, "ECL");
29-    registry.register("Changeling Wayfinder", changeling_wayfinder, "ECL");
30-    registry.register("Chaos Spewer", chaos_spewer, "ECL");
31-    registry.register("Chitinous Graspling", chitinous_graspling, "ECL");
32-    registry.register("Chomping Changeling", chomping_changeling, "ECL");
33-    registry.register("Crossroads Watcher", crossroads_watcher, "ECL");
34-    registry.register("Dawn's Light Archer", dawns_light_archer, "ECL");
35-    registry.register("Deepchannel Duelist", deepchannel_duelist, "ECL");
36:    registry.register("Dream Seizer", dream_seizer, "ECL");
37-    registry.register("Dundoolin Weaver", dundoolin_weaver, "ECL");
38-    registry.register("Eclipsed Boggart", eclipsed_boggart, "ECL");
39-    registry.register("Eclipsed Elf", eclipsed_elf, "ECL");
40-    registry.register("Eclipsed Flamekin", eclipsed_flamekin, "ECL");
41-    registry.register("Eclipsed Kithkin", eclipsed_kithkin, "ECL");
42-    registry.register("Eclipsed Merrow", eclipsed_merrow, "ECL");
43-    registry.register("Elder Auntie", elder_auntie, "ECL");
44-    registry.register("Encumbered Reejerey", encumbered_reejerey, "ECL");
45-    registry.register("Enraged Flamecaster", enraged_flamecaster, "ECL");
46-    registry.register("Explosive Prodigy", explosive_prodigy, "ECL");
--
577-        card_types: vec![CardType::Creature], subtypes: vec![SubType::Merfolk, SubType::Soldier],
578-        power: Some(2), toughness: Some(2), rarity: Rarity::Rare,
579-        abilities: vec![
580-            Ability::static_ability(id,
581-                "Other Merfolk you control get +1/+1.",
582-                vec![StaticEffect::boost_controlled("other Merfolk you control", 1, 1)]),
583-        ],
584-        ..Default::default() }
585-}
586-
587:fn dream_seizer(id: ObjectId, owner: PlayerId) -> CardData {
588-    // 3/2 Faerie Rogue for {3}{B}. Flying. (ETB: blight 1 => opponents discard)
589:    CardData { id, owner, name: "Dream Seizer".into(), mana_cost: ManaCost::parse("{3}{B}"),
590-        card_types: vec![CardType::Creature], subtypes: vec![SubType::Faerie, SubType::Rogue],
591-        power: Some(3), toughness: Some(2), keywords: KeywordAbilities::FLYING,
592-        rarity: Rarity::Common,
593-        abilities: vec![
594-            Ability::enters_battlefield_triggered(id,
595:                "When Dream Seizer enters, put a -1/-1 counter on it. When you do, each opponent discards a card.",
596-                vec![Effect::add_counters("-1/-1", 1), Effect::discard_opponents(1)],
597-                TargetSpec::None),
598-        ],
599-        ..Default::default() }
600-}
601-
602-fn dundoolin_weaver(id: ObjectId, owner: PlayerId) -> CardData {
603-    // 2/1 Kithkin Druid for {1}{G}. (ETB if 3+ creatures: return permanent from graveyard)
604-    CardData { id, owner, name: "Dundoolin Weaver".into(), mana_cost: ManaCost::parse("{1}{G}"),
605-        card_types: vec![CardType::Creature],
```

## Assistant

## Assistant

Dream Seizer is already fixed from Batch 3. The key question is whether Warren Torchmaster's compound self-counter + target effect works correctly. Let me investigate how effect resolution handles targets when mixing self-counter and target effects:

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "limit": 15,
  "offset": 955
}
```

## Assistant

**Result** (success):
```
   955→    }
   956→
   957→    /// Execute a list of effects for a controller with given targets.
   958→    pub fn execute_effects(&mut self, effects: &[Effect], controller: PlayerId, all_targets: &[ObjectId], source: Option<ObjectId>) {
   959→        // For compound fight/bite spells (e.g. [AddCounters, Bite]), pre-fight/bite
   960→        // effects should only apply to the first target (your creature), matching
   961→        // Java's per-effect target assignment where AddCountersTargetEffect targets
   962→        // target 0 while DamageWithPowerFromOneToAnotherTargetEffect uses both.
   963→        let has_fight_or_bite = effects.iter().any(|e| matches!(e, Effect::Fight | Effect::Bite));
   964→
   965→        for effect in effects {
   966→            let targets: &[ObjectId] = if has_fight_or_bite
   967→                && !matches!(effect, Effect::Fight | Effect::Bite)
   968→                && all_targets.len() >= 2
   969→            {
```

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "limit": 15,
  "offset": 970
}
```

## Assistant

**Result** (success):
```
   970→                &all_targets[..1]
   971→            } else {
   972→                all_targets
   973→            };
   974→            match effect {
   975→                Effect::DealDamage { amount } => {
   976→                    // Deal damage to target permanents.
   977→                    // Player targeting is handled separately via SelectedTargets.
   978→                    for &target_id in targets {
   979→                        if let Some(perm) = self.state.battlefield.get_mut(target_id) {
   980→                            perm.apply_damage(*amount);
   981→                        }
   982→                    }
   983→                    // If no permanent targets, deal damage to opponents
   984→                    // (simplified for "deal N damage to target opponent" effects)
```

## Assistant

## Assistant

Let me check how triggered ability resolution handles costs:

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-C": 5,
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "pattern": "resolve.*ability|ability.*resolve|triggered.*cost|Triggered.*cost"
}
```

## Assistant

**Result** (success):
```
846-            countered: false,
847-        };
848-        self.state.stack.push(stack_item);
849-    }
850-
851:    /// Activate a mana ability (resolves immediately, doesn't use the stack).
852-    fn activate_mana_ability(
853-        &mut self,
854-        player_id: PlayerId,
855-        source_id: ObjectId,
856-        ability_id: AbilityId,
```

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-A": 40,
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "pattern": "fn resolve_top_of_stack"
}
```

## Assistant

**Result** (success):
```
697:    fn resolve_top_of_stack(&mut self) {
698-        let item = match self.state.stack.pop() {
699-            Some(item) => item,
700-            None => return,
701-        };
702-
703-        if item.countered {
704-            // Countered spells go to graveyard
705-            self.move_card_to_graveyard(item.id, item.controller);
706-            return;
707-        }
708-
709-        // Fizzle check: if the spell/ability has targets and all targets are
710-        // now illegal (left the battlefield, gained hexproof, etc.), it fizzles.
711-        // Note: targets stored as ObjectIds; player targets are tracked separately
712-        // in the SelectedTargets system (targets.rs). This simplified check only
713-        // validates permanent targets on the battlefield.
714-        if !item.targets.is_empty() {
715-            let any_legal = item.targets.iter().any(|&target_id| {
716-                self.state.battlefield.contains(target_id)
717-                    || self.state.stack.get(target_id).is_some()
718-            });
719-            if !any_legal {
720-                // All targets are illegal — fizzle
721-                match &item.kind {
722-                    crate::zones::StackItemKind::Spell { .. } => {
723-                        self.move_card_to_graveyard(item.id, item.controller);
724-                    }
725-                    crate::zones::StackItemKind::Ability { .. } => {
726-                        // Abilities just cease to exist when fizzled
727-                    }
728-                }
729-                return;
730-            }
731-        }
732-
733-        match &item.kind {
734-            crate::zones::StackItemKind::Spell { card } => {
735-                if card.is_permanent_card() {
736-                    // Register abilities from the card
737-                    for ability in &card.abilities {
```

## Assistant

API Error: 401 {"type":"error","error":{"type":"authentication_error","message":"OAuth token has expired. Please obtain a new token or refresh your existing token."},"request_id":"req_011CY8AdB9bgCSEN5higyL9A"} · Please run /login

## User

<local-command-caveat>Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.</local-command-caveat>

## User

<command-name>/login</command-name>
            <command-message>login</command-message>
            <command-args></command-args>

## User

<local-command-stdout>Login successful</local-command-stdout>
