# Trace: Implement Opponent Discards Effect in Engine

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
    72→
    73→**Engine**: Need `Effect::DiscardOpponents { count }` or `Effect::OpponentDiscards { count }` — similar to `LoseLifeOpponents`. The existing `DiscardCards` targets the controller.
    74→
    75→**Cards:**
    76→- [ ] FDN line ~617: Burglar Rat — ETB `Custom("Each opponent discards a card.")`
    77→- [ ] ECL line ~596: Dream Seizer — `Custom("Put a -1/-1 counter on Dream Seizer. Each opponent discards a card.")` → `Effect::add_counters("-1/-1", 1), Effect::opponent_discards(1)` (self-counter part now works via Batch 2 source fallback; only needs OpponentDiscards)
    78→- [ ] FDN line ~5520: (check card) — `Custom("...if an opponent lost life this turn, each opponent discards a card.")` — conditional, may be harder
    79→
    80→---
    81→
    82→## Batch 4: Simple token creation — stub cards with full token text
    83→
    84→**Engine**: `CreateToken` already works and parses P/T + keywords from name. These cards use `Custom(...)` with text that describes standard token creation.
    85→
    86→**Cards** (grep for `Custom("Create` and `Custom("When this creature enters, create`):
    87→- [ ] FDN line ~6199: (card?) — `Custom("create two 3/3 white Knight creature tokens")`
    88→- [ ] FDN line ~6005: (card?) — `Custom("Create two 1/1 blue Faerie creature tokens with flying...")`
    89→- [ ] FDN line ~5623: (card?) — `Custom("create a Food token")`
    90→- [ ] FDN line ~5899: (card?) — `Custom("create a 1/1 green Elf Warrior creature token")` (conditional)
    91→- [ ] ECL line ~3704: (card?) — `Custom("create two 1/1 green and white Kithkin creature tokens")`
    92→- [ ] ECL line ~3709: (card?) — `Custom("{4}{W}: Create a 1/1 green and white Kithkin creature token")`
    93→- **NOTE**: Some of these are inside stubs where the entire ability structure is placeholder. Check if the `Ability::` wrapper is correct (trigger type, costs) before just swapping the effect. Some may need the whole ability rewritten.
    94→
    95→---
    96→
    97→## Batch 5: Ward keyword
    98→
    99→**Engine**: Ward is a triggered ability ("When this becomes the target of a spell or ability an opponent controls, counter it unless that player pays {cost}"). Options:
   100→- A) Add `KeywordAbilities::WARD` flag + hardcode Ward {2} behavior in the targeting/stack system
   101→- B) Model as a triggered ability with a special `Effect::Ward { cost }`
   102→- C) Simpler: Add `StaticEffect::Ward { cost: String }` that the engine recognizes during targeting
   103→
   104→**Cards (6):**
   105→- [ ] TDM line ~347, ~1990, ~3729: three creatures with `StaticEffect::Custom("Ward {2}")`
   106→- [ ] TDM line ~4078: `StaticEffect::Custom("Ward -- Discard a card.")`
   107→- [ ] ECL line ~2189: `StaticEffect::Custom("Ward {2}")`
   108→- [ ] FDN line ~5603: `StaticEffect::Custom("Ward {2}")`
   109→
   110→---
   111→
   112→## Batch 6: Enters-tapped-unless (conditional ETB tapped)
   113→
   114→**Engine**: May need `StaticEffect::EntersTappedUnless { condition }` or similar.
   115→
   116→**Cards:**
   117→- [ ] TDM line ~2110: `StaticEffect::Custom("Enters tapped unless you control Plains or Island.")`
   118→- [ ] TDM line ~2153: `StaticEffect::Custom("Enters tapped unless you control Swamp or Mountain.")`
   119→
   120→---
   121→
   122→## Batch 7: Mass keyword grant until EOT
   123→
   124→**Engine**: Need `Effect::GrantKeywordAllUntilEndOfTurn { filter, keyword }` or similar mass-buff effect.
   125→
   126→**Cards:**
   127→- [ ] FDN: Crash Through — `Custom("Creatures you control gain trample until end of turn.")` + DrawCards(1)
   128→- [ ] FDN: Overrun — `Custom("Creatures you control get +3/+3 and gain trample until end of turn.")`
   129→- [ ] FDN: Make a Stand — `Custom("Creatures you control get +1/+0 and gain indestructible until end of turn.")`
   130→- [ ] FDN: Heroic Reinforcements — `Custom(...)` mass +1/+1 and haste
   131→- [ ] FDN: Balmor, Battlemage Captain — spell trigger mass +1/+0 and trample
   132→
   133→---
   134→
   135→## Batch 8: Fight/Bite mechanic
   136→
   137→**Engine**: New `Effect::Fight` (mutual damage) and/or `Effect::Bite` (one-way damage based on power).
   138→
   139→**Cards (~10):**
   140→- [ ] FDN: Bite Down
   141→- [ ] ECL: multiple fight cards
   142→- [ ] TLA: Earth Rumble, others
   143→
   144→---
   145→
   146→## Batch 9: AddCountersAll (mass counter placement)
   147→
   148→**Engine**: Need `Effect::AddCountersAll { counter_type, count, filter }` — similar pattern to `DestroyAll`/`DealDamageAll`. Puts N counters on all permanents matching filter.
   149→
   150→**Cards:**
   151→- [ ] ECL: Darkness Descends — `Custom("Put two -1/-1 counters on each creature.")` → `Effect::AddCountersAll { counter_type: "-1/-1", count: 2, filter: "creatures" }`
   152→
   153→---
   154→
   155→## Batch 10: Compound self-counter + target effects
   156→
   157→**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.
   158→
   159→**Cards:**
   160→- [ ] 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.
   161→- [ ] ECL: Dream Seizer — ETB: self -1/-1 counter + each opponent discards (also depends on Batch 3 OpponentDiscards)
   162→
   163→---
   164→
   165→## Batch 11: Modal spells (Choose one/two)
   166→
   167→**Engine**: Modal framework — player chooses mode, effects resolve based on mode selection.
   168→
   169→**Cards (~15+):**
   170→- [ ] FDN: Abrade, Boros Charm, Slagstorm, Valorous Stance, Charming Prince
   171→- [ ] TDM: multiple
   172→- [ ] ECL: multiple
   173→
   174→---
   175→
   176→## Batch 12+: Larger systems
   177→
   178→These require more significant engine work:
   179→- Equipment attach/detach (Batch 12)
   180→- Planeswalker loyalty (Batch 13)
   181→- Token copy (Batch 14)
   182→- X-cost spells (Batch 15)
   183→- Aura attachment (Batch 16)
   184→- Impulse draw / exile-and-play (Batch 17)
   185→- Sagas / lore counters (Batch 18)
   186→- Set mechanics: Earthbend (TLA), Blight (ECL), Vivid (ECL), Behold (ECL) (Batch 19+)
   187→- Cost system: `Cost::Custom` for counter removal (ECL line ~464), `Cost::RemoveCounters`, `Cost::ExileFromGraveyard`
   188→
   189→---
   190→
   191→## How to pick the next batch
   192→
   193→1. Always do the lowest-numbered unchecked batch first
   194→2. If a batch has a NOTE about engine uncertainty, investigate before committing
   195→3. After finishing a batch, update this file AND the per-set remediation docs
   196→4. Run `cargo test --lib` before and after every batch
   197→
```

## Assistant

## Assistant

Next batch is **Batch 3: OpponentDiscards**. Let me read the engine files and find the affected cards.

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

## 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 (25337 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": "Each opponent discards"
}
```

## Assistant

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

## 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": "opponent discards"
}
```

## Assistant

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

## Assistant

## Assistant

Let me read the relevant parts of game.rs and find the cards.

## 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": "LoseLifeOpponents"
}
```

## Assistant

**Result** (success):
```
1013-                    // SelectedTargets for proper player targeting)
1014-                    if let Some(player) = self.state.players.get_mut(&controller) {
1015-                        player.life -= *amount as i32;
1016-                    }
1017-                }
1018:                Effect::LoseLifeOpponents { amount } => {
1019-                    let opponents: Vec<PlayerId> = self.state.turn_order.iter()
1020-                        .filter(|&&id| id != controller)
1021-                        .copied()
1022-                        .collect();
1023-                    for opp in opponents {
```

## 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": "opponent discard|Each opponent discards"
}
```

## Assistant

**Result** (success):
```
mtg-rl/mtg-cards/src/sets/fdn.rs:616:                "When Burglar Rat enters, each opponent discards a card.",
mtg-rl/mtg-cards/src/sets/fdn.rs:617:                vec![Effect::Custom("Each opponent discards a card.".into())],
mtg-rl/mtg-cards/src/sets/fdn.rs:5338:                "When this creature enters, each opponent discards a card and loses 2 life. You draw a card and gain 2 life.",
mtg-rl/mtg-cards/src/sets/fdn.rs:5340:                vec![Effect::Custom("When this creature enters, each opponent discards a card and loses 2 life. You draw a card and gain 2 life.".into())],
mtg-rl/mtg-cards/src/sets/fdn.rs:5518:                "When this creature enters, if an opponent lost life this turn, each opponent discards a card.",
mtg-rl/mtg-cards/src/sets/fdn.rs:5520:                vec![Effect::Custom("When this creature enters, if an opponent lost life this turn, each opponent discards a card.".into())],
mtg-rl/mtg-cards/src/sets/ecl.rs:595:                "When Dream Seizer enters, put a -1/-1 counter on it. When you do, each opponent discards a card.",
mtg-rl/mtg-cards/src/sets/ecl.rs:596:                vec![Effect::Custom("Put a -1/-1 counter on Dream Seizer. Each opponent discards a card.".into())],
mtg-rl/mtg-cards/src/sets/ecl.rs:2031:    // Sorcery for {1}{B}. Choose one: opponent discards nonland permanent card; or target creature gets -2/-2 until EOT.
```

## 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": "DiscardCards"
}
```

## Assistant

**Result** (success):
```
1094-                Effect::AddMana { mana } => {
1095-                    if let Some(player) = self.state.players.get_mut(&controller) {
1096-                        player.mana_pool.add(*mana, None, false);
1097-                    }
1098-                }
1099:                Effect::DiscardCards { count } => {
1100-                    / Controller discards (simplified: discard from the back of hand)
1101-                    let hand: Vec<ObjectId> = self.state.players.get(&controller)
1102-                        .map(|p| p.hand.iter().copied().collect())
1103-                        .unwrap_or_default();
1104-                    let view = crate::decision::GameView::placeholder();
```

## Assistant

## Assistant

Let me read the DiscardCards implementation and the cards in more detail.

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  1094→                Effect::AddMana { mana } => {
  1095→                    if let Some(player) = self.state.players.get_mut(&controller) {
  1096→                        player.mana_pool.add(*mana, None, false);
  1097→                    }
  1098→                }
  1099→                Effect::DiscardCards { count } => {
  1100→                    // Controller discards (simplified: discard from the back of hand)
  1101→                    let hand: Vec<ObjectId> = self.state.players.get(&controller)
  1102→                        .map(|p| p.hand.iter().copied().collect())
  1103→                        .unwrap_or_default();
  1104→                    let view = crate::decision::GameView::placeholder();
  1105→                    let to_discard = if let Some(dm) = self.decision_makers.get_mut(&controller) {
  1106→                        dm.choose_discard(&view, &hand, *count as usize)
  1107→                    } else {
  1108→                        hand.iter().rev().take(*count as usize).copied().collect()
  1109→                    };
  1110→                    for card_id in to_discard {
  1111→                        if let Some(player) = self.state.players.get_mut(&controller) {
  1112→                            player.hand.remove(card_id);
  1113→                        }
  1114→                        self.move_card_to_graveyard_inner(card_id, controller);
  1115→                    }
  1116→                }
  1117→                Effect::Mill { count } => {
  1118→                    for _ in 0..*count {
  1119→                        let card_id = self.state.players.get_mut(&controller)
  1120→                            .and_then(|p| p.library.draw());
  1121→                        if let Some(card_id) = card_id {
  1122→                            self.move_card_to_graveyard_inner(card_id, controller);
  1123→                        }
  1124→                    }
  1125→                }
  1126→                Effect::CreateToken { token_name, count } => {
  1127→                    for _ in 0..*count {
  1128→                        // Create a minimal token permanent
  1129→                        let token_id = ObjectId::new();
  1130→                        let mut card = CardData::new(token_id, controller, token_name);
  1131→                        card.card_types = vec![crate::constants::CardType::Creature];
  1132→                        // Parse token stats from name (e.g. "4/4 Dragon with flying")
  1133→                        let (p, t, kw) = Self::parse_token_stats(token_name);
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  1013→                    // SelectedTargets for proper player targeting)
  1014→                    if let Some(player) = self.state.players.get_mut(&controller) {
  1015→                        player.life -= *amount as i32;
  1016→                    }
  1017→                }
  1018→                Effect::LoseLifeOpponents { amount } => {
  1019→                    let opponents: Vec<PlayerId> = self.state.turn_order.iter()
  1020→                        .filter(|&&id| id != controller)
  1021→                        .copied()
  1022→                        .collect();
  1023→                    for opp in opponents {
  1024→                        if let Some(player) = self.state.players.get_mut(&opp) {
  1025→                            player.life -= *amount as i32;
  1026→                        }
  1027→                    }
  1028→                }
  1029→                Effect::DealDamageOpponents { amount } => {
  1030→                    let opponents: Vec<PlayerId> = self.state.turn_order.iter()
  1031→                        .filter(|&&id| id != controller)
  1032→                        .copied()
  1033→                        .collect();
  1034→                    for opp in opponents {
  1035→                        if let Some(player) = self.state.players.get_mut(&opp) {
  1036→                            player.life -= *amount as i32;
  1037→                        }
  1038→                    }
  1039→                }
  1040→                Effect::AddCounters { counter_type, count } => {
  1041→                    let ct = crate::counters::CounterType::from_name(counter_type);
  1042→                    // If no targets, fall back to source (self-targeting counters)
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
   610→fn burglar_rat(id: ObjectId, owner: PlayerId) -> CardData {
   611→    CardData { id, owner, name: "Burglar Rat".into(), mana_cost: ManaCost::parse("{1}{B}"),
   612→        card_types: vec![CardType::Creature], subtypes: vec![SubType::Rat],
   613→        power: Some(1), toughness: Some(1), rarity: Rarity::Common,
   614→        abilities: vec![
   615→            Ability::enters_battlefield_triggered(id,
   616→                "When Burglar Rat enters, each opponent discards a card.",
   617→                vec![Effect::Custom("Each opponent discards a card.".into())],
   618→                TargetSpec::None),
   619→        ],
   620→        ..Default::default() }
   621→}
   622→
   623→fn campus_guide(id: ObjectId, owner: PlayerId) -> CardData {
   624→    CardData { id, owner, name: "Campus Guide".into(), mana_cost: ManaCost::parse("{2}"),
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  5330→    CardData { id, owner, name: "Arbiter of Woe".into(), mana_cost: ManaCost::parse("{4}{B}{B}"),
  5331→        card_types: vec![CardType::Creature],
  5332→        subtypes: vec![SubType::Demon],
  5333→        power: Some(5), toughness: Some(4),
  5334→        keywords: KeywordAbilities::FLYING,
  5335→        rarity: Rarity::Common,
  5336→        abilities: vec![
  5337→            Ability::triggered(id,
  5338→                "When this creature enters, each opponent discards a card and loses 2 life. You draw a card and gain 2 life.",
  5339→                vec![EventType::EnteredTheBattlefield],
  5340→                vec![Effect::Custom("When this creature enters, each opponent discards a card and loses 2 life. You draw a card and gain 2 life.".into())],
  5341→                TargetSpec::None),
  5342→        ],
  5343→        ..Default::default() }
  5344→}
  5345→
  5346→fn arcane_epiphany(id: ObjectId, owner: PlayerId) -> CardData {
  5347→    CardData { id, owner, name: "Arcane Epiphany".into(), mana_cost: ManaCost::parse("{3}{U}{U}"),
  5348→        card_types: vec![CardType::Instant],
  5349→        rarity: Rarity::Common,
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  5510→    CardData { id, owner, name: "Bloodtithe Collector".into(), mana_cost: ManaCost::parse("{4}{B}"),
  5511→        card_types: vec![CardType::Creature],
  5512→        subtypes: vec![SubType::Vampire, SubType::Noble],
  5513→        power: Some(3), toughness: Some(4),
  5514→        keywords: KeywordAbilities::FLYING,
  5515→        rarity: Rarity::Common,
  5516→        abilities: vec![
  5517→            Ability::triggered(id,
  5518→                "When this creature enters, if an opponent lost life this turn, each opponent discards a card.",
  5519→                vec![EventType::EnteredTheBattlefield],
  5520→                vec![Effect::Custom("When this creature enters, if an opponent lost life this turn, each opponent discards a card.".into())],
  5521→                TargetSpec::None),
  5522→        ],
  5523→        ..Default::default() }
  5524→}
  5525→
  5526→fn bolt_bend(id: ObjectId, owner: PlayerId) -> CardData {
  5527→    CardData { id, owner, name: "Bolt Bend".into(), mana_cost: ManaCost::parse("{3}{R}"),
  5528→        card_types: vec![CardType::Instant],
  5529→        rarity: Rarity::Common,
```

## 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": 588
}
```

## Assistant

**Result** (success):
```
   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::Custom("Put a -1/-1 counter on Dream Seizer. Each opponent discards a card.".into())],
   597→                TargetSpec::None),
   598→        ],
   599→        ..Default::default() }
   600→}
   601→
   602→fn dundoolin_weaver(id: ObjectId, owner: PlayerId) -> CardData {
```

## Assistant

## Assistant

Now let me find the end of the execute_effects match and the test section in game.rs.

## 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": "Effect::Custom"
}
```

## Assistant

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

## 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": "mod tests"
}
```

## Assistant

**Result** (success):
```
1559:mod tests {
```

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-C": 3,
  "-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):
```
429-                    perm.granted_keywords = crate::constants::KeywordAbilities::empty();
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() {
--
1240-                    for &target_id in targets {
1241-                        if let Some(perm) = self.state.battlefield.get_mut(target_id) {
1242-                            // Use a sentinel counter to indicate can't block
1243:                            perm.add_counters(crate::counters::CounterType::Custom("cant_block".into()), 1);
1244-                        }
1245-                    }
1246-                }
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  1559→mod tests {
  1560→    use super::*;
  1561→    use crate::abilities::{Ability, Cost, Effect, TargetSpec};
  1562→    use crate::card::CardData;
  1563→    use crate::constants::{CardType, KeywordAbilities, Outcome};
  1564→    use crate::decision::{
  1565→        AttackerInfo, DamageAssignment, GameView, NamedChoice, PlayerAction,
  1566→        ReplacementEffectChoice, TargetRequirement, UnpaidMana,
  1567→    };
  1568→    use crate::mana::Mana;
  1569→
  1570→    /// A minimal decision maker that always passes priority.
  1571→    struct AlwaysPassPlayer;
  1572→
  1573→    impl PlayerDecisionMaker for AlwaysPassPlayer {
  1574→        fn priority(&mut self, _game: &GameView<'_>, _legal: &[PlayerAction]) -> PlayerAction {
  1575→            PlayerAction::Pass
  1576→        }
  1577→        fn choose_targets(&mut self, _: &GameView<'_>, _: Outcome, _: &TargetRequirement) -> Vec<ObjectId> { vec![] }
  1578→        fn choose_use(&mut self, _: &GameView<'_>, _: Outcome, _: &str) -> bool { false }
  1579→        fn choose_mode(&mut self, _: &GameView<'_>, _: &[NamedChoice]) -> usize { 0 }
  1580→        fn select_attackers(&mut self, _: &GameView<'_>, _: &[ObjectId], _: &[ObjectId]) -> Vec<(ObjectId, ObjectId)> { vec![] }
  1581→        fn select_blockers(&mut self, _: &GameView<'_>, _: &[AttackerInfo]) -> Vec<(ObjectId, ObjectId)> { vec![] }
  1582→        fn assign_damage(&mut self, _: &GameView<'_>, _: &DamageAssignment) -> Vec<(ObjectId, u32)> { vec![] }
  1583→        fn choose_mulligan(&mut self, _: &GameView<'_>, _: &[ObjectId]) -> bool { false }
  1584→        fn choose_cards_to_put_back(&mut self, _: &GameView<'_>, _: &[ObjectId], _: usize) -> Vec<ObjectId> { vec![] }
  1585→        fn choose_discard(&mut self, _: &GameView<'_>, _: &[ObjectId], _: usize) -> Vec<ObjectId> { vec![] }
  1586→        fn choose_amount(&mut self, _: &GameView<'_>, _: &str, min: u32, _: u32) -> u32 { min }
  1587→        fn choose_mana_payment(&mut self, _: &GameView<'_>, _: &UnpaidMana, _: &[PlayerAction]) -> Option<PlayerAction> { None }
  1588→        fn choose_replacement_effect(&mut self, _: &GameView<'_>, _: &[ReplacementEffectChoice]) -> usize { 0 }
  1589→        fn choose_pile(&mut self, _: &GameView<'_>, _: Outcome, _: &str, _: &[ObjectId], _: &[ObjectId]) -> bool { true }
  1590→        fn choose_option(&mut self, _: &GameView<'_>, _: Outcome, _: &str, _: &[NamedChoice]) -> usize { 0 }
  1591→    }
  1592→
  1593→    fn make_basic_land(name: &str, owner: PlayerId) -> CardData {
  1594→        let mut card = CardData::new(ObjectId::new(), owner, name);
  1595→        card.card_types = vec![CardType::Land];
  1596→        card
  1597→    }
  1598→
  1599→    fn make_creature(name: &str, owner: PlayerId, power: i32, toughness: i32) -> CardData {
  1600→        let mut card = CardData::new(ObjectId::new(), owner, name);
  1601→        card.card_types = vec![CardType::Creature];
  1602→        card.power = Some(power);
  1603→        card.toughness = Some(toughness);
  1604→        card.keywords = KeywordAbilities::empty();
  1605→        card
  1606→    }
  1607→
  1608→    fn make_deck(owner: PlayerId) -> Vec<CardData> {
  1609→        let mut deck = Vec::new();
  1610→        // 20 lands
  1611→        for _ in 0..20 {
  1612→            deck.push(make_basic_land("Forest", owner));
  1613→        }
  1614→        // 20 creatures
  1615→        for _ in 0..20 {
  1616→            deck.push(make_creature("Grizzly Bears", owner, 2, 2));
  1617→        }
  1618→        deck
  1619→    }
  1620→
  1621→    #[test]
  1622→    fn game_creation() {
  1623→        let p1 = PlayerId::new();
  1624→        let p2 = PlayerId::new();
  1625→
  1626→        let config = GameConfig {
  1627→            players: vec![
  1628→                PlayerConfig { name: "Alice".to_string(), deck: make_deck(p1) },
  1629→                PlayerConfig { name: "Bob".to_string(), deck: make_deck(p2) },
  1630→            ],
  1631→            starting_life: 20,
  1632→        };
  1633→
  1634→        let game = Game::new_two_player(
  1635→            config,
  1636→            vec![
  1637→                (p1, Box::new(AlwaysPassPlayer)),
  1638→                (p2, Box::new(AlwaysPassPlayer)),
```

## Assistant

## 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 (lose_life_opponents|add_counters_self)"
}
```

## Assistant

**Result** (success):
```
2126:    fn lose_life_opponents_effect() {
2276:    fn add_counters_self_when_no_targets() {
```

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "wc -l /mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "description": "Count lines in game.rs"
}
```

## Assistant

**Result** (success):
```
2327 /mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  2126→    fn lose_life_opponents_effect() {
  2127→        let p1 = PlayerId::new();
  2128→        let p2 = PlayerId::new();
  2129→
  2130→        let config = GameConfig {
  2131→            players: vec![
  2132→                PlayerConfig { name: "Alice".to_string(), deck: make_deck(p1) },
  2133→                PlayerConfig { name: "Bob".to_string(), deck: make_deck(p2) },
  2134→            ],
  2135→            starting_life: 20,
  2136→        };
  2137→
  2138→        let mut game = Game::new_two_player(
  2139→            config,
  2140→            vec![
  2141→                (p1, Box::new(AlwaysPassPlayer)),
  2142→                (p2, Box::new(AlwaysPassPlayer)),
  2143→            ],
  2144→        );
  2145→
  2146→        game.execute_effects(&[Effect::lose_life_opponents(3)], p1, &[], None);
  2147→        // Controller's life should be unchanged
  2148→        assert_eq!(game.state.players.get(&p1).unwrap().life, 20);
  2149→        // Opponent loses 3 life
  2150→        assert_eq!(game.state.players.get(&p2).unwrap().life, 17);
  2151→    }
  2152→
  2153→    #[test]
  2154→    fn exile_effect() {
  2155→        let p1 = PlayerId::new();
  2156→        let p2 = PlayerId::new();
  2157→
  2158→        let config = GameConfig {
  2159→            players: vec![
  2160→                PlayerConfig { name: "Alice".to_string(), deck: make_deck(p1) },
  2161→                PlayerConfig { name: "Bob".to_string(), deck: make_deck(p2) },
  2162→            ],
  2163→            starting_life: 20,
  2164→        };
  2165→
  2166→        let mut game = Game::new_two_player(
  2167→            config,
  2168→            vec![
  2169→                (p1, Box::new(AlwaysPassPlayer)),
  2170→                (p2, Box::new(AlwaysPassPlayer)),
  2171→            ],
  2172→        );
  2173→
  2174→        // Put a creature on the battlefield
  2175→        let bear_id = ObjectId::new();
  2176→        let mut bear = CardData::new(bear_id, p2, "Bear");
  2177→        bear.card_types = vec![CardType::Creature];
  2178→        bear.power = Some(2);
  2179→        bear.toughness = Some(2);
  2180→        bear.keywords = KeywordAbilities::empty();
  2181→        game.state.battlefield.add(Permanent::new(bear, p2));
  2182→
  2183→        // Exile it
  2184→        game.execute_effects(&[Effect::Exile], p1, &[bear_id], None);
  2185→
  2186→        assert!(!game.state.battlefield.contains(bear_id));
  2187→        assert!(game.state.exile.contains(bear_id));
  2188→    }
  2189→
  2190→    #[test]
  2191→    fn bounce_effect() {
  2192→        let p1 = PlayerId::new();
  2193→        let p2 = PlayerId::new();
  2194→
  2195→        let config = GameConfig {
  2196→            players: vec![
  2197→                PlayerConfig { name: "Alice".to_string(), deck: make_deck(p1) },
  2198→                PlayerConfig { name: "Bob".to_string(), deck: make_deck(p2) },
  2199→            ],
  2200→            starting_life: 20,
  2201→        };
  2202→
  2203→        let mut game = Game::new_two_player(
  2204→            config,
  2205→            vec![
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  2276→    fn add_counters_self_when_no_targets() {
  2277→        let p1 = PlayerId::new();
  2278→        let p2 = PlayerId::new();
  2279→
  2280→        let config = GameConfig {
  2281→            players: vec![
  2282→                PlayerConfig { name: "Alice".to_string(), deck: make_deck(p1) },
  2283→                PlayerConfig { name: "Bob".to_string(), deck: make_deck(p2) },
  2284→            ],
  2285→            starting_life: 20,
  2286→        };
  2287→
  2288→        let mut game = Game::new_two_player(
  2289→            config,
  2290→            vec![
  2291→                (p1, Box::new(AlwaysPassPlayer)),
  2292→                (p2, Box::new(AlwaysPassPlayer)),
  2293→            ],
  2294→        );
  2295→
  2296→        // Add a creature to the battlefield
  2297→        let source_id = ObjectId::new();
  2298→        let mut card = CardData::new(source_id, p1, "Blight Creature");
  2299→        card.card_types = vec![CardType::Creature];
  2300→        card.power = Some(3);
  2301→        card.toughness = Some(7);
  2302→        card.keywords = KeywordAbilities::empty();
  2303→        game.state.battlefield.add(Permanent::new(card, p1));
  2304→
  2305→        // Execute AddCounters with no targets but with source — should add to self
  2306→        game.execute_effects(
  2307→            &[Effect::add_counters("-1/-1", 2)],
  2308→            p1,
  2309→            &[],
  2310→            Some(source_id),
  2311→        );
  2312→
  2313→        let perm = game.state.battlefield.get(source_id).unwrap();
  2314→        assert_eq!(perm.counters.get(&CounterType::M1M1), 2);
  2315→
  2316→        // Execute RemoveCounters with no targets but with source — should remove from self
  2317→        game.execute_effects(
  2318→            &[Effect::RemoveCounters { counter_type: "-1/-1".into(), count: 1 }],
  2319→            p1,
  2320→            &[],
  2321→            Some(source_id),
  2322→        );
  2323→
  2324→        let perm = game.state.battlefield.get(source_id).unwrap();
  2325→        assert_eq!(perm.counters.get(&CounterType::M1M1), 1);
  2326→    }
  2327→}
```

## Assistant

Good. Now I have all the context I need. Let me implement the batch:

1. Add `Effect::DiscardOpponents { count }` variant + constructor + match arm in game.rs
2. Add a test
3. Fix the cards

Let me start with the engine changes.

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/abilities.rs",
  "new_string": "    /// Discard cards.\n    DiscardCards { count: u32 },\n    /// Each opponent discards N cards.\n    DiscardOpponents { count: u32 },",
  "old_string": "    /// Discard cards.\n    DiscardCards { 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 in the Effect impl block:

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/abilities.rs",
  "new_string": "    /// \"Discard N cards.\"\n    pub fn discard_cards(count: u32) -> Self {\n        Effect::DiscardCards { count }\n    }\n\n    /// \"Each opponent discards N cards.\"\n    pub fn discard_opponents(count: u32) -> Self {\n        Effect::DiscardOpponents { count }\n    }",
  "old_string": "    /// \"Discard N cards.\"\n    pub fn discard_cards(count: u32) -> Self {\n        Effect::DiscardCards { count }\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. Let me add it right after the existing `DiscardCards` handler:

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  1099→                Effect::DiscardCards { count } => {
  1100→                    // Controller discards (simplified: discard from the back of hand)
  1101→                    let hand: Vec<ObjectId> = self.state.players.get(&controller)
  1102→                        .map(|p| p.hand.iter().copied().collect())
  1103→                        .unwrap_or_default();
  1104→                    let view = crate::decision::GameView::placeholder();
  1105→                    let to_discard = if let Some(dm) = self.decision_makers.get_mut(&controller) {
  1106→                        dm.choose_discard(&view, &hand, *count as usize)
  1107→                    } else {
  1108→                        hand.iter().rev().take(*count as usize).copied().collect()
  1109→                    };
  1110→                    for card_id in to_discard {
  1111→                        if let Some(player) = self.state.players.get_mut(&controller) {
  1112→                            player.hand.remove(card_id);
  1113→                        }
  1114→                        self.move_card_to_graveyard_inner(card_id, controller);
  1115→                    }
  1116→                }
  1117→                Effect::Mill { count } => {
  1118→                    for _ in 0..*count {
```

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "new_string": "                Effect::DiscardOpponents { count } => {\n                    let opponents: Vec<PlayerId> = self.state.turn_order.iter()\n                        .filter(|&&id| id != controller)\n                        .copied()\n                        .collect();\n                    for opp in opponents {\n                        let hand: Vec<ObjectId> = self.state.players.get(&opp)\n                            .map(|p| p.hand.iter().copied().collect())\n                            .unwrap_or_default();\n                        let view = crate::decision::GameView::placeholder();\n                        let to_discard = if let Some(dm) = self.decision_makers.get_mut(&opp) {\n                            dm.choose_discard(&view, &hand, *count as usize)\n                        } else {\n                            hand.iter().rev().take(*count as usize).copied().collect()\n                        };\n                        for card_id in to_discard {\n                            if let Some(player) = self.state.players.get_mut(&opp) {\n                                player.hand.remove(card_id);\n                            }\n                            self.move_card_to_graveyard_inner(card_id, opp);\n                        }\n                    }\n                }\n                Effect::Mill { count } => {\n                    for _ in 0..*count {",
  "old_string": "                Effect::Mill { count } => {\n                    for _ in 0..*count {",
  "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 a test. Let me append it before the closing `}` of the tests module:

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  2336→        let perm = game.state.battlefield.get(source_id).unwrap();
  2337→        assert_eq!(perm.counters.get(&CounterType::M1M1), 2);
  2338→
  2339→        // Execute RemoveCounters with no targets but with source — should remove from self
  2340→        game.execute_effects(
  2341→            &[Effect::RemoveCounters { counter_type: "-1/-1".into(), count: 1 }],
  2342→            p1,
  2343→            &[],
  2344→            Some(source_id),
  2345→        );
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  2345→        );
  2346→
  2347→        let perm = game.state.battlefield.get(source_id).unwrap();
  2348→        assert_eq!(perm.counters.get(&CounterType::M1M1), 1);
  2349→    }
  2350→}
  2351→
```

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "new_string": "        let perm = game.state.battlefield.get(source_id).unwrap();\n        assert_eq!(perm.counters.get(&CounterType::M1M1), 1);\n    }\n\n    #[test]\n    fn discard_opponents_effect() {\n        let p1 = PlayerId::new();\n        let p2 = PlayerId::new();\n\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\n        let mut game = Game::new_two_player(\n            config,\n            vec![\n                (p1, Box::new(AlwaysPassPlayer)),\n                (p2, Box::new(AlwaysPassPlayer)),\n            ],\n        );\n\n        // Give opponent some cards in hand\n        let card1 = CardData::new(ObjectId::new(), p2, \"Card A\");\n        let card2 = CardData::new(ObjectId::new(), p2, \"Card B\");\n        let card3 = CardData::new(ObjectId::new(), p2, \"Card C\");\n        let c1_id = card1.id;\n        let c2_id = card2.id;\n        let c3_id = card3.id;\n        game.state.cards.insert(c1_id, card1);\n        game.state.cards.insert(c2_id, card2);\n        game.state.cards.insert(c3_id, card3);\n        if let Some(player) = game.state.players.get_mut(&p2) {\n            player.hand.add(c1_id);\n            player.hand.add(c2_id);\n            player.hand.add(c3_id);\n        }\n\n        let p1_hand_before = game.state.players.get(&p1).unwrap().hand.len();\n        let p2_hand_before = game.state.players.get(&p2).unwrap().hand.len();\n        assert_eq!(p2_hand_before, 3);\n\n        // Each opponent discards 1\n        game.execute_effects(&[Effect::discard_opponents(1)], p1, &[], None);\n\n        // Controller's hand unchanged\n        assert_eq!(game.state.players.get(&p1).unwrap().hand.len(), p1_hand_before);\n        // Opponent lost 1 card\n        assert_eq!(game.state.players.get(&p2).unwrap().hand.len(), 2);\n    }\n}",
  "old_string": "        let perm = game.state.battlefield.get(source_id).unwrap();\n        assert_eq!(perm.counters.get(&CounterType::M1M1), 1);\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 let me verify the engine compiles and the test passes:

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "cd /mnt/dest-btrfs/home/jc/src/mage/mtg-rl && cargo test --lib -p mtg-engine -- discard_opponents_effect 2>&1",
  "description": "Run the new discard_opponents test",
  "timeout": 120000
}
```

## Assistant

**Result** (error):
```
Exit code 101
   Compiling mtg-engine v0.1.0 (/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine)
error[E0609]: no field `cards` on type `state::GameState`
    --> mtg-engine/src/game.rs:2379:20
     |
2379 |         game.state.cards.insert(c1_id, card1);
     |                    ^^^^^ unknown field
     |
     = note: available fields are: `players`, `turn_order`, `battlefield`, `stack`, `exile` ... and 17 others

error[E0609]: no field `cards` on type `state::GameState`
    --> mtg-engine/src/game.rs:2380:20
     |
2380 |         game.state.cards.insert(c2_id, card2);
     |                    ^^^^^ unknown field
     |
     = note: available fields are: `players`, `turn_order`, `battlefield`, `stack`, `exile` ... and 17 others

error[E0609]: no field `cards` on type `state::GameState`
    --> mtg-engine/src/game.rs:2381:20
     |
2381 |         game.state.cards.insert(c3_id, card3);
     |                    ^^^^^ unknown field
     |
     = note: available fields are: `players`, `turn_order`, `battlefield`, `stack`, `exile` ... and 17 others

For more information about this error, try `rustc --explain E0609`.
error: could not compile `mtg-engine` (lib test) due to 3 previous errors
```

## Assistant

Need to check how cards are added to a player's hand in the existing tests. Let me look at how `hand` works:

## 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": "player\\.hand\\.add|hand\\.add"
}
```

## Assistant

**Result** (success):
```
668-            if !player.mana_pool.try_pay(&mana_cost) {
669-                // Can't pay — put card back in hand
670:                player.hand.add(card_id);
671-                return;
672-            }
--
995-                            let owner = perm.owner();
996-                            if let Some(player) = self.state.players.get_mut(&owner) {
997:                                player.hand.add(target_id);
998-                            }
999-                            self.state.set_zone(target_id, crate::constants::Zone::Hand, Some(owner));
--
1195-                            if let Some(player) = self.state.players.get_mut(&owner_id) {
1196-                                if player.graveyard.remove(target_id) {
1197:                                    player.hand.add(target_id);
1198-                                    self.state.set_zone(target_id, crate::constants::Zone::Hand, Some(owner_id));
1199-                                }
--
1343-                            if let Some(player) = self.state.players.get_mut(&controller) {
1344-                                player.library.remove(card_id);
1345:                                player.hand.add(card_id);
1346-                                self.state.set_zone(card_id, crate::constants::Zone::Hand, Some(controller));
1347-                            }
--
1448-            // Add to hand and update zone
1449-            if let Some(player) = self.state.players.get_mut(&player_id) {
1450:                player.hand.add(card_id);
1451-            }
1452-            self.state.set_zone(card_id, crate::constants::Zone::Hand, Some(player_id));
--
2381-        game.state.cards.insert(c3_id, card3);
2382-        if let Some(player) = game.state.players.get_mut(&p2) {
2383:            player.hand.add(c1_id);
2384:            player.hand.add(c2_id);
2385:            player.hand.add(c3_id);
2386-        }
2387-
```

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src",
  "pattern": "pub hand:"
}
```

## Assistant

**Result** (success):
```
mtg-rl/mtg-engine/src/player.rs:34:    pub hand: Hand,
```

## Assistant

The hand just needs `add` — I don't need to insert into a `cards` map. The `move_card_to_graveyard_inner` only needs the card to exist in the hand (via `hand.remove`). Let me simplify the test:

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "new_string": "        // Give opponent some cards in hand\n        let c1_id = ObjectId::new();\n        let c2_id = ObjectId::new();\n        let c3_id = ObjectId::new();\n        if let Some(player) = game.state.players.get_mut(&p2) {\n            player.hand.add(c1_id);\n            player.hand.add(c2_id);\n            player.hand.add(c3_id);\n        }",
  "old_string": "        // Give opponent some cards in hand\n        let card1 = CardData::new(ObjectId::new(), p2, \"Card A\");\n        let card2 = CardData::new(ObjectId::new(), p2, \"Card B\");\n        let card3 = CardData::new(ObjectId::new(), p2, \"Card C\");\n        let c1_id = card1.id;\n        let c2_id = card2.id;\n        let c3_id = card3.id;\n        game.state.cards.insert(c1_id, card1);\n        game.state.cards.insert(c2_id, card2);\n        game.state.cards.insert(c3_id, card3);\n        if let Some(player) = game.state.players.get_mut(&p2) {\n            player.hand.add(c1_id);\n            player.hand.add(c2_id);\n            player.hand.add(c3_id);\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": "cd /mnt/dest-btrfs/home/jc/src/mage/mtg-rl && cargo test --lib -p mtg-engine -- discard_opponents_effect 2>&1",
  "description": "Run the new discard_opponents test",
  "timeout": 120000
}
```

## Assistant

**Result** (error):
```
Exit code 101
   Compiling mtg-engine v0.1.0 (/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 5.18s
     Running unittests src/lib.rs (target/debug/deps/mtg_engine-fa37b04e04026fe5)

running 1 test
test game::tests::discard_opponents_effect ... FAILED

failures:

---- game::tests::discard_opponents_effect stdout ----

thread 'game::tests::discard_opponents_effect' panicked at mtg-engine/src/game.rs:2392:9:
assertion `left == right` failed
  left: 3
 right: 2
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    game::tests::discard_opponents_effect

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

error: test failed, to rerun pass `-p mtg-engine --lib`
```

## Assistant

## Assistant

The discard isn't working. Let me check `move_card_to_graveyard_inner`:

## 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 move_card_to_graveyard_inner"
}
```

## Assistant

**Result** (success):
```
1415:    fn move_card_to_graveyard_inner(&mut self, card_id: ObjectId, owner: PlayerId) {
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  1415→    fn move_card_to_graveyard_inner(&mut self, card_id: ObjectId, owner: PlayerId) {
  1416→        if let Some(player) = self.state.players.get_mut(&owner) {
  1417→            player.graveyard.add(card_id);
  1418→            self.state.set_zone(card_id, crate::constants::Zone::Graveyard, Some(owner));
  1419→        }
  1420→    }
  1421→
  1422→    /// Move a card to its owner's graveyard.
  1423→    fn move_card_to_graveyard(&mut self, card_id: ObjectId, owner: PlayerId) {
  1424→        if let Some(player) = self.state.players.get_mut(&owner) {
  1425→            player.graveyard.add(card_id);
  1426→            self.state.set_zone(card_id, crate::constants::Zone::Graveyard, Some(owner));
  1427→        }
  1428→    }
  1429→
  1430→    /// Draw N cards for a player.
  1431→    pub fn draw_cards(&mut self, player_id: PlayerId, count: u32) {
  1432→        for _ in 0..count {
  1433→            let card_id = {
  1434→                let player = match self.state.players.get_mut(&player_id) {
  1435→                    Some(p) => p,
  1436→                    None => return,
  1437→                };
  1438→                match player.library.draw() {
  1439→                    Some(id) => id,
  1440→                    None => {
  1441→                        // Tried to draw from empty library — player loses (set flag)
  1442→                        player.lost = true;
  1443→                        return;
  1444→                    }
```

## Assistant

OK so `move_card_to_graveyard_inner` doesn't do `hand.remove`. Looking back at the DiscardCards handler, it does `player.hand.remove(card_id)` separately, then calls `move_card_to_graveyard_inner`. The issue is that in my DiscardOpponents handler, `choose_discard` returns an empty vec because `AlwaysPassPlayer::choose_discard` returns `vec![]`. Let me check:

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  1595→
  1596→    impl PlayerDecisionMaker for AlwaysPassPlayer {
  1597→        fn priority(&mut self, _game: &GameView<'_>, _legal: &[PlayerAction]) -> PlayerAction {
  1598→            PlayerAction::Pass
  1599→        }
```

## Assistant

Yes, `AlwaysPassPlayer::choose_discard` returns `vec![]`, and so the decision maker returns empty — no cards get discarded. The fallback `hand.iter().rev().take(...)` only fires when `self.decision_makers.get_mut(&opp)` returns `None`, but it won't because we registered decision makers for both players.

I need to adjust the test to not register a decision maker for p2, or use a different approach. Let me look at how the existing DiscardCards test works... actually there's no separate DiscardCards test in game.rs. Let me fix the test by directly testing without decision makers (the fallback path), or better, let me check the hand count after the fact properly. The issue is the decision maker returns empty vec.

Let me fix the test to verify the fallback path by creating the game differently, or I can just directly verify the mechanism works by checking that the effect processes without a decision maker:

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "new_string": "    /// A decision maker that actually discards when asked.\n    struct DiscardingPlayer;\n\n    impl PlayerDecisionMaker for DiscardingPlayer {\n        fn priority(&mut self, _game: &GameView<'_>, _legal: &[PlayerAction]) -> PlayerAction {\n            PlayerAction::Pass\n        }\n        fn choose_targets(&mut self, _: &GameView<'_>, _: Outcome, _: &TargetRequirement) -> Vec<ObjectId> { vec![] }\n        fn choose_use(&mut self, _: &GameView<'_>, _: Outcome, _: &str) -> bool { false }\n        fn choose_mode(&mut self, _: &GameView<'_>, _: &[NamedChoice]) -> usize { 0 }\n        fn select_attackers(&mut self, _: &GameView<'_>, _: &[ObjectId], _: &[ObjectId]) -> Vec<(ObjectId, ObjectId)> { vec![] }\n        fn select_blockers(&mut self, _: &GameView<'_>, _: &[AttackerInfo]) -> Vec<(ObjectId, ObjectId)> { vec![] }\n        fn assign_damage(&mut self, _: &GameView<'_>, _: &DamageAssignment) -> Vec<(ObjectId, u32)> { vec![] }\n        fn choose_mulligan(&mut self, _: &GameView<'_>, _: &[ObjectId]) -> bool { false }\n        fn choose_cards_to_put_back(&mut self, _: &GameView<'_>, _: &[ObjectId], _: usize) -> Vec<ObjectId> { vec![] }\n        fn choose_discard(&mut self, _: &GameView<'_>, hand: &[ObjectId], count: usize) -> Vec<ObjectId> {\n            // Actually discard from the back of hand\n            hand.iter().rev().take(count).copied().collect()\n        }\n        fn choose_amount(&mut self, _: &GameView<'_>, _: &str, min: u32, _: u32) -> u32 { min }\n        fn choose_mana_payment(&mut self, _: &GameView<'_>, _: &UnpaidMana, _: &[PlayerAction]) -> Option<PlayerAction> { None }\n        fn choose_replacement_effect(&mut self, _: &GameView<'_>, _: &[ReplacementEffectChoice]) -> usize { 0 }\n        fn choose_pile(&mut self, _: &GameView<'_>, _: Outcome, _: &str, _: &[ObjectId], _: &[ObjectId]) -> bool { true }\n        fn choose_option(&mut self, _: &GameView<'_>, _: Outcome, _: &str, _: &[NamedChoice]) -> usize { 0 }\n    }\n\n    #[test]\n    fn discard_opponents_effect() {\n        let p1 = PlayerId::new();\n        let p2 = PlayerId::new();\n\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\n        let mut game = Game::new_two_player(\n            config,\n            vec![\n                (p1, Box::new(AlwaysPassPlayer)),\n                (p2, Box::new(DiscardingPlayer)),\n            ],\n        );\n\n        // Give opponent some cards in hand\n        let c1_id = ObjectId::new();\n        let c2_id = ObjectId::new();\n        let c3_id = ObjectId::new();\n        if let Some(player) = game.state.players.get_mut(&p2) {\n            player.hand.add(c1_id);\n            player.hand.add(c2_id);\n            player.hand.add(c3_id);\n        }\n\n        let p1_hand_before = game.state.players.get(&p1).unwrap().hand.len();\n        let p2_hand_before = game.state.players.get(&p2).unwrap().hand.len();\n        assert_eq!(p2_hand_before, 3);\n\n        // Each opponent discards 1\n        game.execute_effects(&[Effect::discard_opponents(1)], p1, &[], None);\n\n        // Controller's hand unchanged\n        assert_eq!(game.state.players.get(&p1).unwrap().hand.len(), p1_hand_before);\n        // Opponent lost 1 card\n        assert_eq!(game.state.players.get(&p2).unwrap().hand.len(), 2);\n    }",
  "old_string": "    #[test]\n    fn discard_opponents_effect() {\n        let p1 = PlayerId::new();\n        let p2 = PlayerId::new();\n\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\n        let mut game = Game::new_two_player(\n            config,\n            vec![\n                (p1, Box::new(AlwaysPassPlayer)),\n                (p2, Box::new(AlwaysPassPlayer)),\n            ],\n        );\n\n        // Give opponent some cards in hand\n        let c1_id = ObjectId::new();\n        let c2_id = ObjectId::new();\n        let c3_id = ObjectId::new();\n        if let Some(player) = game.state.players.get_mut(&p2) {\n            player.hand.add(c1_id);\n            player.hand.add(c2_id);\n            player.hand.add(c3_id);\n        }\n\n        let p1_hand_before = game.state.players.get(&p1).unwrap().hand.len();\n        let p2_hand_before = game.state.players.get(&p2).unwrap().hand.len();\n        assert_eq!(p2_hand_before, 3);\n\n        // Each opponent discards 1\n        game.execute_effects(&[Effect::discard_opponents(1)], p1, &[], None);\n\n        // Controller's hand unchanged\n        assert_eq!(game.state.players.get(&p1).unwrap().hand.len(), p1_hand_before);\n        // Opponent lost 1 card\n        assert_eq!(game.state.players.get(&p2).unwrap().hand.len(), 2);\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": "cd /mnt/dest-btrfs/home/jc/src/mage/mtg-rl && cargo test --lib -p mtg-engine -- discard_opponents_effect 2>&1",
  "description": "Run the new discard_opponents test",
  "timeout": 120000
}
```

## Assistant

**Result** (success):
```
   Compiling mtg-engine v0.1.0 (/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 2.75s
     Running unittests src/lib.rs (target/debug/deps/mtg_engine-fa37b04e04026fe5)

running 1 test
test game::tests::discard_opponents_effect ... ok

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

## Assistant

Engine work done. Now let me fix the cards. Let me handle each one:

**Burglar Rat** (FDN line 617) — simple swap to `Effect::discard_opponents(1)`

**Dream Seizer** (ECL line 596) — compound: `add_counters("-1/-1", 1)` + `discard_opponents(1)` (self-counter uses Batch 2 source fallback)

**Arbiter of Woe** (FDN line 5340) — compound: `discard_opponents(1)` + `lose_life_opponents(2)` + `draw_cards(1)` + `gain_life(2)`

**Bloodtithe Collector** (FDN line 5520) — conditional ("if an opponent lost life this turn"), keep as Custom with note, or implement partially

Let me fix the straightforward ones first:

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i '617s|vec!\\[Effect::Custom(\"Each opponent discards a card.\".into())\\]|vec![Effect::discard_opponents(1)]|' mtg-cards/src/sets/fdn.rs",
  "description": "Fix Burglar Rat in FDN"
}
```

## Assistant

**Result** (success):
```
(empty result)
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
   614→        abilities: vec![
   615→            Ability::enters_battlefield_triggered(id,
   616→                "When Burglar Rat enters, each opponent discards a card.",
   617→                vec![Effect::discard_opponents(1)],
   618→                TargetSpec::None),
   619→        ],
   620→        ..Default::default() }
   621→}
```

## Assistant

Good. Now fix Dream Seizer (ECL):

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i '596s|vec!\\[Effect::Custom(\"Put a -1/-1 counter on Dream Seizer. Each opponent discards a card.\".into())\\]|vec![Effect::add_counters(\"-1/-1\", 1), Effect::discard_opponents(1)]|' mtg-cards/src/sets/ecl.rs",
  "description": "Fix Dream Seizer in ECL"
}
```

## Assistant

**Result** (success):
```
(empty result)
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
   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→        ],
```

## Assistant

Good. Now fix Arbiter of Woe (FDN line 5340):

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i '5340s|vec!\\[Effect::Custom(\"When this creature enters, each opponent discards a card and loses 2 life. You draw a card and gain 2 life.\".into())\\]|vec![Effect::discard_opponents(1), Effect::lose_life_opponents(2), Effect::draw_cards(1), Effect::gain_life(2)]|' mtg-cards/src/sets/fdn.rs",
  "description": "Fix Arbiter of Woe in FDN"
}
```

## Assistant

**Result** (success):
```
(empty result)
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  5336→        abilities: vec![
  5337→            Ability::triggered(id,
  5338→                "When this creature enters, each opponent discards a card and loses 2 life. You draw a card and gain 2 life.",
  5339→                vec![EventType::EnteredTheBattlefield],
  5340→                vec![Effect::discard_opponents(1), Effect::lose_life_opponents(2), Effect::draw_cards(1), Effect::gain_life(2)],
  5341→                TargetSpec::None),
  5342→        ],
  5343→        ..Default::default() }
```

## Assistant

Now for Bloodtithe Collector — it's conditional ("if an opponent lost life this turn"). We don't have conditional effect support, so I'll partially fix it by using the typed effect but note it's still conditional:

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  5516→        abilities: vec![
  5517→            Ability::triggered(id,
  5518→                "When this creature enters, if an opponent lost life this turn, each opponent discards a card.",
  5519→                vec![EventType::EnteredTheBattlefield],
  5520→                vec![Effect::Custom("When this creature enters, if an opponent lost life this turn, each opponent discards a card.".into())],
  5521→                TargetSpec::None),
  5522→        ],
  5523→        ..Default::default() }
```

## Assistant

This one has a conditional — "if an opponent lost life this turn". The condition check isn't implemented, but I can still replace the Custom effect with the typed effect. The trigger will fire unconditionally (which is a simplification, but better than being a no-op Custom):

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i '5520s|vec!\\[Effect::Custom(\"When this creature enters, if an opponent lost life this turn, each opponent discards a card.\".into())\\]|vec![Effect::discard_opponents(1)]|' mtg-cards/src/sets/fdn.rs",
  "description": "Fix Bloodtithe Collector in FDN (conditional simplified)"
}
```

## Assistant

**Result** (success):
```
(empty result)
```

## Assistant

Now let me verify compilation and run all tests:

## 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-engine v0.1.0 (/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine)
    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 4.39s
```

## Assistant

### Tool: Bash

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

## 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 9.46s
     Running unittests src/lib.rs (target/debug/deps/mtg_ai-a3cefc6981511e97)

running 52 tests
test action_space::tests::action_mask_with_castable_spell ... ok
test action_space::tests::phase1_activate_ability_roundtrip ... ok
test action_space::tests::action_mask_pass_only ... ok
test action_space::tests::phase1_all_indices_are_unique ... ok
test action_space::tests::phase1_cast_spell_roundtrip ... 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_space_sizes ... ok
test gym::tests::env_reset_returns_correct_size_observation ... ok
test gym::tests::env_step_returns_valid_result ... ok
test heuristic_player::tests::heuristic_attacks_with_all ... ok
test gym::tests::env_truncates_at_max_turns ... ok
test heuristic_player::tests::heuristic_choose_use_says_yes_except_ai_dont_use ... 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_land_over_spell ... ok
test minimax_player::tests::minimax_prefers_spell_over_pass ... ok
test minimax_player::tests::minimax_search_stats_tracked ... ok
test observation::tests::life_normalized_correctly ... ok
test observation::tests::observation_has_correct_size ... ok
test observation::tests::observation_size_constant_matches ... 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_choose_use_returns_bool ... ok
test random_player::tests::random_player_passes_when_only_option ... ok
test random_player::tests::random_player_damage_assignment_sums_correctly ... ok

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

     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::behold::tests::behold_rules_text_format ... ok
test keywords::blight::tests::blight_1_cost ... 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_specific_cards ... ok
test registry::tests::fdn_tier2_cards ... ok
test registry::tests::ecl_tier2_spells ... ok
test registry::tests::fdn_tier3_cards ... ok
test registry::tests::registry_card_counts ... ok
test registry::tests::registry_create_and_lookup ... ok
test registry::tests::registry_set_query ... 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.01s

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

running 160 tests
test abilities::tests::activated_ability ... ok
test abilities::tests::ability_store ... 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_card_mulldrifter ... ok
test abilities::tests::compose_realistic_lord ... 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::blocking ... ok
test combat::tests::combat_clear ... 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::first_strike_timing ... ok
test combat::tests::flying_blocks ... 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::boost_effect ... ok
test effects::tests::keyword_grant_effect ... ok
test effects::tests::continuous_effects_manager ... ok
test effects::tests::remove_from_source ... ok
test effects::tests::replacement_effects ... ok
test effects::tests::timestamp_ordering ... ok
test events::tests::applied_effects_tracking ... ok
test events::tests::combat_events ... ok
test events::tests::damage_player_convenience ... ok
test events::tests::event_builder ... ok
test events::tests::event_log ... ok
test events::tests::prevention ... ok
test events::tests::spell_cast_event ... ok
test filters::tests::filter_card_data ... ok
test events::tests::zone_change_event ... 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 filters::tests::filter_mana_value ... ok
test filters::tests::filter_nonland_permanent ... ok
test filters::tests::filter_not_combinator ... ok
test filters::tests::filter_or_combinator ... ok
test filters::tests::filter_power_comparison ... ok
test filters::tests::filter_tapped_untapped ... ok
test game::tests::activated_ability_goes_on_stack ... ok
test game::tests::add_counters_self_when_no_targets ... ok
test game::tests::bounce_effect ... ok
test game::tests::counter_annihilation_applied ... ok
test game::tests::discard_opponents_effect ... ok
test game::tests::draw_cards_from_empty_library_causes_loss ... ok
test game::tests::exile_effect ... ok
test game::tests::draw_cards_effect ... ok
test game::tests::fizzle_when_target_removed ... ok
test game::tests::gain_life_effect ... ok
test game::tests::game_creation ... ok
test game::tests::lose_life_opponents_effect ... ok
test game::tests::legal_actions_include_pass ... ok
test game::tests::legend_rule_applied ... ok
test game::tests::mana_ability_and_spell_cast ... ok
test mana::tests::can_pay_generic ... ok
test mana::tests::can_pay_colored ... ok
test game::tests::pay_costs_tap_and_sacrifice ... ok
test mana::tests::cost_colors ... ok
test mana::tests::mana_display ... ok
test game::tests::spell_effects_execute_on_resolve ... 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::mana_arithmetic ... ok
test mana_pool::tests::clear_empties_pool ... ok
test mana_pool::tests::color_array ... ok
test mana::tests::cannot_pay_insufficient ... ok
test mana_pool::tests::spend_generic_prefers_colorless ... ok
test permanent::tests::creature_basics ... ok
test permanent::tests::damage_and_lethal ... ok
test mana_pool::tests::try_pay_atomic ... ok
test permanent::tests::haste_ignores_summoning_sickness ... ok
test permanent::tests::tap_untap ... ok
test permanent::tests::counters_modify_pt ... ok
test player::tests::discard_check ... ok
test permanent::tests::keyword_grants ... ok
test player::tests::find_card_in_zones ... ok
test permanent::tests::defender_cannot_attack ... ok
test player::tests::game_over_states ... ok
test player::tests::land_plays ... ok
test player::tests::life_changes ... ok
test player::tests::poison_counters ... ok
test player::tests::starting_values ... ok
test state::tests::can_cast_sorcery ... ok
test state::tests::initial_state ... ok
test state::tests::next_player_wraps ... ok
test state::tests::sba_counter_annihilation ... ok
test state::tests::sba_indestructible_survives_damage ... 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::should_end_one_player_left ... ok
test state::tests::sba_zero_toughness ... 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_creatures_died ... ok
test watchers::tests::track_enters_battlefield ... 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::exile_zones ... ok
test zones::tests::hand_add_remove ... ok
test zones::tests::graveyard_ordering ... ok
test zones::tests::library_draw ... 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. 160 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s

     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 17 tests
test concurrency::tests::game_state_clone_is_independent ... ok
test framework::tests::framework_bolt_kills_bear ... ok
test framework::tests::framework_draw_cards_effect ... ok
test framework::tests::framework_custom_starting_life ... ok
test framework::tests::framework_direct_effect_execution ... ok
test concurrency::tests::gymnasium_env_runs_end_to_end ... ok
test framework::tests::framework_gain_life_effect ... ok
test framework::tests::framework_exile_and_assert ... ok
test framework::tests::framework_life_totals ... ok
test framework::tests::framework_multiple_permanents ... ok
test framework::tests::framework_setup_graveyard ... ok
test framework::tests::framework_setup_battlefield ... ok
test framework::tests::framework_setup_hand ... ok
test concurrency::tests::throughput_baseline ... ok
test concurrency::tests::parallel_games_complete_without_panic ... ok
test concurrency::tests::parallel_games_produce_varied_results ... ok
test concurrency::tests::parallel_gymnasium_envs ... ok

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

## Assistant

All 247 tests pass. Now let me also scan for any other cards across all sets that might use "opponent discards" patterns I missed:

## 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": "discard"
}
```

## Assistant

**Result** (success):
```
mtg-cards/src/sets/fdn.rs:616:                "When Burglar Rat enters, each opponent discards a card.",
mtg-cards/src/sets/fdn.rs:617:                vec![Effect::discard_opponents(1)],
mtg-cards/src/sets/fdn.rs:1851:    // 5/5 Dragon Wizard for {5}{R}{R}. Flying. Combat damage: all discard + draw 7.
mtg-cards/src/sets/fdn.rs:1858:                "Whenever Dragon Mage deals combat damage to a player, each player discards their hand, then draws seven cards.",
mtg-cards/src/sets/fdn.rs:1860:                vec![Effect::Custom("Each player discards their hand, then draws seven cards.".into())],
mtg-cards/src/sets/fdn.rs:2363:    // Sorcery {B}. Target opponent reveals hand; you choose a noncreature, nonland card to discard.
mtg-cards/src/sets/fdn.rs:2368:                vec![Effect::discard_cards(1)],
mtg-cards/src/sets/fdn.rs:2436:    // Sorcery {1}{U}. Draw 2. Discard 1 unless you attacked this turn.
mtg-cards/src/sets/fdn.rs:2441:                vec![Effect::draw_cards(2), Effect::discard_cards(1)],
mtg-cards/src/sets/fdn.rs:2448:    // Sorcery {1}{B}. Return up to 2 creature cards from graveyard to hand, discard 1.
mtg-cards/src/sets/fdn.rs:2453:                vec![Effect::return_from_graveyard(), Effect::discard_cards(1)],
mtg-cards/src/sets/fdn.rs:5338:                "When this creature enters, each opponent discards a card and loses 2 life. You draw a card and gain 2 life.",
mtg-cards/src/sets/fdn.rs:5340:                vec![Effect::discard_opponents(1), Effect::lose_life_opponents(2), Effect::draw_cards(1), Effect::gain_life(2)],
mtg-cards/src/sets/fdn.rs:5518:                "When this creature enters, if an opponent lost life this turn, each opponent discards a card.",
mtg-cards/src/sets/fdn.rs:5520:                vec![Effect::discard_opponents(1)],
mtg-cards/src/sets/fdn.rs:6340:                "When this creature enters, draw a card, then discard a card.",
mtg-cards/src/sets/fdn.rs:6342:                vec![Effect::draw_cards(1), Effect::discard_cards(1)],
mtg-cards/src/sets/fdn.rs:6431:                "When Kiora enters, draw two cards, then discard two cards.",
mtg-cards/src/sets/fdn.rs:6433:                vec![Effect::Custom("When Kiora enters, draw two cards, then discard two cards.".into())],
mtg-cards/src/sets/fdn.rs:6740:                vec![Effect::Custom("Whenever an opponent casts a spell, that player loses 5 life unless they discard a card.".into())],
mtg-cards/src/sets/fdn.rs:6752:                vec![Effect::Custom("Target opponent reveals their hand. You choose a nonland card from it. That player discards that card.".into())],
mtg-cards/src/sets/fdn.rs:6896:                vec![Effect::counter_spell(), Effect::draw_cards(1), Effect::discard_cards(1)],
mtg-cards/src/sets/fdn.rs:7084:                vec![Effect::Custom("As an additional cost to cast this spell, discard a card.".into())],
mtg-cards/src/sets/fdn.rs:7302:                vec![Effect::Custom("As an additional cost to cast this spell, discard a card.".into())],
mtg-cards/src/sets/tla.rs:580:                    vec![Effect::discard_cards(1)],
mtg-cards/src/sets/ecl.rs:588:    // 3/2 Faerie Rogue for {3}{B}. Flying. (ETB: blight 1 => opponents discard)
mtg-cards/src/sets/ecl.rs:595:                "When Dream Seizer enters, put a -1/-1 counter on it. When you do, each opponent discards a card.",
mtg-cards/src/sets/ecl.rs:596:                vec![Effect::add_counters("-1/-1", 1), Effect::discard_opponents(1)],
mtg-cards/src/sets/ecl.rs:806:                "When Flaring Cinder enters, you may draw a card, then discard a card.",
mtg-cards/src/sets/ecl.rs:807:                vec![Effect::draw_cards(1), Effect::discard_cards(1)],
mtg-cards/src/sets/ecl.rs:810:                "Whenever you cast a spell with mana value 4 or greater, you may draw a card, then discard a card.",
mtg-cards/src/sets/ecl.rs:812:                vec![Effect::draw_cards(1), Effect::discard_cards(1)],
mtg-cards/src/sets/ecl.rs:957:                "{T}, Blight 1: Draw a card, then discard a card.",
mtg-cards/src/sets/ecl.rs:959:                vec![Effect::draw_cards(1), Effect::discard_cards(1)],
mtg-cards/src/sets/ecl.rs:999:    // 3/1 Elf Warrior for {1}{B}. (Discard: indestructible + tap)
mtg-cards/src/sets/ecl.rs:1005:                "Discard a card: Iron-Shield Elf gains indestructible until end of turn. Tap it.",
mtg-cards/src/sets/ecl.rs:1006:                vec![Cost::Discard(1)],
mtg-cards/src/sets/ecl.rs:1055:                vec![Cost::pay_mana("{2}"), Cost::Discard(1)],
mtg-cards/src/sets/ecl.rs:1400:    // 2/3 Merfolk Citizen for {2}{U}. (Becomes tapped: draw, discard)
mtg-cards/src/sets/ecl.rs:1407:                "Whenever Silvergill Peddler becomes tapped, draw a card, then discard a card.",
mtg-cards/src/sets/ecl.rs:1409:                vec![Effect::draw_cards(1), Effect::discard_cards(1)],
mtg-cards/src/sets/ecl.rs:1785:    // Instant {3}{U}{U}. Convoke. Draw 3, discard 1.
mtg-cards/src/sets/ecl.rs:1789:        abilities: vec![Ability::spell(id, vec![Effect::draw_cards(3), Effect::discard_cards(1)], TargetSpec::None)],
mtg-cards/src/sets/ecl.rs:2031:    // Sorcery for {1}{B}. Choose one: opponent discards nonland permanent card; or target creature gets -2/-2 until EOT.
mtg-cards/src/sets/ecl.rs:2035:            vec![Effect::Custom("Choose one: target opponent reveals hand, discard a nonland permanent card; or target creature gets -2/-2 until end of turn.".into())],
mtg-cards/src/sets/ecl.rs:2412:    // ETB if {B}{B}: opponent reveals hand, discard nonland card.
mtg-cards/src/sets/ecl.rs:2424:                "When this creature enters, if {B}{B} was spent to cast it, target opponent reveals their hand. You choose a nonland card from it. That player discards that card.",
mtg-cards/src/sets/ecl.rs:2425:                vec![Effect::discard_cards(1)],
mtg-cards/src/sets/ecl.rs:3789:                "When this creature enters, you may discard a card. If you do, search your library for a creature card, reveal it, put it into your hand, then shuffle.",
mtg-cards/src/sets/ecl.rs:3791:                vec![Effect::Custom("When this creature enters, you may discard a card. If you do, search your library for a creature card, reveal it, put it into your hand, then shuffle.".into())],
mtg-cards/src/sets/ecl.rs:4168:                vec![Effect::Custom("Draw three cards. Then discard two cards unless you discard a creature card.".into())],
mtg-cards/src/sets/tdm.rs:908:                "Whenever this creature becomes tapped, you may discard a card. If you do, draw a card.",
mtg-cards/src/sets/tdm.rs:910:                vec![Effect::Custom("You may discard a card. If you do, draw a card.".into())],
mtg-cards/src/sets/tdm.rs:1066:                "When this creature enters, draw a card, then discard a card.",
mtg-cards/src/sets/tdm.rs:1067:                vec![Effect::draw_cards(1), Effect::discard_cards(1)],
mtg-cards/src/sets/tdm.rs:1897:    // Enchantment {1}{U}. ETB: draw 2, discard 1. Dragon ETB: return to hand.
mtg-cards/src/sets/tdm.rs:1902:                "When this enchantment enters, draw two cards, then discard a card.",
mtg-cards/src/sets/tdm.rs:1903:                vec![Effect::draw_cards(2), Effect::discard_cards(1)],
mtg-cards/src/sets/tdm.rs:2344:    // Jeskai: whenever you cast noncreature spell, draw a card then discard.
mtg-cards/src/sets/tdm.rs:2354:                "Jeskai — Whenever you cast a noncreature spell, draw a card, then discard a card.",
mtg-cards/src/sets/tdm.rs:2355:                vec![Effect::draw_cards(1), Effect::discard_cards(1)],
mtg-cards/src/sets/tdm.rs:2599:    // Instant {4}{U}. Costs {2} less if you cast another spell this turn. Draw 3, discard 1.
mtg-cards/src/sets/tdm.rs:2607:                vec![Effect::draw_cards(3), Effect::discard_cards(1)],
mtg-cards/src/sets/tdm.rs:3107:                "At the beginning of your end step, you may discard your hand. If you do, draw cards equal to the number of spells you've cast this turn.",
mtg-cards/src/sets/tdm.rs:3109:                vec![Effect::Custom("You may discard your hand. If you do, draw cards equal to the number of spells you've cast this turn.".into())],
mtg-cards/src/sets/tdm.rs:3328:                    "When this creature enters, it deals 2 damage to any target and you gain 2 life. If a player is dealt damage this way, they discard a card.",
mtg-cards/src/sets/tdm.rs:3330:                         Effect::Custom("If a player is dealt damage this way, they discard a card.".into())],
mtg-cards/src/sets/tdm.rs:3493:                    "Whenever this creature attacks, you may draw a card. If you do, discard a card.",
mtg-cards/src/sets/tdm.rs:3494:                    vec![Effect::DrawCards { count: 1 }, Effect::DiscardCards { count: 1 }],
mtg-cards/src/sets/tdm.rs:3511:                    "When Tersa Lightshatter enters, discard up to two cards, then draw that many cards.",
mtg-cards/src/sets/tdm.rs:3512:                    vec![Effect::Custom("Discard up to two cards, then draw that many cards.".into())],
mtg-cards/src/sets/tdm.rs:3818:                vec![Effect::DrawCards { count: 1 }, Effect::Custom("You may discard a card. When you discard a nonland card this way, Glacial Dragonhunt deals 3 damage to target creature.".into())],
mtg-cards/src/sets/tdm.rs:3981:                "{1}{R}, Discard your hand, Sacrifice this enchantment: Draw two cards.",
mtg-cards/src/sets/tdm.rs:3982:                vec![Cost::pay_mana("{1}{R}"), Cost::Custom("Discard your hand".into()), Cost::SacrificeSelf],
mtg-cards/src/sets/tdm.rs:4077:                "Ward -- Discard a card.",
mtg-cards/src/sets/tdm.rs:4078:                vec![StaticEffect::Custom("Ward -- Discard a card.".into())]),
mtg-cards/src/sets/tdm.rs:4267:                vec![Effect::DrawCards { count: 3 }, Effect::Custom("Discard two cards unless you discard a creature card.".into())],
```

## Assistant

I see one more potential match: FDN line 6433 — Kiora, "draw two cards, then discard two cards" — that's already just controller loot, not opponent discard. The rest are either controller self-discard or complex conditional effects. No additional `opponent discards` patterns missed.

Now let me update the docs. First, the work queue:

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
    69→---
    70→
    71→## Batch 3: OpponentDiscards
    72→
    73→**Engine**: Need `Effect::DiscardOpponents { count }` or `Effect::OpponentDiscards { count }` — similar to `LoseLifeOpponents`. The existing `DiscardCards` targets the controller.
    74→
    75→**Cards:**
    76→- [ ] FDN line ~617: Burglar Rat — ETB `Custom("Each opponent discards a card.")`
    77→- [ ] ECL line ~596: Dream Seizer — `Custom("Put a -1/-1 counter on Dream Seizer. Each opponent discards a card.")` → `Effect::add_counters("-1/-1", 1), Effect::opponent_discards(1)` (self-counter part now works via Batch 2 source fallback; only needs OpponentDiscards)
    78→- [ ] FDN line ~5520: (check card) — `Custom("...if an opponent lost life this turn, each opponent discards a card.")` — conditional, may be harder
    79→
    80→---
    81→
    82→## Batch 4: Simple token creation — stub cards with full token text
    83→
```

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/work-queue.md",
  "new_string": "## Batch 3: OpponentDiscards [DONE 2026-02-13]\n\n**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`.\n\n**Cards fixed (4):**\n- [x] FDN: Burglar Rat — `Custom(\"Each opponent discards a card.\")` → `Effect::discard_opponents(1)`\n- [x] FDN: Arbiter of Woe — compound Custom → `discard_opponents(1), lose_life_opponents(2), draw_cards(1), gain_life(2)`\n- [x] FDN: Bloodtithe Collector — `Custom(conditional)` → `Effect::discard_opponents(1)` (condition \"if an opponent lost life this turn\" not modeled; trigger fires unconditionally)\n- [x] ECL: Dream Seizer — `Custom(compound)` → `add_counters(\"-1/-1\", 1), discard_opponents(1)` (self-counter via Batch 2 source fallback)",
  "old_string": "## Batch 3: OpponentDiscards\n\n**Engine**: Need `Effect::DiscardOpponents { count }` or `Effect::OpponentDiscards { count }` — similar to `LoseLifeOpponents`. The existing `DiscardCards` targets the controller.\n\n**Cards:**\n- [ ] FDN line ~617: Burglar Rat — ETB `Custom(\"Each opponent discards a card.\")`\n- [ ] ECL line ~596: Dream Seizer — `Custom(\"Put a -1/-1 counter on Dream Seizer. Each opponent discards a card.\")` → `Effect::add_counters(\"-1/-1\", 1), Effect::opponent_discards(1)` (self-counter part now works via Batch 2 source fallback; only needs OpponentDiscards)\n- [ ] FDN line ~5520: (check card) — `Custom(\"...if an opponent lost life this turn, each opponent discards a card.\")` — conditional, may be harder",
  "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 update the per-set remediation docs. Let me read them to find the right sections:

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/fdn-remediation.md",
  "pattern": "Burglar Rat|Arbiter of Woe|Bloodtithe Collector"
}
```

## Assistant

**Result** (success):
```
220:- [ ] **Burglar Rat** -- What works: creature body (1/1). What's broken: ETB `Effect::Custom("Each opponent discards a card.")`.
```

## 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": "Dream Seizer"
}
```

## Assistant

**Result** (success):
```
185:- [ ] **Dream Seizer** — What works: flying. What's broken: ETB `Effect::Custom("blight 1, each opponent discards")`.
```

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/fdn-remediation.md",
  "pattern": "Arbiter of Woe|Bloodtithe Collector"
}
```

## Assistant

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

## Assistant

### Tool: Grep

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

## Assistant

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

## Assistant

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-i": true,
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/fdn-remediation.md",
  "pattern": "Tier 3|tier.?3|## (Fixed|Remaining|Not yet)"
}
```

## Assistant

**Result** (success):
```
451:*(Many more partial cards exist in the Tier 3 batch 2+ sections -- see Stub section for cards that degrade further)*
548:*(This section continues for approximately 230 more cards. The pattern is the same: cards from the Tier 3 batch 2 onward (line ~3170 in fdn.rs) through the "New Creatures", "New Instants and Sorceries", "New Lands", "New Artifacts", "New Enchantments", and "Other" sections all use generic placeholder `Effect::Custom("ETB effect.")`, `Effect::Custom("Spell effect.")`, `StaticEffect::Custom("Static effect.")` etc. These are all stubs.)*
```

## Assistant

### Tool: Grep

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

## Assistant

**Result** (success):
```
3:## Overview
13:## How to Fix Cards
15:### Engine Context
85:### Adding a New Effect Type
100:## Complete Cards
216:## Partial Cards
455:## Stub Cards
459:### Cards with generic placeholder Custom effects (all non-functional)
550:### Notable high-value stubs that should be prioritized:
566:## Missing Cards
572:## Priority Remediation Roadmap
574:### Phase 1: Engine effects (unblocks many cards at once)
584:### Phase 2: Fix easy card-level issues
592:### Phase 3: Equipment system
596:### Phase 4: Modal spells and complex effects
602:### Phase 5: Complete remaining stubs
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
   216→## Partial Cards
   217→
   218→These cards have SOME typed effects that work but also use `Effect::Custom(...)`, `StaticEffect::Custom(...)`, or `Cost::Custom(...)` for one or more abilities. The Custom parts are no-ops during gameplay.
   219→
   220→- [ ] **Burglar Rat** -- What works: creature body (1/1). What's broken: ETB `Effect::Custom("Each opponent discards a card.")`.
   221→  - **Java source**: `Mage.Sets/src/mage/cards/b/BurglarRat.java`
   222→  - **What it should do**: Each opponent discards a card on ETB
   223→  - **Fix needed**: Replace with `Effect::DiscardCards { count: 1 }` but modify to target opponents, or add `Effect::OpponentDiscards { count }` variant
   224→
   225→- [ ] **Campus Guide** -- What works: creature body. ETB SearchLibrary NOW WORKS (NOW IMPLEMENTED).
```

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/fdn-remediation.md",
  "new_string": "- [x] **Burglar Rat** -- FIXED (Batch 3). ETB now uses `Effect::discard_opponents(1)`.",
  "old_string": "- [ ] **Burglar Rat** -- What works: creature body (1/1). What's broken: ETB `Effect::Custom(\"Each opponent discards a card.\")`.\n  - **Java source**: `Mage.Sets/src/mage/cards/b/BurglarRat.java`\n  - **What it should do**: Each opponent discards a card on ETB\n  - **Fix needed**: Replace with `Effect::DiscardCards { count: 1 }` but modify to target opponents, or add `Effect::OpponentDiscards { count }` variant",
  "replace_all": false
}
```

## Assistant

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

## Assistant

Now update ECL remediation:

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/ecl-remediation.md",
  "new_string": "- [x] **Dream Seizer** — FIXED (Batch 3). ETB now uses `add_counters(\"-1/-1\", 1), discard_opponents(1)`.",
  "old_string": "- [ ] **Dream Seizer** — What works: flying. What's broken: ETB `Effect::Custom(\"blight 1, each opponent discards\")`.",
  "replace_all": false
}
```

## Assistant

**Result** (error):
```
<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
   183→  - **Fix needed**: Need player choice + conditional self-counter placement.
   184→
   185→- [ ] **Dream Seizer** — What works: flying. What's broken: ETB `Effect::Custom("blight 1, each opponent discards")`.
   186→  - **Java source**: `Mage.Sets/src/mage/cards/d/DreamSeizer.java`
   187→  - **What it should do**: ETB: put -1/-1 counter on it. When you do, each opponent discards.
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
   185→- [ ] **Dream Seizer** — What works: flying. What's broken: ETB `Effect::Custom("blight 1, each opponent discards")`.
   186→  - **Java source**: `Mage.Sets/src/mage/cards/d/DreamSeizer.java`
   187→  - **What it should do**: ETB: put -1/-1 counter on it. When you do, each opponent discards.
   188→  - **Fix needed**: Split into self-counter + opponent discard trigger.
   189→
```

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/ecl-remediation.md",
  "new_string": "- [x] **Dream Seizer** — FIXED (Batch 3). ETB now uses `add_counters(\"-1/-1\", 1), discard_opponents(1)`.",
  "old_string": "- [ ] **Dream Seizer** — What works: flying. What's broken: ETB `Effect::Custom(\"blight 1, each opponent discards\")`.\n  - **Java source**: `Mage.Sets/src/mage/cards/d/DreamSeizer.java`\n  - **What it should do**: ETB: put -1/-1 counter on it. When you do, each opponent discards.\n  - **Fix needed**: Split into self-counter + opponent discard trigger.",
  "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

Now update ROADMAP.md:

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-i": true,
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/ROADMAP.md",
  "pattern": "Opponent|discard|Batch 3|DiscardOpponents"
}
```

## Assistant

**Result** (success):
```
11:[Omitted long matching line]
118:   - ~~Pulse Tracker/Marauding Blight-Priest/Vampire Spawn/Vampire Neonate: `Custom("Each opponent loses N life")` -> `LoseLifeOpponents { amount: N }`~~ **DONE**
119:   - ~~Skirmish Rhino (TDM), Champion of the Weird (ECL), Boggart Mischief (ECL): opponent life loss Custom -> `LoseLifeOpponents`~~ **DONE**
159:16. **Learn** (TLA) -- May discard to draw keyword action.
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
   115→
   116→2. **Fix easy card-level bugs** -- Many cards use `Effect::Custom(...)` when a typed variant already exists. Examples:
   117→   - ~~Phyrexian Arena: `Custom("You lose 1 life.")` -> `LoseLife { amount: 1 }`~~ **DONE**
   118→   - ~~Pulse Tracker/Marauding Blight-Priest/Vampire Spawn/Vampire Neonate: `Custom("Each opponent loses N life")` -> `LoseLifeOpponents { amount: N }`~~ **DONE**
   119→   - ~~Skirmish Rhino (TDM), Champion of the Weird (ECL), Boggart Mischief (ECL): opponent life loss Custom -> `LoseLifeOpponents`~~ **DONE**
   120→   - ~~Diregraf Ghoul: `StaticEffect::Custom("Enters tapped.")` -> `StaticEffect::EntersTapped`~~ **DONE**
   121→   - Incomplete dual lands: copy the Azorius Guildgate / Bloodfell Caves pattern
   122→   - ~~Sourbread Auntie/Sting-Slinger/Blighted Blackthorn: self-counter Custom -> `AddCounters` targeting self~~ **DONE**
   123→   - ~~Day of Judgment: `Custom("Destroy all creatures.")` -> `DestroyAll`~~ **DONE**
   124→   - ~~Frenzied Goblin/Brambleback Brute: `Custom("can't block")` -> `CantBlock`~~ **DONE**
   125→   - ~~Icewind Elemental/Refute: loot/counter Custom -> typed effects~~ **DONE**
   126→   - ~~ECL RemoveCounters cards (Encumbered Reejerey, Reluctant Dounguard, Heirloom Auntie, Bristlebane Battler): Custom -> `RemoveCounters` with source fallback~~ **DONE**
   127→   - ~~Mistmeadow Council: Custom -> `draw_cards(1)`~~ **DONE**
   128→
   129→3. **Fight mechanic** -- New `Effect::Fight` variant. Two creatures deal damage equal to their power to each other. Unblocks ~10 cards.
```

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/ROADMAP.md",
  "new_string": "   - ~~Mistmeadow Council: Custom -> `draw_cards(1)`~~ **DONE**\n   - ~~Burglar Rat/Dream Seizer/Arbiter of Woe/Bloodtithe Collector: opponent discard Custom -> `DiscardOpponents { count }`~~ **DONE**\n\n3. **Fight mechanic**",
  "old_string": "   - ~~Mistmeadow Council: Custom -> `draw_cards(1)`~~ **DONE**\n\n3. **Fight mechanic**",
  "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 update the memory to record the new API:

## Assistant

### Tool: Read

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/.claude/projects/-mnt-dest-btrfs-home-jc-src-mage/memory/MEMORY.md",
  "limit": 10,
  "offset": 40
}
```

## Assistant

**Result** (success):
```
    40→- Use `StaticEffect::Boost { filter: "other Elf you control".into(), power: 1, toughness: 1 }` for lords
    41→- `Effect::LoseLifeOpponents { amount }` — each opponent loses life (distinct from DealDamageOpponents)
    42→
    43→## Card Remediation Loop
    44→- Work queue at `docs/work-queue.md` drives the batch-fix loop
    45→- Process: read queue → implement engine change (if any) → add test → fix cards → verify → update docs → check off batch
    46→- Per-set remediation docs at `docs/{fdn,tla,tdm,ecl}-remediation.md`
    47→- ROADMAP.md tracks engine-level gaps
    48→- Batch 1 (LoseLifeOpponents) done; Batch 2 (easy swaps) is next
    49→
```

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/.claude/projects/-mnt-dest-btrfs-home-jc-src-mage/memory/MEMORY.md",
  "new_string": "- `Effect::LoseLifeOpponents { amount }` — each opponent loses life (distinct from DealDamageOpponents)\n- `Effect::DiscardOpponents { count }` / `Effect::discard_opponents(count)` — each opponent discards N cards\n- AlwaysPassPlayer in tests returns empty from `choose_discard` — use DiscardingPlayer (or no decision maker) if testing discard\n\n## Card Remediation Loop\n- Work queue at `docs/work-queue.md` drives the batch-fix loop\n- Process: read queue → implement engine change (if any) → add test → fix cards → verify → update docs → check off batch\n- Per-set remediation docs at `docs/{fdn,tla,tdm,ecl}-remediation.md`\n- ROADMAP.md tracks engine-level gaps\n- Batches 1-3 done; Batch 4 (simple token creation) is next",
  "old_string": "- `Effect::LoseLifeOpponents { amount }` — each opponent loses life (distinct from DealDamageOpponents)\n\n## Card Remediation Loop\n- Work queue at `docs/work-queue.md` drives the batch-fix loop\n- Process: read queue → implement engine change (if any) → add test → fix cards → verify → update docs → check off batch\n- Per-set remediation docs at `docs/{fdn,tla,tdm,ecl}-remediation.md`\n- ROADMAP.md tracks engine-level gaps\n- Batch 1 (LoseLifeOpponents) done; Batch 2 (easy swaps) is next",
  "replace_all": false
}
```

## Assistant

**Result** (success):
```
The file /mnt/dest-btrfs/home/jc/.claude/projects/-mnt-dest-btrfs-home-jc-src-mage/memory/MEMORY.md has been updated successfully.
```

## Assistant

Let me do a final verification that everything compiles and all tests pass:

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "cargo check -p mtg-cards && cargo test --lib 2>&1 | tail -20",
  "description": "Final check and test run",
  "timeout": 120000
}
```

## Assistant

**Result** (success):
```
test concurrency::tests::game_state_clone_is_independent ... ok
test framework::tests::framework_bolt_kills_bear ... ok
test framework::tests::framework_custom_starting_life ... 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_gain_life_effect ... ok
test concurrency::tests::gymnasium_env_runs_end_to_end ... ok
test framework::tests::framework_life_totals ... ok
test framework::tests::framework_multiple_permanents ... ok
test framework::tests::framework_setup_battlefield ... ok
test framework::tests::framework_setup_graveyard ... ok
test framework::tests::framework_setup_hand ... 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 concurrency::tests::throughput_baseline ... ok

test result: ok. 17 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.61s
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.06s
```

## Assistant

All 247 tests pass (52 + 18 + 160 + 17). Batch 3 is complete.

## Summary: Batch 3 — OpponentDiscards

**Engine changes:**
- Added `Effect::DiscardOpponents { count: u32 }` variant to the Effect enum
- Added `Effect::discard_opponents(count)` constructor
- Added match arm in `execute_effects` — iterates opponents, each calls `choose_discard` on their decision maker, then moves discarded cards to graveyard
- Added test `discard_opponents_effect` (with a `DiscardingPlayer` decision maker that actually selects cards to discard)

**Cards fixed (4):**
| Card | Set | Change |
|------|-----|--------|
| Burglar Rat | FDN | `Custom("Each opponent discards...")` → `discard_opponents(1)` |
| Arbiter of Woe | FDN | Compound Custom → `discard_opponents(1), lose_life_opponents(2), draw_cards(1), gain_life(2)` |
| Bloodtithe Collector | FDN | Conditional Custom → `discard_opponents(1)` (condition not modeled) |
| Dream Seizer | ECL | Compound Custom → `add_counters("-1/-1", 1), discard_opponents(1)` |

**Docs updated:** work-queue.md, fdn-remediation.md, ecl-remediation.md, ROADMAP.md, MEMORY.md
