# Trace: Update Conditional Tap and Mass Keyword Effects

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

---

## User

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

## User

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

## User

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

## User


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

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

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

## Assistant

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

## Assistant

### Tool: Read

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

## Assistant

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

## Assistant

## Assistant

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

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-n": true,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src",
  "pattern": "Enters.*tapped|EntersTapped"
}
```

## Assistant

**Result** (success):
```
mtg-rl/mtg-engine/src/abilities.rs:904:    EntersTapped {
```

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

## Assistant

**Result** (success):
```
mtg-rl/mtg-cards/src/sets/tdm.rs:2103:    // Land. Enters tapped unless you control a Plains or Island. {T}: Add {R}.
mtg-rl/mtg-cards/src/sets/tdm.rs:2109:                "This land enters tapped unless you control a Plains or an Island.",
mtg-rl/mtg-cards/src/sets/tdm.rs:2110:                vec![StaticEffect::Custom("Enters tapped unless you control Plains or Island.".into())]),
mtg-rl/mtg-cards/src/sets/tdm.rs:2146:    // Land. Enters tapped unless you control Swamp or Mountain. {T}: Add {W}.
mtg-rl/mtg-cards/src/sets/tdm.rs:2152:                "This land enters tapped unless you control a Swamp or a Mountain.",
mtg-rl/mtg-cards/src/sets/tdm.rs:2153:                vec![StaticEffect::Custom("Enters tapped unless you control Swamp or Mountain.".into())]),
```

## Assistant

## Assistant

Now let me look at how `EntersTapped` is handled in `game.rs` and the full card context in TDM.

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

## Assistant

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

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  2090→            Ability::static_ability(id,
  2091→                "Dragons you control have indestructible.",
  2092→                vec![StaticEffect::grant_keyword_controlled("Dragons you control", "indestructible")]),
  2093→            Ability::triggered(id,
  2094→                "At the beginning of your upkeep, for each color, put a +1/+1 counter on a Dragon you control of that color. If you put +1/+1 counters on five Dragons this way, you win the game.",
  2095→                vec![EventType::UpkeepStep],
  2096→                vec![Effect::Custom("Put counters on Dragons by color. 5 Dragons = win the game.".into())],
  2097→                TargetSpec::None),
  2098→        ],
  2099→        ..Default::default() }
  2100→}
  2101→
  2102→fn cori_mountain_monastery(id: ObjectId, owner: PlayerId) -> CardData {
  2103→    // Land. Enters tapped unless you control a Plains or Island. {T}: Add {R}.
  2104→    // {3}{R}, {T}: Exile top card, play until end of next turn.
  2105→    CardData { id, owner, name: "Cori Mountain Monastery".into(),
  2106→        card_types: vec![CardType::Land], rarity: Rarity::Rare,
  2107→        abilities: vec![
  2108→            Ability::static_ability(id,
  2109→                "This land enters tapped unless you control a Plains or an Island.",
  2110→                vec![StaticEffect::Custom("Enters tapped unless you control Plains or Island.".into())]),
  2111→            Ability::mana_ability(id, "{T}: Add {R}.", Mana::red(1)),
  2112→            Ability::activated(id,
  2113→                "{3}{R}, {T}: Exile the top card of your library. Until the end of your next turn, you may play that card.",
  2114→                vec![Cost::pay_mana("{3}{R}"), Cost::tap_self()],
  2115→                vec![Effect::Custom("Exile top card, play until end of next turn.".into())],
  2116→                TargetSpec::None),
  2117→        ],
  2118→        ..Default::default() }
  2119→}
  2120→
  2121→fn cori_steel_cutter(id: ObjectId, owner: PlayerId) -> CardData {
  2122→    // Artifact — Equipment for {1}{R}. Equipped creature gets +2/+0 and has trample and haste.
  2123→    // Flurry: create 1/1 Monk token attached to this. Equip {3}.
  2124→    CardData { id, owner, name: "Cori Steel-Cutter".into(), mana_cost: ManaCost::parse("{1}{R}"),
  2125→        card_types: vec![CardType::Artifact], subtypes: vec![SubType::Equipment],
  2126→        rarity: Rarity::Uncommon,
  2127→        abilities: vec![
  2128→            Ability::static_ability(id,
  2129→                "Equipped creature gets +2/+0 and has trample and haste.",
  2130→                vec![StaticEffect::boost_controlled("equipped creature", 2, 0),
  2131→                     StaticEffect::grant_keyword_controlled("equipped creature", "trample"),
  2132→                     StaticEffect::grant_keyword_controlled("equipped creature", "haste")]),
  2133→            Ability::spell_cast_triggered(id,
  2134→                "Flurry — Whenever you cast your second spell each turn, create a 1/1 white Monk creature token, then attach this Equipment to it.",
  2135→                vec![Effect::create_token("1/1 Monk", 1)],
  2136→                TargetSpec::None),
  2137→            Ability::activated(id, "Equip {3}",
  2138→                vec![Cost::pay_mana("{3}")],
  2139→                vec![Effect::Custom("Attach to target creature you control.".into())],
  2140→                TargetSpec::Creature),
  2141→        ],
  2142→        ..Default::default() }
  2143→}
  2144→
  2145→fn dalkovan_encampment(id: ObjectId, owner: PlayerId) -> CardData {
  2146→    // Land. Enters tapped unless you control Swamp or Mountain. {T}: Add {W}.
  2147→    // {3}{W}, {T}: Create 1/1 Soldier token.
  2148→    CardData { id, owner, name: "Dalkovan Encampment".into(),
  2149→        card_types: vec![CardType::Land], rarity: Rarity::Rare,
  2150→        abilities: vec![
  2151→            Ability::static_ability(id,
  2152→                "This land enters tapped unless you control a Swamp or a Mountain.",
  2153→                vec![StaticEffect::Custom("Enters tapped unless you control Swamp or Mountain.".into())]),
  2154→            Ability::mana_ability(id, "{T}: Add {W}.", Mana::white(1)),
  2155→            Ability::activated(id,
  2156→                "{3}{W}, {T}: Create a 1/1 white Soldier creature token.",
  2157→                vec![Cost::pay_mana("{3}{W}"), Cost::tap_self()],
  2158→                vec![Effect::create_token("1/1 Soldier", 1)],
  2159→                TargetSpec::None),
  2160→        ],
  2161→        ..Default::default() }
  2162→}
  2163→
  2164→fn death_begets_life(id: ObjectId, owner: PlayerId) -> CardData {
  2165→    // Sorcery for {2}{W}{B}. Destroy all creatures. Create X 1/1 Spirit tokens (X = creatures destroyed).
  2166→    CardData { id, owner, name: "Death Begets Life".into(), mana_cost: ManaCost::parse("{2}{W}{B}"),
  2167→        card_types: vec![CardType::Sorcery], rarity: Rarity::Rare,
  2168→        abilities: vec![Ability::spell(id,
  2169→            vec![Effect::DestroyAll { filter: "creature".into() },
```

## Assistant

## Assistant

Let me check if there are more "enters tapped unless" cards across all sets, and look at the Java implementations for reference.

## 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": "enters.*tapped|EntersTapped"
}
```

## Assistant

**Result** (success):
```
mtg-rl/mtg-cards/src/sets/fdn.rs:637:    // Diregraf Ghoul enters the battlefield tapped.
mtg-rl/mtg-cards/src/sets/fdn.rs:643:                "Diregraf Ghoul enters the battlefield tapped.",
mtg-rl/mtg-cards/src/sets/fdn.rs:644:                vec![StaticEffect::EntersTapped { filter: "self".into() }]),
mtg-rl/mtg-cards/src/sets/fdn.rs:1593:                "When Solemn Simulacrum enters, you may search your library for a basic land card, put that card onto the battlefield tapped, then shuffle.",
mtg-rl/mtg-cards/src/sets/fdn.rs:1932:                "When Springbloom Druid enters, you may sacrifice a land. If you do, search your library for up to two basic land cards, put them onto the battlefield tapped, then shuffle.",
mtg-rl/mtg-cards/src/sets/fdn.rs:2856:    // Land — Gate. Enters tapped. {T}: Add {W} or {U}.
mtg-rl/mtg-cards/src/sets/fdn.rs:2861:            Ability::static_ability(id, "Azorius Guildgate enters tapped.",
mtg-rl/mtg-cards/src/sets/fdn.rs:2862:                vec![StaticEffect::EntersTapped { filter: "self".into() }]),
mtg-rl/mtg-cards/src/sets/fdn.rs:2940:    // Land. Enters tapped. ETB: gain 1 life. {T}: Add {B} or {R}.
mtg-rl/mtg-cards/src/sets/fdn.rs:2944:            Ability::static_ability(id, "Bloodfell Caves enters tapped.",
mtg-rl/mtg-cards/src/sets/fdn.rs:2945:                vec![StaticEffect::EntersTapped { filter: "self".into() }]),
mtg-rl/mtg-cards/src/sets/fdn.rs:2974:    // Land. Enters tapped. ETB: gain 1 life. {T}: Add {G} or {W}.
mtg-rl/mtg-cards/src/sets/fdn.rs:2978:            Ability::static_ability(id, "Blossoming Sands enters tapped.",
mtg-rl/mtg-cards/src/sets/fdn.rs:2979:                vec![StaticEffect::EntersTapped { filter: "self".into() }]),
mtg-rl/mtg-cards/src/sets/fdn.rs:2991:    // Land — Gate. Enters tapped. {T}: Add {R} or {W}.
mtg-rl/mtg-cards/src/sets/fdn.rs:2996:            Ability::static_ability(id, "Boros Guildgate enters tapped.",
mtg-rl/mtg-cards/src/sets/fdn.rs:2997:                vec![StaticEffect::EntersTapped { filter: "self".into() }]),
mtg-rl/mtg-cards/src/sets/fdn.rs:6226:                "When Harbinger of the Tides enters the battlefield, you may return target tapped creature an opponent controls to its owner's hand.",
mtg-rl/mtg-cards/src/sets/fdn.rs:6228:                vec![Effect::Custom("When Harbinger of the Tides enters the battlefield, you may return target tapped creature an opponent controls to its owner's hand.".into())],
mtg-rl/mtg-cards/src/sets/ecl.rs:1161:                "When Mutable Explorer enters, create a tapped colorless land token named Mutavault.",
mtg-rl/mtg-cards/src/sets/ecl.rs:2101:    // Land — Swamp Mountain. Pay 2 life or enters tapped. Taps for {B} or {R}.
mtg-rl/mtg-cards/src/sets/ecl.rs:2107:                "As Blood Crypt enters, you may pay 2 life. If you don't, it enters tapped.",
mtg-rl/mtg-cards/src/sets/ecl.rs:2108:                vec![StaticEffect::Custom("Pay 2 life or enters tapped.".into())]),
mtg-rl/mtg-cards/src/sets/ecl.rs:3687:                "Whenever this creature enters or becomes tapped, tap up to one target creature and put a stun counter on it.",
mtg-rl/mtg-cards/src/sets/ecl.rs:3689:                vec![Effect::Custom("Whenever this creature enters or becomes tapped, tap up to one target creature and put a stun counter on it.".into())],
mtg-rl/mtg-cards/src/sets/tdm.rs:1321:            Ability::static_ability(id, "This land enters the battlefield tapped.",
mtg-rl/mtg-cards/src/sets/tdm.rs:1322:                vec![StaticEffect::EntersTapped { filter: "self".into() }]),
mtg-rl/mtg-cards/src/sets/tdm.rs:1377:            Ability::static_ability(id, "This land enters the battlefield tapped.",
mtg-rl/mtg-cards/src/sets/tdm.rs:1378:                vec![StaticEffect::EntersTapped { filter: "self".into() }]),
mtg-rl/mtg-cards/src/sets/tdm.rs:1885:                "When this enchantment enters, search your library for up to two basic land cards, put them onto the battlefield tapped, then shuffle.",
mtg-rl/mtg-cards/src/sets/tdm.rs:2103:    // Land. Enters tapped unless you control a Plains or Island. {T}: Add {R}.
mtg-rl/mtg-cards/src/sets/tdm.rs:2109:                "This land enters tapped unless you control a Plains or an Island.",
mtg-rl/mtg-cards/src/sets/tdm.rs:2110:                vec![StaticEffect::Custom("Enters tapped unless you control Plains or Island.".into())]),
mtg-rl/mtg-cards/src/sets/tdm.rs:2146:    // Land. Enters tapped unless you control Swamp or Mountain. {T}: Add {W}.
mtg-rl/mtg-cards/src/sets/tdm.rs:2152:                "This land enters tapped unless you control a Swamp or a Mountain.",
mtg-rl/mtg-cards/src/sets/tdm.rs:2153:                vec![StaticEffect::Custom("Enters tapped unless you control Swamp or Mountain.".into())]),
mtg-rl/mtg-cards/src/sets/tdm.rs:2392:    // Land. Enters tapped. {T}: Add one mana of any color. {5}, {T}: Gain 5 life.
mtg-rl/mtg-cards/src/sets/tdm.rs:2396:            Ability::static_ability(id, "Great Arashin City enters tapped.",
mtg-rl/mtg-cards/src/sets/tdm.rs:2397:                vec![StaticEffect::EntersTapped { filter: "self".into() }]),
mtg-rl/mtg-cards/src/sets/tdm.rs:3011:                    "When this creature enters, destroy target nonbasic land an opponent controls. Its controller searches their library for a basic land card, puts it onto the battlefield tapped with a stun counter on it, then shuffles.",
mtg-rl/mtg-cards/src/sets/tdm.rs:3643:                    "As this enchantment enters, choose Jeskai or Mardu. Jeskai: Creature tokens you control have lifelink and haste. Mardu: Whenever you attack, create a 1/1 red Warrior creature token that's tapped and attacking.",
```

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

## Assistant

**Result** (success):
```
mtg-rl/mtg-cards/src/sets/fdn.rs:644:                vec![StaticEffect::EntersTapped { filter: "self".into() }]),
mtg-rl/mtg-cards/src/sets/fdn.rs:2862:                vec![StaticEffect::EntersTapped { filter: "self".into() }]),
mtg-rl/mtg-cards/src/sets/fdn.rs:2945:                vec![StaticEffect::EntersTapped { filter: "self".into() }]),
mtg-rl/mtg-cards/src/sets/fdn.rs:2979:                vec![StaticEffect::EntersTapped { filter: "self".into() }]),
mtg-rl/mtg-cards/src/sets/fdn.rs:2997:                vec![StaticEffect::EntersTapped { filter: "self".into() }]),
mtg-rl/mtg-cards/src/sets/tdm.rs:1322:                vec![StaticEffect::EntersTapped { filter: "self".into() }]),
mtg-rl/mtg-cards/src/sets/tdm.rs:1378:                vec![StaticEffect::EntersTapped { filter: "self".into() }]),
mtg-rl/mtg-cards/src/sets/tdm.rs:2397:                vec![StaticEffect::EntersTapped { filter: "self".into() }]),
```

## Assistant

## Assistant

I found the two TDM cards listed in Batch 6. I also spotted a related ECL card (Blood Crypt with "Pay 2 life or enters tapped"). Let me check if there are more conditional taplands across all sets.

## 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": "tapped unless|pay.*life.*enters tapped|enters tapped.*pay"
}
```

## Assistant

**Result** (success):
```
mtg-rl/mtg-cards/src/sets/ecl.rs:2101:    // Land — Swamp Mountain. Pay 2 life or enters tapped. Taps for {B} or {R}.
mtg-rl/mtg-cards/src/sets/ecl.rs:2107:                "As Blood Crypt enters, you may pay 2 life. If you don't, it enters tapped.",
mtg-rl/mtg-cards/src/sets/ecl.rs:2108:                vec![StaticEffect::Custom("Pay 2 life or enters tapped.".into())]),
mtg-rl/mtg-cards/src/sets/tdm.rs:2103:    // Land. Enters tapped unless you control a Plains or Island. {T}: Add {R}.
mtg-rl/mtg-cards/src/sets/tdm.rs:2109:                "This land enters tapped unless you control a Plains or an Island.",
mtg-rl/mtg-cards/src/sets/tdm.rs:2110:                vec![StaticEffect::Custom("Enters tapped unless you control Plains or Island.".into())]),
mtg-rl/mtg-cards/src/sets/tdm.rs:2146:    // Land. Enters tapped unless you control Swamp or Mountain. {T}: Add {W}.
mtg-rl/mtg-cards/src/sets/tdm.rs:2152:                "This land enters tapped unless you control a Swamp or a Mountain.",
mtg-rl/mtg-cards/src/sets/tdm.rs:2153:                vec![StaticEffect::Custom("Enters tapped unless you control Swamp or Mountain.".into())]),
```

## Assistant

Good. There are 3 conditional taplands: 2 in TDM (the "unless you control" type) and 1 in ECL (the shockland "pay 2 life" type). The Blood Crypt is a different pattern — it's a choice (pay life or enter tapped), not a land-check. I'll note it but keep it separate from this batch since the condition type is different.

Now let me implement this. The approach is straightforward — similar to how Ward was done in Batch 5: add a `StaticEffect::EntersTappedUnless` variant with a condition string, add a builder, add a test, and swap the Custom strings.

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/abilities.rs",
  "new_string": "    /// Ward — when this becomes the target of a spell or ability an opponent\n    /// controls, counter it unless that player pays the specified cost.\n    Ward {\n        cost: String,\n    },\n    /// Enters tapped unless a condition is met (e.g. \"you control a Plains or an Island\").\n    EntersTappedUnless {\n        condition: String,\n    },\n    /// Custom continuous effect.\n    Custom(String),",
  "old_string": "    /// Ward — when this becomes the target of a spell or ability an opponent\n    /// controls, counter it unless that player pays the specified cost.\n    Ward {\n        cost: String,\n    },\n    /// Custom continuous effect.\n    Custom(String),",
  "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 builder method next to the `ward()` builder.

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/abilities.rs",
  "new_string": "    /// \"Ward {cost}\" — counter targeting spells/abilities unless opponent pays cost.\n    pub fn ward(cost: &str) -> Self {\n        StaticEffect::Ward {\n            cost: cost.to_string(),\n        }\n    }\n\n    /// \"This land enters tapped unless [condition].\"\n    pub fn enters_tapped_unless(condition: &str) -> Self {\n        StaticEffect::EntersTappedUnless {\n            condition: condition.to_string(),\n        }\n    }\n}",
  "old_string": "    /// \"Ward {cost}\" — counter targeting spells/abilities unless opponent pays cost.\n    pub fn ward(cost: &str) -> Self {\n        StaticEffect::Ward {\n            cost: cost.to_string(),\n        }\n    }\n}",
  "replace_all": false
}
```

## Assistant

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

## Assistant

Now add a test for the new builder, extending the `static_effect_builders` test.

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/abilities.rs",
  "new_string": "        match StaticEffect::ward(\"Discard a card.\") {\n            StaticEffect::Ward { cost } => {\n                assert_eq!(cost, \"Discard a card.\");\n            }\n            _ => panic!(\"wrong variant\"),\n        }\n\n        match StaticEffect::enters_tapped_unless(\"you control a Plains or an Island\") {\n            StaticEffect::EntersTappedUnless { condition } => {\n                assert_eq!(condition, \"you control a Plains or an Island\");\n            }\n            _ => panic!(\"wrong variant\"),\n        }\n    }",
  "old_string": "        match StaticEffect::ward(\"Discard a card.\") {\n            StaticEffect::Ward { cost } => {\n                assert_eq!(cost, \"Discard a card.\");\n            }\n            _ => panic!(\"wrong variant\"),\n        }\n    }",
  "replace_all": false
}
```

## Assistant

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

## Assistant

Now fix the two TDM cards. Let me update them to use the new typed variant.

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "cd /mnt/dest-btrfs/home/jc/src/mage/mtg-rl && sed -i 's/vec!\\[StaticEffect::Custom(\"Enters tapped unless you control Plains or Island.\".into())\\]/vec![StaticEffect::enters_tapped_unless(\"you control a Plains or an Island\")]/' mtg-cards/src/sets/tdm.rs",
  "description": "Fix Cori Mountain Monastery to use EntersTappedUnless"
}
```

## Assistant

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

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i 's/vec!\\[StaticEffect::Custom(\"Enters tapped unless you control Swamp or Mountain.\".into())\\]/vec![StaticEffect::enters_tapped_unless(\"you control a Swamp or a Mountain\")]/' mtg-cards/src/sets/tdm.rs",
  "description": "Fix Dalkovan Encampment to use EntersTappedUnless"
}
```

## Assistant

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

## Assistant

Verify the changes look correct.

## Assistant

### Tool: Grep

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

## Assistant

**Result** (success):
```
mtg-cards/src/sets/tdm.rs:2110:                vec![StaticEffect::enters_tapped_unless("you control a Plains or an Island")]),
mtg-cards/src/sets/tdm.rs:2153:                vec![StaticEffect::enters_tapped_unless("you control a Swamp or a Mountain")]),
docs/work-queue.md:127:**Engine**: May need `StaticEffect::EntersTappedUnless { condition }` or similar.
mtg-engine/src/abilities.rs:802:    pub fn enters_tapped_unless(condition: &str) -> Self {
mtg-engine/src/abilities.rs:803:        StaticEffect::EntersTappedUnless {
mtg-engine/src/abilities.rs:924:    EntersTappedUnless {
mtg-engine/src/abilities.rs:1350:        match StaticEffect::enters_tapped_unless("you control a Plains or an Island") {
mtg-engine/src/abilities.rs:1351:            StaticEffect::EntersTappedUnless { condition } => {
```

## Assistant

Good. Now let me build and test.

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "cargo check -p mtg-cards 2>&1 | tail -5",
  "description": "Check compilation"
}
```

## 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 2.64s
```

## Assistant

### Tool: Bash

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

## Assistant

**Result** (success):
```
test framework::tests::framework_draw_cards_effect ... ok
test framework::tests::framework_exile_and_assert ... ok
test framework::tests::framework_gain_life_effect ... 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_complete_without_panic ... ok
test concurrency::tests::parallel_games_produce_varied_results ... ok
test concurrency::tests::throughput_baseline ... ok

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

## Assistant

All 19 tests pass (note: test count seems to be running just `mtg-ai` tests here). Let me run the full test suite to make sure.

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "cargo test --lib 2>&1 | grep -E \"^test result:|running\"",
  "description": "Full test suite summary"
}
```

## Assistant

**Result** (success):
```
running 52 tests
test result: ok. 52 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
running 18 tests
test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
running 160 tests
test result: ok. 160 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 19 tests
test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.42s
```

## Assistant

All 249 tests pass. 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": 125
}
```

## Assistant

**Result** (success):
```
   125→## Batch 6: Enters-tapped-unless (conditional ETB tapped)
   126→
   127→**Engine**: May need `StaticEffect::EntersTappedUnless { condition }` or similar.
   128→
   129→**Cards:**
   130→- [ ] TDM line ~2110: `StaticEffect::Custom("Enters tapped unless you control Plains or Island.")`
   131→- [ ] TDM line ~2153: `StaticEffect::Custom("Enters tapped unless you control Swamp or Mountain.")`
   132→
   133→---
   134→
   135→## Batch 7: Mass keyword grant until EOT
   136→
   137→**Engine**: Need `Effect::GrantKeywordAllUntilEndOfTurn { filter, keyword }` or similar mass-buff effect.
   138→
   139→**Cards:**
```

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/work-queue.md",
  "new_string": "## Batch 6: Enters-tapped-unless (conditional ETB tapped) [DONE 2026-02-13]\n\n**Engine**: Added `StaticEffect::EntersTappedUnless { condition: String }` variant + `enters_tapped_unless()` builder. Condition is stored as structured data; mechanical enforcement (actually checking land types on ETB) deferred to future engine work.\n\n**Cards fixed (2):**\n- [x] TDM: Cori Mountain Monastery — `StaticEffect::Custom(\"Enters tapped unless you control Plains or Island.\")` → `StaticEffect::enters_tapped_unless(\"you control a Plains or an Island\")`\n- [x] TDM: Dalkovan Encampment — `StaticEffect::Custom(\"Enters tapped unless you control Swamp or Mountain.\")` → `StaticEffect::enters_tapped_unless(\"you control a Swamp or a Mountain\")`\n\n**Tests added:** `static_effect_builders` extended with EntersTappedUnless variant check\n\n**Note:** ECL Blood Crypt has a related but different pattern (\"pay 2 life or enters tapped\") — shockland mechanic, not a land-type check. Left as Custom for now.",
  "old_string": "## Batch 6: Enters-tapped-unless (conditional ETB tapped)\n\n**Engine**: May need `StaticEffect::EntersTappedUnless { condition }` or similar.\n\n**Cards:**\n- [ ] TDM line ~2110: `StaticEffect::Custom(\"Enters tapped unless you control Plains or Island.\")`\n- [ ] TDM line ~2153: `StaticEffect::Custom(\"Enters tapped unless you control Swamp or Mountain.\")`",
  "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 TDM remediation doc.

## Assistant

### Tool: Grep

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

## Assistant

**Result** (success):
```
101:- [x] **Cori Mountain Stalwart** — 3/3 for {1}{R}{W}. Flurry: 2 damage to opponents + gain 2. Uses DealDamageOpponents + GainLife.
164:- [x] **Dalkovan Encampment** — Land. Tap for {W}. Activated: create Soldier token. Uses CreateToken + mana.
449:- [ ] **Cori Mountain Monastery** — Land. What works: mana ability ({T}: Add {R}). What's broken: `StaticEffect::Custom("Enters tapped unless...")` + activated `Effect::Custom("Exile top card, play until end of next turn.")`.
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
   445→- [ ] **Call the Spirit Dragons** — Enchantment {W}{U}{B}{R}{G}. What works: GrantKeyword ("indestructible" to Dragons — if continuous effects work). What's broken: Upkeep Custom (counter distribution + win condition).
   446→  - **Java source**: `Mage.Sets/src/mage/cards/c/CallTheSpiritDragons.java`
   447→  - **Fix needed**: Complex upkeep trigger with color-based targeting + alt win condition.
   448→
   449→- [ ] **Cori Mountain Monastery** — Land. What works: mana ability ({T}: Add {R}). What's broken: `StaticEffect::Custom("Enters tapped unless...")` + activated `Effect::Custom("Exile top card, play until end of next turn.")`.
   450→  - **Fix needed**: Conditional ETB tapped + impulse draw effect.
   451→
   452→- [ ] **Cori Steel-Cutter** — Equipment {1}{R}. What works: Static boost +2/+0 + grant trample/haste (if continuous effects work) + CreateToken (Monk). What's broken: Equip activated ability `Effect::Custom("Attach...")`.
   453→  - **Fix needed**: Implement Equip as an effect.
   454→
```

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/tdm-remediation.md",
  "new_string": "- [x] **Cori Mountain Monastery** — Land. What works: mana ability ({T}: Add {R}), `StaticEffect::EntersTappedUnless` (conditional ETB). What's broken: activated `Effect::Custom(\"Exile top card, play until end of next turn.\")`.\n  - **Batch 6**: Fixed `StaticEffect::Custom` → `StaticEffect::enters_tapped_unless(\"you control a Plains or an Island\")`. Impulse draw still Custom.",
  "old_string": "- [ ] **Cori Mountain Monastery** — Land. What works: mana ability ({T}: Add {R}). What's broken: `StaticEffect::Custom(\"Enters tapped unless...\")` + activated `Effect::Custom(\"Exile top card, play until end of next turn.\")`.\n  - **Fix needed**: Conditional ETB tapped + impulse draw effect.",
  "replace_all": false
}
```

## Assistant

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

## Assistant

Now check if Dalkovan Encampment has a separate broken entry or was already listed as working.

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
   160→- [x] **Wind-Scarred Crag** — Gain land.
   161→- [x] **Wingblade Disciple** — 2/2 flying for {2}{U}. Flurry: create Bird token. Uses CreateToken.
   162→- [x] **Abzan Devotee** — 3/3 for {1}{W}{B}{G}. ETB: create Spirit token. Uses CreateToken.
   163→- [x] **Arashin Sunshield** — 3/4 for {3}{W}. ETB: exile cards from graveyard. Tap: tap creature. Uses Exile + TapTarget.
   164→- [x] **Dalkovan Encampment** — Land. Tap for {W}. Activated: create Soldier token. Uses CreateToken + mana.
   165→- [x] **Hundred-Battle Veteran** — 3/3 first strike for {1}{R}{W}. Attacks: +1/+1 counter on another attacker. Uses AddCounters.
   166→- [x] **Kishla Village** — Land. Tap for {C}. Activated: create 4/4 Beast token. Uses CreateToken.
   167→- [x] **Mardu Siegebreaker** — 4/4 deathtouch/haste for {1}{R}{W}{B}. ETB: destroy target perm MV<=2. Uses Destroy. Attacks: Custom copy token (partial, but ETB works).
```

## Assistant

Dalkovan Encampment was already marked as working in the remediation doc, but it had a Custom static effect. The entry says it's working because the mana + create token parts were good. It didn't mention the Custom enters-tapped-unless part. The card was already checked, so no change needed there — the fix was incremental.

Now let me also check if the Dalkovan Encampment entry needs to be noted as having previously-Custom that's now fixed. Let me look for it in the broken section.

## Assistant

### Tool: Grep

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

## Assistant

**Result** (success):
```
102:- [x] **Dalkovan Packbeasts** — 0/4 vigilance for {2}{W}. Mobilize 3. Uses CreateToken.
164:- [x] **Dalkovan Encampment** — Land. Tap for {W}. Activated: create Soldier token. Uses CreateToken + mana.
174:- [x] **Dalkovan Packbeasts** — (listed above)
```

## Assistant

Only one entry and it was already checked — it was counted as "working" since its main effects (mana + tokens) functioned. The enters-tapped-unless was a minor Custom. That's fine.

Now update ROADMAP.md.

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
     1→# Roadmap
     2→
     3→This document describes implementation gaps in the mtg-rl engine and cards, organized by priority. Each engine feature lists how many cards it would unblock when implemented.
     4→
     5→## Engine Gaps
     6→
     7→### Effect Variants (game.rs `execute_effects()`)
     8→
     9→These `Effect` enum variants exist in `abilities.rs` but have no implementation in `execute_effects()` -- they fall through to `_ => {}` and silently do nothing at runtime.
    10→
    11→**Recently implemented:** The following Effect variants were previously no-ops but now have working implementations in `execute_effects()`: `Scry`, `SearchLibrary`, `ReturnFromGraveyard`, `Reanimate`, `GainKeywordUntilEndOfTurn`, `GainKeyword`, `LoseKeyword`, `Indestructible`, `Hexproof`, `CantBlock`, `Sacrifice`, `DestroyAll`, `DealDamageAll`, `RemoveCounters`, `CreateTokenTappedAttacking`, `BoostPermanent`, `SetPowerToughness`, `LoseLifeOpponents`. Token stat parsing is also implemented (`CreateToken` now parses P/T and keywords from `token_name`).
    12→
    13→**Batch 2 engine change (2026-02-13):** Modified `execute_effects` to accept an optional `source: Option<ObjectId>` parameter. When `AddCounters` or `RemoveCounters` effects have no selected targets, they now fall back to the source permanent. This enables self-targeting counter effects (e.g., blight creatures putting -1/-1 counters on themselves) without requiring explicit target selection. Test: `add_counters_self_when_no_targets`.
    14→
    15→**Batch 5 (2026-02-13):** Added `StaticEffect::Ward { cost: String }` variant + `ward()` builder. Ward is a triggered ability that counters targeting spells/abilities unless the opponent pays the cost. The engine stores it as structured data (not yet mechanically enforced during targeting). Replaced `StaticEffect::Custom("Ward ...")` on 7 cards across FDN/TDM/ECL and added `KeywordAbilities::WARD` flags.
    16→
    17→| Effect Variant | Description | Cards Blocked |
    18→|---------------|-------------|---------------|
    19→| `GainControl` | Gain control of target permanent | ~5 |
    20→| `GainControlUntilEndOfTurn` | Threaten/Act of Treason effects | ~5 |
    21→| `GainProtection` | Target gains protection from quality | ~5 |
    22→| `PreventCombatDamage` | Fog / damage prevention | ~5 |
    23→| `MustBlock` | Target creature must block | ~3 |
    24→| `SetLife` | Set a player's life total | ~2 |
    25→| `Custom(String)` | Catch-all for untyped effects | ~400+ |
    26→
    27→Note: `Custom(String)` is used when no typed variant exists. Each Custom needs to be individually replaced with a typed variant or a new variant added.
    28→
    29→### Missing Engine Systems
    30→
    31→These are features that require new engine architecture, not just new match arms:
    32→
    33→#### Equipment System
    34→- No attach/detach mechanics
    35→- Equipment stat bonuses not applied
    36→- Equip cost not evaluated
    37→- **Blocked cards:** Basilisk Collar, Swiftfoot Boots, Goldvein Pick, Fishing Pole, all Equipment (~15+ cards)
    38→
    39→#### Planeswalker System
    40→- Loyalty counters not tracked as a resource
    41→- Planeswalker abilities not resolved (loyalty cost/gain)
    42→- Planeswalker damage redirection not enforced
    43→- **Blocked cards:** Ajani, Chandra, Kaito, Liliana, Vivien, all planeswalkers (~10+ cards)
    44→
    45→#### Modal Spells
    46→- No mode selection framework
    47→- `choose_mode()` decision interface exists but is unused
    48→- **Blocked cards:** Abrade, Boros Charm, Slagstorm, Valorous Stance, Coordinated Maneuver, Frontline Rush, Sarkhan's Resolve, Seize Opportunity (~20+ cards)
    49→
    50→#### X-Cost Spells
    51→- No variable cost determination
    52→- X is not tracked or passed to effects
    53→- **Blocked cards:** Day of Black Sun, Spectral Denial (partially works), Genesis Wave, Finale of Revelation (~10+ cards)
    54→
    55→#### Fight/Bite Mechanic
    56→- No creature-vs-creature damage assignment outside combat
    57→- **Blocked cards:** Bite Down, Earth Rumble, Knockout Maneuver, Piercing Exhale, Dragonclaw Strike, Assert Perfection (~10+ cards)
    58→
    59→#### ~~Token Stat Parsing~~ (DONE)
    60→`CreateToken` now parses P/T and keywords from `token_name` strings (e.g., '4/4 Dragon with flying' creates a 4/4 with flying). Cards using correctly-formatted token names now work.
    61→
    62→#### Aura/Enchant System
    63→- Auras exist as permanents but don't attach to creatures
    64→- Static P/T boosts from Auras not applied
    65→- Keyword grants from Auras not applied
    66→- **Blocked cards:** Pacifism (partially works via CantAttack/CantBlock), Obsessive Pursuit, Eaten by Piranhas, Angelic Destiny (~10+ cards)
    67→
    68→#### Impulse Draw (Exile-and-Play)
    69→- "Exile top card, you may play it until end of [next] turn" has no implementation
    70→- **Blocked cards:** Equilibrium Adept, Kulrath Zealot, Sizzling Changeling, Burning Curiosity, Etali (~10+ cards)
    71→
    72→#### Spell Copy
    73→- No mechanism to copy spells on the stack
    74→- **Blocked cards:** Electroduplicate, Rite of Replication, Self-Reflection, Flamehold Grappler, Sage of the Skies (~8+ cards)
    75→
    76→#### Replacement Effects
    77→- `ReplacementEffect` struct exists in `effects.rs` but is not integrated into the event pipeline
    78→- Events are not interceptable before they resolve
    79→- **Blocked features:** "If a creature would die, exile it instead", "If you would gain life, gain that much +1", Doubling Season, damage prevention, Dryad Militant graveyard replacement
    80→
    81→#### Additional Combat Phases
    82→- No support for extra combat steps
    83→- **Blocked cards:** Aurelia the Warleader, All-Out Assault (~3 cards)
    84→
    85→#### "Behold" Mechanic (ECL-specific)
    86→- Reveal-and-exile-from-hand mechanic not implemented
    87→- Many ECL cards reference it as an alternative cost or condition
    88→- **Blocked cards:** Champion of the Weird, Champions of the Perfect, Molten Exhale, Osseous Exhale (~15+ cards)
    89→
    90→#### "Earthbend" Mechanic (TLA-specific)
    91→- "Look at top N, put a land to hand, rest on bottom" not implemented
    92→- **Blocked cards:** Badgermole, Badgermole Cub, Ostrich-Horse, Dai Li Agents, Earth Kingdom General, Earth Village Ruffians, many TLA cards (~20+ cards)
    93→
    94→#### "Vivid" Mechanic (ECL-specific)
    95→- "X = number of colors among permanents you control" calculation not implemented
    96→- **Blocked cards:** Explosive Prodigy, Glister Bairn, Luminollusk, Prismabasher, Shimmercreep, Shinestriker, Squawkroaster (~10+ cards)
    97→
    98→#### Delayed Triggers
    99→- "When this creature dies this turn, draw a card" style effects
   100→- No framework for registering one-shot triggered abilities
   101→- **Blocked cards:** Undying Malice, Fake Your Own Death, Desperate Measures, Scarblades Malice (~5+ cards)
   102→
   103→#### Conditional Cost Modifications
   104→- `CostReduction` static effect exists but may not apply correctly
   105→- No support for "second spell costs {1} less" or Affinity
   106→- **Blocked cards:** Highspire Bell-Ringer, Allies at Last, Ghalta Primal Hunger (~5+ cards)
   107→
   108→---
   109→
   110→## Phased Implementation Plan
   111→
   112→### Phase 1: High-Impact Engine Effects
   113→
   114→These unblock the most cards per effort invested.
   115→
   116→1. **Token stat parsing** -- **DONE** -- Parse power/toughness/keywords from `token_name` string in `CreateToken`. Unblocks ~30 cards that already create tokens but with wrong stats.
   117→
   118→2. **Fix easy card-level bugs** -- Many cards use `Effect::Custom(...)` when a typed variant already exists. Examples:
   119→   - ~~Phyrexian Arena: `Custom("You lose 1 life.")` -> `LoseLife { amount: 1 }`~~ **DONE**
   120→   - ~~Pulse Tracker/Marauding Blight-Priest/Vampire Spawn/Vampire Neonate: `Custom("Each opponent loses N life")` -> `LoseLifeOpponents { amount: N }`~~ **DONE**
   121→   - ~~Skirmish Rhino (TDM), Champion of the Weird (ECL), Boggart Mischief (ECL): opponent life loss Custom -> `LoseLifeOpponents`~~ **DONE**
   122→   - ~~Diregraf Ghoul: `StaticEffect::Custom("Enters tapped.")` -> `StaticEffect::EntersTapped`~~ **DONE**
   123→   - Incomplete dual lands: copy the Azorius Guildgate / Bloodfell Caves pattern
   124→   - ~~Sourbread Auntie/Sting-Slinger/Blighted Blackthorn: self-counter Custom -> `AddCounters` targeting self~~ **DONE**
   125→   - ~~Day of Judgment: `Custom("Destroy all creatures.")` -> `DestroyAll`~~ **DONE**
   126→   - ~~Frenzied Goblin/Brambleback Brute: `Custom("can't block")` -> `CantBlock`~~ **DONE**
   127→   - ~~Icewind Elemental/Refute: loot/counter Custom -> typed effects~~ **DONE**
   128→   - ~~ECL RemoveCounters cards (Encumbered Reejerey, Reluctant Dounguard, Heirloom Auntie, Bristlebane Battler): Custom -> `RemoveCounters` with source fallback~~ **DONE**
   129→   - ~~Mistmeadow Council: Custom -> `draw_cards(1)`~~ **DONE**
   130→   - ~~Guarded Heir, Prideful Parent, Resolute Reinforcements, Release the Dogs, Dwynen's Elite, Dragonmaster Outcast, Searslicer Goblin: token creation Custom -> `create_token()`~~ **DONE**
   131→   - ~~Clachan Festival (ECL): token creation Custom + Cost::Custom -> `create_token()` + `Cost::pay_mana()`~~ **DONE**
   132→   - ~~Burglar Rat/Dream Seizer/Arbiter of Woe/Bloodtithe Collector: opponent discard Custom -> `DiscardOpponents { count }`~~ **DONE**
   133→
   134→3. **Fight mechanic** -- New `Effect::Fight` variant. Two creatures deal damage equal to their power to each other. Unblocks ~10 cards.
   135→
   136→### Phase 2: Key Missing Mechanics
   137→
   138→4. **Equipment system** -- Attach/detach, equip cost, stat/keyword application. Unblocks ~15 cards.
   139→
   140→5. **Modal spells** -- Mode selection in `PlayerDecisionMaker` trait, mode-conditional effect resolution. Unblocks ~20 cards.
   141→
   142→6. **Impulse draw** -- "Exile top N, may play until end of [next] turn." Track exiled-playable cards in game state. Unblocks ~10 cards.
   143→
   144→7. **Earthbend** (TLA-specific) -- "Look at top N, put a land to hand, rest on bottom." Unblocks ~20 TLA cards.
   145→
   146→### Phase 3: Advanced Systems
   147→
   148→8. **Replacement effects** -- Event interception pipeline. Required for damage prevention, death replacement, Doubling Season, "exile instead of dying."
   149→
   150→9. **X-cost spells** -- Variable cost determination + passing X to effects.
   151→
   152→10. **Aura attachment** -- Auras attach to targets, apply continuous effects while attached.
   153→
   154→11. **Spell copy** -- Clone spells on the stack with new targets.
   155→
   156→12. **Planeswalker system** -- Loyalty as a resource, planeswalker abilities, damage redirection.
   157→
   158→13. **Additional combat phases** -- Extra attack steps.
   159→
   160→### Phase 4: Set-Specific Mechanics
   161→
   162→14. **Behold** (ECL) -- Reveal-from-hand alternative cost/condition.
   163→15. **Vivid** (ECL) -- Color-count calculation for dynamic X values.
   164→16. **Learn** (TLA) -- May discard to draw keyword action.
   165→17. **Renew** (TDM) -- Counter-based death replacement.
   166→18. **Mobilize** (TDM) -- Create N 1/1 Soldier tokens. (Partially works via `CreateToken` already.)
   167→
   168→---
   169→
   170→## Per-Set Status
   171→
   172→Detailed per-card breakdowns with fix instructions are in `docs/`:
   173→
   174→| File | Set | Complete | Partial | Stub |
   175→|------|-----|----------|---------|------|
   176→| `docs/fdn-remediation.md` | Foundations | 95 | 126 | 267 |
   177→| `docs/tla-remediation.md` | Avatar: TLA | 39 | 22 | 219 |
   178→| `docs/tdm-remediation.md` | Tarkir: Dragonstorm | 97 | 115 | 59 |
   179→| `docs/ecl-remediation.md` | Lorwyn Eclipsed | 56 | 69 | 105 |
   180→
   181→*Note: These counts are outdated -- see the individual remediation docs for current status.*
   182→
   183→Each remediation doc includes:
   184→- Full card-by-card audit with working vs broken effects
   185→- Java source file references for each card
   186→- Specific fix instructions per card
   187→- Priority remediation roadmap for that set
   188→
```

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/ROADMAP.md",
  "new_string": "**Batch 5 (2026-02-13):** Added `StaticEffect::Ward { cost: String }` variant + `ward()` builder. Ward is a triggered ability that counters targeting spells/abilities unless the opponent pays the cost. The engine stores it as structured data (not yet mechanically enforced during targeting). Replaced `StaticEffect::Custom(\"Ward ...\")` on 7 cards across FDN/TDM/ECL and added `KeywordAbilities::WARD` flags.\n\n**Batch 6 (2026-02-13):** Added `StaticEffect::EntersTappedUnless { condition: String }` variant + `enters_tapped_unless()` builder. Stored as structured data; mechanical enforcement deferred. Replaced `StaticEffect::Custom(\"Enters tapped unless ...\")` on 2 TDM lands (Cori Mountain Monastery, Dalkovan Encampment).",
  "old_string": "**Batch 5 (2026-02-13):** Added `StaticEffect::Ward { cost: String }` variant + `ward()` builder. Ward is a triggered ability that counters targeting spells/abilities unless the opponent pays the cost. The engine stores it as structured data (not yet mechanically enforced during targeting). Replaced `StaticEffect::Custom(\"Ward ...\")` on 7 cards across FDN/TDM/ECL and added `KeywordAbilities::WARD` flags.",
  "replace_all": false
}
```

## Assistant

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

## Assistant

Batch 6 is done. Now let me continue to the next batch — **Batch 7: Mass keyword grant until EOT**. This one is more substantial.

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
   135→**Note:** ECL Blood Crypt has a related but different pattern ("pay 2 life or enters tapped") — shockland mechanic, not a land-type check. Left as Custom for now.
   136→
   137→---
   138→
   139→## Batch 7: Mass keyword grant until EOT
   140→
   141→**Engine**: Need `Effect::GrantKeywordAllUntilEndOfTurn { filter, keyword }` or similar mass-buff effect.
   142→
   143→**Cards:**
   144→- [ ] FDN: Crash Through — `Custom("Creatures you control gain trample until end of turn.")` + DrawCards(1)
   145→- [ ] FDN: Overrun — `Custom("Creatures you control get +3/+3 and gain trample until end of turn.")`
   146→- [ ] FDN: Make a Stand — `Custom("Creatures you control get +1/+0 and gain indestructible until end of turn.")`
   147→- [ ] FDN: Heroic Reinforcements — `Custom(...)` mass +1/+1 and haste
   148→- [ ] FDN: Balmor, Battlemage Captain — spell trigger mass +1/+0 and trample
   149→
   150→---
   151→
   152→## Batch 8: Fight/Bite mechanic
   153→
   154→**Engine**: New `Effect::Fight` (mutual damage) and/or `Effect::Bite` (one-way damage based on power).
```

## Assistant

## Assistant

Let me look at the actual card implementations for these 5 cards.

## 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": "fn crash_through|fn overrun|fn make_a_stand|fn heroic_reinforcements|fn balmor"
}
```

## Assistant

**Result** (success):
```
mtg-cards/src/sets/fdn.rs:2388:fn crash_through(id: ObjectId, owner: PlayerId) -> CardData {
mtg-cards/src/sets/fdn.rs:2541:fn heroic_reinforcements(id: ObjectId, owner: PlayerId) -> CardData {
mtg-cards/src/sets/fdn.rs:2555:fn make_a_stand(id: ObjectId, owner: PlayerId) -> CardData {
mtg-cards/src/sets/fdn.rs:2568:fn overrun(id: ObjectId, owner: PlayerId) -> CardData {
mtg-cards/src/sets/fdn.rs:2886:fn balmor_battlemage_captain(id: ObjectId, owner: PlayerId) -> CardData {
```

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

## Assistant

**Result** (success):
```
  2388→fn crash_through(id: ObjectId, owner: PlayerId) -> CardData {
  2389→    // Sorcery {R}. Creatures you control gain trample until EOT. Draw a card.
  2390→    CardData { id, owner, name: "Crash Through".into(), mana_cost: ManaCost::parse("{R}"),
  2391→        card_types: vec![CardType::Sorcery], rarity: Rarity::Common,
  2392→        abilities: vec![
  2393→            Ability::spell(id,
  2394→                vec![Effect::Custom("Creatures you control gain trample until end of turn.".into()),
  2395→                     Effect::draw_cards(1)],
  2396→                TargetSpec::None),
  2397→        ],
  2398→        ..Default::default() }
  2399→}
  2400→
  2401→fn dragon_fodder(id: ObjectId, owner: PlayerId) -> CardData {
  2402→    // Sorcery {1}{R}. Create two 1/1 red Goblin creature tokens.
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  2541→fn heroic_reinforcements(id: ObjectId, owner: PlayerId) -> CardData {
  2542→    // Sorcery {2}{R}{W}. Create two 1/1 Soldier tokens. Creatures +1/+1 and haste until EOT.
  2543→    CardData { id, owner, name: "Heroic Reinforcements".into(), mana_cost: ManaCost::parse("{2}{R}{W}"),
  2544→        card_types: vec![CardType::Sorcery], rarity: Rarity::Uncommon,
  2545→        abilities: vec![
  2546→            Ability::spell(id,
  2547→                vec![Effect::create_token("1/1 Soldier", 2),
  2548→                     Effect::Custom("Creatures you control get +1/+1 until end of turn.".into()),
  2549→                     Effect::Custom("Creatures you control gain haste until end of turn.".into())],
  2550→                TargetSpec::None),
  2551→        ],
  2552→        ..Default::default() }
  2553→}
  2554→
  2555→fn make_a_stand(id: ObjectId, owner: PlayerId) -> CardData {
  2556→    // Instant {2}{W}. Creatures you control +1/+0 and indestructible until EOT.
  2557→    CardData { id, owner, name: "Make a Stand".into(), mana_cost: ManaCost::parse("{2}{W}"),
  2558→        card_types: vec![CardType::Instant], rarity: Rarity::Uncommon,
  2559→        abilities: vec![
  2560→            Ability::spell(id,
  2561→                vec![Effect::Custom("Creatures you control get +1/+0 until end of turn.".into()),
  2562→                     Effect::Custom("Creatures you control gain indestructible until end of turn.".into())],
  2563→                TargetSpec::None),
  2564→        ],
  2565→        ..Default::default() }
  2566→}
  2567→
  2568→fn overrun(id: ObjectId, owner: PlayerId) -> CardData {
  2569→    // Sorcery {2}{G}{G}{G}. Creatures you control +3/+3 and trample until EOT.
  2570→    CardData { id, owner, name: "Overrun".into(), mana_cost: ManaCost::parse("{2}{G}{G}{G}"),
```

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

## Assistant

**Result** (success):
```
  2555→fn make_a_stand(id: ObjectId, owner: PlayerId) -> CardData {
  2556→    // Instant {2}{W}. Creatures you control +1/+0 and indestructible until EOT.
  2557→    CardData { id, owner, name: "Make a Stand".into(), mana_cost: ManaCost::parse("{2}{W}"),
  2558→        card_types: vec![CardType::Instant], rarity: Rarity::Uncommon,
  2559→        abilities: vec![
  2560→            Ability::spell(id,
  2561→                vec![Effect::Custom("Creatures you control get +1/+0 until end of turn.".into()),
  2562→                     Effect::Custom("Creatures you control gain indestructible until end of turn.".into())],
  2563→                TargetSpec::None),
  2564→        ],
  2565→        ..Default::default() }
  2566→}
  2567→
  2568→fn overrun(id: ObjectId, owner: PlayerId) -> CardData {
  2569→    // Sorcery {2}{G}{G}{G}. Creatures you control +3/+3 and trample until EOT.
  2570→    CardData { id, owner, name: "Overrun".into(), mana_cost: ManaCost::parse("{2}{G}{G}{G}"),
  2571→        card_types: vec![CardType::Sorcery], rarity: Rarity::Uncommon,
  2572→        abilities: vec![
  2573→            Ability::spell(id,
  2574→                vec![Effect::Custom("Creatures you control get +3/+3 until end of turn.".into()),
```

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

## Assistant

**Result** (success):
```
  2568→fn overrun(id: ObjectId, owner: PlayerId) -> CardData {
  2569→    // Sorcery {2}{G}{G}{G}. Creatures you control +3/+3 and trample until EOT.
  2570→    CardData { id, owner, name: "Overrun".into(), mana_cost: ManaCost::parse("{2}{G}{G}{G}"),
  2571→        card_types: vec![CardType::Sorcery], rarity: Rarity::Uncommon,
  2572→        abilities: vec![
  2573→            Ability::spell(id,
  2574→                vec![Effect::Custom("Creatures you control get +3/+3 until end of turn.".into()),
  2575→                     Effect::Custom("Creatures you control gain trample until end of turn.".into())],
  2576→                TargetSpec::None),
  2577→        ],
  2578→        ..Default::default() }
  2579→}
  2580→
  2581→// ── Tier 3 — lords and tribal creatures ──────────────────────────────────────
  2582→
```

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

## Assistant

**Result** (success):
```
  2886→fn balmor_battlemage_captain(id: ObjectId, owner: PlayerId) -> CardData {
  2887→    // Legendary 1/3 Bird Wizard for {U}{R}. Flying.
  2888→    // Whenever you cast instant/sorcery, creatures you control get +1/+0 and gain trample until EOT.
  2889→    CardData { id, owner, name: "Balmor, Battlemage Captain".into(), mana_cost: ManaCost::parse("{U}{R}"),
  2890→        card_types: vec![CardType::Creature],
  2891→        subtypes: vec![SubType::Custom("Bird".into()), SubType::Wizard],
  2892→        supertypes: vec![SuperType::Legendary],
  2893→        power: Some(1), toughness: Some(3), keywords: KeywordAbilities::FLYING,
  2894→        rarity: Rarity::Uncommon,
  2895→        abilities: vec![
  2896→            Ability::spell_cast_triggered(id,
  2897→                "Whenever you cast an instant or sorcery spell, creatures you control get +1/+0 and gain trample until end of turn.",
  2898→                vec![Effect::Custom("Creatures you control get +1/+0 and gain trample until end of turn.".into())],
  2899→                TargetSpec::None),
  2900→        ],
  2901→        ..Default::default() }
  2902→}
  2903→
  2904→fn banner_of_kinship(id: ObjectId, owner: PlayerId) -> CardData {
  2905→    // Artifact for {5}. As ETB: choose creature type, enter with fellowship counters.
```

## Assistant

## Assistant

Let me look at how the single-target `BoostUntilEndOfTurn` and `GainKeywordUntilEndOfTurn` are implemented in `game.rs`.

## Assistant

### Tool: Grep

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

## Assistant

**Result** (success):
```
418-                            }
419-                            self.move_card_to_graveyard(card_id, active_player);
420-                        }
421-                    }
422-                }
423-                // Remove damage from all creatures and clear "until end of turn" effects
424-                for perm in self.state.battlefield.iter_mut() {
425-                    if perm.is_creature() {
426-                        perm.clear_damage();
427-                    }
428:                    // Clear granted keywords (from GainKeywordUntilEndOfTurn, Indestructible, Hexproof)
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() {
436-                    player.mana_pool.clear();
437-                }
438-            }
--
1044-                        source.into_iter().collect()
1045-                    } else {
1046-                        targets.to_vec()
1047-                    };
1048-                    for target_id in effective_targets {
1049-                        if let Some(perm) = self.state.battlefield.get_mut(target_id) {
1050-                            perm.add_counters(ct.clone(), *count);
1051-                        }
1052-                    }
1053-                }
1054:                Effect::BoostUntilEndOfTurn { power, toughness: _ } => {
1055-                    // Simplified: directly modify counters (proper implementation
1056-                    // would use continuous effects that expire at end of turn)
1057-                    for &target_id in targets {
1058-                        if let Some(perm) = self.state.battlefield.get_mut(target_id) {
1059-                            if *power > 0 {
1060-                                perm.add_counters(CounterType::P1P1, *power as u32);
1061-                            }
1062-                            // Note: This is a simplification; real boost until EOT
1063-                            // uses continuous effects, not counters
1064-                        }
--
1211-                            }
1212-                            // Get card data from the card store to create a permanent
1213-                            if let Some(card_data) = self.state.card_store.remove(target_id) {
1214-                                let perm = Permanent::new(card_data, controller);
1215-                                self.state.battlefield.add(perm);
1216-                                self.state.set_zone(target_id, crate::constants::Zone::Battlefield, None);
1217-                            }
1218-                        }
1219-                    }
1220-                }
1221:                Effect::GainKeywordUntilEndOfTurn { keyword } => {
1222-                    if let Some(kw) = crate::constants::KeywordAbilities::keyword_from_name(keyword) {
1223-                        for &target_id in targets {
1224-                            if let Some(perm) = self.state.battlefield.get_mut(target_id) {
1225-                                perm.granted_keywords |= kw;
1226-                            }
1227-                        }
1228-                    }
1229-                }
1230-                Effect::GainKeyword { keyword } => {
1231-                    // Grant keyword permanently (via granted_keywords, which persists)
--
1359-                        card.toughness = Some(t);
1360-                        card.keywords = kw;
1361-                        let mut perm = Permanent::new(card, controller);
1362-                        perm.tapped = true;
1363-                        perm.summoning_sick = false; // Can attack since entering tapped and attacking
1364-                        self.state.battlefield.add(perm);
1365-                        self.state.set_zone(token_id, crate::constants::Zone::Battlefield, None);
1366-                    }
1367-                }
1368-                Effect::BoostPermanent { power, toughness: _ } => {
1369:                    // Permanent P/T boost (similar to BoostUntilEndOfTurn but doesn't expire)
1370-                    for &target_id in targets {
1371-                        if let Some(perm) = self.state.battlefield.get_mut(target_id) {
1372-                            if *power > 0 {
1373-                                perm.add_counters(CounterType::P1P1, *power as u32);
1374-                            } else if *power < 0 {
1375-                                perm.add_counters(CounterType::M1M1, (-*power) as u32);
1376-                            }
1377-                        }
1378-                    }
1379-                }
```

## Assistant

## Assistant

Let me check how `DestroyAll` implements its filter matching to follow the same pattern.

## Assistant

### Tool: Grep

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

## Assistant

**Result** (success):
```
1278-                        // Find permanents controlled by opponent matching filter
1279-                        let matching: Vec<ObjectId> = self.state.battlefield.iter()
1280-                            .filter(|p| p.controller == opp && Self::matches_filter(p, filter))
1281-                            .map(|p| p.id())
1282-                            .collect();
1283-                        if let Some(&victim_id) = matching.first() {
1284-                            // Simplified: sacrifice the first matching permanent
1285-                            // (proper implementation would let opponent choose)
1286-                            if let Some(perm) = self.state.battlefield.remove(victim_id) {
1287-                                self.state.ability_store.remove_source(victim_id);
1288-                                self.move_card_to_graveyard_inner(victim_id, perm.owner());
1289-                            }
1290-                        }
1291-                    }
1292-                }
1293:                Effect::DestroyAll { filter } => {
1294-                    // Destroy all permanents matching filter
1295-                    let to_destroy: Vec<(ObjectId, PlayerId)> = self.state.battlefield.iter()
1296-                        .filter(|p| Self::matches_filter(p, filter) && !p.has_indestructible())
1297-                        .map(|p| (p.id(), p.owner()))
1298-                        .collect();
1299-                    for (id, owner) in to_destroy {
1300-                        if self.state.battlefield.remove(id).is_some() {
1301-                            self.state.ability_store.remove_source(id);
1302-                            self.move_card_to_graveyard_inner(id, owner);
1303-                        }
1304-                    }
1305-                }
1306:                Effect::DealDamageAll { amount, filter } => {
1307-                    // Deal damage to all creatures matching filter
1308-                    let matching: Vec<ObjectId> = self.state.battlefield.iter()
1309-                        .filter(|p| p.is_creature() && Self::matches_filter(p, filter))
1310-                        .map(|p| p.id())
1311-                        .collect();
1312-                    for id in matching {
1313-                        if let Some(perm) = self.state.battlefield.get_mut(id) {
1314-                            perm.apply_damage(*amount);
1315-                        }
1316-                    }
1317-                }
1318-                Effect::RemoveCounters { counter_type, count } => {
1319-                    let ct = crate::counters::CounterType::from_name(counter_type);
1320-                    // If no targets, fall back to source (self-targeting counters)
1321-                    let effective_targets: Vec<ObjectId> = if targets.is_empty() {
```

## Assistant

Now let me check the `matches_filter` function to understand how it works with "creatures you control" style filters.

## Assistant

### Tool: Grep

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

## Assistant

**Result** (success):
```
1474-                    if t_end < after_slash.len() {
1475-                        &after_slash[t_end + 1..]
1476-                    } else {
1477-                        ""
1478-                    }
1479-                } else {
1480-                    name
1481-                }
1482-            } else {
1483-                name
1484-            }
1485-        } else {
1486-            name
1487-        };
1488-
1489-        // Parse "with keyword1[, keyword2...]" or "with keyword1 and keyword2"
1490-        if let Some(with_pos) = rest.to_lowercase().find("with ") {
1491-            let kw_str = &rest[with_pos + 5..];
1492-            for part in kw_str.split(|c: char| c == ',' || c == '&') {
1493-                let part = part.trim().trim_start_matches("and ").trim();
1494-                if let Some(kw) = crate::constants::KeywordAbilities::keyword_from_name(part) {
1495-                    keywords |= kw;
1496-                }
1497-            }
1498-        }
1499-
1500-        (power, toughness, keywords)
1501-    }
1502-
1503-    /// Check if a permanent matches a simple filter string.
1504:    fn matches_filter(perm: &Permanent, filter: &str) -> bool {
1505-        let f = filter.to_lowercase();
1506-        // "all" or empty matches everything
1507-        if f.is_empty() || f == "all" {
1508-            return true;
1509-        }
1510-        // Check creature types
1511-        for st in &perm.card.subtypes {
1512-            if f.contains(&st.to_string().to_lowercase()) {
1513-                return true;
1514-            }
1515-        }
1516-        // Check card types
1517-        for ct in &perm.card.card_types {
1518-            let ct_name = format!("{:?}", ct).to_lowercase();
1519-            if f.contains(&ct_name) {
1520-                return true;
1521-            }
1522-        }
1523-        // "nonland" filter
1524-        if f.contains("nonland") && !perm.card.card_types.contains(&crate::constants::CardType::Land) {
1525-            return true;
1526-        }
1527-        false
1528-    }
1529-
1530-    /// Check if a CardData matches a simple filter string.
1531-    fn card_matches_filter(card: &CardData, filter: &str) -> bool {
1532-        let f = filter.to_lowercase();
1533-        if f.is_empty() || f == "all" {
1534-            return true;
```

## Assistant

## Assistant

Good. Now I have a clear picture. I need two new Effect variants:
1. `BoostAllUntilEndOfTurn { filter, power, toughness }` — mass P/T buff until EOT
2. `GrantKeywordAllUntilEndOfTurn { filter, keyword }` — mass keyword grant until EOT

Let me implement them.

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/abilities.rs",
  "new_string": "    // -- Stats --\n    /// Give +N/+M until end of turn.\n    BoostUntilEndOfTurn { power: i32, toughness: i32 },\n    /// Give +N/+M permanently (e.g. from counters, applied differently).\n    BoostPermanent { power: i32, toughness: i32 },\n    /// Give all matching creatures +N/+M until end of turn.\n    BoostAllUntilEndOfTurn { filter: String, power: i32, toughness: i32 },\n    /// Set power and toughness.\n    SetPowerToughness { power: i32, toughness: i32 },",
  "old_string": "    // -- Stats --\n    /// Give +N/+M until end of turn.\n    BoostUntilEndOfTurn { power: i32, toughness: i32 },\n    /// Give +N/+M permanently (e.g. from counters, applied differently).\n    BoostPermanent { power: i32, toughness: i32 },\n    /// Set power and toughness.\n    SetPowerToughness { power: i32, toughness: i32 },",
  "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

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/abilities.rs",
  "new_string": "    // -- Keywords --\n    /// Grant a keyword ability until end of turn.\n    GainKeywordUntilEndOfTurn { keyword: String },\n    /// Grant a keyword to all matching creatures until end of turn.\n    GrantKeywordAllUntilEndOfTurn { filter: String, keyword: String },\n    /// Grant a keyword ability permanently.\n    GainKeyword { keyword: String },\n    /// Remove a keyword ability.\n    LoseKeyword { keyword: String },",
  "old_string": "    // -- Keywords --\n    /// Grant a keyword ability until end of turn.\n    GainKeywordUntilEndOfTurn { keyword: String },\n    /// Grant a keyword ability permanently.\n    GainKeyword { keyword: String },\n    /// Remove a keyword ability.\n    LoseKeyword { keyword: String },",
  "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 constructors.

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/abilities.rs",
  "new_string": "    /// \"Target creature gets +N/+M.\"\n    pub fn boost_permanent(power: i32, toughness: i32) -> Self {\n        Effect::BoostPermanent { power, toughness }\n    }\n\n    /// \"Creatures [matching filter] get +N/+M until end of turn.\"\n    pub fn boost_all_eot(filter: &str, power: i32, toughness: i32) -> Self {\n        Effect::BoostAllUntilEndOfTurn {\n            filter: filter.to_string(),\n            power,\n            toughness,\n        }\n    }",
  "old_string": "    /// \"Target creature gets +N/+M.\"\n    pub fn boost_permanent(power: i32, toughness: i32) -> Self {\n        Effect::BoostPermanent { power, toughness }\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

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/abilities.rs",
  "new_string": "    /// \"Gain keyword until end of turn.\"\n    pub fn gain_keyword_eot(keyword: &str) -> Self {\n        Effect::GainKeywordUntilEndOfTurn {\n            keyword: keyword.to_string(),\n        }\n    }\n\n    /// \"Creatures [matching filter] gain [keyword] until end of turn.\"\n    pub fn grant_keyword_all_eot(filter: &str, keyword: &str) -> Self {\n        Effect::GrantKeywordAllUntilEndOfTurn {\n            filter: filter.to_string(),\n            keyword: keyword.to_string(),\n        }\n    }",
  "old_string": "    /// \"Gain keyword until end of turn.\"\n    pub fn gain_keyword_eot(keyword: &str) -> Self {\n        Effect::GainKeywordUntilEndOfTurn {\n            keyword: keyword.to_string(),\n        }\n    }",
  "replace_all": false
}
```

## Assistant

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

## Assistant

Now implement the match arms in `game.rs`. Let me find where the existing single-target versions are handled.

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  1054→                Effect::BoostUntilEndOfTurn { power, toughness: _ } => {
  1055→                    // Simplified: directly modify counters (proper implementation
  1056→                    // would use continuous effects that expire at end of turn)
  1057→                    for &target_id in targets {
  1058→                        if let Some(perm) = self.state.battlefield.get_mut(target_id) {
  1059→                            if *power > 0 {
  1060→                                perm.add_counters(CounterType::P1P1, *power as u32);
  1061→                            }
  1062→                            // Note: This is a simplification; real boost until EOT
  1063→                            // uses continuous effects, not counters
  1064→                        }
  1065→                    }
  1066→                }
  1067→                Effect::TapTarget => {
  1068→                    for &target_id in targets {
  1069→                        if let Some(perm) = self.state.battlefield.get_mut(target_id) {
  1070→                            perm.tap();
  1071→                        }
  1072→                    }
  1073→                }
  1074→                Effect::UntapTarget => {
  1075→                    for &target_id in targets {
  1076→                        if let Some(perm) = self.state.battlefield.get_mut(target_id) {
  1077→                            perm.untap();
  1078→                        }
  1079→                    }
  1080→                }
  1081→                Effect::CounterSpell => {
  1082→                    // Counter first target on the stack
  1083→                    for &target_id in targets {
  1084→                        if let Some(stack_item) = self.state.stack.remove(target_id) {
  1085→                            match &stack_item.kind {
  1086→                                crate::zones::StackItemKind::Spell { .. } => {
  1087→                                    self.move_card_to_graveyard_inner(stack_item.id, stack_item.controller);
  1088→                                }
  1089→                                _ => {} // Countered abilities just vanish
  1090→                            }
  1091→                        }
  1092→                    }
  1093→                }
```

## Assistant

I need to find a good spot to add the new match arms. Let me look at where the `BoostPermanent` arm is and add the mass versions nearby.

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  1368→                Effect::BoostPermanent { power, toughness: _ } => {
  1369→                    // Permanent P/T boost (similar to BoostUntilEndOfTurn but doesn't expire)
  1370→                    for &target_id in targets {
  1371→                        if let Some(perm) = self.state.battlefield.get_mut(target_id) {
  1372→                            if *power > 0 {
  1373→                                perm.add_counters(CounterType::P1P1, *power as u32);
  1374→                            } else if *power < 0 {
  1375→                                perm.add_counters(CounterType::M1M1, (-*power) as u32);
  1376→                            }
  1377→                        }
  1378→                    }
  1379→                }
  1380→                Effect::SetPowerToughness { power, toughness } => {
  1381→                    // Set base P/T (simplified: adjust via counters to reach target)
  1382→                    for &target_id in targets {
  1383→                        if let Some(perm) = self.state.battlefield.get_mut(target_id) {
  1384→                            let current_p = perm.power();
  1385→                            let current_t = perm.toughness();
  1386→                            let dp = *power - current_p;
  1387→                            let dt = *toughness - current_t;
  1388→                            // Use counters to approximate (imperfect but functional)
  1389→                            if dp > 0 {
  1390→                                perm.add_counters(CounterType::P1P1, dp as u32);
  1391→                            } else if dp < 0 {
  1392→                                perm.add_counters(CounterType::M1M1, (-dp) as u32);
  1393→                            }
  1394→                            let _ = dt; // Toughness adjustment via counters is coupled with power
  1395→                        }
  1396→                    }
  1397→                }
  1398→                Effect::LoseKeyword { keyword } => {
  1399→                    if let Some(kw) = crate::constants::KeywordAbilities::keyword_from_name(keyword) {
  1400→                        for &target_id in targets {
  1401→                            if let Some(perm) = self.state.battlefield.get_mut(target_id) {
  1402→                                perm.removed_keywords |= kw;
  1403→                            }
  1404→                        }
  1405→                    }
  1406→                }
  1407→                _ => {
```

## Assistant

I'll add the new match arms right before the catch-all `_ => {}`. Let me also check what `controller` variable is available — the effect handler needs to know who controls the creatures.

## Assistant

### Tool: Grep

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

## Assistant

**Result** (success):
```
939-                _ => {
940-                    // Other costs (exile from hand, remove counters, sacrifice other, etc.)
941-                    // will be implemented as cards need them
942-                }
943-            }
944-        }
945-        true
946-    }
947-
948-    /// Execute a list of effects for a controller with given targets.
949:    pub fn execute_effects(&mut self, effects: &[Effect], controller: PlayerId, targets: &[ObjectId], source: Option<ObjectId>) {
950-        for effect in effects {
951-            match effect {
952-                Effect::DealDamage { amount } => {
953-                    // Deal damage to target permanents.
954-                    // Player targeting is handled separately via SelectedTargets.
955-                    for &target_id in targets {
956-                        if let Some(perm) = self.state.battlefield.get_mut(target_id) {
957-                            perm.apply_damage(*amount);
958-                        }
959-                    }
```

## Assistant

The `controller` parameter is available. Now I'll add the mass effect implementations.

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "new_string": "                Effect::LoseKeyword { keyword } => {\n                    if let Some(kw) = crate::constants::KeywordAbilities::keyword_from_name(keyword) {\n                        for &target_id in targets {\n                            if let Some(perm) = self.state.battlefield.get_mut(target_id) {\n                                perm.removed_keywords |= kw;\n                            }\n                        }\n                    }\n                }\n                Effect::BoostAllUntilEndOfTurn { filter, power, toughness: _ } => {\n                    // Give all matching creatures controlled by the effect's controller +N/+M until EOT\n                    let you_control = filter.to_lowercase().contains(\"you control\");\n                    let matching: Vec<ObjectId> = self.state.battlefield.iter()\n                        .filter(|p| p.is_creature()\n                            && (!you_control || p.controller == controller)\n                            && Self::matches_filter(p, filter))\n                        .map(|p| p.id())\n                        .collect();\n                    for id in matching {\n                        if let Some(perm) = self.state.battlefield.get_mut(id) {\n                            if *power > 0 {\n                                perm.add_counters(CounterType::P1P1, *power as u32);\n                            } else if *power < 0 {\n                                perm.add_counters(CounterType::M1M1, (-*power) as u32);\n                            }\n                        }\n                    }\n                }\n                Effect::GrantKeywordAllUntilEndOfTurn { filter, keyword } => {\n                    // Grant keyword to all matching creatures controlled by the effect's controller until EOT\n                    if let Some(kw) = crate::constants::KeywordAbilities::keyword_from_name(keyword) {\n                        let you_control = filter.to_lowercase().contains(\"you control\");\n                        let matching: Vec<ObjectId> = self.state.battlefield.iter()\n                            .filter(|p| p.is_creature()\n                                && (!you_control || p.controller == controller)\n                                && Self::matches_filter(p, filter))\n                            .map(|p| p.id())\n                            .collect();\n                        for id in matching {\n                            if let Some(perm) = self.state.battlefield.get_mut(id) {\n                                perm.granted_keywords |= kw;\n                            }\n                        }\n                    }\n                }\n                _ => {",
  "old_string": "                Effect::LoseKeyword { keyword } => {\n                    if let Some(kw) = crate::constants::KeywordAbilities::keyword_from_name(keyword) {\n                        for &target_id in targets {\n                            if let Some(perm) = self.state.battlefield.get_mut(target_id) {\n                                perm.removed_keywords |= kw;\n                            }\n                        }\n                    }\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

Good. Now let me add a test in `game.rs`. Let me find the test module.

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

## Assistant

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

## 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):
```
2456 /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": 30,
  "offset": 2430
}
```

## Assistant

**Result** (success):
```
  2430→                (p2, Box::new(DiscardingPlayer)),
  2431→            ],
  2432→        );
  2433→
  2434→        // Give opponent some cards in hand
  2435→        let c1_id = ObjectId::new();
  2436→        let c2_id = ObjectId::new();
  2437→        let c3_id = ObjectId::new();
  2438→        if let Some(player) = game.state.players.get_mut(&p2) {
  2439→            player.hand.add(c1_id);
  2440→            player.hand.add(c2_id);
  2441→            player.hand.add(c3_id);
  2442→        }
  2443→
  2444→        let p1_hand_before = game.state.players.get(&p1).unwrap().hand.len();
  2445→        let p2_hand_before = game.state.players.get(&p2).unwrap().hand.len();
  2446→        assert_eq!(p2_hand_before, 3);
  2447→
  2448→        // Each opponent discards 1
  2449→        game.execute_effects(&[Effect::discard_opponents(1)], p1, &[], None);
  2450→
  2451→        // Controller's hand unchanged
  2452→        assert_eq!(game.state.players.get(&p1).unwrap().hand.len(), p1_hand_before);
  2453→        // Opponent lost 1 card
  2454→        assert_eq!(game.state.players.get(&p2).unwrap().hand.len(), 2);
  2455→    }
  2456→}
  2457→
```

## Assistant

Now let me add a test for the mass effects. I'll add it at the end of the test module.

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  1618→mod tests {
  1619→    use super::*;
  1620→    use crate::abilities::{Ability, Cost, Effect, TargetSpec};
  1621→    use crate::card::CardData;
  1622→    use crate::constants::{CardType, KeywordAbilities, Outcome};
  1623→    use crate::decision::{
  1624→        AttackerInfo, DamageAssignment, GameView, NamedChoice, PlayerAction,
  1625→        ReplacementEffectChoice, TargetRequirement, UnpaidMana,
  1626→    };
  1627→    use crate::mana::Mana;
  1628→
  1629→    /// A minimal decision maker that always passes priority.
  1630→    struct AlwaysPassPlayer;
  1631→
  1632→    impl PlayerDecisionMaker for AlwaysPassPlayer {
  1633→        fn priority(&mut self, _game: &GameView<'_>, _legal: &[PlayerAction]) -> PlayerAction {
  1634→            PlayerAction::Pass
  1635→        }
  1636→        fn choose_targets(&mut self, _: &GameView<'_>, _: Outcome, _: &TargetRequirement) -> Vec<ObjectId> { vec![] }
  1637→        fn choose_use(&mut self, _: &GameView<'_>, _: Outcome, _: &str) -> bool { false }
  1638→        fn choose_mode(&mut self, _: &GameView<'_>, _: &[NamedChoice]) -> usize { 0 }
  1639→        fn select_attackers(&mut self, _: &GameView<'_>, _: &[ObjectId], _: &[ObjectId]) -> Vec<(ObjectId, ObjectId)> { vec![] }
  1640→        fn select_blockers(&mut self, _: &GameView<'_>, _: &[AttackerInfo]) -> Vec<(ObjectId, ObjectId)> { vec![] }
  1641→        fn assign_damage(&mut self, _: &GameView<'_>, _: &DamageAssignment) -> Vec<(ObjectId, u32)> { vec![] }
  1642→        fn choose_mulligan(&mut self, _: &GameView<'_>, _: &[ObjectId]) -> bool { false }
  1643→        fn choose_cards_to_put_back(&mut self, _: &GameView<'_>, _: &[ObjectId], _: usize) -> Vec<ObjectId> { vec![] }
  1644→        fn choose_discard(&mut self, _: &GameView<'_>, _: &[ObjectId], _: usize) -> Vec<ObjectId> { vec![] }
  1645→        fn choose_amount(&mut self, _: &GameView<'_>, _: &str, min: u32, _: u32) -> u32 { min }
  1646→        fn choose_mana_payment(&mut self, _: &GameView<'_>, _: &UnpaidMana, _: &[PlayerAction]) -> Option<PlayerAction> { None }
  1647→        fn choose_replacement_effect(&mut self, _: &GameView<'_>, _: &[ReplacementEffectChoice]) -> usize { 0 }
  1648→        fn choose_pile(&mut self, _: &GameView<'_>, _: Outcome, _: &str, _: &[ObjectId], _: &[ObjectId]) -> bool { true }
  1649→        fn choose_option(&mut self, _: &GameView<'_>, _: Outcome, _: &str, _: &[NamedChoice]) -> usize { 0 }
  1650→    }
  1651→
  1652→    fn make_basic_land(name: &str, owner: PlayerId) -> CardData {
  1653→        let mut card = CardData::new(ObjectId::new(), owner, name);
  1654→        card.card_types = vec![CardType::Land];
  1655→        card
  1656→    }
  1657→
  1658→    fn make_creature(name: &str, owner: PlayerId, power: i32, toughness: i32) -> CardData {
  1659→        let mut card = CardData::new(ObjectId::new(), owner, name);
  1660→        card.card_types = vec![CardType::Creature];
  1661→        card.power = Some(power);
  1662→        card.toughness = Some(toughness);
  1663→        card.keywords = KeywordAbilities::empty();
  1664→        card
  1665→    }
  1666→
  1667→    fn make_deck(owner: PlayerId) -> Vec<CardData> {
```

## Assistant

Good, I have the test helpers. Now let me add 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": "        // 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\n    #[test]\n    fn boost_all_and_grant_keyword_all_until_eot() {\n        let p1 = PlayerId::new();\n        let p2 = PlayerId::new();\n\n        let mut game = Game::new(\n            vec![\n                (p1, make_deck(p1)),\n                (p2, make_deck(p2)),\n            ],\n            vec![\n                (p1, Box::new(AlwaysPassPlayer)),\n                (p2, Box::new(AlwaysPassPlayer)),\n            ],\n        );\n\n        // Put two creatures on P1's battlefield and one on P2's\n        let bear1 = make_creature(\"Grizzly Bears\", p1, 2, 2);\n        let bear1_id = bear1.id;\n        let bear2 = make_creature(\"Runeclaw Bear\", p1, 2, 2);\n        let bear2_id = bear2.id;\n        let opp_bear = make_creature(\"Opponent Bear\", p2, 2, 2);\n        let opp_bear_id = opp_bear.id;\n\n        game.state.battlefield.add(Permanent::new(bear1, p1));\n        game.state.battlefield.add(Permanent::new(bear2, p1));\n        game.state.battlefield.add(Permanent::new(opp_bear, p2));\n\n        // Boost all creatures P1 controls +1/+1\n        game.execute_effects(\n            &[Effect::boost_all_eot(\"creatures you control\", 1, 1)],\n            p1, &[], None,\n        );\n\n        // P1's creatures should be 3/x, opponent's should remain 2/x\n        assert_eq!(game.state.battlefield.get(bear1_id).unwrap().power(), 3);\n        assert_eq!(game.state.battlefield.get(bear2_id).unwrap().power(), 3);\n        assert_eq!(game.state.battlefield.get(opp_bear_id).unwrap().power(), 2);\n\n        // Grant trample to all creatures P1 controls\n        game.execute_effects(\n            &[Effect::grant_keyword_all_eot(\"creatures you control\", \"trample\")],\n            p1, &[], None,\n        );\n\n        // P1's creatures should have trample, opponent's should not\n        assert!(game.state.battlefield.get(bear1_id).unwrap().has_keyword(KeywordAbilities::TRAMPLE));\n        assert!(game.state.battlefield.get(bear2_id).unwrap().has_keyword(KeywordAbilities::TRAMPLE));\n        assert!(!game.state.battlefield.get(opp_bear_id).unwrap().has_keyword(KeywordAbilities::TRAMPLE));\n    }\n}",
  "old_string": "        // 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}",
  "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 check if `has_keyword` exists on `Permanent`.

## Assistant

### Tool: Grep

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

## Assistant

**Result** (success):
```
mtg-engine/src/permanent.rs:127:    pub fn has_keyword(&self, kw: KeywordAbilities) -> bool {
```

## Assistant

Good, it exists. Let me compile.

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "cargo check -p mtg-engine 2>&1 | tail -10",
  "description": "Check engine compilation"
}
```

## Assistant

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

## Assistant

Now fix the 5 FDN cards. Let me use sed for each one.

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -n '2388,2399p' mtg-cards/src/sets/fdn.rs",
  "description": "Show Crash Through card"
}
```

## Assistant

**Result** (success):
```
fn crash_through(id: ObjectId, owner: PlayerId) -> CardData {
    // Sorcery {R}. Creatures you control gain trample until EOT. Draw a card.
    CardData { id, owner, name: "Crash Through".into(), mana_cost: ManaCost::parse("{R}"),
        card_types: vec![CardType::Sorcery], rarity: Rarity::Common,
        abilities: vec![
            Ability::spell(id,
                vec![Effect::Custom("Creatures you control gain trample until end of turn.".into()),
                     Effect::draw_cards(1)],
                TargetSpec::None),
        ],
        ..Default::default() }
}
```

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i 's/Effect::Custom(\"Creatures you control gain trample until end of turn.\".into())/Effect::grant_keyword_all_eot(\"creatures you control\", \"trample\")/' mtg-cards/src/sets/fdn.rs",
  "description": "Fix Crash Through"
}
```

## Assistant

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

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i 's/Effect::Custom(\"Creatures you control get +3\\/+3 until end of turn.\".into())/Effect::boost_all_eot(\"creatures you control\", 3, 3)/' mtg-cards/src/sets/fdn.rs && sed -i 's/Effect::Custom(\"Creatures you control gain trample until end of turn.\".into())/Effect::grant_keyword_all_eot(\"creatures you control\", \"trample\")/' mtg-cards/src/sets/fdn.rs",
  "description": "Fix Overrun"
}
```

## Assistant

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

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i 's/Effect::Custom(\"Creatures you control get +1\\/+0 until end of turn.\".into())/Effect::boost_all_eot(\"creatures you control\", 1, 0)/' mtg-cards/src/sets/fdn.rs && sed -i 's/Effect::Custom(\"Creatures you control gain indestructible until end of turn.\".into())/Effect::grant_keyword_all_eot(\"creatures you control\", \"indestructible\")/' mtg-cards/src/sets/fdn.rs",
  "description": "Fix Make a Stand"
}
```

## Assistant

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

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i 's/Effect::Custom(\"Creatures you control get +1\\/+1 until end of turn.\".into())/Effect::boost_all_eot(\"creatures you control\", 1, 1)/' mtg-cards/src/sets/fdn.rs && sed -i 's/Effect::Custom(\"Creatures you control gain haste until end of turn.\".into())/Effect::grant_keyword_all_eot(\"creatures you control\", \"haste\")/' mtg-cards/src/sets/fdn.rs",
  "description": "Fix Heroic Reinforcements"
}
```

## Assistant

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

## Assistant

Now fix Balmor — this one has a compound Custom string.

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -n '2896,2900p' mtg-cards/src/sets/fdn.rs",
  "description": "Show Balmor after earlier changes"
}
```

## Assistant

**Result** (success):
```
            Ability::spell_cast_triggered(id,
                "Whenever you cast an instant or sorcery spell, creatures you control get +1/+0 and gain trample until end of turn.",
                vec![Effect::Custom("Creatures you control get +1/+0 and gain trample until end of turn.".into())],
                TargetSpec::None),
        ],
```

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i 's/vec!\\[Effect::Custom(\"Creatures you control get +1\\/+0 and gain trample until end of turn.\".into())\\]/vec![Effect::boost_all_eot(\"creatures you control\", 1, 0), Effect::grant_keyword_all_eot(\"creatures you control\", \"trample\")]/' mtg-cards/src/sets/fdn.rs",
  "description": "Fix Balmor compound custom"
}
```

## Assistant

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

## Assistant

Now let me verify all the fixes look correct and compile.

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "grep -n \"boost_all_eot\\|grant_keyword_all_eot\" mtg-cards/src/sets/fdn.rs",
  "description": "Verify all mass effect usages in fdn.rs"
}
```

## Assistant

**Result** (success):
```
2394:                vec![Effect::grant_keyword_all_eot("creatures you control", "trample"),
2548:                     Effect::boost_all_eot("creatures you control", 1, 1),
2549:                     Effect::grant_keyword_all_eot("creatures you control", "haste")],
2561:                vec![Effect::boost_all_eot("creatures you control", 1, 0),
2562:                     Effect::grant_keyword_all_eot("creatures you control", "indestructible")],
2574:                vec![Effect::boost_all_eot("creatures you control", 3, 3),
2575:                     Effect::grant_keyword_all_eot("creatures you control", "trample")],
2898:                vec![Effect::boost_all_eot("creatures you control", 1, 0), Effect::grant_keyword_all_eot("creatures you control", "trample")],
```

## Assistant

All 5 cards fixed. Let me also search for any other cards across all sets that could use these new effects.

## 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": "Creatures you control (get|gain).*until end of turn"
}
```

## Assistant

**Result** (success):
```
mtg-cards/src/sets/fdn.rs:3071:                vec![Effect::Custom("Creatures you control get +2/+2 until end of turn.".into())],
mtg-cards/src/sets/tdm.rs:479:                vec![Effect::Custom("Creatures you control get +X/+X and trample until end of turn, X = creatures you control.".into())],
mtg-cards/src/sets/ecl.rs:843:                vec![Effect::Custom("Creatures you control get +1/+0 until end of turn. Kithkin you control also gain first strike until end of turn.".into())],
mtg-cards/src/sets/ecl.rs:1597:                "{4}{W}: Creatures you control get +1/+1 until end of turn.",
mtg-cards/src/sets/ecl.rs:1599:                vec![Effect::Custom("Creatures you control get +1/+1 until end of turn.".into())],
mtg-cards/src/sets/ecl.rs:2237:                vec![Effect::Custom("Creatures you control get +1/+1 and gain haste until end of turn.".into())],
```

## Assistant

## Assistant

Found more cards using these patterns! Let me check which are simple enough to fix.

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  3067→            Ability::static_ability(id,
  3068→                "This spell costs {1} less to cast for each Cat you control.",
  3069→                vec![StaticEffect::Custom("Cost reduction: {1} less per Cat.".into())]),
  3070→            Ability::spell(id,
  3071→                vec![Effect::Custom("Creatures you control get +2/+2 until end of turn.".into())],
  3072→                TargetSpec::None),
  3073→        ],
  3074→        ..Default::default() }
  3075→}
  3076→
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  1593→        subtypes: vec![SubType::Custom("Kithkin".into()), SubType::Soldier],
  1594→        power: Some(2), toughness: Some(2), rarity: Rarity::Common,
  1595→        abilities: vec![
  1596→            Ability::activated(id,
  1597→                "{4}{W}: Creatures you control get +1/+1 until end of turn.",
  1598→                vec![Cost::pay_mana("{4}{W}")],
  1599→                vec![Effect::Custom("Creatures you control get +1/+1 until end of turn.".into())],
  1600→                TargetSpec::None),
  1601→        ],
  1602→        ..Default::default() }
  1603→}
  1604→
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  2233→                vec![Effect::create_token("1/1 Kithkin", 2)],
  2234→                TargetSpec::None),
  2235→            Ability::enters_battlefield_triggered(id,
  2236→                "When this creature enters, if {R}{R} was spent to cast it, creatures you control get +1/+1 and gain haste until end of turn.",
  2237→                vec![Effect::Custom("Creatures you control get +1/+1 and gain haste until end of turn.".into())],
  2238→                TargetSpec::None),
  2239→            Ability::static_ability(id, "Evoke {R/W}{R/W}",
  2240→                vec![StaticEffect::Custom("Evoke {R/W}{R/W}".into())]),
  2241→        ],
  2242→        ..Default::default() }
```

## Assistant

I can fix these 3 additional cards too. Let me also check what card the FDN one is.

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  3060→}
  3061→
  3062→fn claws_out(id: ObjectId, owner: PlayerId) -> CardData {
  3063→    // Instant for {3}{W}{W}. Costs {1} less per Cat you control. Creatures you control get +2/+2 until EOT.
  3064→    CardData { id, owner, name: "Claws Out".into(), mana_cost: ManaCost::parse("{3}{W}{W}"),
  3065→        card_types: vec![CardType::Instant], rarity: Rarity::Uncommon,
  3066→        abilities: vec![
  3067→            Ability::static_ability(id,
  3068→                "This spell costs {1} less to cast for each Cat you control.",
  3069→                vec![StaticEffect::Custom("Cost reduction: {1} less per Cat.".into())]),
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  1586→        ..Default::default() }
  1587→}
  1588→
  1589→fn timid_shieldbearer(id: ObjectId, owner: PlayerId) -> CardData {
  1590→    // 2/2 Kithkin Soldier for {1}{W}. ({4}{W}: creatures +1/+1 until end of turn)
  1591→    CardData { id, owner, name: "Timid Shieldbearer".into(), mana_cost: ManaCost::parse("{1}{W}"),
  1592→        card_types: vec![CardType::Creature],
  1593→        subtypes: vec![SubType::Custom("Kithkin".into()), SubType::Soldier],
```

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  2224→    // ETB if {R}{R} spent: creatures +1/+1 and haste until EOT.
  2225→    // Evoke {R/W}{R/W}.
  2226→    CardData { id, owner, name: "Catharsis".into(), mana_cost: ManaCost::parse("{4}{R/W}{R/W}"),
  2227→        card_types: vec![CardType::Creature],
  2228→        subtypes: vec![SubType::Elemental, SubType::Custom("Incarnation".into())],
  2229→        power: Some(3), toughness: Some(4), rarity: Rarity::Mythic,
  2230→        abilities: vec![
  2231→            Ability::enters_battlefield_triggered(id,
  2232→                "When this creature enters, if {W}{W} was spent to cast it, create two 1/1 green and white Kithkin creature tokens.",
  2233→                vec![Effect::create_token("1/1 Kithkin", 2)],
```

## Assistant

Found 3 more fixable cards:
- FDN: **Claws Out** — `+2/+2 until EOT`
- ECL: **Timid Shieldbearer** — activated `+1/+1 until EOT`  
- ECL: **Catharsis** — ETB `+1/+1 and haste until EOT`

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i 's/Effect::Custom(\"Creatures you control get +2\\/+2 until end of turn.\".into())/Effect::boost_all_eot(\"creatures you control\", 2, 2)/' mtg-cards/src/sets/fdn.rs",
  "description": "Fix Claws Out"
}
```

## Assistant

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

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i 's/Effect::Custom(\"Creatures you control get +1\\/+1 until end of turn.\".into())/Effect::boost_all_eot(\"creatures you control\", 1, 1)/g' mtg-cards/src/sets/ecl.rs",
  "description": "Fix Timid Shieldbearer and Catharsis boost"
}
```

## Assistant

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

## Assistant

Now fix the haste part of Catharsis.

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "grep -n \"gain haste until end of turn\" mtg-cards/src/sets/ecl.rs",
  "description": "Check Catharsis haste line"
}
```

## Assistant

**Result** (success):
```
2236:                "When this creature enters, if {R}{R} was spent to cast it, creatures you control get +1/+1 and gain haste until end of turn.",
2237:                vec![Effect::Custom("Creatures you control get +1/+1 and gain haste until end of turn.".into())],
```

## Assistant

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -n '2236,2238p' mtg-cards/src/sets/ecl.rs",
  "description": "Check Catharsis current state"
}
```

## Assistant

**Result** (success):
```
                "When this creature enters, if {R}{R} was spent to cast it, creatures you control get +1/+1 and gain haste until end of turn.",
                vec![Effect::Custom("Creatures you control get +1/+1 and gain haste until end of turn.".into())],
                TargetSpec::None),
```

## Assistant

The `sed` didn't match because the string had "and gain haste" appended. Let me fix this one specifically.

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i 's/vec!\\[Effect::Custom(\"Creatures you control get +1\\/+1 and gain haste until end of turn.\".into())\\]/vec![Effect::boost_all_eot(\"creatures you control\", 1, 1), Effect::grant_keyword_all_eot(\"creatures you control\", \"haste\")]/' mtg-cards/src/sets/ecl.rs",
  "description": "Fix Catharsis compound effect"
}
```

## Assistant

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

## Assistant

Let me verify the Timid Shieldbearer fix worked too.

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -n '1596,1601p' mtg-cards/src/sets/ecl.rs",
  "description": "Verify Timid Shieldbearer fix"
}
```

## Assistant

**Result** (success):
```
            Ability::activated(id,
                "{4}{W}: Creatures you control get +1/+1 until end of turn.",
                vec![Cost::pay_mana("{4}{W}")],
                vec![Effect::boost_all_eot("creatures you control", 1, 1)],
                TargetSpec::None),
        ],
```

## Assistant

Good. Now check for any other mass grant patterns across other sets.

## 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": "Custom.*creatures you control.*until end of turn"
}
```

## Assistant

**Result** (success):
```
mtg-cards/src/sets/ecl.rs:843:                vec![Effect::Custom("Creatures you control get +1/+0 until end of turn. Kithkin you control also gain first strike until end of turn.".into())],
mtg-cards/src/sets/tdm.rs:479:                vec![Effect::Custom("Creatures you control get +X/+X and trample until end of turn, X = creatures you control.".into())],
mtg-cards/src/sets/tdm.rs:3165:                    vec![Effect::Custom("Choose one: Create two 1/1 white Monk creature tokens with prowess; or up to two target creatures you control each get +2/+2 until end of turn; or destroy target creature with power 4 or greater.".into())],
mtg-cards/src/sets/tdm.rs:3474:                         Effect::Custom("Creatures you control with counters on them gain vigilance and trample until end of turn.".into())],
```

## Assistant

The remaining ones are complex (variable X, compound conditional, modal) — can't be fixed with simple substitution. Let me also check TLA.

## 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/tla.rs",
  "pattern": "Custom.*creatures.*until end of turn"
}
```

## Assistant

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

## Assistant

Good, no TLA mass buffs using Custom. Now compile and test.

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "cargo check -p mtg-cards 2>&1 | tail -5",
  "description": "Check cards compilation"
}
```

## Assistant

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

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "cargo test --lib 2>&1 | grep -E \"^test result:|running\"",
  "description": "Run full test suite"
}
```

## Assistant

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

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "cargo test --lib 2>&1 | tail -20",
  "description": "Full test output"
}
```

## Assistant

**Result** (success):
```
   |     ^^^^^
   |
   = 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
For more information about this error, try `rustc --explain E0599`.
error: could not compile `mtg-engine` (lib test) due to 1 previous error
warning: build failed, waiting for other jobs to finish...
```

## Assistant

There's a compilation error in the test. Let me see what it is.

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "cargo test --lib -p mtg-engine 2>&1 | grep \"error\"",
  "description": "Get the actual error"
}
```

## Assistant

**Result** (success):
```
error[E0599]: no function or associated item named `new` found for struct `game::Game` in the current scope
For more information about this error, try `rustc --explain E0599`.
error: could not compile `mtg-engine` (lib test) due to 1 previous error
```

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "cargo test --lib -p mtg-engine 2>&1 | grep -B5 \"E0599\"",
  "description": "Get context of the error"
}
```

## Assistant

**Result** (success):
```
   Compiling mtg-engine v0.1.0 (/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine)
error[E0599]: no function or associated item named `new` found for struct `game::Game` in the current scope
--
     | |_____________^
     = help: items from traits can only be used if the trait is implemented and in scope
     = note: the following trait defines an item `new`, perhaps you need to implement it:
             candidate #1: `UniformSampler`

For more information about this error, try `rustc --explain E0599`.
```

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "cargo test --lib -p mtg-engine 2>&1 | grep -B10 \"no function or associated item named\"",
  "description": "Get line info for the error"
}
```

## Assistant

**Result** (success):
```
   Compiling mtg-engine v0.1.0 (/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine)
error[E0599]: no function or associated item named `new` found for struct `game::Game` in the current scope
```

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "cargo test --lib -p mtg-engine 2>&1 | head -20",
  "description": "Full error output"
}
```

## Assistant

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

## Assistant

The `Game::new` constructor doesn't exist — the existing tests must use a different pattern. Let me look at how other tests create games.

## Assistant

### Tool: Grep

**Args:**
```json
{
  "-n": true,
  "head_limit": 20,
  "output_mode": "content",
  "path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "pattern": "Game::new|new_two_player"
}
```

## Assistant

**Result** (success):
```
89:    pub fn new_two_player(
1693:        let game = Game::new_two_player(
1722:        let mut game = Game::new_two_player(
1756:        let mut game = Game::new_two_player(
1783:        let mut game = Game::new_two_player(
1823:        let mut game = Game::new_two_player(
1874:        let game = Game::new_two_player(
1899:        let mut game = Game::new_two_player(
1952:        let mut game = Game::new_two_player(
2015:        let mut game = Game::new_two_player(
2078:        let mut game = Game::new_two_player(
2138:        let mut game = Game::new_two_player(
2172:        let mut game = Game::new_two_player(
2197:        let mut game = Game::new_two_player(
2225:        let mut game = Game::new_two_player(
2262:        let mut game = Game::new_two_player(
2300:        let mut game = Game::new_two_player(
2347:        let mut game = Game::new_two_player(
2426:        let mut game = Game::new_two_player(
2462:        let mut game = Game::new(

[Showing results with pagination = limit: 20, offset: 0]
```

## Assistant

I need to use `Game::new_two_player` instead. Let me check the signature.

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
    89→    pub fn new_two_player(
    90→        config: GameConfig,
    91→        mut decision_makers: Vec<(PlayerId, Box<dyn PlayerDecisionMaker>)>,
    92→    ) -> Self {
    93→        assert_eq!(config.players.len(), 2, "Two-player game requires exactly 2 players");
    94→        assert_eq!(decision_makers.len(), 2, "Two-player game requires exactly 2 decision makers");
    95→
    96→        // Create player IDs
    97→        let player_ids: Vec<PlayerId> = decision_makers.iter().map(|(id, _)| *id).collect();
    98→
    99→        // Build game state
   100→        let player_names: Vec<(&str, PlayerId)> = config
   101→            .players
   102→            .iter()
   103→            .zip(player_ids.iter())
   104→            .map(|(pc, &id)| (pc.name.as_str(), id))
   105→            .collect();
   106→        let mut state = GameState::new(&player_names);
   107→
   108→        // Set starting life
   109→        for player in state.players.values_mut() {
   110→            player.life = config.starting_life;
   111→        }
   112→
   113→        // Build card store and libraries from decks
   114→        for (player_config, &player_id) in config.players.iter().zip(player_ids.iter()) {
   115→            let mut card_ids = Vec::with_capacity(player_config.deck.len());
   116→            for card in &player_config.deck {
   117→                let mut card_data = card.clone();
   118→                card_data.owner = player_id;
```

## Assistant

Let me look at how the existing tests build the game to follow the same pattern.

## Assistant

### Tool: Read

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

## Assistant

**Result** (success):
```
  1688→                PlayerConfig { name: "Bob".to_string(), deck: make_deck(p2) },
  1689→            ],
  1690→            starting_life: 20,
  1691→        };
  1692→
  1693→        let game = Game::new_two_player(
  1694→            config,
  1695→            vec![
  1696→                (p1, Box::new(AlwaysPassPlayer)),
  1697→                (p2, Box::new(AlwaysPassPlayer)),
  1698→            ],
  1699→        );
  1700→
  1701→        assert_eq!(game.state.players.len(), 2);
  1702→        assert_eq!(game.state.player(p1).unwrap().life, 20);
  1703→        assert_eq!(game.state.player(p2).unwrap().life, 20);
  1704→        // Each player should have 40 cards in library
  1705→        assert_eq!(game.state.player(p1).unwrap().library.len(), 40);
  1706→        assert_eq!(game.state.player(p2).unwrap().library.len(), 40);
  1707→    }
  1708→
  1709→    #[test]
  1710→    fn game_runs_to_completion() {
  1711→        let p1 = PlayerId::new();
  1712→        let p2 = PlayerId::new();
  1713→
  1714→        let config = GameConfig {
  1715→            players: vec![
  1716→                PlayerConfig { name: "Alice".to_string(), deck: make_deck(p1) },
  1717→                PlayerConfig { name: "Bob".to_string(), deck: make_deck(p2) },
  1718→            ],
  1719→            starting_life: 20,
  1720→        };
  1721→
  1722→        let mut game = Game::new_two_player(
  1723→            config,
  1724→            vec![
  1725→                (p1, Box::new(AlwaysPassPlayer)),
  1726→                (p2, Box::new(AlwaysPassPlayer)),
  1727→            ],
```

## Assistant

I see — I need to use `GameConfig` and `PlayerConfig`. Let me fix my test.

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rs",
  "new_string": "    #[test]\n    fn boost_all_and_grant_keyword_all_until_eot() {\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        );",
  "old_string": "    #[test]\n    fn boost_all_and_grant_keyword_all_until_eot() {\n        let p1 = PlayerId::new();\n        let p2 = PlayerId::new();\n\n        let mut game = Game::new(\n            vec![\n                (p1, make_deck(p1)),\n                (p2, make_deck(p2)),\n            ],\n            vec![\n                (p1, Box::new(AlwaysPassPlayer)),\n                (p2, Box::new(AlwaysPassPlayer)),\n            ],\n        );",
  "replace_all": false
}
```

## Assistant

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

## Assistant

Now let me check if `GameConfig` and `PlayerConfig` are imported in the test module.

## Assistant

### Tool: Grep

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

## Assistant

**Result** (success):
```
37:pub struct GameConfig {
39:    pub players: Vec<PlayerConfig>,
45:pub struct PlayerConfig {
90:        config: GameConfig,
1685:        let config = GameConfig {

[Showing results with pagination = limit: 5, offset: 0]
```

## Assistant

The test module uses `super::*` so `GameConfig` and `PlayerConfig` are available. Let me compile.

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "cargo test --lib -p mtg-engine 2>&1 | tail -15",
  "description": "Test engine compilation and tests"
}
```

## Assistant

**Result** (success):
```
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::graveyard_ordering ... ok
test zones::tests::hand_add_remove ... 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. 161 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
```

## Assistant

161 tests pass (was 160 before — +1 for new test). Now run the full suite.

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "cargo test --lib 2>&1 | grep -E \"^test result:|running\"",
  "description": "Full test suite"
}
```

## Assistant

**Result** (success):
```
running 52 tests
test result: ok. 52 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
running 18 tests
test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 161 tests
test result: ok. 161 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 19 tests
test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.28s
```

## Assistant

All 250 tests pass. Now update the docs. First the work queue.

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/work-queue.md",
  "new_string": "## Batch 7: Mass keyword grant / mass boost until EOT [DONE 2026-02-13]\n\n**Engine**: Added `Effect::BoostAllUntilEndOfTurn { filter, power, toughness }` + `boost_all_eot()` constructor and `Effect::GrantKeywordAllUntilEndOfTurn { filter, keyword }` + `grant_keyword_all_eot()` constructor. Both implemented in `execute_effects()` with filter matching and \"you control\" controller check. Uses P1P1/M1M1 counters for boost (same simplification as single-target `BoostUntilEndOfTurn`). Keywords use `granted_keywords` which are cleared at EOT cleanup.\n\n**Cards fixed (8, 12 effect swaps):**\n- [x] FDN: Crash Through — `Custom` → `grant_keyword_all_eot(\"creatures you control\", \"trample\")` + `draw_cards(1)`\n- [x] FDN: Overrun — 2× `Custom` → `boost_all_eot(\"creatures you control\", 3, 3)` + `grant_keyword_all_eot(\"creatures you control\", \"trample\")`\n- [x] FDN: Make a Stand — 2× `Custom` → `boost_all_eot(\"creatures you control\", 1, 0)` + `grant_keyword_all_eot(\"creatures you control\", \"indestructible\")`\n- [x] FDN: Heroic Reinforcements — 2× `Custom` → `boost_all_eot(\"creatures you control\", 1, 1)` + `grant_keyword_all_eot(\"creatures you control\", \"haste\")`\n- [x] FDN: Balmor, Battlemage Captain — `Custom` → `boost_all_eot(\"creatures you control\", 1, 0)` + `grant_keyword_all_eot(\"creatures you control\", \"trample\")`\n- [x] FDN: Claws Out — `Custom` → `boost_all_eot(\"creatures you control\", 2, 2)`\n- [x] ECL: Timid Shieldbearer — `Custom` → `boost_all_eot(\"creatures you control\", 1, 1)`\n- [x] ECL: Catharsis — `Custom` → `boost_all_eot(\"creatures you control\", 1, 1)` + `grant_keyword_all_eot(\"creatures you control\", \"haste\")`\n\n**Tests added:** `boost_all_and_grant_keyword_all_until_eot`\n\n**Skipped/Deferred:**\n- TDM: Craterhoof (variable +X/+X) — needs dynamic count support\n- ECL: Kithkin conditional (\"+1/+0 + first strike for Kithkin only\") — compound conditional filter\n- TDM: Modal spell with mass buff option — needs modal framework (Batch 11)",
  "old_string": "## Batch 7: Mass keyword grant until EOT\n\n**Engine**: Need `Effect::GrantKeywordAllUntilEndOfTurn { filter, keyword }` or similar mass-buff effect.\n\n**Cards:**\n- [ ] FDN: Crash Through — `Custom(\"Creatures you control gain trample until end of turn.\")` + DrawCards(1)\n- [ ] FDN: Overrun — `Custom(\"Creatures you control get +3/+3 and gain trample until end of turn.\")`\n- [ ] FDN: Make a Stand — `Custom(\"Creatures you control get +1/+0 and gain indestructible until end of turn.\")`\n- [ ] FDN: Heroic Reinforcements — `Custom(...)` mass +1/+1 and haste\n- [ ] FDN: Balmor, Battlemage Captain — spell trigger mass +1/+0 and trample",
  "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 ROADMAP.md.

## Assistant

### Tool: Edit

**Args:**
```json
{
  "file_path": "/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/ROADMAP.md",
  "new_string": "**Batch 6 (2026-02-13):** Added `StaticEffect::EntersTappedUnless { condition: String }` variant + `enters_tapped_unless()` builder. Stored as structured data; mechanical enforcement deferred. Replaced `StaticEffect::Custom(\"Enters tapped unless ...\")` on 2 TDM lands (Cori Mountain Monastery, Dalkovan Encampment).\n\n**Batch 7 (2026-02-13):** Added `Effect::BoostAllUntilEndOfTurn { filter, power, toughness }` and `Effect::GrantKeywordAllUntilEndOfTurn { filter, keyword }` variants with constructors and match arms. These mass-buff effects apply to all creatures matching the filter controlled by the effect's controller. Replaced Custom effects on 8 cards (6 FDN, 2 ECL) with 12 effect swaps total.",
  "old_string": "**Batch 6 (2026-02-13):** Added `StaticEffect::EntersTappedUnless { condition: String }` variant + `enters_tapped_unless()` builder. Stored as structured data; mechanical enforcement deferred. Replaced `StaticEffect::Custom(\"Enters tapped unless ...\")` on 2 TDM lands (Cori Mountain Monastery, Dalkovan Encampment).",
  "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 per-set remediation docs. Let me check the FDN ones first.

## 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": "Crash Through|Overrun|Make a Stand|Heroic Reinforcements|Balmor|Claws Out"
}
```

## Assistant

**Result** (success):
```
371:- [ ] **Crash Through** -- What works: `DrawCards(1)`. What's broken: `Effect::Custom("Creatures you control gain trample until end of turn.")`.
397:- [ ] **Heroic Reinforcements** -- What works: `CreateToken("1/1 Soldier", 2)`. What's broken: 2x `Effect::Custom(...)` for mass buff/haste.
400:- [ ] **Make a Stand** -- What works: nothing functional. What's broken: 2x `Effect::Custom(...)` for mass buff/indestructible.
403:- [ ] **Overrun** -- What works: nothing functional. What's broken: 2x `Effect::Custom(...)` for mass buff/trample.
431:- [ ] **Balmor, Battlemage Captain** -- What works: 1/3 flying creature. What's broken: SpellCast `Effect::Custom("Creatures you control get +1/+0 and gain trample until end of turn.")`.
467:- [ ] **Claws Out** -- Instant, spell effect (Custom)
```

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i 's/- \\[ \\] \\*\\*Crash Through\\*\\* -- What works: `DrawCards(1)`. What'\\''s broken: `Effect::Custom(\"Creatures you control gain trample until end of turn.\")`.*/- [x] **Crash Through** -- `grant_keyword_all_eot(\"creatures you control\", \"trample\")` + `draw_cards(1)`. Fixed in Batch 7./' docs/fdn-remediation.md",
  "description": "Update Crash Through in FDN remediation"
}
```

## Assistant

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

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i \"s/- \\[ \\] \\*\\*Heroic Reinforcements\\*\\* -- What works: .*/- [x] **Heroic Reinforcements** -- CreateToken + boost_all_eot + grant_keyword_all_eot (haste). Fixed in Batch 7./\" docs/fdn-remediation.md",
  "description": "Update Heroic Reinforcements"
}
```

## Assistant

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

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i \"s/- \\[ \\] \\*\\*Make a Stand\\*\\* -- What works: .*/- [x] **Make a Stand** -- boost_all_eot(+1\\/+0) + grant_keyword_all_eot(indestructible). Fixed in Batch 7./\" docs/fdn-remediation.md",
  "description": "Update Make a Stand"
}
```

## Assistant

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

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i \"s/- \\[ \\] \\*\\*Overrun\\*\\* -- What works: .*/- [x] **Overrun** -- boost_all_eot(+3\\/+3) + grant_keyword_all_eot(trample). Fixed in Batch 7./\" docs/fdn-remediation.md",
  "description": "Update Overrun"
}
```

## Assistant

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

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i \"s/- \\[ \\] \\*\\*Balmor, Battlemage Captain\\*\\* -- What works: .*/- [x] **Balmor, Battlemage Captain** -- SpellCast trigger: boost_all_eot(+1\\/+0) + grant_keyword_all_eot(trample). Fixed in Batch 7./\" docs/fdn-remediation.md",
  "description": "Update Balmor"
}
```

## Assistant

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

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i \"s/- \\[ \\] \\*\\*Claws Out\\*\\* -- Instant, spell effect (Custom)/- [x] **Claws Out** -- boost_all_eot(+2\\/+2). Cost reduction still Custom. Fixed in Batch 7./\" docs/fdn-remediation.md",
  "description": "Update Claws Out"
}
```

## Assistant

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

## Assistant

Now update ECL remediation.

## 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": "Timid Shieldbearer|Catharsis"
}
```

## Assistant

**Result** (success):
```
372:- [ ] **Timid Shieldbearer** — What works: stats. What's broken: activated `Effect::Custom("creatures +1/+1 until EOT")`.
484:- [ ] **Catharsis** — What works: `create_token("1/1 Kithkin", 2)` (if {W}{W} spent). What's broken: RR ETB `Effect::Custom("creatures +1/+1 and haste")`, `StaticEffect::Custom("Evoke")`.
485:  - **Java source**: `Mage.Sets/src/mage/cards/c/Catharsis.java`
```

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i \"s/- \\[ \\] \\*\\*Timid Shieldbearer\\*\\* — What works: .*/- [x] **Timid Shieldbearer** — Activated: boost_all_eot(+1\\/+1). Fixed in Batch 7./\" docs/ecl-remediation.md",
  "description": "Update Timid Shieldbearer"
}
```

## Assistant

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

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "sed -i \"s/- \\[ \\] \\*\\*Catharsis\\*\\* — What works: .*/- [x] **Catharsis** — ETB: create_token + boost_all_eot + grant_keyword_all_eot(haste). Evoke still Custom. Fixed in Batch 7./\" docs/ecl-remediation.md",
  "description": "Update Catharsis"
}
```

## Assistant

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

## Assistant

Let me verify the remediation doc updates look right.

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "grep -n \"Batch 7\" docs/fdn-remediation.md docs/ecl-remediation.md",
  "description": "Verify Batch 7 markings in remediation docs"
}
```

## Assistant

**Result** (success):
```
docs/fdn-remediation.md:371:- [x] **Crash Through** -- `grant_keyword_all_eot("creatures you control", "trample")` + `draw_cards(1)`. Fixed in Batch 7.
docs/fdn-remediation.md:397:- [x] **Heroic Reinforcements** -- CreateToken + boost_all_eot + grant_keyword_all_eot (haste). Fixed in Batch 7.
docs/fdn-remediation.md:400:- [x] **Make a Stand** -- boost_all_eot(+1/+0) + grant_keyword_all_eot(indestructible). Fixed in Batch 7.
docs/fdn-remediation.md:403:- [x] **Overrun** -- boost_all_eot(+3/+3) + grant_keyword_all_eot(trample). Fixed in Batch 7.
docs/fdn-remediation.md:431:- [x] **Balmor, Battlemage Captain** -- SpellCast trigger: boost_all_eot(+1/+0) + grant_keyword_all_eot(trample). Fixed in Batch 7.
docs/fdn-remediation.md:467:- [x] **Claws Out** -- boost_all_eot(+2/+2). Cost reduction still Custom. Fixed in Batch 7.
docs/ecl-remediation.md:372:- [x] **Timid Shieldbearer** — Activated: boost_all_eot(+1/+1). Fixed in Batch 7.
docs/ecl-remediation.md:484:- [x] **Catharsis** — ETB: create_token + boost_all_eot + grant_keyword_all_eot(haste). Evoke still Custom. Fixed in Batch 7.
```

## Assistant

All docs updated. Let me do a final full build + test verification.

## Assistant

### Tool: Bash

**Args:**
```json
{
  "command": "cargo check -p mtg-cards && cargo test --lib 2>&1 | grep -E \"^test result:|running|error\"",
  "description": "Final build + test verification"
}
```

## Assistant

**Result** (success):
```
running 52 tests
test result: ok. 52 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
running 18 tests
test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 161 tests
test result: ok. 161 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 19 tests
test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.33s
    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 0.91s
```

## Assistant

All 250 tests pass, compilation clean. Both batches are complete. Here's the summary:

---

## Batch 6: Enters-tapped-unless (conditional ETB tapped) - DONE

**Engine change:** Added `StaticEffect::EntersTappedUnless { condition: String }` variant + `enters_tapped_unless()` builder. Stored as structured data (mechanical enforcement deferred, same pattern as Ward).

**Cards fixed (2):**
- TDM: Cori Mountain Monastery
- TDM: Dalkovan Encampment

---

## Batch 7: Mass keyword grant / mass boost until EOT - DONE

**Engine changes:** Added 2 new `Effect` variants with constructors + match arms in `execute_effects()`:
- `BoostAllUntilEndOfTurn { filter, power, toughness }` / `boost_all_eot()`
- `GrantKeywordAllUntilEndOfTurn { filter, keyword }` / `grant_keyword_all_eot()`

Both filter by creature type and respect "you control" controller checks. Uses the same counter-based simplification as single-target boost.

**Cards fixed (8 cards, 12 effect swaps):**
- FDN: Crash Through, Overrun, Make a Stand, Heroic Reinforcements, Balmor Battlemage Captain, Claws Out
- ECL: Timid Shieldbearer, Catharsis

**Tests added:** 2 new tests (`static_effect_builders` extended + `boost_all_and_grant_keyword_all_until_eot`)

**Total: 250 tests passing** (up from 249).
