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.
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.
clear
Read docs/work-queue.md and do the next unchecked batch. For each batch:
mtg-engine/src/abilities.rs, mtg-engine/src/game.rs) to understand current Effect/StaticEffect variantsgame.rs mod tests)cargo check -p mtg-cards && cargo test --libdocs/{fdn,tla,tdm,ecl}-remediation.md, update ROADMAP.mddocs/work-queue.md with today's dateIf 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.
I'll start by reading the work queue to find the next unchecked batch.
The next unchecked batch is Batch 6: Enters-tapped-unless (conditional ETB tapped). Let me read the engine files to understand the current state, then investigate what's needed.
Let me read the relevant engine files and the TDM set file to understand the cards.
The next unchecked batch is Batch 6: Enters-tapped-unless (conditional ETB tapped). Let me read the engine files and investigate the cards.
mtg-rl/mtg-engine/src/abilities.rs:904: EntersTapped {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())]),Now let me understand the TDM cards better. Let me look at the full card implementations around those lines, and also check how EntersTapped is handled in game.rs.
Now let me look at how EntersTapped is handled in game.rs and the full card context in TDM.
No matches found
It looks like EntersTapped exists as a StaticEffect variant but isn't actually handled in game.rs. Let me search more carefully for how it's used. The batch wants me to add EntersTappedUnless with a condition string. These are "fastlands" / conditional taplands.
Let me check: is EntersTapped handled in game.rs at all? It says "No matches found" which means this static effect isn't mechanically enforced yet. That's consistent with our approach - we store it as structured data but mechanical enforcement comes later.
For Batch 6, the approach should be simple: add StaticEffect::EntersTappedUnless { condition: String } and a builder, then swap the Custom strings. The mechanical enforcement (actually checking the condition) is deferred like Ward was.
Now I'm scanning through the card database to see if there are other cards with this "enters tapped unless" mechanic across different sets.
Let me check if there are more "enters tapped unless" cards across all sets, and look at the Java implementations for reference.
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.",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() }]),I also notice the ECL shockland (Blood Crypt) has "Pay 2 life or enters tapped" which is a different pattern but related. Let me also check FDN for more conditional taplands.
Now let me check if there are any more conditional taplands in FDN or ECL. Let me also search for "tapped unless" more broadly.
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.
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())]),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.
Now add the builder method next to the ward() builder.
Now add a test for the new builder, extending the static_effect_builders test.
Now fix the two TDM cards. Let me update them to use the new typed variant.
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.rssed -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.rsVerify the changes look correct.
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 } => {Good. Now let me build and test.
cargo check -p mtg-cards 2>&1 | tail -5cargo test --lib 2>&1 | tail -15All 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.
cargo test --lib 2>&1 | grep -E "^test result:|running"All 249 tests pass. Now let me update the docs. First, the work queue.
Now update the TDM remediation doc.
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.")`.Now check if Dalkovan Encampment has a separate broken entry or was already listed as working.
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.
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)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.
Batch 6 is done. Now let me continue to the next batch — Batch 7: Mass keyword grant until EOT. This one is more substantial.
This batch needs two new effects:
Effect::BoostAllUntilEndOfTurn { filter, power, toughness } — mass +N/+M until end of turnEffect::GrantKeywordAllUntilEndOfTurn { filter, keyword } — mass keyword grant until end of turnOr we could combine them, but the cards show separate needs:
Crash Through: just keyword grant (trample) + draw
Overrun: both +3/+3 AND trample
Make a Stand: +1/+0 AND indestructible
Heroic Reinforcements: +1/+1 AND haste (+ create tokens)
Balmor: +1/+0 AND trample (triggered)
Both variants are definitely needed. Let me check the actual card implementations to see the full scope.
Let me look at the actual card implementations for these 5 cards.
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 {Now I see the pattern clearly. I need two new Effect variants:
BoostAllUntilEndOfTurn { filter, power, toughness } — mass +N/+M until end of turnGrantKeywordAllUntilEndOfTurn { filter, keyword } — mass keyword grant until end of turnLet me also check how BoostUntilEndOfTurn and GainKeywordUntilEndOfTurn are implemented in game.rs to understand the pattern I should follow.
Let me look at how the single-target BoostUntilEndOfTurn and GainKeywordUntilEndOfTurn are implemented in game.rs.
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- }I see the pattern now. For BoostUntilEndOfTurn, the current implementation uses P1P1 counters as a simplification (not ideal, but it works). And GainKeywordUntilEndOfTurn uses granted_keywords which gets cleared at end of turn.
For the mass versions, I need to:
Add Effect::BoostAllUntilEndOfTurn { filter, power, toughness }
Add Effect::GrantKeywordAllUntilEndOfTurn { filter, keyword }
Implement match arms in game.rs — iterate all permanents matching the filter owned by the active player
Add constructors for these new effects
Let me check how DestroyAll and DealDamageAll handle their filter matching to follow the same pattern.
Let me check how DestroyAll implements its filter matching to follow the same pattern.
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() {Now let me check the matches_filter function to understand how it works with "creatures you control" style filters.
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;I see - matches_filter doesn't check controller. But for "creatures you control" effects, I need to filter by the controller being the active player. Looking at how the existing mass effects work - DestroyAll destroys ALL matching creatures, not just "yours". That's correct for Day of Judgment.
For mass buffs, the filter strings are "Creatures you control" — so I need to handle the "you control" part by checking the controller. Let me create two new effect variants and implement them in game.rs.
The implementation needs to:
Filter by creature type
Filter by controller (the active player / source controller)
Apply the buff
I'll define new effect types for mass boosts and keyword grants that parse the "you control" condition from the filter string, then apply the effects to all matching creatures in the match arms.
Good. Now I have a clear picture. I need two new Effect variants:
BoostAllUntilEndOfTurn { filter, power, toughness } — mass P/T buff until EOTGrantKeywordAllUntilEndOfTurn { filter, keyword } — mass keyword grant until EOTLet me implement them.
Now add the constructors.
Now implement the match arms in game.rs. Let me find where the existing single-target versions are handled.
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.
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.
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- }The controller parameter is available. Now I'll add the mass effect implementations.
Good. Now let me add a test in game.rs. Let me find the test module.
No matches found
1618:mod tests {wc -l /mnt/dest-btrfs/home/jc/src/mage/mtg-rl/mtg-engine/src/game.rsNow let me add a test for the mass effects. I'll add it at the end of the test module.
Good, I have the test helpers. Now let me add the test.
Now let me check if has_keyword exists on Permanent.
mtg-engine/src/permanent.rs:127: pub fn has_keyword(&self, kw: KeywordAbilities) -> bool {Good, it exists. Let me compile.
cargo check -p mtg-engine 2>&1 | tail -10Now fix the 5 FDN cards. Let me use sed for each one.
sed -n '2388,2399p' mtg-cards/src/sets/fdn.rssed -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.rssed -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.rssed -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.rssed -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.rsNow fix Balmor — this one has a compound Custom string.
sed -n '2896,2900p' mtg-cards/src/sets/fdn.rssed -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.rsNow let me verify all the fixes look correct and compile.
grep -n "boost_all_eot\|grant_keyword_all_eot" mtg-cards/src/sets/fdn.rsAll 5 cards fixed. Let me also search for any other cards across all sets that could use these new effects.
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())],Let me fix the additional cards I found:
FDN line 3071: "Creatures you control get +2/+2 until end of turn." — simple boost_all_eot
TDM line 479: "Creatures you control get +X/+X and trample..." — has variable X, can't fix with simple variant
ECL line 843: "Creatures you control get +1/+0... Kithkin also gain first strike..." — compound/conditional, skip
ECL line 1599: "Creatures you control get +1/+1 until end of turn." — simple boost_all_eot
ECL line 2237: "Creatures you control get +1/+1 and gain haste until end of turn." — needs both boost_all_eot and grant_keyword_all_eot combined
Now I need to check the exact card text for the ones I can actually fix.
Found more cards using these patterns! Let me check which are simple enough to fix.
I can fix these 3 additional cards too. Let me also check what card the FDN one is.
Found 3 more fixable cards:
+2/+2 until EOT+1/+1 until EOT+1/+1 and haste until EOTsed -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.rssed -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.rsNow fix the haste part of Catharsis.
grep -n "gain haste until end of turn" mtg-cards/src/sets/ecl.rsWait, I already replaced the +1/+1 part but the original string was the compound one "get +1/+1 and gain haste until end of turn." Let me check what the current state of the file is.
sed -n '2236,2238p' mtg-cards/src/sets/ecl.rsThe sed didn't match because the string had "and gain haste" appended. Let me fix this one specifically.