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