837: vec![Effect::Custom("Each opponent loses 1 life.".into())],
850: vec![Effect::Custom("Each opponent loses 1 life.".into())],
968: vec![Effect::Custom("Each opponent loses 2 life, you gain 2 life.".into())],
1412: vec![Effect::draw_cards(1), Effect::Custom("You lose 1 life.".into())],
1705: vec![Effect::Custom("Each opponent loses 1 life, you gain 1 life.".into())],
5979: vec![Effect::Custom("Each opponent loses X life. You gain life equal to the life lost this way.".into())],Now let me also read the Complete cards section to understand the count and where to add the newly fixed cards.
Now I have all the context. Let me make all the doc updates. I'll update fdn-remediation.md, then ecl-remediation.md, tdm-remediation.md, and ROADMAP.md.
Now update the Phase 2 roadmap section and the overview counts.
Now I have all the context I need. Let me also check the DealDamageOpponents implementation in game.rs to match the pattern.
Now update the ECL and TDM remediation docs.
Need to read those files first.
Good, I have all the context. Let me also check the DealDamageOpponents implementation in game.rs for reference.
1007- player.life += *amount as i32;
1008- }
1009- }
1010- Effect::LoseLife { amount } => {
1011- // Controller loses life (target player effects will use
1012- // SelectedTargets for proper player targeting)
1013- if let Some(player) = self.state.players.get_mut(&controller) {
1014- player.life -= *amount as i32;
1015- }
1016- }
1017: Effect::DealDamageOpponents { amount } => {
1018- let opponents: Vec<PlayerId> = self.state.turn_order.iter()
1019- .filter(|&&id| id != controller)
1020- .copied()
1021- .collect();
1022- for opp in opponents {
1023- if let Some(player) = self.state.players.get_mut(&opp) {
1024- player.life -= *amount as i32;
1025- }
1026- }
1027- }Now update the ROADMAP.md.
Now let me run the full test suite one more time to confirm everything is clean.
>cargo test --lib 2>&1 | tail -20All 244 tests pass. Here's a summary of everything done:
New Effect::LoseLifeOpponents { amount } variant added to:
mtg-engine/src/abilities.rs — enum variant + lose_life_opponents() constructormtg-engine/src/game.rs — match arm in execute_effects() (iterates opponents, subtracts life)This is semantically distinct from DealDamageOpponents — when damage prevention is added later, only damage effects will be affected, not life loss.
FDN (5 cards):
Custom → lose_life_opponents(1)Custom → lose_life_opponents(1)Custom → lose_life_opponents(2) + gain_life(2)Custom("You lose 1 life.") → lose_life(1)Custom → lose_life_opponents(1) + gain_life(1)ECL (2 cards):
TDM (1 card):
Custom → lose_life_opponents(2) (already had gain_life(2))docs/fdn-remediation.md — Added LoseLifeOpponents to effects table, marked 5 cards fixed, updated Phase 2 roadmapdocs/ecl-remediation.md — Updated Champion of the Weird and Boggart Mischief entriesdocs/tdm-remediation.md — Marked Skirmish Rhino as fixedROADMAP.md — Added LoseLifeOpponents to recently-implemented list, marked card-level fixes as doneDid we add tests for our new effect? If not let's do that.
Good catch — we didn't. Let me check where the existing effect tests live.
1931: fn spell_effects_execute_on_resolve() {
2054: fn draw_cards_effect() {
2088: fn gain_life_effect() {
2113: fn exile_effect() {
2150: fn bounce_effect() {Good, I see the pattern. Tests use Game::new_two_player with AlwaysPassPlayer and call execute_effects directly. Let me add a test for LoseLifeOpponents right after gain_life_effect.
>cargo test --lib -p mtg-engine lose_life_opponents 2>&1Test passes. Now 158 engine tests (was 157) and 245 total.
How can we best do this work in a loop? Find a chunk of cards that share a common piece of missing implementation, implement the missing effect (if any) and implement the cards. Add tests for new effects, and update the documents. We should repeat this process over and over until we are done, using documents to maintain state instead of our context window.
The user wants to create a repeatable process for fixing cards in batches. Let me think about what they're asking:
They want this to be sustainable across context windows, using the docs as state. Let me think about the best approach.
The key insight is that we need a "work queue" document that tracks what's been done and what's next, organized by shared missing implementation. Let me look at what's currently in the remediation docs to understand the landscape of fixable card groups.
Now I'm reviewing the existing card groups to see what's already been addressed and what patterns emerge. I can see LoseLifeOpponents is done, there are several cards that just need simple effect swaps since the implementations already exist, and there are larger groups organized by the type of missing effect they need.
I'm thinking about how to structure this work systematically—organizing cards by their shared missing pieces, estimating the effort for each group, and tracking dependencies so we can tackle things in the right order. I should create a work queue document that groups everything clearly and gives us a concrete path forward.
Good question. The key is a work queue document that drives the loop — organized by shared missing implementation, not by card. Each iteration, you (or I) read the queue, pick the next batch, do the work, update docs, and move on. The context window never needs to hold the full history.
Let me scan the current state to build that queue.
The user wants me to design a repeatable process for fixing cards in batches. The exploration agent gave me a good overview of the groups. Now I need to:
Let me also do my own quick verification of the "easy swap" cases - cards where a typed effect already exists but the card still uses Custom. Those are the quickest wins and should be the first batches.
Let me grep for specific easy-swap patterns to get accurate counts before building the queue.
Good data from the scan. Before building the work queue, let me verify the easy-swap cases precisely — cards where a typed effect already exists but the card still uses Custom.
>grep -n 'Effect::Custom(' mtg-cards/src/sets/*.rs | grep -viE '(effect\.|static effect\.|activated effect\.|spell effect\.|attack trigger\.|etb effect\.|dies effect\.|spell cast trigger\.|combat damage trigger\.|triggered effect\.)' | grep -iE '(destroy all|each opponent discard|enters tapped|can.t block this turn|target creature can.t block|loses? \d+ life|gains? \d+ life|deal.? \d+ damage|draw.? (a |one |\d+ )card|scry|mill|create.*token|search.*library|return.*graveyard.*hand|exile target|counter target|tap target|untap target|sacrifice a|discard a card)' | head -60>grep -n 'StaticEffect::Custom(' mtg-cards/src/sets/*.rs | grep -viE '(static effect\.)' | head -40Now let me also count the easy-swap candidates precisely.
># Count exact easy-swap patterns
echo "=== Enters tapped ==="
grep -n 'StaticEffect::Custom("Enters tapped' mtg-cards/src/sets/*.rs
echo ""
echo "=== Destroy all creatures ==="
grep -n 'Effect::Custom("Destroy all creatures' mtg-cards/src/sets/*.rs
echo ""
echo "=== Each opponent discards a card ==="
grep -n 'Effect::Custom("Each opponent discards a card' mtg-cards/src/sets/*.rs
echo ""
echo "=== Target creature can't block this turn ==="
grep -n "Effect::Custom(\"Target creature can.t block this turn" mtg-cards/src/sets/*.rs
echo ""
echo "=== draw a card (simple ETB/trigger) ==="
grep -n 'Effect::Custom("When this creature enters, draw a card' mtg-cards/src/sets/*.rs
echo ""
echo "=== draw a card, then discard ==="
grep -n 'Effect::Custom(".*draw a card, then discard a card' mtg-cards/src/sets/*.rs
echo ""
echo "=== Ward {2} ==="
grep -n 'StaticEffect::Custom("Ward' mtg-cards/src/sets/*.rs
echo ""
echo "=== Self -1/-1 counter ==="
grep -n 'Effect::Custom("Put.*-1/-1 counter.*on.*\(this\|Sourbread\|Heirloom\|Sting\|Blighted\|Dream\)' mtg-cards/src/sets/*.rsNow I have a clear picture. Let me create the work queue document.
>ls /mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/Now let me update the memory file so future sessions know about this process.
Here's the approach:
Created docs/work-queue.md — a batch-oriented work queue that drives each session. The process for every batch:
docs/work-queue.md → find next unchecked batchgame.rs), if neededcargo check && cargo test --libwork-queue.md| # | Batch | Engine work? | Cards |
|---|---|---|---|
| 1 | LoseLifeOpponents | New variant | 8 DONE |
| 2 | Easy swaps (DestroyAll, EntersTapped, CantBlock, self-counters, loot) | None | ~12 |
| 3 | OpponentDiscards | New variant | 3+ |
| 4 | Simple token creation (stubs with full text) | None | 6+ |
| 5 | Ward keyword | New StaticEffect | 6 |
| 6 | Enters-tapped-unless | New StaticEffect | 2 |
| 7 | Mass keyword grant EOT | New Effect | 5+ |
| 8 | Fight/Bite | New Effect | ~10 |
| 9 | Modal spells | New framework | 15+ |
| 10+ | Equipment, Planeswalkers, Token copy, X-cost, etc. | Major systems | many |
The key insight: docs are the state, not the context window. Any session can pick up where the last left off by reading work-queue.md. Want me to start on Batch 2 now?
What instructions would I give you to run the loop?
The user wants a prompt/instruction they can paste into a new Claude Code session to kick off the loop. Let me think about what that would look like.
The key requirements:
Let me draft something concise but complete.
Something like this:
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 variants2. If engine work is needed: add the variant, constructor, and match arm3. Add a test for any new engine effect (follow the pattern in game.rs mod tests)4. Fix all cards listed in the batch (grep to find exact lines, fix across all 4 set files)5. cargo check -p mtg-cards && cargo test --lib6. Update docs: mark cards in docs/{fdn,tla,tdm,ecl}-remediation.md, update ROADMAP.md7. Check off the batch in docs/work-queue.md with today's date
Then do the next batch. Repeat until context is getting long, then stop and make sure work-queue.md is current.That's the core. A few things to consider:
What makes this work well:
work-queue.md has all the specifics (line numbers, exact substitutions, notes about gotchas)What could go wrong:
AddCounters works on self without a target. That needs investigation before fixing. The instructions should handle uncertainty gracefully.Optional additions:
If a batch has a NOTE about uncertainty, investigate first and update the queue with findings before committing the fix.If you discover new cards that fit an existing batch while working, add them.If a batch turns out to be wrong or more complex than described, update the queue and move on to the next one.Want me to put this into a file (like a CLAUDE.md section or a script) so it's always available, or is copy-paste sufficient?
Let's create a file called docs/work-queue-instructions.md. If you think the optional additions are valuable, add those as well.
Implement the following plan:
The FDN remediation doc identifies "Partial" cards using Effect::Custom(...) for abilities that should use typed variants. Four of these cards say "each opponent loses N life" — the Java implementation uses LoseLifeOpponentsEffect (life loss), which is mechanically distinct from damage in MTG rules. Our engine has DealDamageOpponents but no life-loss-to-opponents variant. We'll add LoseLifeOpponents to match the Java model, then fix 5 cards.
Effect::LoseLifeOpponents variantmtg-engine/src/abilities.rs
Effect enum in the // -- Life -- section (~line 77):
/// Each opponent loses life.LoseLifeOpponents { amount: u32 },pub fn lose_life_opponents(amount: u32) -> Self { Effect::LoseLifeOpponents { amount }}mtg-engine/src/game.rs — Add match arm in execute_effects() (after LoseLife arm, ~line 1015):
Effect::LoseLifeOpponents { amount } => { let opponents: Vec<PlayerId> = self.state.turn_order.iter() .filter(|&&id| id != controller) .copied() .collect(); for opp in opponents { if let Some(player) = self.state.players.get_mut(&opp) { player.life -= *amount as i32; } }}(Same implementation as DealDamageOpponents for now, but semantically correct — when damage prevention is added later, only DealDamageOpponents will be affected.)
All in mtg-cards/src/sets/fdn.rs — single-line replacements:
| Card | Current | Fix |
|---|---|---|
| Marauding Blight-Priest (~line 836) | Effect::Custom("Each opponent loses 1 life.".into()) | Effect::lose_life_opponents(1) |
| Pulse Tracker (~line 850) | Effect::Custom("Each opponent loses 1 life.".into()) | Effect::lose_life_opponents(1) |
| Vampire Spawn (~line 967) | Effect::Custom("Each opponent loses 2 life, you gain 2 life.".into()) | Effect::lose_life_opponents(2), Effect::gain_life(2) |
| Vampire Neonate (~line 1702) | Effect::Custom("Each opponent loses 1 life, you gain 1 life.".into()) | Effect::lose_life_opponents(1), Effect::gain_life(1) |
| Phyrexian Arena (~line 1410) | Effect::Custom("You lose 1 life.".into()) | Effect::lose_life(1) |
cargo check -p mtg-engine — new variant compilescargo check -p mtg-cards — card changes compilecargo test --lib — all tests passIf you need specific details from before exiting plan mode (like exact code snippets, error messages, or content you generated), read the full transcript at: /mnt/dest-btrfs/home/jc/.claude/projects/-mnt-dest-btrfs-home-jc-src-mage/a83bffdc-973c-4730-8a88-2ef219ba63de.jsonl
If this plan can be broken down into multiple independent tasks, consider using the TeamCreate tool to create a team and parallelize the work.
Done. The file is at docs/work-queue-instructions.md — you can just paste its contents into a fresh session to kick off the loop. The queue state lives in docs/work-queue.md so sessions pick up where the last one left off.
Now I have all the exact lines. Let me make all the changes.
Now add the match arm in game.rs and fix all 5 cards in fdn.rs.
I'll implement this plan step by step. Let me start by reading the relevant files.
Need to read game.rs first. Let me do that and retry all the edits.
All edits done. Now let's verify compilation and run tests.
>cd /mnt/dest-btrfs/home/jc/src/mage/mtg-rl && cargo check -p mtg-engine 2>&1>cargo check -p mtg-cards 2>&1>cargo test --lib 2>&1All 244 tests pass (52 + 18 + 157 + 17). Here's a summary of the changes:
mtg-engine/src/abilities.rs:
LoseLifeOpponents { amount: u32 } variant to the Effect enumlose_life_opponents(amount) constructor methodmtg-engine/src/game.rs:
LoseLifeOpponents in execute_effects() — iterates opponents and subtracts lifemtg-cards/src/sets/fdn.rs — Fixed 5 cards:
Custom(...) → lose_life_opponents(1)Custom(...) → lose_life_opponents(1)Custom(...) → lose_life_opponents(2), gain_life(2)Custom("You lose 1 life.") → lose_life(1)Custom(...) → lose_life_opponents(1), gain_life(1)Let's update the @docs/fdn-remediation.md doc and @ROADMAP.md to reflect that we did this. Let's also fix other cards that are now possible because we added this effect.
Let me first check what other cards across all sets use similar "each opponent loses" Custom effects that can now be fixed with LoseLifeOpponents.
mtg-cards/src/sets/fdn.rs:5979: vec![Effect::Custom("Each opponent loses X life. You gain life equal to the life lost this way.".into())],
mtg-cards/src/sets/ecl.rs:465: vec![Effect::Custom("Each opponent loses 2 life.".into())],
mtg-cards/src/sets/ecl.rs:1833: vec![Effect::Custom("Each opponent loses 1 life, you gain 1 life.".into())],
mtg-cards/src/sets/tdm.rs:1040: vec![Effect::Custom("Each opponent loses 2 life.".into()), Effect::gain_life(2)],No matches foundmtg-cards/src/sets/fdn.rs:5340: vec![Effect::Custom("When this creature enters, each opponent discards a card and loses 2 life. You draw a card and gain 2 life.".into())],
mtg-cards/src/sets/fdn.rs:6740: vec![Effect::Custom("Whenever an opponent casts a spell, that player loses 5 life unless they discard a card.".into())],
mtg-cards/src/sets/tdm.rs:1040: vec![Effect::Custom("Each opponent loses 2 life.".into()), Effect::gain_life(2)],
mtg-cards/src/sets/ecl.rs:465: vec![Effect::Custom("Each opponent loses 2 life.".into())],
mtg-cards/src/sets/ecl.rs:974: vec![Effect::Custom("Blight 2 or lose 3 life.".into())],
mtg-cards/src/sets/ecl.rs:1833: vec![Effect::Custom("Each opponent loses 1 life, you gain 1 life.".into())],
mtg-cards/src/sets/ecl.rs:3742: vec![Effect::Custom("When this creature enters, mill three cards. Then if there is an Elf card in your graveyard, each opponent loses 2 life and you gain 2 life.".into())],Good. I can see 3 clean matches across ECL and TDM that can be fixed the same way. Let me read the surrounding context for each.
1005- Effect::GainLife { amount } => {
1006- if let Some(player) = self.state.players.get_mut(&controller) {
1007- player.life += *amount as i32;
1008- }
1009- }
1010: Effect::LoseLife { amount } => {
1011- // Controller loses life (target player effects will use
1012- // SelectedTargets for proper player targeting)
1013- if let Some(player) = self.state.players.get_mut(&controller) {
1014- player.life -= *amount as i32;
1015- }Now let me fix these 3 additional cards and also read the remediation docs so I can update them.
Now let me verify everything still compiles.
>cargo check -p mtg-cards 2>&1Good. Now let me check the remediation docs for ECL and TDM to understand what needs updating there too.
docs/ecl-remediation.md-168- - **What it should do**: {2}{W}: Gains indestructible until end of turn.
docs/ecl-remediation.md-169- - **Status**: Keyword grant now implemented. Card may be fully functional.
/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/ecl-remediation.md-170-
docs/ecl-remediation.md:171:- [ ] **Champion of the Weird** — What works: none of the effects. What's broken: Both `Cost::Custom` and `Effect::Custom` on all abilities.
docs/ecl-remediation.md-172- - **Java source**: `Mage.Sets/src/mage/cards/c/ChampionOfTheWeird.java`
docs/ecl-remediation.md-173- - **What it should do**: Behold a Goblin + exile it. {1}{B}, blight 1: each opponent loses 2 life. Leaves: return exiled card.
docs/ecl-remediation.md-174- - **Fix needed**: Implement behold mechanic, `LoseLife` for opponents, and return-exiled-card effect.
--
docs/ecl-remediation.md-423- - **What it should do**: +1/+0, then fights opponent's creature.
docs/ecl-remediation.md-424- - **Fix needed**: Implement `Effect::Fight`.
/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/ecl-remediation.md-425-
docs/ecl-remediation.md:426:- [ ] **Boggart Mischief** — What works: stats. What's broken: Both ETB and dies triggers are `Effect::Custom`.
docs/ecl-remediation.md-427- - **Java source**: `Mage.Sets/src/mage/cards/b/BoggartMischief.java`
docs/ecl-remediation.md-428- - **What it should do**: ETB: blight 1 to create 2 Goblin tokens. Goblin dies: opponents lose 1, you gain 1.
docs/ecl-remediation.md-429- - **Fix needed**: Complex — needs blight choice + conditional token + drain trigger.
--
docs/tdm-remediation.md-354- - **Java source**: `Mage.Sets/src/mage/cards/s/SibsigAppraiser.java`
docs/tdm-remediation.md-355- - **Fix needed**: "Look at top N, distribute" effect.
/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/tdm-remediation.md-356-
docs/tdm-remediation.md:357:- [ ] **Skirmish Rhino** — 3/4 trample for {W}{B}{G}. What works: GainLife 2. What's broken: `Effect::Custom("Each opponent loses 2 life.")` (should use DealDamageOpponents).
docs/tdm-remediation.md-358- - **Fix needed**: Replace `Custom("Each opponent loses 2 life.")` with `Effect::DealDamageOpponents { amount: 2 }`.
/mnt/dest-btrfs/home/jc/src/mage/mtg-rl/docs/tdm-remediation.md-359-
docs/tdm-remediation.md-360-- [ ] **Snakeskin Veil** — Instant {G}. What works: AddCounters +1/+1 + Hexproof until EOT. **(NOW IMPLEMENTED — Hexproof NOW WORKS)**Now let me read the specific sections of the docs that need updating. Let me also check the FDN remediation doc for the entries about the 5 cards we fixed, and read the current "Effects that ACTUALLY WORK" table line count so I can add LoseLifeOpponents.
Now let me read the Partial Cards sections for the cards we fixed (Marauding Blight-Priest, Pulse Tracker, Vampire Spawn, Phyrexian Arena, Vampire Neonate) and the Phase 2 roadmap section.
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 variants2. If engine work is needed: add the variant, constructor, and match arm3. Add a test for any new engine effect (follow the pattern in game.rs mod tests)4. Fix all cards listed in the batch (grep to find exact lines, fix across all 4 set files)5. cargo check -p mtg-cards && cargo test --lib6. Update docs: mark cards in docs/{fdn,tla,tdm,ecl}-remediation.md, update ROADMAP.md7. Check off the batch in docs/work-queue.md with today's date
Then do the next batch. Repeat until context is getting long, then stop and make sure work-queue.md is current./// Each opponent loses life.LoseLifeOpponents { amount: u32 },pub fn lose_life_opponents(amount: u32) -> Self { Effect::LoseLifeOpponents { amount }}Effect::LoseLifeOpponents { amount } => { let opponents: Vec<PlayerId> = self.state.turn_order.iter() .filter(|&&id| id != controller) .copied() .collect(); for opp in opponents { if let Some(player) = self.state.players.get_mut(&opp) { player.life -= *amount as i32; } }}