1use 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 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#[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
345pub 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 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
383pub 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
391pub 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 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
499pub 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#[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
1827fn 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
1870pub 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 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 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 let type_matches = match type_part {
1942 "Card" | "Any" => true, "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, named if named.to_ascii_lowercase().starts_with("named") => {
1955 card.card_name.eq_ignore_ascii_case(named[5..].trim())
1956 }
1957 "Player" | "You" | "Opponent" | "Each" | "ActivePlayer" | "NonActivePlayer" => false,
1959 _ => {
1960 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 card.has_subtype(type_part)
1978 }
1979 }
1980 };
1981
1982 if !type_matches {
1983 return false;
1984 }
1985
1986 for &qualifier in qualifiers {
1988 let sub_parts: Vec<&str> = qualifier.split('+').collect();
1990 for sub in &sub_parts {
1991 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 {
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 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 if !card.damage_sources_this_turn.contains(&source.id) {
2216 return false;
2217 }
2218 }
2219 "equippedby" | "enchantedby" | "attachedby" => {
2220 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 if !card.was_cast() {
2255 return false;
2256 }
2257 }
2258 "wascastbyyou" => {
2259 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 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 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 if !check_power_condition(rest, card) {
2288 return false;
2289 }
2290 } else if let Some(rest) = sub_lower.strip_prefix("toughness") {
2291 if !check_toughness_condition(rest, card) {
2293 return false;
2294 }
2295 } else if let Some(color) = Color::from_name(&sub_lower) {
2296 if !card.color.has_color(color) {
2298 return false;
2299 }
2300 } else if let Some(kw) = sub_lower.strip_prefix("with") {
2301 if kw.strip_prefix("out").is_some() {
2303 let kw_name = &sub[7..]; if card.has_keyword(kw_name) {
2306 return false;
2307 }
2308 } else if !kw.is_empty() {
2309 let kw_name = &sub[4..]; if !card.has_keyword(kw_name) {
2312 return false;
2313 }
2314 }
2315 } else if let Some(negated) = sub_lower.strip_prefix("non") {
2316 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 card.has_subtype(
2333 &sub[3..], )
2335 }
2336 }
2337 };
2338 if should_negate {
2339 return false;
2340 }
2341 } else if sub_lower == "chosentype" {
2342 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 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 if !card.has_subtype(sub) {
2371 return false;
2372 }
2373 }
2374 }
2375 }
2376 }
2377 }
2378 }
2379
2380 true
2381}
2382
2383pub 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 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
2400pub 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
2412pub 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 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
2508pub 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
2520pub 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 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 _ => true, }
2600}
2601
2602fn 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
2630fn 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 }
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 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
2702fn 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 }
2732
2733fn 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 }
2763
2764pub 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, }
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
3051fn parse_zone_name(name: &str) -> Option<ZoneType> {
3053 ZoneType::from_str_compat(name)
3054}