Skip to main content

manabrew_engine/card/
valid_filter.rs

1//! Shared card/player filter matching used across triggers, static abilities,
2//! replacement effects, and combat. Consolidates 34 duplicate implementations.
3//!
4//! This module provides canonical implementations for matching cards and players
5//! against filter expressions like "Creature.YouCtrl" or "Opponent".
6//!
7//! **Filter Syntax:**
8//!
9//! Card filters are dot-separated: "Creature.YouCtrl.nonToken"
10//! - Comma separates OR conditions: "Creature,Artifact" matches either
11//! - Dot separates qualifiers: "Creature.YouCtrl" matches creatures you control
12//! - Plus separates compound conditions: "YouCtrl+kicked" matches both
13//!
14//! **Type parts:**
15//! - Card, Permanent, Creature, Land, Artifact, Enchantment, Planeswalker, Instant, Sorcery
16//! - Subtypes: "Zombie", "Wall", "Forest", etc.
17//!
18//! **Qualifiers:**
19//! - Controller: YouCtrl, OppCtrl, YouControl, OpponentCtrl
20//! - Self: Self, Other, StrictlyOther
21//! - Token: token, nonToken
22//! - Type negation: nonCreature, nonLand
23//! - State: tapped, untapped, kicked
24//! - Counters: counters_GE3_P1P1, counters_EQ1_Charge
25//! - CMC: cmcEQ1, cmcLE3, cmcGE5
26//! - Color: White, Blue, Black, Red, Green, Colorless, multicolor
27//! - Combat: DamagedBy
28//! - Attachment: EnchantedBy
29
30use forge_foundation::color::Color;
31use forge_foundation::mana::ManaAtom;
32use forge_foundation::ZoneType;
33
34use crate::card::Card;
35use crate::combat::{CombatState, DefenderId};
36use crate::core::HasSVars;
37use crate::game::GameState;
38use crate::ids::{CardId, PlayerId};
39use crate::parsing::compare::compare_expr;
40use crate::parsing::keys;
41use crate::parsing::{
42    cached_compiled_selector, CardColorSelector, CardIdentitySelector, CardSelectorType,
43    CardStateSelector, CardSupertypeSelector, CompiledSelector, ContextPredicate,
44    ControllerSelector, NumericSelectorProperty, ParsedParams, RelationPredicate, Selector,
45    SelectorCompareOperator, SelectorNumericOperand, SelectorPredicate, TargetRef,
46};
47use crate::spellability::SpellAbility;
48
49fn requirement_controller(game: &GameState, source: &Card) -> PlayerId {
50    let mut controller = source.controller;
51
52    // Java parity: during trigger resolution, use the resolving trigger's
53    // activating player rather than the host card's current controller.
54    if !game.stack.is_empty()
55        && game.stack.is_resolving()
56        && game.stack.cur_resolving_card() == Some(source.id)
57    {
58        if let Some(entry) = game.stack.peek() {
59            if entry.spell_ability.is_trigger {
60                controller = entry.spell_ability.activating_player;
61            }
62        }
63    }
64
65    controller
66}
67
68fn requirement_amount(
69    source: &Card,
70    svar_source: &dyn HasSVars,
71    expr: &str,
72    game: &GameState,
73) -> i32 {
74    let raw_value = svar_source
75        .get_svar(expr)
76        .or_else(|| source.get_s_var(expr))
77        .unwrap_or(expr)
78        .trim();
79
80    if let Ok(n) = raw_value.parse::<i32>() {
81        return n;
82    }
83    if let Some(stripped) = raw_value.strip_prefix('+') {
84        if let Ok(n) = stripped.parse::<i32>() {
85            return n;
86        }
87    }
88    if let Some(stripped) = raw_value.strip_prefix('-') {
89        return -requirement_amount(source, svar_source, stripped.trim(), game);
90    }
91
92    if raw_value.starts_with("Count$") {
93        return crate::svar::resolve_count_svar(raw_value, game, source.id, source.controller);
94    }
95
96    let sa = crate::spellability::SpellAbility::new_simple(
97        Some(source.id),
98        requirement_controller(game, source),
99        &format!("DB$ Internal | Amount$ {raw_value}"),
100    );
101    let resolved = crate::svar::resolve_numeric_value(game, &sa, raw_value, i32::MIN);
102    if resolved != i32::MIN {
103        return resolved;
104    }
105
106    0
107}
108
109fn compare_requirement_amount(
110    source: &Card,
111    svar_source: &dyn HasSVars,
112    compare: &str,
113    game: &GameState,
114    left: i32,
115) -> bool {
116    let operator = compare.get(..compare.len().min(2)).unwrap_or("GE");
117    let operand_expr = compare.get(compare.len().min(2)..).unwrap_or("1");
118    let operand = requirement_amount(source, svar_source, operand_expr, game);
119    compare_expr(left, &format!("{operator}{operand}"))
120}
121
122/// Typed requirement bag for Java `CardTraitBase.meetsCommonRequirements(params)`.
123///
124/// Only params consumed by that Java method belong here. This keeps the shared
125/// requirement gate reusable across trigger, spell ability, static ability, and
126/// replacement IRs without passing the full legacy `Params` map at runtime.
127#[derive(Debug, Clone, PartialEq, Eq, Default)]
128pub struct CardTraitRequirementsIr {
129    metalcraft: Option<String>,
130    delirium: Option<String>,
131    threshold: Option<String>,
132    hellbent: Option<String>,
133    bloodthirst: Option<String>,
134    fateful_hour: Option<String>,
135    monarch: Option<String>,
136    revolt: Option<String>,
137    desert: Option<String>,
138    blessing: Option<String>,
139    day_time: Option<String>,
140    adamant: Option<String>,
141    life_total: Option<String>,
142    life_amount: Option<String>,
143    is_present: Option<String>,
144    is_present_selector: Option<CompiledSelector>,
145    present_compare: Option<String>,
146    present_player: Option<String>,
147    present_zone: Option<String>,
148    present_defined: Option<String>,
149    is_present2: Option<String>,
150    is_present2_selector: Option<CompiledSelector>,
151    present_compare2: Option<String>,
152    present_player2: Option<String>,
153    present_zone2: Option<String>,
154    check_defined_player: Option<String>,
155    defined_player_compare: Option<String>,
156    check_svar: Option<String>,
157    svar_compare: Option<String>,
158    check_second_svar: Option<String>,
159    second_svar_compare: Option<String>,
160    mana_spent: Option<String>,
161    mana_not_spent: Option<String>,
162    werewolf_transform_condition: bool,
163    werewolf_untransform_condition: bool,
164    class_level: Option<String>,
165    condition: Option<String>,
166}
167
168impl CardTraitRequirementsIr {
169    pub fn from_key_values<'a, I>(
170        entries: I,
171        is_present_selector: Option<CompiledSelector>,
172        is_present2_selector: Option<CompiledSelector>,
173    ) -> Self
174    where
175        I: IntoIterator<Item = (&'a str, &'a str)>,
176    {
177        let mut ir = Self::default();
178        for (key, value) in entries {
179            ir.set(key, value);
180        }
181        ir.is_present_selector = is_present_selector;
182        ir.is_present2_selector = is_present2_selector;
183        ir
184    }
185
186    pub fn from_parsed(params: &ParsedParams<'_>) -> Self {
187        let mut ir = Self::from_key_values(
188            params
189                .entries()
190                .iter()
191                .map(|entry| (entry.key, entry.value)),
192            None,
193            None,
194        );
195        ir.is_present_selector = ir.is_present.as_deref().map(cached_compiled_selector);
196        ir.is_present2_selector = ir.is_present2.as_deref().map(cached_compiled_selector);
197        ir
198    }
199
200    pub fn is_empty(&self) -> bool {
201        self.metalcraft.is_none()
202            && self.delirium.is_none()
203            && self.threshold.is_none()
204            && self.hellbent.is_none()
205            && self.bloodthirst.is_none()
206            && self.fateful_hour.is_none()
207            && self.monarch.is_none()
208            && self.revolt.is_none()
209            && self.desert.is_none()
210            && self.blessing.is_none()
211            && self.day_time.is_none()
212            && self.adamant.is_none()
213            && self.life_total.is_none()
214            && self.is_present.is_none()
215            && self.is_present2.is_none()
216            && self.check_defined_player.is_none()
217            && self.check_svar.is_none()
218            && self.check_second_svar.is_none()
219            && self.mana_spent.is_none()
220            && self.mana_not_spent.is_none()
221            && !self.werewolf_transform_condition
222            && !self.werewolf_untransform_condition
223            && self.class_level.is_none()
224            && self.condition.is_none()
225    }
226
227    pub fn meets(&self, game: &GameState, source: &Card, svar_source: &dyn HasSVars) -> bool {
228        meets_card_trait_requirements(self, game, source, svar_source)
229    }
230
231    fn set(&mut self, key: &str, value: &str) {
232        match key {
233            "Metalcraft" => self.metalcraft = Some(value.to_string()),
234            "Delirium" => self.delirium = Some(value.to_string()),
235            "Threshold" => self.threshold = Some(value.to_string()),
236            "Hellbent" => self.hellbent = Some(value.to_string()),
237            "Bloodthirst" => self.bloodthirst = Some(value.to_string()),
238            "FatefulHour" => self.fateful_hour = Some(value.to_string()),
239            "Monarch" => self.monarch = Some(value.to_string()),
240            "Revolt" => self.revolt = Some(value.to_string()),
241            "Desert" => self.desert = Some(value.to_string()),
242            "Blessing" => self.blessing = Some(value.to_string()),
243            "DayTime" => self.day_time = Some(value.to_string()),
244            "Adamant" => self.adamant = Some(value.to_string()),
245            "LifeTotal" => self.life_total = Some(value.to_string()),
246            "LifeAmount" => self.life_amount = Some(value.to_string()),
247            keys::IS_PRESENT => self.is_present = Some(value.to_string()),
248            keys::PRESENT_COMPARE => self.present_compare = Some(value.to_string()),
249            keys::PRESENT_PLAYER => self.present_player = Some(value.to_string()),
250            keys::PRESENT_ZONE => self.present_zone = Some(value.to_string()),
251            "PresentDefined" => self.present_defined = Some(value.to_string()),
252            "IsPresent2" => self.is_present2 = Some(value.to_string()),
253            "PresentCompare2" => self.present_compare2 = Some(value.to_string()),
254            "PresentPlayer2" => self.present_player2 = Some(value.to_string()),
255            "PresentZone2" => self.present_zone2 = Some(value.to_string()),
256            "CheckDefinedPlayer" => self.check_defined_player = Some(value.to_string()),
257            "DefinedPlayerCompare" => self.defined_player_compare = Some(value.to_string()),
258            keys::CHECK_SVAR => self.check_svar = Some(value.to_string()),
259            keys::SVAR_COMPARE => self.svar_compare = Some(value.to_string()),
260            "CheckSecondSVar" => self.check_second_svar = Some(value.to_string()),
261            "SecondSVarCompare" => self.second_svar_compare = Some(value.to_string()),
262            "ManaSpent" => self.mana_spent = Some(value.to_string()),
263            "ManaNotSpent" => self.mana_not_spent = Some(value.to_string()),
264            "WerewolfTransformCondition" => self.werewolf_transform_condition = true,
265            "WerewolfUntransformCondition" => self.werewolf_untransform_condition = true,
266            "ClassLevel" => self.class_level = Some(value.to_string()),
267            keys::CONDITION => self.condition = Some(value.to_string()),
268            _ => {}
269        }
270    }
271}
272
273fn check_boolean_requirement(value: Option<&str>, actual: bool) -> bool {
274    value.is_none_or(|value| value.eq_ignore_ascii_case("True") == actual)
275}
276
277fn player_life_for_requirement(game: &GameState, source: &Card, who: &str) -> i32 {
278    let controller = requirement_controller(game, source);
279    match who {
280        "You" => game.player(controller).life,
281        "OpponentSmallest" => game
282            .alive_players()
283            .into_iter()
284            .filter(|&pid| pid != controller)
285            .map(|pid| game.player(pid).life)
286            .min()
287            .unwrap_or(1),
288        "OpponentGreatest" => game
289            .alive_players()
290            .into_iter()
291            .filter(|&pid| pid != controller)
292            .map(|pid| game.player(pid).life)
293            .max()
294            .unwrap_or(1),
295        "ActivePlayer" => game.player(game.active_player()).life,
296        _ => 1,
297    }
298}
299
300fn collect_present_cards(
301    game: &GameState,
302    source: &Card,
303    defined: Option<&str>,
304    present_player: &str,
305    present_zone: ZoneType,
306) -> Vec<CardId> {
307    if let Some(defined) = defined {
308        return crate::ability::ability_utils::get_defined_cards(
309            game,
310            Some(source.id),
311            defined,
312            Some(requirement_controller(game, source)),
313        );
314    }
315
316    let controller = requirement_controller(game, source);
317    let mut cards = Vec::new();
318
319    if present_player.eq_ignore_ascii_case("You") || present_player.eq_ignore_ascii_case("Any") {
320        cards.extend(game.cards_in_zone(present_zone, controller).iter().copied());
321    }
322    if present_player.eq_ignore_ascii_case("Opponent") || present_player.eq_ignore_ascii_case("Any")
323    {
324        for pid in game.alive_players() {
325            if pid != controller {
326                cards.extend(game.cards_in_zone(present_zone, pid).iter().copied());
327            }
328        }
329    }
330
331    cards
332}
333
334fn paying_color_count(paying_mana_to_cast: &[u16], color_mask: u16) -> usize {
335    paying_mana_to_cast
336        .iter()
337        .filter(|&&atom| atom == color_mask)
338        .count()
339}
340
341fn has_all_spent_colors(colors_spent_to_cast: u16, colors: u16) -> bool {
342    colors != 0 && (colors_spent_to_cast & colors) == colors
343}
344
345/// Check if a card matches a filter expression like "Creature.YouCtrl".
346/// Returns true if `valid` is empty or the card satisfies all parts.
347///
348/// # Examples
349///
350/// ```ignore
351/// // Matches any creature you control:
352/// matches_valid_card("Creature.YouCtrl", creature, source)
353///
354/// // Matches either creatures or artifacts:
355/// matches_valid_card("Creature,Artifact", card, source)
356///
357/// // Matches creatures you control that are tokens:
358/// matches_valid_card("Creature.YouCtrl.token", card, source)
359/// ```
360pub fn matches_valid_card(valid: &str, card: &Card, source: &Card) -> bool {
361    matches_valid_card_selector(&cached_compiled_selector(valid), card, source)
362}
363
364#[cfg(debug_assertions)]
365fn legacy_matches_valid_card(valid: &str, card: &Card, context: MatchContext<'_>) -> bool {
366    let valid = valid.trim();
367    if valid.is_empty() {
368        return true;
369    }
370
371    // Comma-separated = OR conditions.
372    // Each comma-delimited part is a separate filter; the card matches if ANY part matches.
373    // Parts may contain dots (e.g. "Card.Self,Elemental.Other+YouCtrl").
374    if valid.contains(',') {
375        return valid
376            .split(',')
377            .any(|part| matches_single_valid_card(part.trim(), card, context));
378    }
379
380    matches_single_valid_card(valid, card, context)
381}
382
383/// Convenience wrapper: None means "no filter" → always matches.
384pub fn matches_valid_card_opt(valid: Option<&str>, card: &Card, source: &Card) -> bool {
385    match valid {
386        None => true,
387        Some(v) => matches_valid_card(v, card, source),
388    }
389}
390
391/// Match a precompiled selector against a card without reparsing the
392/// comma/dot/plus selector structure.
393pub fn matches_valid_card_selector(
394    selector: &CompiledSelector,
395    card: &Card,
396    source: &Card,
397) -> bool {
398    matches_valid_card_selector_with_context(selector, card, MatchContext::from_source(source))
399}
400
401pub fn matches_valid_card_selector_in_game(
402    selector: &CompiledSelector,
403    card: &Card,
404    source: &Card,
405    game: &GameState,
406) -> bool {
407    matches_valid_card_selector_with_context(
408        selector,
409        card,
410        MatchContext::from_source(source).with_game(game),
411    )
412}
413
414#[derive(Debug, Clone, Copy)]
415pub struct MatchContext<'a> {
416    pub source_card: &'a Card,
417    pub source_controller: PlayerId,
418    pub targeted_cards: &'a [CardId],
419    pub targeted_players: &'a [PlayerId],
420    pub remembered_cards: &'a [CardId],
421    pub remembered_players: &'a [PlayerId],
422    pub trigger_remembered_cards: &'a [CardId],
423    pub triggering_card: Option<CardId>,
424    pub triggering_player: Option<PlayerId>,
425    pub combat: Option<&'a CombatState>,
426    pub game: Option<&'a GameState>,
427    pub spell_ability: Option<&'a SpellAbility>,
428}
429
430impl<'a> MatchContext<'a> {
431    pub fn from_source(source_card: &'a Card) -> Self {
432        Self {
433            source_card,
434            source_controller: source_card.controller,
435            targeted_cards: &[],
436            targeted_players: &[],
437            remembered_cards: &source_card.remembered_cards,
438            remembered_players: &source_card.remembered_players,
439            trigger_remembered_cards: &[],
440            triggering_card: None,
441            triggering_player: None,
442            combat: None,
443            game: None,
444            spell_ability: None,
445        }
446    }
447
448    pub fn with_game(mut self, game: &'a GameState) -> Self {
449        self.game = Some(game);
450        self
451    }
452
453    pub fn with_spell_ability(mut self, spell_ability: &'a SpellAbility) -> Self {
454        self.spell_ability = Some(spell_ability);
455        self
456    }
457
458    /// Override `source_controller` with an iterated player. Mirrors Java's
459    /// `CardLists.getValidCardCount(..., player, source, ctb)` signature
460    /// (`AbilityUtils.java:3380, 3389`), where `YouCtrl` resolves against the
461    /// iterated player rather than the source's controller — needed by
462    /// `PlayerCountOpponents$HighestValid X.YouCtrl` style SVars.
463    pub fn with_source_controller(mut self, player: PlayerId) -> Self {
464        self.source_controller = player;
465        self
466    }
467
468    pub fn with_combat(mut self, combat: &'a CombatState) -> Self {
469        self.combat = Some(combat);
470        self
471    }
472
473    pub fn with_targets(
474        mut self,
475        targeted_cards: &'a [CardId],
476        targeted_players: &'a [PlayerId],
477    ) -> Self {
478        self.targeted_cards = targeted_cards;
479        self.targeted_players = targeted_players;
480        self
481    }
482
483    pub fn with_triggering(
484        mut self,
485        triggering_card: Option<CardId>,
486        triggering_player: Option<PlayerId>,
487    ) -> Self {
488        self.triggering_card = triggering_card;
489        self.triggering_player = triggering_player;
490        self
491    }
492
493    pub fn with_trigger_remembered_cards(mut self, cards: &'a [CardId]) -> Self {
494        self.trigger_remembered_cards = cards;
495        self
496    }
497}
498
499/// Match a precompiled selector with explicit contextual state for predicates
500/// like `Attacking`, `TopLibrary`, and `ExiledWithSource`.
501pub fn matches_valid_card_selector_with_context(
502    selector: &CompiledSelector,
503    card: &Card,
504    context: MatchContext<'_>,
505) -> bool {
506    crate::perf::increment(crate::perf::Metric::SelectorMatches, 1);
507    let result = matches_card_selector_ir(&selector.ir, card, context);
508    #[cfg(debug_assertions)]
509    report_selector_drift(
510        "card",
511        result,
512        legacy_matches_valid_card(&selector.as_raw(), card, context),
513        &selector.as_raw(),
514    );
515    result
516}
517
518/// Known pre-existing compiled-vs-legacy divergences surface in normal games
519/// (e.g. `Creature.Artifact`, `Card.IsRemembered+YouOwn`), so the drift guard
520/// reports once per selector instead of panicking; set `FORGE_SELECTOR_ASSERT`
521/// to make it fatal while debugging a specific divergence.
522#[cfg(debug_assertions)]
523fn report_selector_drift(kind: &str, compiled: bool, legacy: bool, raw: &str) {
524    use std::collections::HashSet;
525    use std::sync::{Mutex, OnceLock};
526
527    if compiled == legacy {
528        return;
529    }
530    if std::env::var_os("FORGE_SELECTOR_ASSERT").is_some() {
531        panic!(
532            "compiled {kind} selector diverged from string matcher for {raw:?}: compiled={compiled} legacy={legacy}"
533        );
534    }
535    static REPORTED: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
536    let mut reported = REPORTED
537        .get_or_init(|| Mutex::new(HashSet::new()))
538        .lock()
539        .unwrap();
540    if reported.insert(raw.to_string()) {
541        eprintln!("[selector-drift] {kind} selector {raw:?}: compiled={compiled} legacy={legacy}");
542    }
543}
544
545fn matches_card_selector_ir(selector: &Selector, card: &Card, context: MatchContext<'_>) -> bool {
546    match selector.alternatives.as_slice() {
547        [] => true,
548        [alternative] => matches_card_selector_alt(alternative, card, context),
549        alternatives => {
550            for alternative in alternatives {
551                if matches_card_selector_alt(alternative, card, context) {
552                    return true;
553                }
554            }
555            false
556        }
557    }
558}
559
560#[inline]
561fn matches_card_selector_alt(
562    alternative: &crate::parsing::SelectorAlt,
563    card: &Card,
564    context: MatchContext<'_>,
565) -> bool {
566    match alternative.predicates.as_slice() {
567        [] => true,
568        [predicate] => matches_card_predicate(predicate, card, context),
569        [first, second] => {
570            matches_card_predicate(first, card, context)
571                && matches_card_predicate(second, card, context)
572        }
573        predicates => {
574            for predicate in predicates {
575                if !matches_card_predicate(predicate, card, context) {
576                    return false;
577                }
578            }
579            true
580        }
581    }
582}
583
584#[inline(always)]
585fn matches_card_predicate(
586    predicate: &SelectorPredicate,
587    card: &Card,
588    context: MatchContext<'_>,
589) -> bool {
590    match predicate {
591        SelectorPredicate::Any => true,
592        SelectorPredicate::CardType(card_type) => matches_card_type_predicate(card_type, card),
593        SelectorPredicate::CardController(controller) => {
594            matches_card_controller(*controller, card.controller, context.source_controller)
595        }
596        SelectorPredicate::Tapped(tapped) => card.tapped == *tapped,
597        SelectorPredicate::Zone(zone) => card.zone == *zone,
598        SelectorPredicate::Token(token) => card.is_token == *token,
599        SelectorPredicate::Color(color) => matches_card_color(*color, card),
600        SelectorPredicate::Colorless => card.color.is_colorless(),
601        SelectorPredicate::CardOwner(controller) => {
602            matches_card_controller(*controller, card.owner, context.source_controller)
603        }
604        SelectorPredicate::StartedTurnTapped(tapped) => card.started_turn_tapped == *tapped,
605        SelectorPredicate::CameUnderControlSinceLastUpkeep => {
606            card.came_under_control_since_last_upkeep()
607        }
608        SelectorPredicate::Multicolor => card.color.is_multicolor(),
609        SelectorPredicate::Commander => card.is_commander,
610        SelectorPredicate::Legendary => card.type_line.is_legendary(),
611        SelectorPredicate::Kicked => card.kicked,
612        SelectorPredicate::CardSupertype(supertype) => {
613            matches_card_supertype_predicate(*supertype, card)
614        }
615        SelectorPredicate::CardIdentity(identity) => {
616            matches_card_identity(*identity, card, context)
617        }
618        SelectorPredicate::Player | SelectorPredicate::PlayerController(_) => false,
619        SelectorPredicate::RememberedCard => context.remembered_cards.contains(&card.id),
620        SelectorPredicate::TriggerRememberedCard => {
621            context.trigger_remembered_cards.contains(&card.id)
622        }
623        SelectorPredicate::EffectSource => context.source_card.effect_source == Some(card.id),
624        SelectorPredicate::SourceColor(color) => matches_card_color(*color, card),
625        SelectorPredicate::SourceColorless => card.color.is_colorless(),
626        SelectorPredicate::ChosenColorSource => matches_chosen_color_source(card, context),
627        SelectorPredicate::CardState(state) => matches_card_state(*state, card, context),
628        SelectorPredicate::Context(predicate) => {
629            matches_context_predicate(predicate, card, context)
630        }
631        SelectorPredicate::Relation(predicate) => {
632            matches_relation_predicate(predicate, card, context)
633        }
634        SelectorPredicate::DamagedBy => card
635            .damage_sources_this_turn
636            .contains(&context.source_card.id),
637        SelectorPredicate::AttachedBy => context.source_card.attached_to == Some(card.id),
638        SelectorPredicate::WasCast { by_you } => {
639            card.was_cast() && (!by_you || card.controller == context.source_controller)
640        }
641        SelectorPredicate::ChosenType => context
642            .source_card
643            .chosen_type
644            .as_ref()
645            .is_some_and(|ct| card.type_line.has_subtype(ct) || card.has_keyword("Changeling")),
646        SelectorPredicate::Keyword { name, present } => card.has_keyword(name) == *present,
647        SelectorPredicate::NumericComparison {
648            property,
649            operator,
650            value,
651        } => matches_numeric_comparison(*property, *operator, value, card, context),
652        SelectorPredicate::NumericParity { property, even } => {
653            resolve_numeric_property(*property, card, context)
654                .is_some_and(|actual| (actual % 2 == 0) == *even)
655        }
656        SelectorPredicate::CounterComparison {
657            operator,
658            value,
659            counter_type,
660        } => matches_counter_comparison(*operator, value, counter_type, card, context),
661        SelectorPredicate::Not(predicate) => !matches_card_predicate(predicate, card, context),
662        SelectorPredicate::Raw(raw) => matches_raw_card_predicate(raw, card, context),
663    }
664}
665
666#[inline]
667fn matches_chosen_color_source(card: &Card, context: MatchContext<'_>) -> bool {
668    context
669        .source_card
670        .chosen_colors
671        .iter()
672        .filter_map(|color| color_from_name_no_alloc(color))
673        .any(|color| card.color.has_color(color))
674}
675
676#[inline]
677fn color_from_name_no_alloc(value: &str) -> Option<Color> {
678    match value.as_bytes() {
679        [b'W'] | [b'w'] => Some(Color::White),
680        [b'U'] | [b'u'] => Some(Color::Blue),
681        [b'B'] | [b'b'] => Some(Color::Black),
682        [b'R'] | [b'r'] => Some(Color::Red),
683        [b'G'] | [b'g'] => Some(Color::Green),
684        _ if value.eq_ignore_ascii_case("white") => Some(Color::White),
685        _ if value.eq_ignore_ascii_case("blue") => Some(Color::Blue),
686        _ if value.eq_ignore_ascii_case("black") => Some(Color::Black),
687        _ if value.eq_ignore_ascii_case("red") => Some(Color::Red),
688        _ if value.eq_ignore_ascii_case("green") => Some(Color::Green),
689        _ => None,
690    }
691}
692
693#[inline(always)]
694fn matches_card_type_predicate(card_type: &CardSelectorType, card: &Card) -> bool {
695    match card_type {
696        CardSelectorType::Card => true,
697        CardSelectorType::Creature => card.is_creature(),
698        CardSelectorType::Land => card.is_land(),
699        CardSelectorType::Instant => card.type_line.is_instant(),
700        CardSelectorType::Sorcery => card.type_line.is_sorcery(),
701        CardSelectorType::Artifact => card.type_line.is_artifact(),
702        CardSelectorType::Enchantment => card.type_line.is_enchantment(),
703        CardSelectorType::Planeswalker => card.type_line.is_planeswalker(),
704        CardSelectorType::Permanent => card.is_permanent(),
705        CardSelectorType::Spell => true,
706        CardSelectorType::NonLand => !card.is_land(),
707        CardSelectorType::NonCreature => !card.is_creature(),
708        CardSelectorType::Named(name) => card.card_name.eq_ignore_ascii_case(name),
709        CardSelectorType::Subtype(subtype) => card.has_subtype(subtype),
710    }
711}
712
713#[inline(always)]
714fn matches_card_supertype_predicate(supertype: CardSupertypeSelector, card: &Card) -> bool {
715    match supertype {
716        CardSupertypeSelector::Basic => card.type_line.is_basic(),
717        CardSupertypeSelector::Snow => card.type_line.is_snow(),
718    }
719}
720
721#[inline(always)]
722fn matches_card_controller(
723    controller: ControllerSelector,
724    card_controller: PlayerId,
725    source_controller: PlayerId,
726) -> bool {
727    match controller {
728        ControllerSelector::You => card_controller == source_controller,
729        ControllerSelector::Opponent => card_controller != source_controller,
730    }
731}
732
733#[inline(always)]
734fn matches_card_identity(
735    identity: CardIdentitySelector,
736    card: &Card,
737    context: MatchContext<'_>,
738) -> bool {
739    match identity {
740        CardIdentitySelector::Self_ => card.id == context.source_card.id,
741        CardIdentitySelector::Other => card.id != context.source_card.id,
742    }
743}
744
745fn matches_card_state(state: CardStateSelector, card: &Card, context: MatchContext<'_>) -> bool {
746    match state {
747        CardStateSelector::FaceDown => card.face_down,
748        CardStateSelector::Paired => card.paired_with.is_some(),
749        CardStateSelector::PairedWithSource => card.paired_with == Some(context.source_card.id),
750        CardStateSelector::Attached => {
751            card.attached_to.is_some() || card.attached_to_player.is_some()
752        }
753        CardStateSelector::Equipped => card.attached_to.is_some() && card.type_line.is_artifact(),
754        CardStateSelector::Enchanted => {
755            card.attached_to.is_some() && card.type_line.is_enchantment()
756        }
757        CardStateSelector::HasCounters => card.counters.values().any(|count| *count > 0),
758        CardStateSelector::IsImprinted => context.source_card.imprinted_cards.contains(&card.id),
759        CardStateSelector::Chosen => {
760            context.source_card.chosen_cards.contains(&card.id)
761                || context
762                    .source_card
763                    .named_cards
764                    .iter()
765                    .any(|name| card.card_name.eq_ignore_ascii_case(name))
766        }
767        CardStateSelector::ChosenCard => context.source_card.chosen_cards.contains(&card.id),
768        CardStateSelector::NamedCard => context
769            .source_card
770            .named_cards
771            .iter()
772            .any(|name| card.card_name.eq_ignore_ascii_case(name)),
773        CardStateSelector::ChosenColor => context
774            .source_card
775            .chosen_colors
776            .iter()
777            .filter_map(|color| color_from_name_no_alloc(color))
778            .any(|color| card.color.has_color(color)),
779        CardStateSelector::EnteredThisTurn => card.entered_this_turn(),
780        CardStateSelector::WasDealtDamageThisTurn => !card.damage_sources_this_turn.is_empty(),
781        CardStateSelector::Historic => {
782            card.type_line.is_artifact()
783                || card.type_line.is_legendary()
784                || card.has_subtype("Saga")
785        }
786        CardStateSelector::Modified => {
787            card.counters.values().any(|count| *count > 0)
788                || card.attached_to.is_some()
789                || card.power_modifier != 0
790                || card.toughness_modifier != 0
791                || card.static_power_modifier != 0
792                || card.static_toughness_modifier != 0
793        }
794        CardStateSelector::Saddled => card.get_s_var("SaddledBy").is_some(),
795        CardStateSelector::MayPlaySource => card.may_play(context.source_controller),
796        CardStateSelector::Suspended => card.has_keyword("Suspend") && card.zone == ZoneType::Exile,
797        CardStateSelector::SingleTarget => false,
798        CardStateSelector::PromisedGift => card.promised_gift.is_some(),
799        CardStateSelector::RingBearer => context
800            .game
801            .is_some_and(|game| game.player(card.controller).ring_bearer == Some(card.id)),
802    }
803}
804
805fn matches_context_predicate(
806    predicate: &ContextPredicate,
807    card: &Card,
808    context: MatchContext<'_>,
809) -> bool {
810    match predicate {
811        ContextPredicate::Attacking(target) => {
812            matches_attacking_predicate(target.as_ref(), card, context)
813        }
814        ContextPredicate::Blocking(target) => {
815            matches_blocking_predicate(target.as_ref(), card, context)
816        }
817        ContextPredicate::BlockedByValidThisTurn(target) => {
818            matches_blocked_by_valid_this_turn_target(target, card, context)
819        }
820        ContextPredicate::BlockedByValidThisTurnType(card_type) => {
821            matches_blocked_by_valid_this_turn_type(card_type, card, context)
822        }
823        ContextPredicate::BlockedValidThisTurn(card_type)
824        | ContextPredicate::BlockingValid(card_type) => {
825            matches_blocked_valid_this_turn_type(card_type, card, context)
826        }
827        ContextPredicate::Blocked => context.combat.map_or(
828            card.damage_history.creature_got_blocked_this_combat,
829            |combat| combat.was_blocked_this_combat(card.id),
830        ),
831        ContextPredicate::AttackedThisTurn => !card.damage_history.attacked_this_turn.is_empty(),
832        ContextPredicate::BlockingSource => context.combat.is_some_and(|combat| {
833            combat
834                .get_attackers_for(card.id)
835                .contains(&context.source_card.id)
836        }),
837        ContextPredicate::BlockedBySource => context.combat.is_some_and(|combat| {
838            combat
839                .get_blockers_for(card.id)
840                .contains(&context.source_card.id)
841        }),
842        ContextPredicate::WasCastFrom(_) => false,
843        ContextPredicate::EnteredThisTurnFrom(zone) => {
844            matches_entered_this_turn_from(*zone, card, context)
845        }
846        ContextPredicate::EnteredUnder(target) => {
847            card.entered_this_turn()
848                && relation_target_player_any(target, context, |player| card.controller == player)
849        }
850        ContextPredicate::TopLibrary => false,
851        ContextPredicate::ExiledWithSource => {
852            context.source_card.imprinted_cards.contains(&card.id)
853        }
854        ContextPredicate::RememberedPlayerCtrl => {
855            context.remembered_players.contains(&card.controller)
856        }
857        ContextPredicate::TargetedPlayerCtrl => context.targeted_players.contains(&card.controller),
858        ContextPredicate::ControlledBy(reference) => {
859            matches_controlled_by_reference(reference, card, context)
860        }
861        ContextPredicate::ActivePlayerCtrl
862        | ContextPredicate::DefenderCtrl
863        | ContextPredicate::EnchantedController
864        | ContextPredicate::NotDefinedTargeted => false,
865    }
866}
867
868fn matches_controlled_by_reference(
869    reference: &str,
870    card: &Card,
871    context: MatchContext<'_>,
872) -> bool {
873    if reference.eq_ignore_ascii_case("You") || reference.eq_ignore_ascii_case("YouCtrl") {
874        card.controller == context.source_controller
875    } else if reference.eq_ignore_ascii_case("Opponent")
876        || reference.eq_ignore_ascii_case("OppCtrl")
877        || reference.eq_ignore_ascii_case("OpponentCtrl")
878    {
879        card.controller != context.source_controller
880    } else if reference.eq_ignore_ascii_case("Remembered")
881        || reference.eq_ignore_ascii_case("RememberedPlayer")
882        || reference.eq_ignore_ascii_case("RememberedController")
883    {
884        context.remembered_players.contains(&card.controller)
885    } else if reference.eq_ignore_ascii_case("Targeted")
886        || reference.eq_ignore_ascii_case("TargetedPlayer")
887        || reference.eq_ignore_ascii_case("TargetedController")
888    {
889        context.targeted_players.contains(&card.controller)
890    } else if let Some(target) = raw_target_ref(reference) {
891        relation_target_player_any(&target, context, |player| card.controller == player)
892    } else {
893        false
894    }
895}
896
897fn matches_relation_predicate(
898    predicate: &RelationPredicate,
899    card: &Card,
900    context: MatchContext<'_>,
901) -> bool {
902    match predicate {
903        RelationPredicate::SharesNameWith(target) => {
904            relation_target_card_any(target, card, context, |target| {
905                card.card_name.eq_ignore_ascii_case(&target.card_name)
906            })
907        }
908        RelationPredicate::DoesNotShareNameWith(target) => {
909            !relation_target_card_any(target, card, context, |target| {
910                card.card_name.eq_ignore_ascii_case(&target.card_name)
911            })
912        }
913
914        RelationPredicate::SharesCardTypeWith(target) => {
915            relation_target_card_any(target, card, context, |target| {
916                card.shares_card_type_with(target)
917            })
918        }
919        RelationPredicate::SharesCreatureTypeWith(target) => {
920            relation_target_card_any(target, card, context, |target| {
921                shares_creature_type(card, target)
922            })
923        }
924        RelationPredicate::SharesColorWith(target) => {
925            relation_target_card_any(target, card, context, |target| {
926                card.color.shares_color_with(target.color)
927            })
928        }
929        RelationPredicate::SharesManaValueWith(target) => {
930            relation_target_card_any(target, card, context, |target| {
931                card.mana_cost.cmc() == target.mana_cost.cmc()
932            })
933        }
934        RelationPredicate::AttachedTo(target) => {
935            card.attached_to.is_some_and(|attached_to| {
936                relation_target_contains_id(target, attached_to, context)
937            }) || card.attached_to_player.is_some_and(|attached_to| {
938                relation_target_player_any(target, context, |player| player == attached_to)
939            })
940        }
941        RelationPredicate::AttachedToType(card_type) => {
942            let Some(game) = context.game else {
943                return false;
944            };
945            let Some(attached_to) = card.attached_to else {
946                return false;
947            };
948            matches_card_type_predicate(card_type, game.card(attached_to))
949        }
950        RelationPredicate::OwnedBy(target) => {
951            relation_target_player_any(target, context, |player| card.owner == player)
952        }
953        RelationPredicate::OpponentOf(target) => {
954            relation_target_player_any(target, context, |player| card.controller != player)
955        }
956        RelationPredicate::IsTargeting(target) => {
957            relation_target_contains_id(target, card.id, context)
958                || relation_target_player_any(target, context, |player| {
959                    context.targeted_players.contains(&player)
960                })
961        }
962    }
963}
964
965fn relation_target_card_any(
966    target: &TargetRef,
967    subject: &Card,
968    context: MatchContext<'_>,
969    mut predicate: impl FnMut(&Card) -> bool,
970) -> bool {
971    match target {
972        TargetRef::Source => predicate(context.source_card),
973        TargetRef::Remembered | TargetRef::RememberedLki => context.game.is_some_and(|game| {
974            context
975                .remembered_cards
976                .iter()
977                .any(|id| predicate(game.card(*id)))
978        }),
979        TargetRef::Imprinted => context.game.is_some_and(|game| {
980            context
981                .source_card
982                .imprinted_cards
983                .iter()
984                .any(|id| predicate(game.card(*id)))
985        }),
986        TargetRef::ChosenCard => context.game.is_some_and(|game| {
987            context
988                .source_card
989                .chosen_cards
990                .iter()
991                .any(|id| predicate(game.card(*id)))
992        }),
993        TargetRef::Targeted => {
994            let Some(game) = context.game else {
995                return false;
996            };
997            context
998                .targeted_cards
999                .iter()
1000                .any(|id| predicate(game.card(*id)))
1001        }
1002        TargetRef::Battlefield => context.game.is_some_and(|game| {
1003            game.cards_in_all_zones(ZoneType::Battlefield)
1004                .any(|id| predicate(game.card(id)))
1005        }),
1006        TargetRef::OtherYourBattlefield => context.game.is_some_and(|game| {
1007            game.cards_in_zone(ZoneType::Battlefield, context.source_controller)
1008                .iter()
1009                .copied()
1010                .filter(|id| *id != subject.id)
1011                .any(|id| predicate(game.card(id)))
1012        }),
1013        TargetRef::YourGraveyard => context.game.is_some_and(|game| {
1014            game.cards_in_zone(ZoneType::Graveyard, context.source_controller)
1015                .iter()
1016                .any(|id| predicate(game.card(*id)))
1017        }),
1018        TargetRef::Player
1019        | TargetRef::Opponent
1020        | TargetRef::ChosenPlayer
1021        | TargetRef::TriggeredPlayer
1022        | TargetRef::TriggeredCardController
1023        | TargetRef::TriggeredDefendingPlayer
1024        | TargetRef::TriggeredAttackedTarget => false,
1025        TargetRef::TriggeredTarget | TargetRef::TriggeredCard => {
1026            let (Some(game), Some(card_id)) = (context.game, context.triggering_card) else {
1027                return false;
1028            };
1029            predicate(game.card(card_id))
1030        }
1031        TargetRef::Commander => context.game.is_some_and(|game| {
1032            game.player_registered_commanders(context.source_controller)
1033                .iter()
1034                .any(|id| predicate(game.card(*id)))
1035        }),
1036    }
1037}
1038
1039fn relation_target_player_any(
1040    target: &TargetRef,
1041    context: MatchContext<'_>,
1042    mut predicate: impl FnMut(PlayerId) -> bool,
1043) -> bool {
1044    match target {
1045        TargetRef::Source => predicate(context.source_controller),
1046        TargetRef::Remembered | TargetRef::RememberedLki => {
1047            context.remembered_players.iter().copied().any(predicate)
1048        }
1049        TargetRef::Imprinted | TargetRef::ChosenCard => false,
1050        TargetRef::ChosenPlayer => context.source_card.chosen_player.is_some_and(predicate),
1051        TargetRef::Targeted => context.targeted_players.iter().copied().any(predicate),
1052        TargetRef::Player => {
1053            context.targeted_players.iter().copied().any(&mut predicate)
1054                || context.remembered_players.iter().copied().any(predicate)
1055        }
1056        TargetRef::Opponent => context
1057            .game
1058            .is_some_and(|game| predicate(game.opponent_of(context.source_controller))),
1059        TargetRef::Battlefield | TargetRef::OtherYourBattlefield | TargetRef::YourGraveyard => {
1060            false
1061        }
1062        TargetRef::TriggeredTarget => {
1063            context.triggering_player.is_some_and(&mut predicate)
1064                || context
1065                    .triggering_card
1066                    .and_then(|card_id| context.game.map(|game| game.card(card_id).controller))
1067                    .is_some_and(predicate)
1068        }
1069        TargetRef::TriggeredCard => context
1070            .triggering_card
1071            .and_then(|card_id| context.game.map(|game| game.card(card_id).controller))
1072            .is_some_and(predicate),
1073        TargetRef::TriggeredPlayer => context.triggering_player.is_some_and(predicate),
1074        TargetRef::TriggeredCardController => context
1075            .triggering_card
1076            .and_then(|card_id| context.game.map(|game| game.card(card_id).controller))
1077            .is_some_and(predicate),
1078        TargetRef::TriggeredDefendingPlayer | TargetRef::TriggeredAttackedTarget => {
1079            triggered_defending_player(context).is_some_and(predicate)
1080        }
1081        TargetRef::Commander => predicate(context.source_controller),
1082    }
1083}
1084
1085fn relation_target_contains_id(
1086    target: &TargetRef,
1087    card_id: CardId,
1088    context: MatchContext<'_>,
1089) -> bool {
1090    match target {
1091        TargetRef::Source => context.source_card.id == card_id,
1092        TargetRef::Remembered | TargetRef::RememberedLki => {
1093            context.remembered_cards.contains(&card_id)
1094        }
1095        TargetRef::Imprinted => context.source_card.imprinted_cards.contains(&card_id),
1096        TargetRef::ChosenCard => context.source_card.chosen_cards.contains(&card_id),
1097        TargetRef::Targeted => context.targeted_cards.contains(&card_id),
1098        TargetRef::OtherYourBattlefield => context.game.is_some_and(|game| {
1099            card_id != context.source_card.id
1100                && game.card(card_id).zone == ZoneType::Battlefield
1101                && game.card(card_id).controller == context.source_controller
1102        }),
1103        TargetRef::Player
1104        | TargetRef::Opponent
1105        | TargetRef::Battlefield
1106        | TargetRef::YourGraveyard
1107        | TargetRef::ChosenPlayer
1108        | TargetRef::TriggeredPlayer
1109        | TargetRef::TriggeredCardController
1110        | TargetRef::TriggeredDefendingPlayer
1111        | TargetRef::TriggeredAttackedTarget => false,
1112        TargetRef::TriggeredTarget | TargetRef::TriggeredCard => {
1113            context.triggering_card == Some(card_id)
1114        }
1115        TargetRef::Commander => context.game.is_some_and(|game| {
1116            game.player_registered_commanders(context.source_controller)
1117                .contains(&card_id)
1118        }),
1119    }
1120}
1121
1122fn matches_attacking_predicate(
1123    target: Option<&TargetRef>,
1124    card: &Card,
1125    context: MatchContext<'_>,
1126) -> bool {
1127    let Some(target) = target else {
1128        return context
1129            .combat
1130            .map_or(card.attacking_player.is_some(), |combat| {
1131                combat.is_attacking(card.id)
1132            });
1133    };
1134    if let Some(combat) = context.combat {
1135        return combat
1136            .attackers
1137            .iter()
1138            .find(|(attacker, _)| *attacker == card.id)
1139            .is_some_and(|(_, defender)| matches_defender_target(*defender, target, context));
1140    }
1141    card.attacking_player
1142        .is_some_and(|player| matches_player_target(player, target, context))
1143}
1144
1145fn matches_blocking_predicate(
1146    target: Option<&TargetRef>,
1147    card: &Card,
1148    context: MatchContext<'_>,
1149) -> bool {
1150    let Some(target) = target else {
1151        return context
1152            .combat
1153            .map_or(card.damage_history.creature_blocked_this_combat, |combat| {
1154                combat.was_blocking(card.id)
1155            });
1156    };
1157    let Some(combat) = context.combat else {
1158        return false;
1159    };
1160    combat
1161        .blockers
1162        .iter()
1163        .filter(|(blocker, _)| *blocker == card.id)
1164        .any(|(_, attacker)| relation_target_contains_id(target, *attacker, context))
1165}
1166
1167fn matches_blocked_by_valid_this_turn_target(
1168    target: &TargetRef,
1169    card: &Card,
1170    context: MatchContext<'_>,
1171) -> bool {
1172    let Some(combat) = context.combat else {
1173        return false;
1174    };
1175    combat
1176        .blockers
1177        .iter()
1178        .filter(|(_, attacker)| *attacker == card.id)
1179        .any(|(blocker, _)| relation_target_contains_id(target, *blocker, context))
1180}
1181
1182fn matches_blocked_by_valid_this_turn_type(
1183    card_type: &CardSelectorType,
1184    card: &Card,
1185    context: MatchContext<'_>,
1186) -> bool {
1187    let (Some(combat), Some(game)) = (context.combat, context.game) else {
1188        return false;
1189    };
1190    combat
1191        .blockers
1192        .iter()
1193        .filter(|(_, attacker)| *attacker == card.id)
1194        .any(|(blocker, _)| matches_card_type_predicate(card_type, game.card(*blocker)))
1195}
1196
1197fn matches_blocked_valid_this_turn_type(
1198    card_type: &CardSelectorType,
1199    card: &Card,
1200    context: MatchContext<'_>,
1201) -> bool {
1202    let (Some(combat), Some(game)) = (context.combat, context.game) else {
1203        return false;
1204    };
1205    combat
1206        .blockers
1207        .iter()
1208        .filter(|(blocker, _)| *blocker == card.id)
1209        .any(|(_, attacker)| matches_card_type_predicate(card_type, game.card(*attacker)))
1210}
1211
1212fn matches_defender_target(
1213    defender: DefenderId,
1214    target: &TargetRef,
1215    context: MatchContext<'_>,
1216) -> bool {
1217    match defender {
1218        DefenderId::Player(player) => matches_player_target(player, target, context),
1219        DefenderId::Permanent(card_id) => relation_target_contains_id(target, card_id, context),
1220    }
1221}
1222
1223fn matches_player_target(player: PlayerId, target: &TargetRef, context: MatchContext<'_>) -> bool {
1224    match target {
1225        TargetRef::Player => true,
1226        TargetRef::Opponent => player != context.source_controller,
1227        _ => relation_target_player_any(target, context, |target_player| player == target_player),
1228    }
1229}
1230
1231fn triggered_defending_player(context: MatchContext<'_>) -> Option<PlayerId> {
1232    context
1233        .triggering_player
1234        .or_else(|| context.combat.and_then(|combat| combat.defending_player))
1235}
1236
1237fn shares_creature_type(card: &Card, target: &Card) -> bool {
1238    card.is_creature()
1239        && target.is_creature()
1240        && card
1241            .type_line
1242            .subtypes
1243            .iter()
1244            .any(|subtype| target.type_line.has_subtype(subtype))
1245}
1246
1247fn matches_entered_this_turn_from(zone: ZoneType, card: &Card, _context: MatchContext<'_>) -> bool {
1248    match zone {
1249        ZoneType::Battlefield => card.entered_this_turn(),
1250        _ => false,
1251    }
1252}
1253
1254fn matches_card_color(color: CardColorSelector, card: &Card) -> bool {
1255    match color {
1256        CardColorSelector::White => card.color.has_white(),
1257        CardColorSelector::Blue => card.color.has_blue(),
1258        CardColorSelector::Black => card.color.has_black(),
1259        CardColorSelector::Red => card.color.has_red(),
1260        CardColorSelector::Green => card.color.has_green(),
1261    }
1262}
1263
1264fn matches_numeric_comparison(
1265    property: NumericSelectorProperty,
1266    operator: SelectorCompareOperator,
1267    threshold: &SelectorNumericOperand,
1268    card: &Card,
1269    context: MatchContext<'_>,
1270) -> bool {
1271    let Some(threshold) = resolve_selector_operand(threshold, context) else {
1272        return true;
1273    };
1274    let Some(value) = resolve_numeric_property(property, card, context) else {
1275        return true;
1276    };
1277    compare_selector_value(value, operator, threshold)
1278}
1279
1280fn resolve_numeric_property(
1281    property: NumericSelectorProperty,
1282    card: &Card,
1283    context: MatchContext<'_>,
1284) -> Option<i32> {
1285    match property {
1286        NumericSelectorProperty::ManaValue => Some(effective_mana_value(card, context)),
1287        NumericSelectorProperty::Power => Some(card.power()),
1288        NumericSelectorProperty::Toughness => Some(card.toughness()),
1289        NumericSelectorProperty::TargetCount => {
1290            Some((context.targeted_cards.len() + context.targeted_players.len()) as i32)
1291        }
1292        NumericSelectorProperty::ManaSpent => {
1293            Some(context.source_card.paying_mana_to_cast.len() as i32)
1294        }
1295    }
1296}
1297
1298fn effective_mana_value(card: &Card, context: MatchContext<'_>) -> i32 {
1299    let mut mana_value = card.mana_cost.cmc();
1300    if let Some(sa) = context.spell_ability {
1301        if sa.source == Some(card.id) {
1302            mana_value += sa.x_mana_cost_paid as i32 * card.mana_cost.count_x() as i32;
1303        }
1304    }
1305    mana_value
1306}
1307
1308fn matches_counter_comparison(
1309    operator: SelectorCompareOperator,
1310    threshold: &SelectorNumericOperand,
1311    counter_type: &str,
1312    card: &Card,
1313    context: MatchContext<'_>,
1314) -> bool {
1315    let Some(threshold) = resolve_selector_operand(threshold, context) else {
1316        return true;
1317    };
1318    use crate::ability::effects::parse_counter_type;
1319    let counter_type = parse_counter_type(counter_type);
1320    let count = card.counter_count(&counter_type);
1321    compare_selector_value(count, operator, threshold)
1322}
1323
1324fn resolve_selector_operand(
1325    operand: &SelectorNumericOperand,
1326    context: MatchContext<'_>,
1327) -> Option<i32> {
1328    match operand {
1329        SelectorNumericOperand::Literal(value) => Some(*value),
1330        SelectorNumericOperand::Symbol(symbol) => {
1331            if symbol.eq_ignore_ascii_case("X") {
1332                if let Some(sa) = context.spell_ability {
1333                    return Some(sa.x_mana_cost_paid as i32);
1334                }
1335            }
1336            let value = context.source_card.get_s_var(symbol)?;
1337            if let Ok(parsed) = value.trim().parse::<i32>() {
1338                return Some(parsed);
1339            }
1340            if value == "TriggeredCard$CardManaCost" {
1341                let game = context.game?;
1342                let card = context.triggering_card?;
1343                return Some(game.card(card).mana_cost.cmc());
1344            }
1345            if value == "TriggeredCard$CardPower" {
1346                let game = context.game?;
1347                let card = context.triggering_card?;
1348                return Some(crate::lki::resolve_lki_power(game, card));
1349            }
1350            if value == "TriggeredCard$CardToughness" {
1351                let game = context.game?;
1352                let card = context.triggering_card?;
1353                return Some(crate::lki::resolve_lki_toughness(game, card));
1354            }
1355            if value.starts_with("Count$") || value.starts_with("PlayerCount") {
1356                let game = context.game?;
1357                let sa = crate::spellability::SpellAbility::new_empty(
1358                    Some(context.source_card.id),
1359                    context.source_controller,
1360                );
1361                return Some(crate::svar::resolve_svar_expression(
1362                    value,
1363                    game,
1364                    context.source_card.id,
1365                    context.source_controller,
1366                    &sa,
1367                ));
1368            }
1369            None
1370        }
1371    }
1372}
1373
1374fn compare_selector_value(actual: i32, operator: SelectorCompareOperator, expected: i32) -> bool {
1375    match operator {
1376        SelectorCompareOperator::Eq => actual == expected,
1377        SelectorCompareOperator::Ne => actual != expected,
1378        SelectorCompareOperator::Lt => actual < expected,
1379        SelectorCompareOperator::Le => actual <= expected,
1380        SelectorCompareOperator::Gt => actual > expected,
1381        SelectorCompareOperator::Ge => actual >= expected,
1382    }
1383}
1384
1385fn matches_raw_card_predicate(raw: &str, card: &Card, context: MatchContext<'_>) -> bool {
1386    crate::perf::increment(crate::perf::Metric::SelectorRawPredicates, 1);
1387    if let Some(result) = matches_domain_predicate(raw, card, context) {
1388        return result;
1389    }
1390    legacy_matches_card_atom(raw, card, context)
1391}
1392
1393fn legacy_matches_card_atom(raw: &str, card: &Card, context: MatchContext<'_>) -> bool {
1394    let source = context.source_card;
1395    let raw = raw.trim();
1396    if raw.is_empty() {
1397        return true;
1398    }
1399    if let Some(result) = matches_domain_predicate(raw, card, context) {
1400        return result;
1401    }
1402    let (negated, value) = if let Some(stripped) = raw.strip_prefix('!') {
1403        (true, stripped)
1404    } else {
1405        (false, raw)
1406    };
1407    let value_lower = value.to_ascii_lowercase();
1408    if negated {
1409        let positive_match = match value_lower.as_str() {
1410            "token" => card.is_token,
1411            "creature" => card.is_creature(),
1412            "land" => card.is_land(),
1413            "artifact" => card.type_line.is_artifact(),
1414            "enchantment" => card.type_line.is_enchantment(),
1415            "legendary" => card.type_line.is_legendary(),
1416            "basic" => card.type_line.is_basic(),
1417            "snow" => card.type_line.is_snow(),
1418            _ => card.has_subtype(value),
1419        };
1420        return !positive_match;
1421    }
1422
1423    match value_lower.as_str() {
1424        "self" | "strictlyself" | "card.self" => card.id == source.id,
1425        "other" | "strictlyother" => card.id != source.id,
1426        "youctrl" | "youcontrol" | "you" => card.controller == source.controller,
1427        "youown" => card.owner == source.controller,
1428        "youdontctrl" => card.controller != source.controller,
1429        "youdontown" => card.owner != source.controller,
1430        "isremembered" | "card.isremembered" => source.remembered_cards.contains(&card.id),
1431        "istriggerremembered" | "card.istriggerremembered" => {
1432            context.trigger_remembered_cards.contains(&card.id)
1433        }
1434        "effectsource" | "card.effectsource" => source.effect_source == Some(card.id),
1435        "oppctrl" | "opponentctrl" | "opponent" => card.controller != source.controller,
1436        "chosenctrl" => Some(card.controller) == source.chosen_player,
1437        "oppown" | "opponentown" => card.owner != source.controller,
1438        "targetedplayerown" | "targetedown" | "targetedowner" => {
1439            context.targeted_players.contains(&card.owner)
1440        }
1441        "iscommander" => card.is_commander,
1442        "legendary" => card.type_line.is_legendary(),
1443        "basic" => card.type_line.is_basic(),
1444        "snow" => card.type_line.is_snow(),
1445        "kicked" => card.kicked,
1446        "cameundercontrolsincelastupkeep" => card.came_under_control_since_last_upkeep(),
1447        "noncreature" => !card.is_creature(),
1448        "nonland" => !card.is_land(),
1449        "nonlegendary" => !card.type_line.is_legendary(),
1450        "nonbasic" => !card.type_line.is_basic(),
1451        "nonsnow" => !card.type_line.is_snow(),
1452        "token" => card.is_token,
1453        "nontoken" => !card.is_token,
1454        "tapped" => card.tapped,
1455        "untapped" => !card.tapped,
1456        "startedtheturnuntapped" => !card.started_turn_tapped,
1457        "startedtheturntapped" => card.started_turn_tapped,
1458        "multicolor" => card.color.is_multicolor(),
1459        "colorless" => card.color.is_colorless(),
1460        "whitesource" => matches_card_color(CardColorSelector::White, card),
1461        "bluesource" => matches_card_color(CardColorSelector::Blue, card),
1462        "blacksource" => matches_card_color(CardColorSelector::Black, card),
1463        "redsource" => matches_card_color(CardColorSelector::Red, card),
1464        "greensource" => matches_card_color(CardColorSelector::Green, card),
1465        "colorlesssource" => card.color.is_colorless(),
1466        "chosencolorsource" => matches_chosen_color_source(card, context),
1467        "attacking" => matches_context_predicate(&ContextPredicate::Attacking(None), card, context),
1468        "attackingyou" => matches_context_predicate(
1469            &ContextPredicate::Attacking(Some(TargetRef::Source)),
1470            card,
1471            context,
1472        ),
1473        "blocking" => matches_context_predicate(&ContextPredicate::Blocking(None), card, context),
1474        "blocked" => matches_context_predicate(&ContextPredicate::Blocked, card, context),
1475        "attackedthisturn" => {
1476            matches_context_predicate(&ContextPredicate::AttackedThisTurn, card, context)
1477        }
1478        "blockingsource" => {
1479            matches_context_predicate(&ContextPredicate::BlockingSource, card, context)
1480        }
1481        "blockedbysource" => {
1482            matches_context_predicate(&ContextPredicate::BlockedBySource, card, context)
1483        }
1484        "samename" => matches_relation_predicate(
1485            &RelationPredicate::SharesNameWith(TargetRef::Source),
1486            card,
1487            context,
1488        ),
1489        shares if shares.starts_with("sharesnamewith") => {
1490            raw_target_ref(&value["sharesNameWith".len()..]).is_some_and(|target| {
1491                matches_relation_predicate(
1492                    &RelationPredicate::SharesNameWith(target),
1493                    card,
1494                    context,
1495                )
1496            })
1497        }
1498        shares if shares.starts_with("doesnotsharenamewith") => {
1499            raw_target_ref(&value["doesNotShareNameWith".len()..]).is_some_and(|target| {
1500                matches_relation_predicate(
1501                    &RelationPredicate::DoesNotShareNameWith(target),
1502                    card,
1503                    context,
1504                )
1505            })
1506        }
1507        shares if shares.starts_with("sharescardtypewith") => {
1508            raw_target_ref(&value["sharesCardTypeWith".len()..]).is_some_and(|target| {
1509                matches_relation_predicate(
1510                    &RelationPredicate::SharesCardTypeWith(target),
1511                    card,
1512                    context,
1513                )
1514            })
1515        }
1516        shares if shares.starts_with("sharescolorwith") => {
1517            raw_target_ref(&value["SharesColorWith".len()..]).is_some_and(|target| {
1518                matches_relation_predicate(
1519                    &RelationPredicate::SharesColorWith(target),
1520                    card,
1521                    context,
1522                )
1523            })
1524        }
1525        shares if shares.starts_with("sharescmcwith") => {
1526            raw_target_ref(&value["SharesCMCWith".len()..]).is_some_and(|target| {
1527                matches_relation_predicate(
1528                    &RelationPredicate::SharesManaValueWith(target),
1529                    card,
1530                    context,
1531                )
1532            })
1533        }
1534        shares if shares.starts_with("sharescreaturetypewith") => {
1535            raw_target_ref(&value["sharesCreatureTypeWith".len()..]).is_some_and(|target| {
1536                matches_relation_predicate(
1537                    &RelationPredicate::SharesCreatureTypeWith(target),
1538                    card,
1539                    context,
1540                )
1541            })
1542        }
1543        attacking if attacking.starts_with("attacking ") => {
1544            raw_target_ref(&value["attacking ".len()..]).is_some_and(|target| {
1545                matches_context_predicate(&ContextPredicate::Attacking(Some(target)), card, context)
1546            })
1547        }
1548        blocked_by if blocked_by.starts_with("blockedbyvalidthisturn ") => {
1549            raw_blocked_by_valid_this_turn(&value["blockedByValidThisTurn ".len()..])
1550                .is_some_and(|predicate| matches_context_predicate(&predicate, card, context))
1551        }
1552        blocked if blocked.starts_with("blockedvalidthisturn ") => raw_card_selector_type(
1553            &value["blockedValidThisTurn ".len()..],
1554        )
1555        .is_some_and(|card_type| {
1556            matches_context_predicate(
1557                &ContextPredicate::BlockedValidThisTurn(card_type),
1558                card,
1559                context,
1560            )
1561        }),
1562        blocking_valid if blocking_valid.starts_with("blockingvalid ") => {
1563            raw_card_selector_type(&value["blockingValid ".len()..]).is_some_and(|card_type| {
1564                matches_context_predicate(
1565                    &ContextPredicate::BlockingValid(card_type),
1566                    card,
1567                    context,
1568                )
1569            })
1570        }
1571        blocking if blocking.starts_with("blocking ") => {
1572            raw_target_ref(&value["blocking ".len()..]).is_some_and(|target| {
1573                matches_context_predicate(&ContextPredicate::Blocking(Some(target)), card, context)
1574            })
1575        }
1576        controlled if controlled.starts_with("controlledby ") => {
1577            matches_controlled_by_reference(value["ControlledBy ".len()..].trim(), card, context)
1578        }
1579        attached if attached.starts_with("attachedto ") => {
1580            let relation = raw_attached_to_relation(value["AttachedTo ".len()..].trim());
1581            relation.is_some_and(|relation| matches_relation_predicate(&relation, card, context))
1582        }
1583        owned if owned.starts_with("ownedby ") => raw_target_ref(&value["OwnedBy ".len()..])
1584            .is_some_and(|target| {
1585                matches_relation_predicate(&RelationPredicate::OwnedBy(target), card, context)
1586            }),
1587        opponent if opponent.starts_with("opponentof ") => {
1588            raw_target_ref(&value["OpponentOf ".len()..]).is_some_and(|target| {
1589                matches_relation_predicate(&RelationPredicate::OpponentOf(target), card, context)
1590            })
1591        }
1592        targeting if targeting.starts_with("istargeting ") => {
1593            raw_target_ref(&value["IsTargeting ".len()..]).is_some_and(|target| {
1594                matches_relation_predicate(&RelationPredicate::IsTargeting(target), card, context)
1595            })
1596        }
1597        "inzonebattlefield" => card.zone == forge_foundation::ZoneType::Battlefield,
1598        "inzonegraveyard" => card.zone == forge_foundation::ZoneType::Graveyard,
1599        "inzonehand" => card.zone == forge_foundation::ZoneType::Hand,
1600        "inzoneexile" => card.zone == forge_foundation::ZoneType::Exile,
1601        "inzonestack" => card.zone == forge_foundation::ZoneType::Stack,
1602        "damagedby" => card
1603            .damage_sources_this_turn
1604            .contains(&context.source_card.id),
1605        "equippedby" | "enchantedby" | "attachedby" => {
1606            context.source_card.attached_to == Some(card.id)
1607        }
1608        "facedown" => matches_card_state(CardStateSelector::FaceDown, card, context),
1609        "paired" => matches_card_state(CardStateSelector::Paired, card, context),
1610        "pairedwith" => matches_card_state(CardStateSelector::PairedWithSource, card, context),
1611        "attached" => matches_card_state(CardStateSelector::Attached, card, context),
1612        "equipped" => matches_card_state(CardStateSelector::Equipped, card, context),
1613        "enchanted" => matches_card_state(CardStateSelector::Enchanted, card, context),
1614        "hascounters" => matches_card_state(CardStateSelector::HasCounters, card, context),
1615        "isimprinted" => matches_card_state(CardStateSelector::IsImprinted, card, context),
1616        "chosen" => matches_card_state(CardStateSelector::Chosen, card, context),
1617        "chosencard" | "chosencardstrict" => {
1618            matches_card_state(CardStateSelector::ChosenCard, card, context)
1619        }
1620        "namedcard" => matches_card_state(CardStateSelector::NamedCard, card, context),
1621        "chosencolor" => matches_card_state(CardStateSelector::ChosenColor, card, context),
1622        "thisturnentered" | "thisturnenteredfrom_battlefield" => {
1623            matches_card_state(CardStateSelector::EnteredThisTurn, card, context)
1624        }
1625        entered if entered.starts_with("enteredunder ") => {
1626            raw_target_ref(&value["EnteredUnder ".len()..]).is_some_and(|target| {
1627                matches_context_predicate(&ContextPredicate::EnteredUnder(target), card, context)
1628            })
1629        }
1630        "wasdealtdamagethisturn" => {
1631            matches_card_state(CardStateSelector::WasDealtDamageThisTurn, card, context)
1632        }
1633        dealt if dealt.starts_with("dealtcombatdamagethisturn") => {
1634            let Some(target_text) = value.split_once(' ').map(|(_, target)| target.trim()) else {
1635                return card
1636                    .damage_history
1637                    .damage_done_this_turn
1638                    .iter()
1639                    .any(|damage| damage.is_combat && damage.amount > 0);
1640            };
1641            let Some(target) = raw_target_ref(target_text) else {
1642                return false;
1643            };
1644            card.damage_history.damage_done_this_turn.iter().any(|damage| {
1645                damage.is_combat
1646                    && damage.amount > 0
1647                    && matches!(
1648                        damage.target,
1649                        Some(crate::card::card_damage_history::TrackedEntity::Player(player))
1650                            if relation_target_player_any(&target, context, |target_player| target_player == player)
1651                    )
1652            })
1653        }
1654        "historic" => matches_card_state(CardStateSelector::Historic, card, context),
1655        "modified" => matches_card_state(CardStateSelector::Modified, card, context),
1656        "issaddled" => matches_card_state(CardStateSelector::Saddled, card, context),
1657        "mayplaysource" => matches_card_state(CardStateSelector::MayPlaySource, card, context),
1658        "exiledwithsource" => {
1659            matches_context_predicate(&ContextPredicate::ExiledWithSource, card, context)
1660        }
1661        "toplibrary" => matches_context_predicate(&ContextPredicate::TopLibrary, card, context),
1662        "suspended" => matches_card_state(CardStateSelector::Suspended, card, context),
1663        "singletarget" => matches_card_state(CardStateSelector::SingleTarget, card, context),
1664        "promisedgift" => matches_card_state(CardStateSelector::PromisedGift, card, context),
1665        "isringbearer" => matches_card_state(CardStateSelector::RingBearer, card, context),
1666        "rememberedplayerctrl" => {
1667            matches_context_predicate(&ContextPredicate::RememberedPlayerCtrl, card, context)
1668        }
1669        "wascast" => card.was_cast(),
1670        "wascastbyyou" => card.was_cast() && card.controller == context.source_controller,
1671        was_cast_from if was_cast_from.starts_with("wascastfrom") => false,
1672        named if named.starts_with("named") => {
1673            card.card_name.eq_ignore_ascii_case(value[5..].trim())
1674        }
1675        _ if value.starts_with("counters_") => check_counter_condition(value, card),
1676        _ => {
1677            if value_lower.starts_with("cmc") {
1678                let original_rest = &value[3..];
1679                check_cmc_condition_with_context(original_rest, card, Some(context))
1680            } else if let Some(rest) = value_lower.strip_prefix("power") {
1681                check_power_condition(rest, card)
1682            } else if let Some(rest) = value_lower.strip_prefix("toughness") {
1683                check_toughness_condition(rest, card)
1684            } else if let Some(color) = Color::from_name(&value_lower) {
1685                card.color.has_color(color)
1686            } else if let Some(keyword_suffix) = value_lower.strip_prefix("with") {
1687                if keyword_suffix.strip_prefix("out").is_some() {
1688                    !card.has_keyword(&value[7..])
1689                } else if !keyword_suffix.is_empty() {
1690                    card.has_keyword(&value[4..])
1691                } else {
1692                    true
1693                }
1694            } else if let Some(negated_value) = value_lower.strip_prefix("non") {
1695                let positive_match = match negated_value {
1696                    "creature" => card.is_creature(),
1697                    "land" => card.is_land(),
1698                    "artifact" => card.type_line.is_artifact(),
1699                    "enchantment" => card.type_line.is_enchantment(),
1700                    "token" => card.is_token,
1701                    "colorless" => card.color.is_colorless(),
1702                    _ => {
1703                        if let Some(color) = Color::from_name(negated_value) {
1704                            card.color.has_color(color)
1705                        } else {
1706                            card.has_subtype(&value[3..])
1707                        }
1708                    }
1709                };
1710                !positive_match
1711            } else if value_lower == "chosentype" {
1712                source.chosen_type.as_ref().is_some_and(|ct| {
1713                    card.type_line.has_subtype(ct) || card.has_keyword("Changeling")
1714                })
1715            } else {
1716                let color_name = value.strip_suffix("Source").unwrap_or(value);
1717                if let Some(color) = Color::from_name(&color_name.to_lowercase()) {
1718                    card.color.has_color(color)
1719                } else if color_name.eq_ignore_ascii_case("Colorless") {
1720                    card.color.is_colorless()
1721                } else {
1722                    card.has_subtype(value)
1723                }
1724            }
1725        }
1726    }
1727}
1728
1729fn matches_domain_predicate(raw: &str, card: &Card, context: MatchContext<'_>) -> Option<bool> {
1730    crate::ability::selector_domain::matches_selector_domain_predicate(raw, card, context)
1731        .or_else(|| {
1732            crate::cost::selector_domain::matches_selector_domain_predicate(raw, card, context)
1733        })
1734        .or_else(|| {
1735            crate::trigger::selector_domain::matches_selector_domain_predicate(raw, card, context)
1736        })
1737        .or_else(|| {
1738            crate::combat::selector_domain::matches_selector_domain_predicate(raw, card, context)
1739        })
1740}
1741
1742fn raw_target_ref(value: &str) -> Option<TargetRef> {
1743    let value = value.trim();
1744    if value.is_empty()
1745        || value.eq_ignore_ascii_case("Self")
1746        || value.eq_ignore_ascii_case("Source")
1747        || value.eq_ignore_ascii_case("You")
1748        || value.eq_ignore_ascii_case("YouCtrl")
1749    {
1750        Some(TargetRef::Source)
1751    } else if value.eq_ignore_ascii_case("Remembered") {
1752        Some(TargetRef::Remembered)
1753    } else if value.eq_ignore_ascii_case("RememberedLKI") {
1754        Some(TargetRef::RememberedLki)
1755    } else if value.eq_ignore_ascii_case("Imprinted") {
1756        Some(TargetRef::Imprinted)
1757    } else if value.eq_ignore_ascii_case("ChosenCard") {
1758        Some(TargetRef::ChosenCard)
1759    } else if value.eq_ignore_ascii_case("ChosenPlayer") {
1760        Some(TargetRef::ChosenPlayer)
1761    } else if value.eq_ignore_ascii_case("Targeted") {
1762        Some(TargetRef::Targeted)
1763    } else if value.eq_ignore_ascii_case("TargetedPlayer")
1764        || value.eq_ignore_ascii_case("TargetedController")
1765    {
1766        Some(TargetRef::Targeted)
1767    } else if value.eq_ignore_ascii_case("Player") {
1768        Some(TargetRef::Player)
1769    } else if value.eq_ignore_ascii_case("Opponent") {
1770        Some(TargetRef::Opponent)
1771    } else if value.eq_ignore_ascii_case("Battlefield") {
1772        Some(TargetRef::Battlefield)
1773    } else if value.eq_ignore_ascii_case("OtherYourBattlefield") {
1774        Some(TargetRef::OtherYourBattlefield)
1775    } else if value.eq_ignore_ascii_case("YourGraveyard") {
1776        Some(TargetRef::YourGraveyard)
1777    } else if value.eq_ignore_ascii_case("TriggeredTarget") {
1778        Some(TargetRef::TriggeredTarget)
1779    } else if value.eq_ignore_ascii_case("TriggeredPlayer") {
1780        Some(TargetRef::TriggeredPlayer)
1781    } else if value.eq_ignore_ascii_case("TriggeredCard") {
1782        Some(TargetRef::TriggeredCard)
1783    } else if value.eq_ignore_ascii_case("TriggeredCardController") {
1784        Some(TargetRef::TriggeredCardController)
1785    } else if value.eq_ignore_ascii_case("TriggeredDefendingPlayer") {
1786        Some(TargetRef::TriggeredDefendingPlayer)
1787    } else if value.eq_ignore_ascii_case("TriggeredAttackedTarget") {
1788        Some(TargetRef::TriggeredAttackedTarget)
1789    } else if value.eq_ignore_ascii_case("Commander") {
1790        Some(TargetRef::Commander)
1791    } else {
1792        None
1793    }
1794}
1795
1796fn raw_blocked_by_valid_this_turn(value: &str) -> Option<ContextPredicate> {
1797    if let Some(target) = raw_target_ref(value) {
1798        return Some(ContextPredicate::BlockedByValidThisTurn(target));
1799    }
1800    raw_card_selector_type(value).map(ContextPredicate::BlockedByValidThisTurnType)
1801}
1802
1803fn raw_card_selector_type(value: &str) -> Option<CardSelectorType> {
1804    let value = value.trim();
1805    match value.to_ascii_lowercase().as_str() {
1806        "card" => Some(CardSelectorType::Card),
1807        "creature" => Some(CardSelectorType::Creature),
1808        "land" => Some(CardSelectorType::Land),
1809        "artifact" => Some(CardSelectorType::Artifact),
1810        "enchantment" => Some(CardSelectorType::Enchantment),
1811        "planeswalker" => Some(CardSelectorType::Planeswalker),
1812        "permanent" => Some(CardSelectorType::Permanent),
1813        "nonland" => Some(CardSelectorType::NonLand),
1814        "noncreature" => Some(CardSelectorType::NonCreature),
1815        _ if value
1816            .chars()
1817            .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '\'') =>
1818        {
1819            Some(CardSelectorType::Subtype(value.to_string()))
1820        }
1821        _ => None,
1822    }
1823}
1824
1825fn raw_attached_to_relation(value: &str) -> Option<RelationPredicate> {
1826    if let Some(target) = raw_target_ref(value) {
1827        return Some(RelationPredicate::AttachedTo(target));
1828    }
1829    let card_type = match value.to_ascii_lowercase().as_str() {
1830        "card" => CardSelectorType::Card,
1831        "creature" => CardSelectorType::Creature,
1832        "land" => CardSelectorType::Land,
1833        "artifact" => CardSelectorType::Artifact,
1834        "enchantment" => CardSelectorType::Enchantment,
1835        "permanent" => CardSelectorType::Permanent,
1836        _ => return None,
1837    };
1838    Some(RelationPredicate::AttachedToType(card_type))
1839}
1840
1841/// Convenience wrapper: None means "no filter" -> always matches.
1842pub fn matches_valid_card_selector_opt(
1843    selector: Option<&CompiledSelector>,
1844    card: &Card,
1845    source: &Card,
1846) -> bool {
1847    match selector {
1848        None => true,
1849        Some(selector) => matches_valid_card_selector(selector, card, source),
1850    }
1851}
1852
1853pub fn matches_valid_card_selector_opt_with_context(
1854    selector: Option<&CompiledSelector>,
1855    card: &Card,
1856    context: MatchContext<'_>,
1857) -> bool {
1858    match selector {
1859        None => true,
1860        Some(selector) => matches_valid_card_selector_with_context(selector, card, context),
1861    }
1862}
1863
1864pub fn matches_valid_card_selector_opt_in_game(
1865    selector: Option<&CompiledSelector>,
1866    card: &Card,
1867    source: &Card,
1868    game: &GameState,
1869) -> bool {
1870    matches_valid_card_selector_opt_with_context(
1871        selector,
1872        card,
1873        MatchContext::from_source(source).with_game(game),
1874    )
1875}
1876
1877#[cfg(debug_assertions)]
1878fn matches_single_valid_card(filter: &str, card: &Card, context: MatchContext<'_>) -> bool {
1879    // Handle comma-separated types with qualifiers (e.g. "Creature.YouCtrl,Artifact.YouCtrl")
1880    if filter.contains(',') {
1881        return filter
1882            .split(',')
1883            .any(|alt| matches_type_and_qualifiers(alt.trim(), card, context));
1884    }
1885
1886    matches_type_and_qualifiers(filter, card, context)
1887}
1888
1889#[cfg(debug_assertions)]
1890fn matches_type_and_qualifiers(filter: &str, card: &Card, context: MatchContext<'_>) -> bool {
1891    // Split on dots for compound filters (e.g. "Creature.Other", "Card.Self")
1892    let parts: Vec<&str> = filter.split('.').collect();
1893    if parts.is_empty() {
1894        return true;
1895    }
1896
1897    let type_part = parts[0];
1898    let qualifiers = &parts[1..];
1899
1900    matches_type_and_qualifier_parts(type_part, qualifiers, card, context)
1901}
1902
1903#[cfg(debug_assertions)]
1904fn matches_type_and_qualifier_parts(
1905    type_part: &str,
1906    qualifiers: &[&str],
1907    card: &Card,
1908    context: MatchContext<'_>,
1909) -> bool {
1910    let source = context.source_card;
1911    // Check the type portion
1912    let type_matches = match type_part {
1913        "Card" | "Any" => true, // matches any card
1914        "Creature" => card.is_creature(),
1915        "Land" => card.is_land(),
1916        "Instant" => card.type_line.is_instant(),
1917        "Sorcery" => card.type_line.is_sorcery(),
1918        "Artifact" => card.type_line.is_artifact(),
1919        "Enchantment" => card.type_line.is_enchantment(),
1920        "Planeswalker" => card.type_line.is_planeswalker(),
1921        "nonland" | "nonLand" | "NonLand" => !card.is_land(),
1922        "noncreature" | "nonCreature" | "NonCreature" => !card.is_creature(),
1923        "Permanent" => card.is_permanent(),
1924        "Spell" => true, // used in some contexts
1925        named if named.to_ascii_lowercase().starts_with("named") => {
1926            card.card_name.eq_ignore_ascii_case(named[5..].trim())
1927        }
1928        // Player-type filters: players are not cards, so never match.
1929        "Player" | "You" | "Opponent" | "Each" | "ActivePlayer" | "NonActivePlayer" => false,
1930        _ => {
1931            // Try comma-separated types within the type portion (e.g. "Instant,Sorcery")
1932            if type_part.contains(',') {
1933                type_part.split(',').any(|t| match t.trim() {
1934                    "Creature" => card.is_creature(),
1935                    "Land" => card.is_land(),
1936                    "Instant" => card.type_line.is_instant(),
1937                    "Sorcery" => card.type_line.is_sorcery(),
1938                    "Artifact" => card.type_line.is_artifact(),
1939                    "Enchantment" => card.type_line.is_enchantment(),
1940                    "Planeswalker" => card.type_line.is_planeswalker(),
1941                    "Card" => true,
1942                    _ => false,
1943                })
1944            } else {
1945                // Try matching as subtype (e.g. "Zombie", "Wall", "Dragon").
1946                // This must be changeling-aware for creature types, matching
1947                // Java's CardType.hasStringType()/hasCreatureType() path.
1948                card.has_subtype(type_part)
1949            }
1950        }
1951    };
1952
1953    if !type_matches {
1954        return false;
1955    }
1956
1957    // Check qualifiers — handle compound "+" syntax (e.g. "Self+kicked", "YouCtrl+nonBlack")
1958    for &qualifier in qualifiers {
1959        // Split compound qualifiers on '+' (e.g. "Self+kicked" → ["Self", "kicked"])
1960        let sub_parts: Vec<&str> = qualifier.split('+').collect();
1961        for sub in &sub_parts {
1962            // Handle "!" prefix as negation (e.g. "!token" → "nontoken")
1963            let (negated, raw) = if let Some(stripped) = sub.strip_prefix('!') {
1964                (true, stripped)
1965            } else {
1966                (false, *sub)
1967            };
1968            let sub_lower = raw.to_ascii_lowercase();
1969            // If negated, invert the boolean result of the positive match.
1970            // "!token" is equivalent to "nontoken", "!Creature" to "nonCreature", etc.
1971            if negated {
1972                let positive_match = match sub_lower.as_str() {
1973                    "token" => card.is_token,
1974                    "creature" => card.is_creature(),
1975                    "land" => card.is_land(),
1976                    "artifact" => card.type_line.is_artifact(),
1977                    "enchantment" => card.type_line.is_enchantment(),
1978                    "legendary" => card.type_line.is_legendary(),
1979                    "basic" => card.type_line.is_basic(),
1980                    "snow" => card.type_line.is_snow(),
1981                    _ => {
1982                        // Try subtype match
1983                        card.has_subtype(raw)
1984                    }
1985                };
1986                if positive_match {
1987                    return false;
1988                }
1989                continue;
1990            }
1991            match sub_lower.as_str() {
1992                "self" => {
1993                    if card.id != source.id {
1994                        return false;
1995                    }
1996                }
1997                "strictlyself" => {
1998                    if card.id != source.id {
1999                        return false;
2000                    }
2001                }
2002                "other" | "strictlyother" => {
2003                    if card.id == source.id {
2004                        return false;
2005                    }
2006                }
2007                "youctrl" | "youcontrol" | "you" => {
2008                    if card.controller != source.controller {
2009                        return false;
2010                    }
2011                }
2012                "youdontctrl" => {
2013                    if card.controller == source.controller {
2014                        return false;
2015                    }
2016                }
2017                "youown" => {
2018                    if card.owner != source.controller {
2019                        return false;
2020                    }
2021                }
2022                "youdontown" => {
2023                    if card.owner == source.controller {
2024                        return false;
2025                    }
2026                }
2027                "isremembered" => {
2028                    if !source.remembered_cards.contains(&card.id) {
2029                        return false;
2030                    }
2031                }
2032                "istriggerremembered" => {
2033                    if !context.trigger_remembered_cards.contains(&card.id) {
2034                        return false;
2035                    }
2036                }
2037                "effectsource" => {
2038                    if source.effect_source != Some(card.id) {
2039                        return false;
2040                    }
2041                }
2042                "oppctrl" | "opponentctrl" | "opponent" => {
2043                    if card.controller == source.controller {
2044                        return false;
2045                    }
2046                }
2047                "chosenctrl" => {
2048                    if Some(card.controller) != source.chosen_player {
2049                        return false;
2050                    }
2051                }
2052                "oppown" | "opponentown" => {
2053                    if card.owner == source.controller {
2054                        return false;
2055                    }
2056                }
2057                "iscommander" => {
2058                    if !card.is_commander {
2059                        return false;
2060                    }
2061                }
2062                "isringbearer" => {
2063                    if !context.game.is_some_and(|game| {
2064                        game.player(card.controller).ring_bearer == Some(card.id)
2065                    }) {
2066                        return false;
2067                    }
2068                }
2069                "legendary" => {
2070                    if !card.type_line.is_legendary() {
2071                        return false;
2072                    }
2073                }
2074                "basic" => {
2075                    if !card.type_line.is_basic() {
2076                        return false;
2077                    }
2078                }
2079                "snow" => {
2080                    if !card.type_line.is_snow() {
2081                        return false;
2082                    }
2083                }
2084                "kicked" => {
2085                    if !card.kicked {
2086                        return false;
2087                    }
2088                }
2089                "cameundercontrolsincelastupkeep" => {
2090                    if !card.came_under_control_since_last_upkeep() {
2091                        return false;
2092                    }
2093                }
2094                "noncreature" => {
2095                    if card.is_creature() {
2096                        return false;
2097                    }
2098                }
2099                "nonland" => {
2100                    if card.is_land() {
2101                        return false;
2102                    }
2103                }
2104                "token" => {
2105                    if !card.is_token {
2106                        return false;
2107                    }
2108                }
2109                "nontoken" => {
2110                    if card.is_token {
2111                        return false;
2112                    }
2113                }
2114                "tapped" => {
2115                    if !card.tapped {
2116                        return false;
2117                    }
2118                }
2119                "untapped" => {
2120                    if card.tapped {
2121                        return false;
2122                    }
2123                }
2124                "startedtheturnuntapped" => {
2125                    if card.started_turn_tapped {
2126                        return false;
2127                    }
2128                }
2129                "startedtheturntapped" => {
2130                    if !card.started_turn_tapped {
2131                        return false;
2132                    }
2133                }
2134                "multicolor" => {
2135                    if !card.color.is_multicolor() {
2136                        return false;
2137                    }
2138                }
2139                "colorless" => {
2140                    if !card.color.is_colorless() {
2141                        return false;
2142                    }
2143                }
2144                "attacking"
2145                | "attackingyou"
2146                | "chosencolorsource"
2147                | "blocking"
2148                | "blocked"
2149                | "attackedthisturn"
2150                | "blockingsource"
2151                | "blockedbysource"
2152                | "toplibrary"
2153                | "exiledwithsource"
2154                | "rememberedplayerctrl" => {
2155                    if !legacy_matches_card_atom(raw, card, context) {
2156                        return false;
2157                    }
2158                }
2159                controlled if controlled.starts_with("controlledby ") => {
2160                    if !legacy_matches_card_atom(raw, card, context) {
2161                        return false;
2162                    }
2163                }
2164                "inzonebattlefield" => {
2165                    if card.zone != forge_foundation::ZoneType::Battlefield {
2166                        return false;
2167                    }
2168                }
2169                "inzonegraveyard" => {
2170                    if card.zone != forge_foundation::ZoneType::Graveyard {
2171                        return false;
2172                    }
2173                }
2174                "inzonehand" => {
2175                    if card.zone != forge_foundation::ZoneType::Hand {
2176                        return false;
2177                    }
2178                }
2179                "inzoneexile" => {
2180                    if card.zone != forge_foundation::ZoneType::Exile {
2181                        return false;
2182                    }
2183                }
2184                "damagedby" => {
2185                    // Check if this card was dealt damage by the source card this turn
2186                    if !card.damage_sources_this_turn.contains(&source.id) {
2187                        return false;
2188                    }
2189                }
2190                "equippedby" | "enchantedby" | "attachedby" => {
2191                    // Check if source is attached to this card
2192                    if source.attached_to != Some(card.id) {
2193                        return false;
2194                    }
2195                }
2196                "facedown"
2197                | "paired"
2198                | "pairedwith"
2199                | "equipped"
2200                | "enchanted"
2201                | "hascounters"
2202                | "isimprinted"
2203                | "chosen"
2204                | "chosencard"
2205                | "chosencardstrict"
2206                | "namedcard"
2207                | "chosencolor"
2208                | "thisturnentered"
2209                | "thisturnenteredfrom_battlefield"
2210                | "wasdealtdamagethisturn"
2211                | "historic"
2212                | "modified"
2213                | "issaddled"
2214                | "mayplaysource"
2215                | "suspended"
2216                | "singletarget"
2217                | "promisedgift" => {
2218                    if !legacy_matches_card_atom(raw, card, context) {
2219                        return false;
2220                    }
2221                }
2222                "wascast" => {
2223                    // Mirrors Java CardProperty.java:1923-1926 — card must have been
2224                    // cast (not put onto the battlefield by some other means).
2225                    if !card.was_cast() {
2226                        return false;
2227                    }
2228                }
2229                "wascastbyyou" => {
2230                    // Mirrors Java CardProperty.java:1923-1929: wasCast AND the
2231                    // spell's activating player equals source's controller.
2232                    // Rust doesn't track castSA.activatingPlayer separately; the
2233                    // card's controller at ETB time equals the caster for normal
2234                    // casts, which covers Sunderflock-style triggers.
2235                    if !card.was_cast() || card.controller != source.controller {
2236                        return false;
2237                    }
2238                }
2239                named if named.starts_with("named") => {
2240                    if !card.card_name.eq_ignore_ascii_case(raw[5..].trim()) {
2241                        return false;
2242                    }
2243                }
2244                _ => {
2245                    // Check counters_GE/GT/LT/LE/EQ patterns like "counters_GE3_P1P1"
2246                    if sub.starts_with("counters_") {
2247                        if !check_counter_condition(sub, card) {
2248                            return false;
2249                        }
2250                    } else if sub_lower.starts_with("cmc") {
2251                        // CMC comparisons: cmcEQ1, cmcLE3, cmcGE5
2252                        let original_rest = &sub[3..];
2253                        if !check_cmc_condition_with_context(original_rest, card, Some(context)) {
2254                            return false;
2255                        }
2256                    } else if let Some(rest) = sub_lower.strip_prefix("power") {
2257                        // Power comparisons: powerLE2, powerGE3, etc.
2258                        if !check_power_condition(rest, card) {
2259                            return false;
2260                        }
2261                    } else if let Some(rest) = sub_lower.strip_prefix("toughness") {
2262                        // Toughness comparisons: toughnessLE2, toughnessGE3, etc.
2263                        if !check_toughness_condition(rest, card) {
2264                            return false;
2265                        }
2266                    } else if let Some(color) = Color::from_name(&sub_lower) {
2267                        // Color names: white, blue, black, red, green
2268                        if !card.color.has_color(color) {
2269                            return false;
2270                        }
2271                    } else if let Some(kw) = sub_lower.strip_prefix("with") {
2272                        // "withFlying", "withoutFlying", etc.
2273                        if kw.strip_prefix("out").is_some() {
2274                            // "withoutFlying" — card must NOT have this keyword
2275                            let kw_name = &sub[7..]; // original case
2276                            if card.has_keyword(kw_name) {
2277                                return false;
2278                            }
2279                        } else if !kw.is_empty() {
2280                            // "withFlying" — card must have this keyword
2281                            let kw_name = &sub[4..]; // original case
2282                            if !card.has_keyword(kw_name) {
2283                                return false;
2284                            }
2285                        }
2286                    } else if let Some(negated) = sub_lower.strip_prefix("non") {
2287                        // Negated qualifier: "nonBlack", "nonArtifact", "nonFlying", etc.
2288                        let should_negate = match negated {
2289                            "creature" => card.is_creature(),
2290                            "land" => card.is_land(),
2291                            "artifact" => card.type_line.is_artifact(),
2292                            "enchantment" => card.type_line.is_enchantment(),
2293                            "legendary" => card.type_line.is_legendary(),
2294                            "basic" => card.type_line.is_basic(),
2295                            "snow" => card.type_line.is_snow(),
2296                            "token" => card.is_token,
2297                            "colorless" => card.color.is_colorless(),
2298                            _ => {
2299                                if let Some(color) = Color::from_name(negated) {
2300                                    card.color.has_color(color)
2301                                } else {
2302                                    // nonSubtype: e.g., "nonHuman", "nonWall"
2303                                    card.has_subtype(
2304                                        &sub[3..], // use original case for subtype
2305                                    )
2306                                }
2307                            }
2308                        };
2309                        if should_negate {
2310                            return false;
2311                        }
2312                    } else if sub_lower == "chosentype" {
2313                        // "ChosenType" — card must have the source card's chosen
2314                        // creature type. Changeling counts as all creature types.
2315                        // Mirrors Java CardTraitBase.isValid() ChosenType path.
2316                        let matches = if let Some(ref ct) = source.chosen_type {
2317                            card.type_line.has_subtype(ct) || card.has_keyword("Changeling")
2318                        } else {
2319                            false
2320                        };
2321                        if !matches {
2322                            return false;
2323                        }
2324                    } else if !sub.is_empty() {
2325                        // Color source check: "RedSource", "WhiteSource", "BlackSource", etc.
2326                        // Mirrors Java ForgeScript.cardStateHasProperty: strip "Source" suffix,
2327                        // then check card color. Also handles "nonRedSource" via the non- prefix above.
2328                        let color_name = sub.strip_suffix("Source").unwrap_or(sub);
2329                        if let Some(color) = Color::from_name(&color_name.to_lowercase()) {
2330                            if !card.color.has_color(color) {
2331                                return false;
2332                            }
2333                        } else if color_name.eq_ignore_ascii_case("Colorless") {
2334                            if !card.color.is_colorless() {
2335                                return false;
2336                            }
2337                        } else {
2338                            // Fall through: check as creature subtype (Wall, Zombie, etc.)
2339                            // Mirrors card_has_property behavior: unrecognized qualifiers
2340                            // are checked against the card's type_line subtypes.
2341                            if !card.has_subtype(sub) {
2342                                return false;
2343                            }
2344                        }
2345                    }
2346                }
2347            }
2348        }
2349    }
2350
2351    true
2352}
2353
2354/// Check if a player matches a filter expression like "You", "Opponent", "Each".
2355pub fn matches_valid_player(filter: &str, player: PlayerId, source_controller: PlayerId) -> bool {
2356    let filter = filter.trim();
2357    if filter.is_empty() {
2358        return true;
2359    }
2360
2361    // Handle comma-separated alternatives
2362    if filter.contains(',') {
2363        return filter
2364            .split(',')
2365            .any(|part| matches_single_valid_player(part.trim(), player, source_controller));
2366    }
2367
2368    matches_single_valid_player(filter, player, source_controller)
2369}
2370
2371/// Convenience wrapper: None means "no filter" → always matches.
2372pub fn matches_valid_player_opt(
2373    filter: Option<&str>,
2374    player: PlayerId,
2375    source_controller: PlayerId,
2376) -> bool {
2377    match filter {
2378        None => true,
2379        Some(v) => matches_valid_player(v, player, source_controller),
2380    }
2381}
2382
2383/// Match a precompiled selector against a player without reparsing the
2384/// comma-separated alternatives.
2385pub fn matches_valid_player_selector(
2386    selector: &CompiledSelector,
2387    player: PlayerId,
2388    source_controller: PlayerId,
2389) -> bool {
2390    let result = matches_player_selector_ir(&selector.ir, player, source_controller);
2391    #[cfg(debug_assertions)]
2392    report_selector_drift(
2393        "player",
2394        result,
2395        matches_valid_player(&selector.as_raw(), player, source_controller),
2396        &selector.as_raw(),
2397    );
2398    result
2399}
2400
2401fn matches_player_selector_ir(
2402    selector: &Selector,
2403    player: PlayerId,
2404    source_controller: PlayerId,
2405) -> bool {
2406    if selector.alternatives.is_empty() {
2407        return true;
2408    }
2409
2410    selector.alternatives.iter().any(|alternative| {
2411        alternative
2412            .predicates
2413            .iter()
2414            .all(|predicate| matches_player_predicate(predicate, player, source_controller))
2415    })
2416}
2417
2418fn matches_player_predicate(
2419    predicate: &SelectorPredicate,
2420    player: PlayerId,
2421    source_controller: PlayerId,
2422) -> bool {
2423    match predicate {
2424        SelectorPredicate::Any | SelectorPredicate::Player => true,
2425        SelectorPredicate::PlayerController(controller)
2426        | SelectorPredicate::CardController(controller) => {
2427            matches_player_controller(*controller, player, source_controller)
2428        }
2429        SelectorPredicate::Raw(raw) => matches_single_valid_player(raw, player, source_controller),
2430        // Legacy player matching treats unknown card-oriented predicates as
2431        // permissive, so keep that behavior for mixed ValidTarget paths.
2432        SelectorPredicate::CardType(_)
2433        | SelectorPredicate::CardSupertype(_)
2434        | SelectorPredicate::CardIdentity(_)
2435        | SelectorPredicate::CardOwner(_)
2436        | SelectorPredicate::Tapped(_)
2437        | SelectorPredicate::StartedTurnTapped(_)
2438        | SelectorPredicate::CameUnderControlSinceLastUpkeep
2439        | SelectorPredicate::Zone(_)
2440        | SelectorPredicate::RememberedCard
2441        | SelectorPredicate::TriggerRememberedCard
2442        | SelectorPredicate::EffectSource
2443        | SelectorPredicate::Commander
2444        | SelectorPredicate::Legendary
2445        | SelectorPredicate::Kicked
2446        | SelectorPredicate::Token(_)
2447        | SelectorPredicate::Color(_)
2448        | SelectorPredicate::Multicolor
2449        | SelectorPredicate::Colorless
2450        | SelectorPredicate::SourceColor(_)
2451        | SelectorPredicate::SourceColorless
2452        | SelectorPredicate::ChosenColorSource
2453        | SelectorPredicate::CardState(_)
2454        | SelectorPredicate::Context(_)
2455        | SelectorPredicate::Relation(_)
2456        | SelectorPredicate::DamagedBy
2457        | SelectorPredicate::AttachedBy
2458        | SelectorPredicate::WasCast { .. }
2459        | SelectorPredicate::ChosenType
2460        | SelectorPredicate::Keyword { .. }
2461        | SelectorPredicate::NumericComparison { .. }
2462        | SelectorPredicate::NumericParity { .. }
2463        | SelectorPredicate::CounterComparison { .. }
2464        | SelectorPredicate::Not(_) => true,
2465    }
2466}
2467
2468fn matches_player_controller(
2469    controller: ControllerSelector,
2470    player: PlayerId,
2471    source_controller: PlayerId,
2472) -> bool {
2473    match controller {
2474        ControllerSelector::You => player == source_controller,
2475        ControllerSelector::Opponent => player != source_controller,
2476    }
2477}
2478
2479/// Convenience wrapper: None means "no filter" -> always matches.
2480pub fn matches_valid_player_selector_opt(
2481    selector: Option<&CompiledSelector>,
2482    player: PlayerId,
2483    source_controller: PlayerId,
2484) -> bool {
2485    match selector {
2486        None => true,
2487        Some(selector) => matches_valid_player_selector(selector, player, source_controller),
2488    }
2489}
2490
2491/// Mirrors Java's `CardTraitBase.matchesValid(Object, String[], Card, Player)`.
2492///
2493/// Java uses polymorphic dispatch via `GameObject.isValid()` — both Card and
2494/// Player implement it. In Rust, we take both as Options and try card first,
2495/// then player, mirroring the `instanceof` chain in Java.
2496///
2497/// This eliminates the need for callers to guess whether a filter string can
2498/// match a player (the old `filter_can_match_player` heuristic).
2499pub fn matches_valid(
2500    filter: &str,
2501    card: Option<&Card>,
2502    player: Option<PlayerId>,
2503    source: &Card,
2504    source_controller: PlayerId,
2505) -> bool {
2506    if let Some(card) = card {
2507        matches_valid_card(filter, card, source)
2508    } else if let Some(player) = player {
2509        // Java parity: `Player.isValid` rejects card-oriented filter heads
2510        // (e.g. "Card.Self", "Permanent.YouCtrl"). Without this guard the
2511        // permissive fallback in `matches_single_valid_player` matches any
2512        // unknown head, causing triggers like Ward (`ValidTarget$ Card.Self`)
2513        // to fire on player targets.
2514        if !filter_head_can_match_player(filter) {
2515            return false;
2516        }
2517        matches_valid_player(filter, player, source_controller)
2518    } else {
2519        false
2520    }
2521}
2522
2523fn filter_head_can_match_player(filter: &str) -> bool {
2524    filter.split(',').any(|alternative| {
2525        let head = alternative
2526            .trim()
2527            .split(['.', '+'])
2528            .next()
2529            .unwrap_or("")
2530            .to_ascii_lowercase();
2531        matches!(
2532            head.as_str(),
2533            "" | "you"
2534                | "youctrl"
2535                | "youcontroller"
2536                | "opponent"
2537                | "oppctrl"
2538                | "opponentctrl"
2539                | "any"
2540                | "each"
2541                | "player"
2542                | "active"
2543                | "nonactive"
2544                | "remembered"
2545                | "isremembered"
2546                | "targetedplayer"
2547                | "playerctrl"
2548                | "playercontroller"
2549                | "controller"
2550                | "all"
2551        )
2552    })
2553}
2554
2555fn matches_single_valid_player(
2556    filter: &str,
2557    player: PlayerId,
2558    source_controller: PlayerId,
2559) -> bool {
2560    let filter_lower = filter.to_ascii_lowercase();
2561    if let Some(rest) = filter_lower.strip_prefix("player.") {
2562        return matches_single_valid_player(rest, player, source_controller);
2563    }
2564    match filter_lower.as_str() {
2565        "you" | "youctrl" => player == source_controller,
2566        "opponent" | "oppctrl" | "opponentctrl" => player != source_controller,
2567        "any" | "each" | "player" | "player.ingame" => true,
2568        // "Active" / "NonActive" would need turn info — not currently supported
2569        _ => true, // unknown filter, match all (permissive fallback)
2570    }
2571}
2572
2573/// Check a counter condition like "counters_GE3_P1P1".
2574/// Format: counters_{op}{num}_{counter_type}
2575fn check_counter_condition(condition: &str, card: &Card) -> bool {
2576    use crate::ability::effects::parse_counter_type;
2577    let rest = &condition["counters_".len()..];
2578    if rest.len() < 3 {
2579        return true;
2580    }
2581    let op = &rest[..2];
2582    let after_op = &rest[2..];
2583    let (num_str, counter_type_str) = match after_op.find('_') {
2584        Some(idx) => (&after_op[..idx], &after_op[idx + 1..]),
2585        None => return true,
2586    };
2587    let threshold: i32 = num_str.parse().unwrap_or(0);
2588    let counter_type = parse_counter_type(counter_type_str);
2589    let count = card.counter_count(&counter_type);
2590    match op {
2591        "GE" => count >= threshold,
2592        "GT" => count > threshold,
2593        "LE" => count <= threshold,
2594        "LT" => count < threshold,
2595        "EQ" => count == threshold,
2596        "NE" => count != threshold,
2597        _ => true,
2598    }
2599}
2600
2601/// Check a CMC condition like "cmcEQ1", "cmcLE3", or "cmcLEY".
2602fn check_cmc_condition_with_context(
2603    rest: &str,
2604    card: &Card,
2605    context: Option<MatchContext<'_>>,
2606) -> bool {
2607    let cmc = context
2608        .map(|ctx| effective_mana_value(card, ctx))
2609        .unwrap_or_else(|| card.mana_cost.cmc());
2610    let lower = rest.to_ascii_lowercase();
2611    if lower.starts_with("eq") {
2612        if let Some(n) = parse_cmc_threshold(&rest[2..], context) {
2613            return cmc == n;
2614        }
2615    } else if lower.starts_with("le") {
2616        if let Some(n) = parse_cmc_threshold(&rest[2..], context) {
2617            return cmc <= n;
2618        }
2619    } else if lower.starts_with("ge") {
2620        if let Some(n) = parse_cmc_threshold(&rest[2..], context) {
2621            return cmc >= n;
2622        }
2623    } else if lower.starts_with("lt") {
2624        if let Some(n) = parse_cmc_threshold(&rest[2..], context) {
2625            return cmc < n;
2626        }
2627    } else if lower.starts_with("gt") {
2628        if let Some(n) = parse_cmc_threshold(&rest[2..], context) {
2629            return cmc > n;
2630        }
2631    } else if lower.starts_with("ne") {
2632        if let Some(n) = parse_cmc_threshold(&rest[2..], context) {
2633            return cmc != n;
2634        }
2635    }
2636    true // fallback: unknown format passes
2637}
2638
2639fn parse_cmc_threshold(value: &str, context: Option<MatchContext<'_>>) -> Option<i32> {
2640    if let Ok(n) = value.parse::<i32>() {
2641        return Some(n);
2642    }
2643    let context = context?;
2644    // Keep in sync with resolve_selector_operand: X reads the paid X
2645    // from the live spell ability (Java passes the casting SA into
2646    // calculateAmount), not the host SVar evaluated without it.
2647    if value.eq_ignore_ascii_case("X") {
2648        if let Some(sa) = context.spell_ability {
2649            return Some(sa.x_mana_cost_paid as i32);
2650        }
2651    }
2652    let raw = context.source_card.get_s_var(value)?;
2653    if let Ok(n) = raw.trim().parse::<i32>() {
2654        return Some(n);
2655    }
2656    if raw.starts_with("Count$") || raw.starts_with("PlayerCount") {
2657        let game = context.game?;
2658        let sa = crate::spellability::SpellAbility::new_empty(
2659            Some(context.source_card.id),
2660            context.source_controller,
2661        );
2662        return Some(crate::svar::resolve_svar_expression(
2663            raw,
2664            game,
2665            context.source_card.id,
2666            context.source_controller,
2667            &sa,
2668        ));
2669    }
2670    None
2671}
2672
2673/// Check a power condition like "LE2", "GE3", "EQ0".
2674fn check_power_condition(rest: &str, card: &Card) -> bool {
2675    let power = card.power();
2676    if let Some(num_str) = rest.strip_prefix("eq") {
2677        if let Ok(n) = num_str.parse::<i32>() {
2678            return power == n;
2679        }
2680    } else if let Some(num_str) = rest.strip_prefix("le") {
2681        if let Ok(n) = num_str.parse::<i32>() {
2682            return power <= n;
2683        }
2684    } else if let Some(num_str) = rest.strip_prefix("ge") {
2685        if let Ok(n) = num_str.parse::<i32>() {
2686            return power >= n;
2687        }
2688    } else if let Some(num_str) = rest.strip_prefix("lt") {
2689        if let Ok(n) = num_str.parse::<i32>() {
2690            return power < n;
2691        }
2692    } else if let Some(num_str) = rest.strip_prefix("gt") {
2693        if let Ok(n) = num_str.parse::<i32>() {
2694            return power > n;
2695        }
2696    } else if let Some(num_str) = rest.strip_prefix("ne") {
2697        if let Ok(n) = num_str.parse::<i32>() {
2698            return power != n;
2699        }
2700    }
2701    true // fallback: unknown format passes
2702}
2703
2704/// Check a toughness condition like "LE2", "GE3", "EQ0".
2705fn check_toughness_condition(rest: &str, card: &Card) -> bool {
2706    let toughness = card.toughness();
2707    if let Some(num_str) = rest.strip_prefix("eq") {
2708        if let Ok(n) = num_str.parse::<i32>() {
2709            return toughness == n;
2710        }
2711    } else if let Some(num_str) = rest.strip_prefix("le") {
2712        if let Ok(n) = num_str.parse::<i32>() {
2713            return toughness <= n;
2714        }
2715    } else if let Some(num_str) = rest.strip_prefix("ge") {
2716        if let Ok(n) = num_str.parse::<i32>() {
2717            return toughness >= n;
2718        }
2719    } else if let Some(num_str) = rest.strip_prefix("lt") {
2720        if let Ok(n) = num_str.parse::<i32>() {
2721            return toughness < n;
2722        }
2723    } else if let Some(num_str) = rest.strip_prefix("gt") {
2724        if let Ok(n) = num_str.parse::<i32>() {
2725            return toughness > n;
2726        }
2727    } else if let Some(num_str) = rest.strip_prefix("ne") {
2728        if let Ok(n) = num_str.parse::<i32>() {
2729            return toughness != n;
2730        }
2731    }
2732    true // fallback: unknown format passes
2733}
2734
2735// ── Common requirement checks ───────────────────────────────────────────────
2736//
2737// These mirror Java's `CardTraitBase.meetsCommonRequirements()` — shared
2738// validation logic used by triggers, static abilities, replacement effects,
2739// and cost adjustment. Previously duplicated in 4+ locations.
2740
2741pub fn check_svar_requirement(
2742    game: &GameState,
2743    source: &Card,
2744    svar_source: &dyn HasSVars,
2745    check_name: &str,
2746    compare: &str,
2747) -> bool {
2748    let value = requirement_amount(source, svar_source, check_name, game);
2749    compare_requirement_amount(source, svar_source, compare, game, value)
2750}
2751
2752fn check_condition_value(game: &GameState, condition: Option<&str>, source: &Card) -> bool {
2753    let Some(condition) = condition else {
2754        return true;
2755    };
2756    let controller = requirement_controller(game, source);
2757    match condition {
2758        "PlayerTurn" => game.active_player() == controller,
2759        "NotPlayerTurn" => game.active_player() != controller,
2760        "Threshold" => game.player_has_threshold(controller),
2761        "Hellbent" => game.player_has_hellbent(controller),
2762        "Metalcraft" => game.player_has_metalcraft(controller),
2763        "Delirium" => game.player_has_delirium(controller),
2764        "Ferocious" => game.player_has_ferocious(controller),
2765        "Desert" => game.player_has_desert(controller),
2766        "Blessing" => game.player_has_blessing(controller),
2767        "Monarch" => game.monarch == Some(controller),
2768        "Night" => game.is_night,
2769        "FatefulHour" => game.player(controller).life <= 5,
2770        _ => true, // unknown condition — permissive fallback
2771    }
2772}
2773
2774fn meets_card_trait_requirements(
2775    requirements: &CardTraitRequirementsIr,
2776    game: &GameState,
2777    source: &Card,
2778    svar_source: &dyn HasSVars,
2779) -> bool {
2780    if requirements.is_empty() {
2781        return true;
2782    }
2783
2784    let _perf_scope =
2785        crate::perf::ParamsLookupScopeGuard::enter(crate::perf::ParamsLookupScope::ValidFilter);
2786    let controller = requirement_controller(game, source);
2787
2788    if !check_boolean_requirement(
2789        requirements.metalcraft.as_deref(),
2790        game.player_has_metalcraft(controller),
2791    ) {
2792        return false;
2793    }
2794    if !check_boolean_requirement(
2795        requirements.delirium.as_deref(),
2796        game.player_has_delirium(controller),
2797    ) {
2798        return false;
2799    }
2800    if !check_boolean_requirement(
2801        requirements.threshold.as_deref(),
2802        game.player_has_threshold(controller),
2803    ) {
2804        return false;
2805    }
2806    if !check_boolean_requirement(
2807        requirements.hellbent.as_deref(),
2808        game.player_has_hellbent(controller),
2809    ) {
2810        return false;
2811    }
2812    if !check_boolean_requirement(
2813        requirements.bloodthirst.as_deref(),
2814        game.player_has_bloodthirst(controller),
2815    ) {
2816        return false;
2817    }
2818    if !check_boolean_requirement(
2819        requirements.fateful_hour.as_deref(),
2820        game.player(controller).life <= 5,
2821    ) {
2822        return false;
2823    }
2824    if !check_boolean_requirement(
2825        requirements.monarch.as_deref(),
2826        game.monarch == Some(controller),
2827    ) {
2828        return false;
2829    }
2830    if let Some(revolt) = requirements.revolt.as_deref() {
2831        if revolt.eq_ignore_ascii_case("True") != game.player_has_revolt(controller) {
2832            return false;
2833        } else if revolt.eq_ignore_ascii_case("None")
2834            && game
2835                .alive_players()
2836                .into_iter()
2837                .any(|pid| game.player_has_revolt(pid))
2838        {
2839            return false;
2840        }
2841    }
2842    if !check_boolean_requirement(
2843        requirements.desert.as_deref(),
2844        game.player_has_desert(controller),
2845    ) {
2846        return false;
2847    }
2848    if !check_boolean_requirement(
2849        requirements.blessing.as_deref(),
2850        game.player_has_blessing(controller),
2851    ) {
2852        return false;
2853    }
2854
2855    if let Some(day_time) = requirements.day_time.as_deref() {
2856        if day_time.eq_ignore_ascii_case("Day") {
2857            if !game.is_day() {
2858                return false;
2859            }
2860        } else if day_time.eq_ignore_ascii_case("Night") {
2861            if !game.is_night {
2862                return false;
2863            }
2864        } else if day_time.eq_ignore_ascii_case("Neither") && !game.is_neither_day_nor_night() {
2865            return false;
2866        }
2867    }
2868
2869    if let Some(adamant) = requirements.adamant.as_deref() {
2870        let color_mask = ManaAtom::from_name(&adamant.to_ascii_lowercase());
2871        if adamant.eq_ignore_ascii_case("Any") {
2872            let has_three = [
2873                ManaAtom::WHITE,
2874                ManaAtom::BLUE,
2875                ManaAtom::BLACK,
2876                ManaAtom::RED,
2877                ManaAtom::GREEN,
2878            ]
2879            .into_iter()
2880            .any(|mask| paying_color_count(&source.paying_mana_to_cast, mask) >= 3);
2881            if !has_three {
2882                return false;
2883            }
2884        } else if paying_color_count(&source.paying_mana_to_cast, color_mask) < 3 {
2885            return false;
2886        }
2887    }
2888
2889    if let Some(life_total) = requirements.life_total.as_deref() {
2890        let compare = requirements.life_amount.as_deref().unwrap_or("GE1");
2891        let life = player_life_for_requirement(game, source, life_total);
2892        if !compare_requirement_amount(source, svar_source, compare, game, life) {
2893            return false;
2894        }
2895    }
2896
2897    if let Some(is_present) = requirements.is_present.as_deref() {
2898        let present_compare = requirements.present_compare.as_deref().unwrap_or("GE1");
2899        let present_player = requirements.present_player.as_deref().unwrap_or("Any");
2900        let present_zone = requirements
2901            .present_zone
2902            .as_deref()
2903            .and_then(parse_zone_name)
2904            .unwrap_or(ZoneType::Battlefield);
2905        let selector = requirements
2906            .is_present_selector
2907            .clone()
2908            .unwrap_or_else(|| cached_compiled_selector(is_present));
2909        let count = collect_present_cards(
2910            game,
2911            source,
2912            requirements.present_defined.as_deref(),
2913            present_player,
2914            present_zone,
2915        )
2916        .into_iter()
2917        .filter(|&cid| matches_valid_card_selector_in_game(&selector, game.card(cid), source, game))
2918        .count() as i32;
2919        if !compare_requirement_amount(source, svar_source, present_compare, game, count) {
2920            return false;
2921        }
2922    }
2923
2924    if let Some(is_present) = requirements.is_present2.as_deref() {
2925        let present_compare = requirements.present_compare2.as_deref().unwrap_or("GE1");
2926        let present_player = requirements.present_player2.as_deref().unwrap_or("Any");
2927        let present_zone = requirements
2928            .present_zone2
2929            .as_deref()
2930            .and_then(parse_zone_name)
2931            .unwrap_or(ZoneType::Battlefield);
2932        let selector = requirements
2933            .is_present2_selector
2934            .clone()
2935            .unwrap_or_else(|| cached_compiled_selector(is_present));
2936        let count = collect_present_cards(game, source, None, present_player, present_zone)
2937            .into_iter()
2938            .filter(|&cid| {
2939                matches_valid_card_selector_in_game(&selector, game.card(cid), source, game)
2940            })
2941            .count() as i32;
2942        if !compare_requirement_amount(source, svar_source, present_compare, game, count) {
2943            return false;
2944        }
2945    }
2946
2947    if let Some(defined_players) = requirements.check_defined_player.as_deref() {
2948        let players = crate::ability::ability_utils::get_defined_players(
2949            game,
2950            Some(source.id),
2951            defined_players,
2952            Some(controller),
2953        );
2954        let compare = requirements
2955            .defined_player_compare
2956            .as_deref()
2957            .unwrap_or("GE1");
2958        if !compare_requirement_amount(source, svar_source, compare, game, players.len() as i32) {
2959            return false;
2960        }
2961    }
2962
2963    if let Some(check_name) = requirements.check_svar.as_deref() {
2964        let compare = requirements.svar_compare.as_deref().unwrap_or("GE1");
2965        if !check_svar_requirement(game, source, svar_source, check_name, compare) {
2966            return false;
2967        }
2968        if let Some(check_name) = requirements.check_second_svar.as_deref() {
2969            let compare = requirements.second_svar_compare.as_deref().unwrap_or("GE1");
2970            if !check_svar_requirement(game, source, svar_source, check_name, compare) {
2971                return false;
2972            }
2973        }
2974    }
2975
2976    if let Some(mana_spent) = requirements.mana_spent.as_deref() {
2977        let colors = ManaAtom::from_name(&mana_spent.to_ascii_lowercase());
2978        if !has_all_spent_colors(source.colors_spent_to_cast, colors) {
2979            return false;
2980        }
2981    }
2982    if let Some(mana_not_spent) = requirements.mana_not_spent.as_deref() {
2983        let colors = ManaAtom::from_name(&mana_not_spent.to_ascii_lowercase());
2984        if has_all_spent_colors(source.colors_spent_to_cast, colors) {
2985            return false;
2986        }
2987    }
2988
2989    if requirements.werewolf_transform_condition
2990        && !game.stack.get_spells_cast_last_turn().is_empty()
2991    {
2992        return false;
2993    }
2994    if requirements.werewolf_untransform_condition {
2995        let cast_last_turn = game.stack.get_spells_cast_last_turn();
2996        let mut condition_met = false;
2997        for pid in game.alive_players() {
2998            let count = cast_last_turn
2999                .iter()
3000                .filter(|&&cid| game.card(cid).controller == pid)
3001                .count();
3002            if count > 1 {
3003                condition_met = true;
3004                break;
3005            }
3006        }
3007        if !condition_met {
3008            return false;
3009        }
3010    }
3011
3012    if let Some(class_level) = requirements.class_level.as_deref() {
3013        let min = class_level.parse::<i32>().unwrap_or(0);
3014        if source.class_level < min {
3015            return false;
3016        }
3017    }
3018
3019    check_condition_value(game, requirements.condition.as_deref(), source)
3020}
3021
3022/// Parse a zone name string into ZoneType.
3023fn parse_zone_name(name: &str) -> Option<ZoneType> {
3024    ZoneType::from_str_compat(name)
3025}