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") => {
1672            matches_was_cast_from(&value[11..], card, context)
1673        }
1674        named if named.starts_with("named") => {
1675            card.card_name.eq_ignore_ascii_case(value[5..].trim())
1676        }
1677        _ if value.starts_with("counters_") => check_counter_condition(value, card),
1678        _ => {
1679            if value_lower.starts_with("cmc") {
1680                let original_rest = &value[3..];
1681                check_cmc_condition_with_context(original_rest, card, Some(context))
1682            } else if let Some(rest) = value_lower.strip_prefix("power") {
1683                check_power_condition(rest, card)
1684            } else if let Some(rest) = value_lower.strip_prefix("toughness") {
1685                check_toughness_condition(rest, card)
1686            } else if let Some(color) = Color::from_name(&value_lower) {
1687                card.color.has_color(color)
1688            } else if let Some(keyword_suffix) = value_lower.strip_prefix("with") {
1689                if keyword_suffix.strip_prefix("out").is_some() {
1690                    !card.has_keyword(&value[7..])
1691                } else if !keyword_suffix.is_empty() {
1692                    card.has_keyword(&value[4..])
1693                } else {
1694                    true
1695                }
1696            } else if let Some(negated_value) = value_lower.strip_prefix("non") {
1697                let positive_match = match negated_value {
1698                    "creature" => card.is_creature(),
1699                    "land" => card.is_land(),
1700                    "artifact" => card.type_line.is_artifact(),
1701                    "enchantment" => card.type_line.is_enchantment(),
1702                    "token" => card.is_token,
1703                    "colorless" => card.color.is_colorless(),
1704                    _ => {
1705                        if let Some(color) = Color::from_name(negated_value) {
1706                            card.color.has_color(color)
1707                        } else {
1708                            card.has_subtype(&value[3..])
1709                        }
1710                    }
1711                };
1712                !positive_match
1713            } else if value_lower == "chosentype" {
1714                source.chosen_type.as_ref().is_some_and(|ct| {
1715                    card.type_line.has_subtype(ct) || card.has_keyword("Changeling")
1716                })
1717            } else {
1718                let color_name = value.strip_suffix("Source").unwrap_or(value);
1719                if let Some(color) = Color::from_name(&color_name.to_lowercase()) {
1720                    card.color.has_color(color)
1721                } else if color_name.eq_ignore_ascii_case("Colorless") {
1722                    card.color.is_colorless()
1723                } else {
1724                    card.has_subtype(value)
1725                }
1726            }
1727        }
1728    }
1729}
1730
1731fn matches_domain_predicate(raw: &str, card: &Card, context: MatchContext<'_>) -> Option<bool> {
1732    crate::ability::selector_domain::matches_selector_domain_predicate(raw, card, context)
1733        .or_else(|| {
1734            crate::cost::selector_domain::matches_selector_domain_predicate(raw, card, context)
1735        })
1736        .or_else(|| {
1737            crate::trigger::selector_domain::matches_selector_domain_predicate(raw, card, context)
1738        })
1739        .or_else(|| {
1740            crate::combat::selector_domain::matches_selector_domain_predicate(raw, card, context)
1741        })
1742}
1743
1744fn raw_target_ref(value: &str) -> Option<TargetRef> {
1745    let value = value.trim();
1746    if value.is_empty()
1747        || value.eq_ignore_ascii_case("Self")
1748        || value.eq_ignore_ascii_case("Source")
1749        || value.eq_ignore_ascii_case("You")
1750        || value.eq_ignore_ascii_case("YouCtrl")
1751    {
1752        Some(TargetRef::Source)
1753    } else if value.eq_ignore_ascii_case("Remembered") {
1754        Some(TargetRef::Remembered)
1755    } else if value.eq_ignore_ascii_case("RememberedLKI") {
1756        Some(TargetRef::RememberedLki)
1757    } else if value.eq_ignore_ascii_case("Imprinted") {
1758        Some(TargetRef::Imprinted)
1759    } else if value.eq_ignore_ascii_case("ChosenCard") {
1760        Some(TargetRef::ChosenCard)
1761    } else if value.eq_ignore_ascii_case("ChosenPlayer") {
1762        Some(TargetRef::ChosenPlayer)
1763    } else if value.eq_ignore_ascii_case("Targeted") {
1764        Some(TargetRef::Targeted)
1765    } else if value.eq_ignore_ascii_case("TargetedPlayer")
1766        || value.eq_ignore_ascii_case("TargetedController")
1767    {
1768        Some(TargetRef::Targeted)
1769    } else if value.eq_ignore_ascii_case("Player") {
1770        Some(TargetRef::Player)
1771    } else if value.eq_ignore_ascii_case("Opponent") {
1772        Some(TargetRef::Opponent)
1773    } else if value.eq_ignore_ascii_case("Battlefield") {
1774        Some(TargetRef::Battlefield)
1775    } else if value.eq_ignore_ascii_case("OtherYourBattlefield") {
1776        Some(TargetRef::OtherYourBattlefield)
1777    } else if value.eq_ignore_ascii_case("YourGraveyard") {
1778        Some(TargetRef::YourGraveyard)
1779    } else if value.eq_ignore_ascii_case("TriggeredTarget") {
1780        Some(TargetRef::TriggeredTarget)
1781    } else if value.eq_ignore_ascii_case("TriggeredPlayer") {
1782        Some(TargetRef::TriggeredPlayer)
1783    } else if value.eq_ignore_ascii_case("TriggeredCard") {
1784        Some(TargetRef::TriggeredCard)
1785    } else if value.eq_ignore_ascii_case("TriggeredCardController") {
1786        Some(TargetRef::TriggeredCardController)
1787    } else if value.eq_ignore_ascii_case("TriggeredDefendingPlayer") {
1788        Some(TargetRef::TriggeredDefendingPlayer)
1789    } else if value.eq_ignore_ascii_case("TriggeredAttackedTarget") {
1790        Some(TargetRef::TriggeredAttackedTarget)
1791    } else if value.eq_ignore_ascii_case("Commander") {
1792        Some(TargetRef::Commander)
1793    } else {
1794        None
1795    }
1796}
1797
1798fn raw_blocked_by_valid_this_turn(value: &str) -> Option<ContextPredicate> {
1799    if let Some(target) = raw_target_ref(value) {
1800        return Some(ContextPredicate::BlockedByValidThisTurn(target));
1801    }
1802    raw_card_selector_type(value).map(ContextPredicate::BlockedByValidThisTurnType)
1803}
1804
1805fn raw_card_selector_type(value: &str) -> Option<CardSelectorType> {
1806    let value = value.trim();
1807    match value.to_ascii_lowercase().as_str() {
1808        "card" => Some(CardSelectorType::Card),
1809        "creature" => Some(CardSelectorType::Creature),
1810        "land" => Some(CardSelectorType::Land),
1811        "artifact" => Some(CardSelectorType::Artifact),
1812        "enchantment" => Some(CardSelectorType::Enchantment),
1813        "planeswalker" => Some(CardSelectorType::Planeswalker),
1814        "permanent" => Some(CardSelectorType::Permanent),
1815        "nonland" => Some(CardSelectorType::NonLand),
1816        "noncreature" => Some(CardSelectorType::NonCreature),
1817        _ if value
1818            .chars()
1819            .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '\'') =>
1820        {
1821            Some(CardSelectorType::Subtype(value.to_string()))
1822        }
1823        _ => None,
1824    }
1825}
1826
1827/// Mirrors Java `CardProperty`'s `wasCastFrom` branch. Java reads the owner off
1828/// `Card.castFrom` (a `Zone`); Rust stores only the zone type, so `Your`/`Their`
1829/// resolve the origin zone's owner as the card's owner.
1830fn matches_was_cast_from(suffix: &str, card: &Card, context: MatchContext<'_>) -> bool {
1831    let Some(cast_from) = card.cast_from else {
1832        return false;
1833    };
1834    let (zone_owner, rest) = if let Some(rest) = suffix.strip_prefix("Your") {
1835        (Some(context.source_controller), rest)
1836    } else if let Some(rest) = suffix.strip_prefix("Their") {
1837        (Some(card.controller), rest)
1838    } else {
1839        (None, suffix)
1840    };
1841    let (by_you, zone_name) = match rest.strip_suffix("ByYou") {
1842        Some(zone_name) => (true, zone_name),
1843        None => (false, rest),
1844    };
1845    if zone_owner.is_some_and(|owner| owner != card.owner) {
1846        return false;
1847    }
1848    if by_you && card.controller != context.source_controller {
1849        return false;
1850    }
1851    ZoneType::from_str_compat(zone_name) == Some(cast_from)
1852}
1853
1854fn raw_attached_to_relation(value: &str) -> Option<RelationPredicate> {
1855    if let Some(target) = raw_target_ref(value) {
1856        return Some(RelationPredicate::AttachedTo(target));
1857    }
1858    let card_type = match value.to_ascii_lowercase().as_str() {
1859        "card" => CardSelectorType::Card,
1860        "creature" => CardSelectorType::Creature,
1861        "land" => CardSelectorType::Land,
1862        "artifact" => CardSelectorType::Artifact,
1863        "enchantment" => CardSelectorType::Enchantment,
1864        "permanent" => CardSelectorType::Permanent,
1865        _ => return None,
1866    };
1867    Some(RelationPredicate::AttachedToType(card_type))
1868}
1869
1870/// Convenience wrapper: None means "no filter" -> always matches.
1871pub fn matches_valid_card_selector_opt(
1872    selector: Option<&CompiledSelector>,
1873    card: &Card,
1874    source: &Card,
1875) -> bool {
1876    match selector {
1877        None => true,
1878        Some(selector) => matches_valid_card_selector(selector, card, source),
1879    }
1880}
1881
1882pub fn matches_valid_card_selector_opt_with_context(
1883    selector: Option<&CompiledSelector>,
1884    card: &Card,
1885    context: MatchContext<'_>,
1886) -> bool {
1887    match selector {
1888        None => true,
1889        Some(selector) => matches_valid_card_selector_with_context(selector, card, context),
1890    }
1891}
1892
1893pub fn matches_valid_card_selector_opt_in_game(
1894    selector: Option<&CompiledSelector>,
1895    card: &Card,
1896    source: &Card,
1897    game: &GameState,
1898) -> bool {
1899    matches_valid_card_selector_opt_with_context(
1900        selector,
1901        card,
1902        MatchContext::from_source(source).with_game(game),
1903    )
1904}
1905
1906#[cfg(debug_assertions)]
1907fn matches_single_valid_card(filter: &str, card: &Card, context: MatchContext<'_>) -> bool {
1908    // Handle comma-separated types with qualifiers (e.g. "Creature.YouCtrl,Artifact.YouCtrl")
1909    if filter.contains(',') {
1910        return filter
1911            .split(',')
1912            .any(|alt| matches_type_and_qualifiers(alt.trim(), card, context));
1913    }
1914
1915    matches_type_and_qualifiers(filter, card, context)
1916}
1917
1918#[cfg(debug_assertions)]
1919fn matches_type_and_qualifiers(filter: &str, card: &Card, context: MatchContext<'_>) -> bool {
1920    // Split on dots for compound filters (e.g. "Creature.Other", "Card.Self")
1921    let parts: Vec<&str> = filter.split('.').collect();
1922    if parts.is_empty() {
1923        return true;
1924    }
1925
1926    let type_part = parts[0];
1927    let qualifiers = &parts[1..];
1928
1929    matches_type_and_qualifier_parts(type_part, qualifiers, card, context)
1930}
1931
1932#[cfg(debug_assertions)]
1933fn matches_type_and_qualifier_parts(
1934    type_part: &str,
1935    qualifiers: &[&str],
1936    card: &Card,
1937    context: MatchContext<'_>,
1938) -> bool {
1939    let source = context.source_card;
1940    // Check the type portion
1941    let type_matches = match type_part {
1942        "Card" | "Any" => true, // matches any card
1943        "Creature" => card.is_creature(),
1944        "Land" => card.is_land(),
1945        "Instant" => card.type_line.is_instant(),
1946        "Sorcery" => card.type_line.is_sorcery(),
1947        "Artifact" => card.type_line.is_artifact(),
1948        "Enchantment" => card.type_line.is_enchantment(),
1949        "Planeswalker" => card.type_line.is_planeswalker(),
1950        "nonland" | "nonLand" | "NonLand" => !card.is_land(),
1951        "noncreature" | "nonCreature" | "NonCreature" => !card.is_creature(),
1952        "Permanent" => card.is_permanent(),
1953        "Spell" => true, // used in some contexts
1954        named if named.to_ascii_lowercase().starts_with("named") => {
1955            card.card_name.eq_ignore_ascii_case(named[5..].trim())
1956        }
1957        // Player-type filters: players are not cards, so never match.
1958        "Player" | "You" | "Opponent" | "Each" | "ActivePlayer" | "NonActivePlayer" => false,
1959        _ => {
1960            // Try comma-separated types within the type portion (e.g. "Instant,Sorcery")
1961            if type_part.contains(',') {
1962                type_part.split(',').any(|t| match t.trim() {
1963                    "Creature" => card.is_creature(),
1964                    "Land" => card.is_land(),
1965                    "Instant" => card.type_line.is_instant(),
1966                    "Sorcery" => card.type_line.is_sorcery(),
1967                    "Artifact" => card.type_line.is_artifact(),
1968                    "Enchantment" => card.type_line.is_enchantment(),
1969                    "Planeswalker" => card.type_line.is_planeswalker(),
1970                    "Card" => true,
1971                    _ => false,
1972                })
1973            } else {
1974                // Try matching as subtype (e.g. "Zombie", "Wall", "Dragon").
1975                // This must be changeling-aware for creature types, matching
1976                // Java's CardType.hasStringType()/hasCreatureType() path.
1977                card.has_subtype(type_part)
1978            }
1979        }
1980    };
1981
1982    if !type_matches {
1983        return false;
1984    }
1985
1986    // Check qualifiers — handle compound "+" syntax (e.g. "Self+kicked", "YouCtrl+nonBlack")
1987    for &qualifier in qualifiers {
1988        // Split compound qualifiers on '+' (e.g. "Self+kicked" → ["Self", "kicked"])
1989        let sub_parts: Vec<&str> = qualifier.split('+').collect();
1990        for sub in &sub_parts {
1991            // Handle "!" prefix as negation (e.g. "!token" → "nontoken")
1992            let (negated, raw) = if let Some(stripped) = sub.strip_prefix('!') {
1993                (true, stripped)
1994            } else {
1995                (false, *sub)
1996            };
1997            let sub_lower = raw.to_ascii_lowercase();
1998            // If negated, invert the boolean result of the positive match.
1999            // "!token" is equivalent to "nontoken", "!Creature" to "nonCreature", etc.
2000            if negated {
2001                let positive_match = match sub_lower.as_str() {
2002                    "token" => card.is_token,
2003                    "creature" => card.is_creature(),
2004                    "land" => card.is_land(),
2005                    "artifact" => card.type_line.is_artifact(),
2006                    "enchantment" => card.type_line.is_enchantment(),
2007                    "legendary" => card.type_line.is_legendary(),
2008                    "basic" => card.type_line.is_basic(),
2009                    "snow" => card.type_line.is_snow(),
2010                    _ => {
2011                        // Try subtype match
2012                        card.has_subtype(raw)
2013                    }
2014                };
2015                if positive_match {
2016                    return false;
2017                }
2018                continue;
2019            }
2020            match sub_lower.as_str() {
2021                "self" => {
2022                    if card.id != source.id {
2023                        return false;
2024                    }
2025                }
2026                "strictlyself" => {
2027                    if card.id != source.id {
2028                        return false;
2029                    }
2030                }
2031                "other" | "strictlyother" => {
2032                    if card.id == source.id {
2033                        return false;
2034                    }
2035                }
2036                "youctrl" | "youcontrol" | "you" => {
2037                    if card.controller != source.controller {
2038                        return false;
2039                    }
2040                }
2041                "youdontctrl" => {
2042                    if card.controller == source.controller {
2043                        return false;
2044                    }
2045                }
2046                "youown" => {
2047                    if card.owner != source.controller {
2048                        return false;
2049                    }
2050                }
2051                "youdontown" => {
2052                    if card.owner == source.controller {
2053                        return false;
2054                    }
2055                }
2056                "isremembered" => {
2057                    if !source.remembered_cards.contains(&card.id) {
2058                        return false;
2059                    }
2060                }
2061                "istriggerremembered" => {
2062                    if !context.trigger_remembered_cards.contains(&card.id) {
2063                        return false;
2064                    }
2065                }
2066                "effectsource" => {
2067                    if source.effect_source != Some(card.id) {
2068                        return false;
2069                    }
2070                }
2071                "oppctrl" | "opponentctrl" | "opponent" => {
2072                    if card.controller == source.controller {
2073                        return false;
2074                    }
2075                }
2076                "chosenctrl" => {
2077                    if Some(card.controller) != source.chosen_player {
2078                        return false;
2079                    }
2080                }
2081                "oppown" | "opponentown" => {
2082                    if card.owner == source.controller {
2083                        return false;
2084                    }
2085                }
2086                "iscommander" => {
2087                    if !card.is_commander {
2088                        return false;
2089                    }
2090                }
2091                "isringbearer" => {
2092                    if !context.game.is_some_and(|game| {
2093                        game.player(card.controller).ring_bearer == Some(card.id)
2094                    }) {
2095                        return false;
2096                    }
2097                }
2098                "legendary" => {
2099                    if !card.type_line.is_legendary() {
2100                        return false;
2101                    }
2102                }
2103                "basic" => {
2104                    if !card.type_line.is_basic() {
2105                        return false;
2106                    }
2107                }
2108                "snow" => {
2109                    if !card.type_line.is_snow() {
2110                        return false;
2111                    }
2112                }
2113                "kicked" => {
2114                    if !card.kicked {
2115                        return false;
2116                    }
2117                }
2118                "cameundercontrolsincelastupkeep" => {
2119                    if !card.came_under_control_since_last_upkeep() {
2120                        return false;
2121                    }
2122                }
2123                "noncreature" => {
2124                    if card.is_creature() {
2125                        return false;
2126                    }
2127                }
2128                "nonland" => {
2129                    if card.is_land() {
2130                        return false;
2131                    }
2132                }
2133                "token" => {
2134                    if !card.is_token {
2135                        return false;
2136                    }
2137                }
2138                "nontoken" => {
2139                    if card.is_token {
2140                        return false;
2141                    }
2142                }
2143                "tapped" => {
2144                    if !card.tapped {
2145                        return false;
2146                    }
2147                }
2148                "untapped" => {
2149                    if card.tapped {
2150                        return false;
2151                    }
2152                }
2153                "startedtheturnuntapped" => {
2154                    if card.started_turn_tapped {
2155                        return false;
2156                    }
2157                }
2158                "startedtheturntapped" => {
2159                    if !card.started_turn_tapped {
2160                        return false;
2161                    }
2162                }
2163                "multicolor" => {
2164                    if !card.color.is_multicolor() {
2165                        return false;
2166                    }
2167                }
2168                "colorless" => {
2169                    if !card.color.is_colorless() {
2170                        return false;
2171                    }
2172                }
2173                "attacking"
2174                | "attackingyou"
2175                | "chosencolorsource"
2176                | "blocking"
2177                | "blocked"
2178                | "attackedthisturn"
2179                | "blockingsource"
2180                | "blockedbysource"
2181                | "toplibrary"
2182                | "exiledwithsource"
2183                | "rememberedplayerctrl" => {
2184                    if !legacy_matches_card_atom(raw, card, context) {
2185                        return false;
2186                    }
2187                }
2188                controlled if controlled.starts_with("controlledby ") => {
2189                    if !legacy_matches_card_atom(raw, card, context) {
2190                        return false;
2191                    }
2192                }
2193                "inzonebattlefield" => {
2194                    if card.zone != forge_foundation::ZoneType::Battlefield {
2195                        return false;
2196                    }
2197                }
2198                "inzonegraveyard" => {
2199                    if card.zone != forge_foundation::ZoneType::Graveyard {
2200                        return false;
2201                    }
2202                }
2203                "inzonehand" => {
2204                    if card.zone != forge_foundation::ZoneType::Hand {
2205                        return false;
2206                    }
2207                }
2208                "inzoneexile" => {
2209                    if card.zone != forge_foundation::ZoneType::Exile {
2210                        return false;
2211                    }
2212                }
2213                "damagedby" => {
2214                    // Check if this card was dealt damage by the source card this turn
2215                    if !card.damage_sources_this_turn.contains(&source.id) {
2216                        return false;
2217                    }
2218                }
2219                "equippedby" | "enchantedby" | "attachedby" => {
2220                    // Check if source is attached to this card
2221                    if source.attached_to != Some(card.id) {
2222                        return false;
2223                    }
2224                }
2225                "facedown"
2226                | "paired"
2227                | "pairedwith"
2228                | "equipped"
2229                | "enchanted"
2230                | "hascounters"
2231                | "isimprinted"
2232                | "chosen"
2233                | "chosencard"
2234                | "chosencardstrict"
2235                | "namedcard"
2236                | "chosencolor"
2237                | "thisturnentered"
2238                | "thisturnenteredfrom_battlefield"
2239                | "wasdealtdamagethisturn"
2240                | "historic"
2241                | "modified"
2242                | "issaddled"
2243                | "mayplaysource"
2244                | "suspended"
2245                | "singletarget"
2246                | "promisedgift" => {
2247                    if !legacy_matches_card_atom(raw, card, context) {
2248                        return false;
2249                    }
2250                }
2251                "wascast" => {
2252                    // Mirrors Java CardProperty.java:1923-1926 — card must have been
2253                    // cast (not put onto the battlefield by some other means).
2254                    if !card.was_cast() {
2255                        return false;
2256                    }
2257                }
2258                "wascastbyyou" => {
2259                    // Mirrors Java CardProperty.java:1923-1929: wasCast AND the
2260                    // spell's activating player equals source's controller.
2261                    // Rust doesn't track castSA.activatingPlayer separately; the
2262                    // card's controller at ETB time equals the caster for normal
2263                    // casts, which covers Sunderflock-style triggers.
2264                    if !card.was_cast() || card.controller != source.controller {
2265                        return false;
2266                    }
2267                }
2268                named if named.starts_with("named") => {
2269                    if !card.card_name.eq_ignore_ascii_case(raw[5..].trim()) {
2270                        return false;
2271                    }
2272                }
2273                _ => {
2274                    // Check counters_GE/GT/LT/LE/EQ patterns like "counters_GE3_P1P1"
2275                    if sub.starts_with("counters_") {
2276                        if !check_counter_condition(sub, card) {
2277                            return false;
2278                        }
2279                    } else if sub_lower.starts_with("cmc") {
2280                        // CMC comparisons: cmcEQ1, cmcLE3, cmcGE5
2281                        let original_rest = &sub[3..];
2282                        if !check_cmc_condition_with_context(original_rest, card, Some(context)) {
2283                            return false;
2284                        }
2285                    } else if let Some(rest) = sub_lower.strip_prefix("power") {
2286                        // Power comparisons: powerLE2, powerGE3, etc.
2287                        if !check_power_condition(rest, card) {
2288                            return false;
2289                        }
2290                    } else if let Some(rest) = sub_lower.strip_prefix("toughness") {
2291                        // Toughness comparisons: toughnessLE2, toughnessGE3, etc.
2292                        if !check_toughness_condition(rest, card) {
2293                            return false;
2294                        }
2295                    } else if let Some(color) = Color::from_name(&sub_lower) {
2296                        // Color names: white, blue, black, red, green
2297                        if !card.color.has_color(color) {
2298                            return false;
2299                        }
2300                    } else if let Some(kw) = sub_lower.strip_prefix("with") {
2301                        // "withFlying", "withoutFlying", etc.
2302                        if kw.strip_prefix("out").is_some() {
2303                            // "withoutFlying" — card must NOT have this keyword
2304                            let kw_name = &sub[7..]; // original case
2305                            if card.has_keyword(kw_name) {
2306                                return false;
2307                            }
2308                        } else if !kw.is_empty() {
2309                            // "withFlying" — card must have this keyword
2310                            let kw_name = &sub[4..]; // original case
2311                            if !card.has_keyword(kw_name) {
2312                                return false;
2313                            }
2314                        }
2315                    } else if let Some(negated) = sub_lower.strip_prefix("non") {
2316                        // Negated qualifier: "nonBlack", "nonArtifact", "nonFlying", etc.
2317                        let should_negate = match negated {
2318                            "creature" => card.is_creature(),
2319                            "land" => card.is_land(),
2320                            "artifact" => card.type_line.is_artifact(),
2321                            "enchantment" => card.type_line.is_enchantment(),
2322                            "legendary" => card.type_line.is_legendary(),
2323                            "basic" => card.type_line.is_basic(),
2324                            "snow" => card.type_line.is_snow(),
2325                            "token" => card.is_token,
2326                            "colorless" => card.color.is_colorless(),
2327                            _ => {
2328                                if let Some(color) = Color::from_name(negated) {
2329                                    card.color.has_color(color)
2330                                } else {
2331                                    // nonSubtype: e.g., "nonHuman", "nonWall"
2332                                    card.has_subtype(
2333                                        &sub[3..], // use original case for subtype
2334                                    )
2335                                }
2336                            }
2337                        };
2338                        if should_negate {
2339                            return false;
2340                        }
2341                    } else if sub_lower == "chosentype" {
2342                        // "ChosenType" — card must have the source card's chosen
2343                        // creature type. Changeling counts as all creature types.
2344                        // Mirrors Java CardTraitBase.isValid() ChosenType path.
2345                        let matches = if let Some(ref ct) = source.chosen_type {
2346                            card.type_line.has_subtype(ct) || card.has_keyword("Changeling")
2347                        } else {
2348                            false
2349                        };
2350                        if !matches {
2351                            return false;
2352                        }
2353                    } else if !sub.is_empty() {
2354                        // Color source check: "RedSource", "WhiteSource", "BlackSource", etc.
2355                        // Mirrors Java ForgeScript.cardStateHasProperty: strip "Source" suffix,
2356                        // then check card color. Also handles "nonRedSource" via the non- prefix above.
2357                        let color_name = sub.strip_suffix("Source").unwrap_or(sub);
2358                        if let Some(color) = Color::from_name(&color_name.to_lowercase()) {
2359                            if !card.color.has_color(color) {
2360                                return false;
2361                            }
2362                        } else if color_name.eq_ignore_ascii_case("Colorless") {
2363                            if !card.color.is_colorless() {
2364                                return false;
2365                            }
2366                        } else {
2367                            // Fall through: check as creature subtype (Wall, Zombie, etc.)
2368                            // Mirrors card_has_property behavior: unrecognized qualifiers
2369                            // are checked against the card's type_line subtypes.
2370                            if !card.has_subtype(sub) {
2371                                return false;
2372                            }
2373                        }
2374                    }
2375                }
2376            }
2377        }
2378    }
2379
2380    true
2381}
2382
2383/// Check if a player matches a filter expression like "You", "Opponent", "Each".
2384pub fn matches_valid_player(filter: &str, player: PlayerId, source_controller: PlayerId) -> bool {
2385    let filter = filter.trim();
2386    if filter.is_empty() {
2387        return true;
2388    }
2389
2390    // Handle comma-separated alternatives
2391    if filter.contains(',') {
2392        return filter
2393            .split(',')
2394            .any(|part| matches_single_valid_player(part.trim(), player, source_controller));
2395    }
2396
2397    matches_single_valid_player(filter, player, source_controller)
2398}
2399
2400/// Convenience wrapper: None means "no filter" → always matches.
2401pub fn matches_valid_player_opt(
2402    filter: Option<&str>,
2403    player: PlayerId,
2404    source_controller: PlayerId,
2405) -> bool {
2406    match filter {
2407        None => true,
2408        Some(v) => matches_valid_player(v, player, source_controller),
2409    }
2410}
2411
2412/// Match a precompiled selector against a player without reparsing the
2413/// comma-separated alternatives.
2414pub fn matches_valid_player_selector(
2415    selector: &CompiledSelector,
2416    player: PlayerId,
2417    source_controller: PlayerId,
2418) -> bool {
2419    let result = matches_player_selector_ir(&selector.ir, player, source_controller);
2420    #[cfg(debug_assertions)]
2421    report_selector_drift(
2422        "player",
2423        result,
2424        matches_valid_player(&selector.as_raw(), player, source_controller),
2425        &selector.as_raw(),
2426    );
2427    result
2428}
2429
2430fn matches_player_selector_ir(
2431    selector: &Selector,
2432    player: PlayerId,
2433    source_controller: PlayerId,
2434) -> bool {
2435    if selector.alternatives.is_empty() {
2436        return true;
2437    }
2438
2439    selector.alternatives.iter().any(|alternative| {
2440        alternative
2441            .predicates
2442            .iter()
2443            .all(|predicate| matches_player_predicate(predicate, player, source_controller))
2444    })
2445}
2446
2447fn matches_player_predicate(
2448    predicate: &SelectorPredicate,
2449    player: PlayerId,
2450    source_controller: PlayerId,
2451) -> bool {
2452    match predicate {
2453        SelectorPredicate::Any | SelectorPredicate::Player => true,
2454        SelectorPredicate::PlayerController(controller)
2455        | SelectorPredicate::CardController(controller) => {
2456            matches_player_controller(*controller, player, source_controller)
2457        }
2458        SelectorPredicate::Raw(raw) => matches_single_valid_player(raw, player, source_controller),
2459        // Legacy player matching treats unknown card-oriented predicates as
2460        // permissive, so keep that behavior for mixed ValidTarget paths.
2461        SelectorPredicate::CardType(_)
2462        | SelectorPredicate::CardSupertype(_)
2463        | SelectorPredicate::CardIdentity(_)
2464        | SelectorPredicate::CardOwner(_)
2465        | SelectorPredicate::Tapped(_)
2466        | SelectorPredicate::StartedTurnTapped(_)
2467        | SelectorPredicate::CameUnderControlSinceLastUpkeep
2468        | SelectorPredicate::Zone(_)
2469        | SelectorPredicate::RememberedCard
2470        | SelectorPredicate::TriggerRememberedCard
2471        | SelectorPredicate::EffectSource
2472        | SelectorPredicate::Commander
2473        | SelectorPredicate::Legendary
2474        | SelectorPredicate::Kicked
2475        | SelectorPredicate::Token(_)
2476        | SelectorPredicate::Color(_)
2477        | SelectorPredicate::Multicolor
2478        | SelectorPredicate::Colorless
2479        | SelectorPredicate::SourceColor(_)
2480        | SelectorPredicate::SourceColorless
2481        | SelectorPredicate::ChosenColorSource
2482        | SelectorPredicate::CardState(_)
2483        | SelectorPredicate::Context(_)
2484        | SelectorPredicate::Relation(_)
2485        | SelectorPredicate::DamagedBy
2486        | SelectorPredicate::AttachedBy
2487        | SelectorPredicate::WasCast { .. }
2488        | SelectorPredicate::ChosenType
2489        | SelectorPredicate::Keyword { .. }
2490        | SelectorPredicate::NumericComparison { .. }
2491        | SelectorPredicate::NumericParity { .. }
2492        | SelectorPredicate::CounterComparison { .. }
2493        | SelectorPredicate::Not(_) => true,
2494    }
2495}
2496
2497fn matches_player_controller(
2498    controller: ControllerSelector,
2499    player: PlayerId,
2500    source_controller: PlayerId,
2501) -> bool {
2502    match controller {
2503        ControllerSelector::You => player == source_controller,
2504        ControllerSelector::Opponent => player != source_controller,
2505    }
2506}
2507
2508/// Convenience wrapper: None means "no filter" -> always matches.
2509pub fn matches_valid_player_selector_opt(
2510    selector: Option<&CompiledSelector>,
2511    player: PlayerId,
2512    source_controller: PlayerId,
2513) -> bool {
2514    match selector {
2515        None => true,
2516        Some(selector) => matches_valid_player_selector(selector, player, source_controller),
2517    }
2518}
2519
2520/// Mirrors Java's `CardTraitBase.matchesValid(Object, String[], Card, Player)`.
2521///
2522/// Java uses polymorphic dispatch via `GameObject.isValid()` — both Card and
2523/// Player implement it. In Rust, we take both as Options and try card first,
2524/// then player, mirroring the `instanceof` chain in Java.
2525///
2526/// This eliminates the need for callers to guess whether a filter string can
2527/// match a player (the old `filter_can_match_player` heuristic).
2528pub fn matches_valid(
2529    filter: &str,
2530    card: Option<&Card>,
2531    player: Option<PlayerId>,
2532    source: &Card,
2533    source_controller: PlayerId,
2534) -> bool {
2535    if let Some(card) = card {
2536        matches_valid_card(filter, card, source)
2537    } else if let Some(player) = player {
2538        // Java parity: `Player.isValid` rejects card-oriented filter heads
2539        // (e.g. "Card.Self", "Permanent.YouCtrl"). Without this guard the
2540        // permissive fallback in `matches_single_valid_player` matches any
2541        // unknown head, causing triggers like Ward (`ValidTarget$ Card.Self`)
2542        // to fire on player targets.
2543        if !filter_head_can_match_player(filter) {
2544            return false;
2545        }
2546        matches_valid_player(filter, player, source_controller)
2547    } else {
2548        false
2549    }
2550}
2551
2552fn filter_head_can_match_player(filter: &str) -> bool {
2553    filter.split(',').any(|alternative| {
2554        let head = alternative
2555            .trim()
2556            .split(['.', '+'])
2557            .next()
2558            .unwrap_or("")
2559            .to_ascii_lowercase();
2560        matches!(
2561            head.as_str(),
2562            "" | "you"
2563                | "youctrl"
2564                | "youcontroller"
2565                | "opponent"
2566                | "oppctrl"
2567                | "opponentctrl"
2568                | "any"
2569                | "each"
2570                | "player"
2571                | "active"
2572                | "nonactive"
2573                | "remembered"
2574                | "isremembered"
2575                | "targetedplayer"
2576                | "playerctrl"
2577                | "playercontroller"
2578                | "controller"
2579                | "all"
2580        )
2581    })
2582}
2583
2584fn matches_single_valid_player(
2585    filter: &str,
2586    player: PlayerId,
2587    source_controller: PlayerId,
2588) -> bool {
2589    let filter_lower = filter.to_ascii_lowercase();
2590    if let Some(rest) = filter_lower.strip_prefix("player.") {
2591        return matches_single_valid_player(rest, player, source_controller);
2592    }
2593    match filter_lower.as_str() {
2594        "you" | "youctrl" => player == source_controller,
2595        "opponent" | "oppctrl" | "opponentctrl" => player != source_controller,
2596        "any" | "each" | "player" | "player.ingame" => true,
2597        // "Active" / "NonActive" would need turn info — not currently supported
2598        _ => true, // unknown filter, match all (permissive fallback)
2599    }
2600}
2601
2602/// Check a counter condition like "counters_GE3_P1P1".
2603/// Format: counters_{op}{num}_{counter_type}
2604fn check_counter_condition(condition: &str, card: &Card) -> bool {
2605    use crate::ability::effects::parse_counter_type;
2606    let rest = &condition["counters_".len()..];
2607    if rest.len() < 3 {
2608        return true;
2609    }
2610    let op = &rest[..2];
2611    let after_op = &rest[2..];
2612    let (num_str, counter_type_str) = match after_op.find('_') {
2613        Some(idx) => (&after_op[..idx], &after_op[idx + 1..]),
2614        None => return true,
2615    };
2616    let threshold: i32 = num_str.parse().unwrap_or(0);
2617    let counter_type = parse_counter_type(counter_type_str);
2618    let count = card.counter_count(&counter_type);
2619    match op {
2620        "GE" => count >= threshold,
2621        "GT" => count > threshold,
2622        "LE" => count <= threshold,
2623        "LT" => count < threshold,
2624        "EQ" => count == threshold,
2625        "NE" => count != threshold,
2626        _ => true,
2627    }
2628}
2629
2630/// Check a CMC condition like "cmcEQ1", "cmcLE3", or "cmcLEY".
2631fn check_cmc_condition_with_context(
2632    rest: &str,
2633    card: &Card,
2634    context: Option<MatchContext<'_>>,
2635) -> bool {
2636    let cmc = context
2637        .map(|ctx| effective_mana_value(card, ctx))
2638        .unwrap_or_else(|| card.mana_cost.cmc());
2639    let lower = rest.to_ascii_lowercase();
2640    if lower.starts_with("eq") {
2641        if let Some(n) = parse_cmc_threshold(&rest[2..], context) {
2642            return cmc == n;
2643        }
2644    } else if lower.starts_with("le") {
2645        if let Some(n) = parse_cmc_threshold(&rest[2..], context) {
2646            return cmc <= n;
2647        }
2648    } else if lower.starts_with("ge") {
2649        if let Some(n) = parse_cmc_threshold(&rest[2..], context) {
2650            return cmc >= n;
2651        }
2652    } else if lower.starts_with("lt") {
2653        if let Some(n) = parse_cmc_threshold(&rest[2..], context) {
2654            return cmc < n;
2655        }
2656    } else if lower.starts_with("gt") {
2657        if let Some(n) = parse_cmc_threshold(&rest[2..], context) {
2658            return cmc > n;
2659        }
2660    } else if lower.starts_with("ne") {
2661        if let Some(n) = parse_cmc_threshold(&rest[2..], context) {
2662            return cmc != n;
2663        }
2664    }
2665    true // fallback: unknown format passes
2666}
2667
2668fn parse_cmc_threshold(value: &str, context: Option<MatchContext<'_>>) -> Option<i32> {
2669    if let Ok(n) = value.parse::<i32>() {
2670        return Some(n);
2671    }
2672    let context = context?;
2673    // Keep in sync with resolve_selector_operand: X reads the paid X
2674    // from the live spell ability (Java passes the casting SA into
2675    // calculateAmount), not the host SVar evaluated without it.
2676    if value.eq_ignore_ascii_case("X") {
2677        if let Some(sa) = context.spell_ability {
2678            return Some(sa.x_mana_cost_paid as i32);
2679        }
2680    }
2681    let raw = context.source_card.get_s_var(value)?;
2682    if let Ok(n) = raw.trim().parse::<i32>() {
2683        return Some(n);
2684    }
2685    if raw.starts_with("Count$") || raw.starts_with("PlayerCount") {
2686        let game = context.game?;
2687        let sa = crate::spellability::SpellAbility::new_empty(
2688            Some(context.source_card.id),
2689            context.source_controller,
2690        );
2691        return Some(crate::svar::resolve_svar_expression(
2692            raw,
2693            game,
2694            context.source_card.id,
2695            context.source_controller,
2696            &sa,
2697        ));
2698    }
2699    None
2700}
2701
2702/// Check a power condition like "LE2", "GE3", "EQ0".
2703fn check_power_condition(rest: &str, card: &Card) -> bool {
2704    let power = card.power();
2705    if let Some(num_str) = rest.strip_prefix("eq") {
2706        if let Ok(n) = num_str.parse::<i32>() {
2707            return power == n;
2708        }
2709    } else if let Some(num_str) = rest.strip_prefix("le") {
2710        if let Ok(n) = num_str.parse::<i32>() {
2711            return power <= n;
2712        }
2713    } else if let Some(num_str) = rest.strip_prefix("ge") {
2714        if let Ok(n) = num_str.parse::<i32>() {
2715            return power >= n;
2716        }
2717    } else if let Some(num_str) = rest.strip_prefix("lt") {
2718        if let Ok(n) = num_str.parse::<i32>() {
2719            return power < n;
2720        }
2721    } else if let Some(num_str) = rest.strip_prefix("gt") {
2722        if let Ok(n) = num_str.parse::<i32>() {
2723            return power > n;
2724        }
2725    } else if let Some(num_str) = rest.strip_prefix("ne") {
2726        if let Ok(n) = num_str.parse::<i32>() {
2727            return power != n;
2728        }
2729    }
2730    true // fallback: unknown format passes
2731}
2732
2733/// Check a toughness condition like "LE2", "GE3", "EQ0".
2734fn check_toughness_condition(rest: &str, card: &Card) -> bool {
2735    let toughness = card.toughness();
2736    if let Some(num_str) = rest.strip_prefix("eq") {
2737        if let Ok(n) = num_str.parse::<i32>() {
2738            return toughness == n;
2739        }
2740    } else if let Some(num_str) = rest.strip_prefix("le") {
2741        if let Ok(n) = num_str.parse::<i32>() {
2742            return toughness <= n;
2743        }
2744    } else if let Some(num_str) = rest.strip_prefix("ge") {
2745        if let Ok(n) = num_str.parse::<i32>() {
2746            return toughness >= n;
2747        }
2748    } else if let Some(num_str) = rest.strip_prefix("lt") {
2749        if let Ok(n) = num_str.parse::<i32>() {
2750            return toughness < n;
2751        }
2752    } else if let Some(num_str) = rest.strip_prefix("gt") {
2753        if let Ok(n) = num_str.parse::<i32>() {
2754            return toughness > n;
2755        }
2756    } else if let Some(num_str) = rest.strip_prefix("ne") {
2757        if let Ok(n) = num_str.parse::<i32>() {
2758            return toughness != n;
2759        }
2760    }
2761    true // fallback: unknown format passes
2762}
2763
2764// ── Common requirement checks ───────────────────────────────────────────────
2765//
2766// These mirror Java's `CardTraitBase.meetsCommonRequirements()` — shared
2767// validation logic used by triggers, static abilities, replacement effects,
2768// and cost adjustment. Previously duplicated in 4+ locations.
2769
2770pub fn check_svar_requirement(
2771    game: &GameState,
2772    source: &Card,
2773    svar_source: &dyn HasSVars,
2774    check_name: &str,
2775    compare: &str,
2776) -> bool {
2777    let value = requirement_amount(source, svar_source, check_name, game);
2778    compare_requirement_amount(source, svar_source, compare, game, value)
2779}
2780
2781fn check_condition_value(game: &GameState, condition: Option<&str>, source: &Card) -> bool {
2782    let Some(condition) = condition else {
2783        return true;
2784    };
2785    let controller = requirement_controller(game, source);
2786    match condition {
2787        "PlayerTurn" => game.active_player() == controller,
2788        "NotPlayerTurn" => game.active_player() != controller,
2789        "Threshold" => game.player_has_threshold(controller),
2790        "Hellbent" => game.player_has_hellbent(controller),
2791        "Metalcraft" => game.player_has_metalcraft(controller),
2792        "Delirium" => game.player_has_delirium(controller),
2793        "Ferocious" => game.player_has_ferocious(controller),
2794        "Desert" => game.player_has_desert(controller),
2795        "Blessing" => game.player_has_blessing(controller),
2796        "Monarch" => game.monarch == Some(controller),
2797        "Night" => game.is_night,
2798        "FatefulHour" => game.player(controller).life <= 5,
2799        _ => true, // unknown condition — permissive fallback
2800    }
2801}
2802
2803fn meets_card_trait_requirements(
2804    requirements: &CardTraitRequirementsIr,
2805    game: &GameState,
2806    source: &Card,
2807    svar_source: &dyn HasSVars,
2808) -> bool {
2809    if requirements.is_empty() {
2810        return true;
2811    }
2812
2813    let _perf_scope =
2814        crate::perf::ParamsLookupScopeGuard::enter(crate::perf::ParamsLookupScope::ValidFilter);
2815    let controller = requirement_controller(game, source);
2816
2817    if !check_boolean_requirement(
2818        requirements.metalcraft.as_deref(),
2819        game.player_has_metalcraft(controller),
2820    ) {
2821        return false;
2822    }
2823    if !check_boolean_requirement(
2824        requirements.delirium.as_deref(),
2825        game.player_has_delirium(controller),
2826    ) {
2827        return false;
2828    }
2829    if !check_boolean_requirement(
2830        requirements.threshold.as_deref(),
2831        game.player_has_threshold(controller),
2832    ) {
2833        return false;
2834    }
2835    if !check_boolean_requirement(
2836        requirements.hellbent.as_deref(),
2837        game.player_has_hellbent(controller),
2838    ) {
2839        return false;
2840    }
2841    if !check_boolean_requirement(
2842        requirements.bloodthirst.as_deref(),
2843        game.player_has_bloodthirst(controller),
2844    ) {
2845        return false;
2846    }
2847    if !check_boolean_requirement(
2848        requirements.fateful_hour.as_deref(),
2849        game.player(controller).life <= 5,
2850    ) {
2851        return false;
2852    }
2853    if !check_boolean_requirement(
2854        requirements.monarch.as_deref(),
2855        game.monarch == Some(controller),
2856    ) {
2857        return false;
2858    }
2859    if let Some(revolt) = requirements.revolt.as_deref() {
2860        if revolt.eq_ignore_ascii_case("True") != game.player_has_revolt(controller) {
2861            return false;
2862        } else if revolt.eq_ignore_ascii_case("None")
2863            && game
2864                .alive_players()
2865                .into_iter()
2866                .any(|pid| game.player_has_revolt(pid))
2867        {
2868            return false;
2869        }
2870    }
2871    if !check_boolean_requirement(
2872        requirements.desert.as_deref(),
2873        game.player_has_desert(controller),
2874    ) {
2875        return false;
2876    }
2877    if !check_boolean_requirement(
2878        requirements.blessing.as_deref(),
2879        game.player_has_blessing(controller),
2880    ) {
2881        return false;
2882    }
2883
2884    if let Some(day_time) = requirements.day_time.as_deref() {
2885        if day_time.eq_ignore_ascii_case("Day") {
2886            if !game.is_day() {
2887                return false;
2888            }
2889        } else if day_time.eq_ignore_ascii_case("Night") {
2890            if !game.is_night {
2891                return false;
2892            }
2893        } else if day_time.eq_ignore_ascii_case("Neither") && !game.is_neither_day_nor_night() {
2894            return false;
2895        }
2896    }
2897
2898    if let Some(adamant) = requirements.adamant.as_deref() {
2899        let color_mask = ManaAtom::from_name(&adamant.to_ascii_lowercase());
2900        if adamant.eq_ignore_ascii_case("Any") {
2901            let has_three = [
2902                ManaAtom::WHITE,
2903                ManaAtom::BLUE,
2904                ManaAtom::BLACK,
2905                ManaAtom::RED,
2906                ManaAtom::GREEN,
2907            ]
2908            .into_iter()
2909            .any(|mask| paying_color_count(&source.paying_mana_to_cast, mask) >= 3);
2910            if !has_three {
2911                return false;
2912            }
2913        } else if paying_color_count(&source.paying_mana_to_cast, color_mask) < 3 {
2914            return false;
2915        }
2916    }
2917
2918    if let Some(life_total) = requirements.life_total.as_deref() {
2919        let compare = requirements.life_amount.as_deref().unwrap_or("GE1");
2920        let life = player_life_for_requirement(game, source, life_total);
2921        if !compare_requirement_amount(source, svar_source, compare, game, life) {
2922            return false;
2923        }
2924    }
2925
2926    if let Some(is_present) = requirements.is_present.as_deref() {
2927        let present_compare = requirements.present_compare.as_deref().unwrap_or("GE1");
2928        let present_player = requirements.present_player.as_deref().unwrap_or("Any");
2929        let present_zone = requirements
2930            .present_zone
2931            .as_deref()
2932            .and_then(parse_zone_name)
2933            .unwrap_or(ZoneType::Battlefield);
2934        let selector = requirements
2935            .is_present_selector
2936            .clone()
2937            .unwrap_or_else(|| cached_compiled_selector(is_present));
2938        let count = collect_present_cards(
2939            game,
2940            source,
2941            requirements.present_defined.as_deref(),
2942            present_player,
2943            present_zone,
2944        )
2945        .into_iter()
2946        .filter(|&cid| matches_valid_card_selector_in_game(&selector, game.card(cid), source, game))
2947        .count() as i32;
2948        if !compare_requirement_amount(source, svar_source, present_compare, game, count) {
2949            return false;
2950        }
2951    }
2952
2953    if let Some(is_present) = requirements.is_present2.as_deref() {
2954        let present_compare = requirements.present_compare2.as_deref().unwrap_or("GE1");
2955        let present_player = requirements.present_player2.as_deref().unwrap_or("Any");
2956        let present_zone = requirements
2957            .present_zone2
2958            .as_deref()
2959            .and_then(parse_zone_name)
2960            .unwrap_or(ZoneType::Battlefield);
2961        let selector = requirements
2962            .is_present2_selector
2963            .clone()
2964            .unwrap_or_else(|| cached_compiled_selector(is_present));
2965        let count = collect_present_cards(game, source, None, present_player, present_zone)
2966            .into_iter()
2967            .filter(|&cid| {
2968                matches_valid_card_selector_in_game(&selector, game.card(cid), source, game)
2969            })
2970            .count() as i32;
2971        if !compare_requirement_amount(source, svar_source, present_compare, game, count) {
2972            return false;
2973        }
2974    }
2975
2976    if let Some(defined_players) = requirements.check_defined_player.as_deref() {
2977        let players = crate::ability::ability_utils::get_defined_players(
2978            game,
2979            Some(source.id),
2980            defined_players,
2981            Some(controller),
2982        );
2983        let compare = requirements
2984            .defined_player_compare
2985            .as_deref()
2986            .unwrap_or("GE1");
2987        if !compare_requirement_amount(source, svar_source, compare, game, players.len() as i32) {
2988            return false;
2989        }
2990    }
2991
2992    if let Some(check_name) = requirements.check_svar.as_deref() {
2993        let compare = requirements.svar_compare.as_deref().unwrap_or("GE1");
2994        if !check_svar_requirement(game, source, svar_source, check_name, compare) {
2995            return false;
2996        }
2997        if let Some(check_name) = requirements.check_second_svar.as_deref() {
2998            let compare = requirements.second_svar_compare.as_deref().unwrap_or("GE1");
2999            if !check_svar_requirement(game, source, svar_source, check_name, compare) {
3000                return false;
3001            }
3002        }
3003    }
3004
3005    if let Some(mana_spent) = requirements.mana_spent.as_deref() {
3006        let colors = ManaAtom::from_name(&mana_spent.to_ascii_lowercase());
3007        if !has_all_spent_colors(source.colors_spent_to_cast, colors) {
3008            return false;
3009        }
3010    }
3011    if let Some(mana_not_spent) = requirements.mana_not_spent.as_deref() {
3012        let colors = ManaAtom::from_name(&mana_not_spent.to_ascii_lowercase());
3013        if has_all_spent_colors(source.colors_spent_to_cast, colors) {
3014            return false;
3015        }
3016    }
3017
3018    if requirements.werewolf_transform_condition
3019        && !game.stack.get_spells_cast_last_turn().is_empty()
3020    {
3021        return false;
3022    }
3023    if requirements.werewolf_untransform_condition {
3024        let cast_last_turn = game.stack.get_spells_cast_last_turn();
3025        let mut condition_met = false;
3026        for pid in game.alive_players() {
3027            let count = cast_last_turn
3028                .iter()
3029                .filter(|&&cid| game.card(cid).controller == pid)
3030                .count();
3031            if count > 1 {
3032                condition_met = true;
3033                break;
3034            }
3035        }
3036        if !condition_met {
3037            return false;
3038        }
3039    }
3040
3041    if let Some(class_level) = requirements.class_level.as_deref() {
3042        let min = class_level.parse::<i32>().unwrap_or(0);
3043        if source.class_level < min {
3044            return false;
3045        }
3046    }
3047
3048    check_condition_value(game, requirements.condition.as_deref(), source)
3049}
3050
3051/// Parse a zone name string into ZoneType.
3052fn parse_zone_name(name: &str) -> Option<ZoneType> {
3053    ZoneType::from_str_compat(name)
3054}