1use std::collections::{HashSet, VecDeque};
4
5use forge_foundation::{CardStateName, CardTypeLine, Color, ColorSet, ZoneType};
6
7use crate::ability::ability_factory;
8use crate::ability::ability_utils;
9use crate::ability::api_type::ApiType;
10use crate::card::card_collection::CardCollection;
11use crate::card::card_lists::CardLists;
12use crate::card::valid_filter;
13use crate::card::Card;
14use crate::game::GameState;
15use crate::ids::{CardId, PlayerId};
16use crate::parsing::cached_compiled_selector;
17use crate::spellability::target_restrictions;
18use crate::spellability::SpellAbility;
19
20const MODIFIABLE_KEYWORDS: &[&str] = &[
21 "Enchant",
22 "Protection",
23 "Cumulative upkeep",
24 "Equip",
25 "Buyback",
26 "Cycling",
27 "Echo",
28 "Kicker",
29 "Flashback",
30 "Madness",
31 "Morph",
32 "Affinity",
33 "Entwine",
34 "Splice",
35 "Ninjutsu",
36 "Transmute",
37 "Replicate",
38 "Recover",
39 "Squad",
40 "Suspend",
41 "Aura swap",
42 "Fortify",
43 "Transfigure",
44 "Champion",
45 "Evoke",
46 "Prowl",
47 "Freerunning",
48 "Reinforce",
49 "Unearth",
50 "Level up",
51 "Miracle",
52 "Overload",
53 "Cleave",
54 "Scavenge",
55 "Encore",
56 "Bestow",
57 "Outlast",
58 "Dash",
59 "Surge",
60 "Emerge",
61 "Hexproof:",
62 "Bands with other",
63 "Landwalk",
64 "Offering",
65 "etbCounter",
66 "Reflect",
67 "Ward",
68];
69
70const NON_STACKING_LIST: &[&str] = &[];
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct CardCharacteristic {
75 pub state: CardStateName,
76 pub name: String,
77 pub type_line: CardTypeLine,
78 pub base_power: Option<i32>,
79 pub base_toughness: Option<i32>,
80}
81
82pub fn is_keyword_modifiable(keyword: &str) -> bool {
83 MODIFIABLE_KEYWORDS
84 .iter()
85 .any(|prefix| keyword.starts_with(prefix))
86}
87
88pub fn is_stacking_keyword(keyword: &str) -> bool {
89 let kw = keyword.strip_prefix("HIDDEN").unwrap_or(keyword);
90 !kw.starts_with("Protection") && !NON_STACKING_LIST.contains(&kw)
91}
92
93pub fn get_this_turn_entered(
94 game: &GameState,
95 to: ZoneType,
96 from: ZoneType,
97 valid: &str,
98 src: CardId,
99 controller: PlayerId,
100) -> Vec<CardId> {
101 let mut res = Vec::new();
102 if to != ZoneType::Stack {
103 for &pid in &game.player_order {
104 res.extend(
105 game.zone(to, pid)
106 .cards_added_this_turn
107 .iter()
108 .filter(|(origin, _)| *origin == from)
109 .map(|(_, cid)| *cid),
110 );
111 }
112 } else {
113 res.extend(game.stack.get_spells_cast_this_turn().iter().copied());
114 }
115 filter_valid_cards(game, res, valid, src, controller)
116}
117
118pub fn get_last_turn_entered(
119 game: &GameState,
120 to: ZoneType,
121 from: ZoneType,
122 valid: &str,
123 src: CardId,
124 controller: PlayerId,
125) -> Vec<CardId> {
126 let mut res = Vec::new();
127 if to != ZoneType::Stack {
128 for &pid in &game.player_order {
129 res.extend(
130 game.zone(to, pid)
131 .cards_added_last_turn
132 .iter()
133 .filter(|(origin, _)| *origin == from)
134 .map(|(_, cid)| *cid),
135 );
136 }
137 } else {
138 res.extend(game.stack.get_spells_cast_last_turn().iter().copied());
139 }
140 filter_valid_cards(game, res, valid, src, controller)
141}
142
143pub fn get_this_turn_cast(
144 game: &GameState,
145 valid: &str,
146 src: CardId,
147 controller: PlayerId,
148) -> Vec<CardId> {
149 filter_valid_cards(
150 game,
151 game.stack.get_spells_cast_this_turn().to_vec(),
152 valid,
153 src,
154 controller,
155 )
156}
157
158pub fn get_last_turn_cast(
159 game: &GameState,
160 valid: &str,
161 src: CardId,
162 controller: PlayerId,
163) -> Vec<CardId> {
164 filter_valid_cards(
165 game,
166 game.stack.get_spells_cast_last_turn().to_vec(),
167 valid,
168 src,
169 controller,
170 )
171}
172
173pub fn get_this_turn_activated(
174 game: &GameState,
175 valid: &str,
176 src: CardId,
177 controller: PlayerId,
178) -> Vec<CardId> {
179 let activated: Vec<CardId> = game
180 .cards
181 .iter()
182 .filter(|card| card.activated_this_turn())
183 .map(|card| card.id)
184 .collect();
185 filter_valid_cards(game, activated, valid, src, controller)
186}
187
188pub fn get_cast_since_beginning_of_your_last_turn(
189 game: &GameState,
190 valid: &str,
191 src: CardId,
192 controller: PlayerId,
193) -> Vec<CardId> {
194 filter_valid_cards(
195 game,
196 game.player(controller).cards_cast_this_turn.clone(),
197 valid,
198 src,
199 controller,
200 )
201}
202
203pub fn get_radiance(game: &GameState, sa: &SpellAbility) -> CardCollection {
204 let Some(targeted) = find_sa_targeting_card(sa) else {
205 return CardCollection::new();
206 };
207 if !targeted.uses_targeting() || !targeted.ir.radiance {
208 return CardCollection::new();
209 }
210
211 let Some(source) = targeted.source else {
212 return CardCollection::new();
213 };
214 let Some(target) = targeted.target_chosen.target_card else {
215 return CardCollection::new();
216 };
217
218 let valid_tokens: Vec<&str> = targeted
219 .ir
220 .valid_tgts_text
221 .as_deref()
222 .map(|value| value.split(',').map(str::trim).collect())
223 .unwrap_or_default();
224 let valid_selectors: Vec<_> = valid_tokens
225 .iter()
226 .map(|token| cached_compiled_selector(token))
227 .collect();
228
229 let combined = game.card(target).color;
230 let mut out = CardCollection::new();
231 for color in combined.iter() {
232 for &pid in &game.player_order {
233 for &cid in game.cards_in_zone(ZoneType::Battlefield, pid) {
234 if cid == target || out.iter().any(|existing| *existing == cid) {
235 continue;
236 }
237 let card = game.card(cid);
238 if !card.color.has_color(color) {
239 continue;
240 }
241 if valid_selectors.is_empty()
242 || valid_selectors.iter().any(|selector| {
243 valid_filter::matches_valid_card_selector_in_game(
244 selector,
245 card,
246 game.card(source),
247 game,
248 )
249 })
250 {
251 out.push(cid);
252 }
253 }
254 }
255 }
256
257 out
258}
259
260pub fn get_colors_from_cards(game: &GameState, list: &[CardId]) -> ColorSet {
261 let mut mask = 0u8;
262 for &cid in list {
263 mask |= game.card(cid).color.mask();
264 }
265 ColorSet::from_mask(mask)
266}
267
268pub fn get_face_down_characteristic(card: &Card) -> CardCharacteristic {
269 get_face_down_characteristic_with_state(card, CardStateName::FaceDown)
270}
271
272pub fn get_face_down_characteristic_with_state(
273 _card: &Card,
274 state: CardStateName,
275) -> CardCharacteristic {
276 CardCharacteristic {
277 state,
278 name: String::new(),
279 type_line: CardTypeLine::parse("Creature"),
280 base_power: Some(2),
281 base_toughness: Some(2),
282 }
283}
284
285pub fn get_empty_room_characteristic(card: &Card) -> CardCharacteristic {
286 get_empty_room_characteristic_with_state(card, CardStateName::EmptyRoom)
287}
288
289pub fn get_empty_room_characteristic_with_state(
290 _card: &Card,
291 state: CardStateName,
292) -> CardCharacteristic {
293 CardCharacteristic {
294 state,
295 name: String::new(),
296 type_line: CardTypeLine::parse("Enchantment Room"),
297 base_power: None,
298 base_toughness: None,
299 }
300}
301
302pub fn get_reflectable_mana_colors(game: &GameState, sa: &SpellAbility) -> HashSet<String> {
303 get_reflectable_mana_colors_inner(game, sa, sa, HashSet::new(), Vec::new())
304}
305
306fn get_reflectable_mana_colors_inner(
307 game: &GameState,
308 root_sa: &SpellAbility,
309 ab_mana: &SpellAbility,
310 mut colors: HashSet<String>,
311 mut parents: Vec<CardId>,
312) -> HashSet<String> {
313 if ab_mana.api != Some(ApiType::ManaReflected) {
314 return colors;
315 }
316
317 let color_or_type = root_sa.ir.color_or_type.as_deref().unwrap_or("Color");
318 let reflect_property = root_sa.ir.reflect_property.as_deref().unwrap_or("Is");
319 let max_choices = if color_or_type.eq_ignore_ascii_case("Type") {
320 6
321 } else {
322 5
323 };
324
325 let cards = collect_reflectable_cards(game, ab_mana, root_sa, &parents);
326 if root_sa.ir.valid_filter_text.is_some() && cards.is_empty() {
327 return colors;
328 }
329
330 match reflect_property {
331 "Is" => {
332 for cid in cards {
333 for color in game.card(cid).color.iter() {
334 colors.insert(color_long_name(color).to_string());
335 if colors.len() == max_choices {
336 break;
337 }
338 }
339 }
340 }
341 "Produced" => {
342 if let Some(produced_colors) =
343 ab_mana.get_triggering_object(crate::ability::AbilityKey::Produced)
344 {
345 for color in Color::ALL {
346 if produced_colors.contains(color.short_name()) {
347 colors.insert(color_long_name(color).to_string());
348 }
349 }
350 if max_choices == 6
351 && (produced_colors.contains('1') || produced_colors.contains('C'))
352 {
353 colors.insert("Colorless".to_string());
354 }
355 }
356 }
357 "Produce" => {
358 let mut reflect_abilities = VecDeque::new();
359 for cid in cards {
360 let card = game.card(cid);
361 for ab in &card.activated_abilities {
362 let ab = ability_factory::build_spell_ability(
363 game,
364 card.id,
365 &ab.ability_text,
366 card.controller,
367 );
368 if ab.is_spell || ab.is_land_ability {
369 continue;
370 }
371 if colors.len() == max_choices {
372 break;
373 }
374 if !parents.contains(&card.id) {
375 parents.push(card.id);
376 }
377 if ab.api == Some(ApiType::ManaReflected)
378 && ab.ir.reflect_property.as_deref().unwrap_or("") != "Produced"
379 {
380 reflect_abilities.push_back(ab);
381 } else {
382 colors = can_produce(max_choices, Some(&ab), colors);
383 }
384 }
385 for trig in &card.triggers {
386 let Some(mut trig_sa) = trig.ensure_ability(game, card.id, card.controller)
387 else {
388 continue;
389 };
390 if trig_sa.is_spell || trig_sa.is_land_ability {
391 continue;
392 }
393 if colors.len() == max_choices {
394 break;
395 }
396 if !parents.contains(&card.id) {
397 parents.push(card.id);
398 }
399 trig_sa.activating_player = card.controller;
400 if trig_sa.api == Some(ApiType::ManaReflected)
401 && trig_sa.ir.reflect_property.as_deref().unwrap_or("") != "Produced"
402 {
403 reflect_abilities.push_back(trig_sa);
404 } else {
405 colors = can_produce(max_choices, Some(&trig_sa), colors);
406 }
407 }
408 }
409
410 while let Some(reflect_sa) = reflect_abilities.pop_front() {
411 if colors.len() == max_choices {
412 break;
413 }
414 colors = get_reflectable_mana_colors_inner(
415 game,
416 root_sa,
417 &reflect_sa,
418 colors,
419 parents.clone(),
420 );
421 }
422 }
423 _ => {}
424 }
425
426 colors
427}
428
429fn collect_reflectable_cards(
430 game: &GameState,
431 ab_mana: &SpellAbility,
432 root_sa: &SpellAbility,
433 parents: &[CardId],
434) -> Vec<CardId> {
435 let Some(host) = ab_mana.source else {
436 return Vec::new();
437 };
438 let Some(valid_card) = root_sa.ir.valid_filter_text.as_deref() else {
439 return Vec::new();
440 };
441
442 let mut cards = if let Some(defined) = valid_card.strip_prefix("Defined.") {
443 ability_utils::get_defined_cards(game, Some(host), defined, Some(ab_mana.activating_player))
444 } else {
445 let valid_selector = root_sa
446 .ir
447 .valid_filter_selector
448 .clone()
449 .unwrap_or_else(|| cached_compiled_selector(valid_card));
450 game.player_order
451 .iter()
452 .flat_map(|&pid| {
453 game.cards_in_zone(ZoneType::Battlefield, pid)
454 .iter()
455 .copied()
456 })
457 .filter(|&cid| {
458 let card = game.card(cid);
459 valid_filter::matches_valid_card_selector_in_game(
460 &valid_selector,
461 card,
462 game.card(host),
463 game,
464 )
465 })
466 .collect()
467 };
468
469 cards.retain(|cid| !parents.contains(cid));
470 cards
471}
472
473pub fn can_produce(
475 max_choices: usize,
476 sa: Option<&SpellAbility>,
477 mut colors: HashSet<String>,
478) -> HashSet<String> {
479 let Some(sa) = sa else {
480 return colors;
481 };
482
483 for (short, long) in [
484 ("W", "white"),
485 ("U", "blue"),
486 ("B", "black"),
487 ("R", "red"),
488 ("G", "green"),
489 ] {
490 if sa.can_produce(short) {
491 colors.insert(long.to_string());
492 }
493 }
494 if max_choices == 6 && sa.can_produce("C") {
495 colors.insert("colorless".to_string());
496 }
497
498 colors
499}
500
501pub fn card_can_produce_color_mana(
502 game: &GameState,
503 card_id: CardId,
504 colors: &HashSet<String>,
505) -> bool {
506 let card = game.card(card_id);
507 for ab in &card.activated_abilities {
508 if !ab.is_mana_ability {
509 continue;
510 }
511 let sa = crate::ability::ability_factory::build_spell_ability(
512 game,
513 card_id,
514 &ab.ability_text,
515 card.controller,
516 );
517 for color in colors {
518 if sa.api == Some(ApiType::ManaReflected) {
519 if get_reflectable_mana_colors(game, &sa).contains(color) {
520 return true;
521 }
522 } else if sa.can_produce(color_short_name(color)) {
523 return true;
524 }
525 }
526 }
527 false
528}
529
530pub fn card_can_produce_same_mana_type_with(
531 game: &GameState,
532 card_id: CardId,
533 other_id: CardId,
534) -> bool {
535 let card = game.card(card_id);
536 if !card.activated_abilities.iter().any(|ab| ab.is_mana_ability) {
537 return false;
538 }
539
540 let other = game.card(other_id);
541 let mut colors = HashSet::new();
542 for ab in &other.activated_abilities {
543 if !ab.is_mana_ability {
544 continue;
545 }
546 let sa = crate::ability::ability_factory::build_spell_ability(
547 game,
548 other_id,
549 &ab.ability_text,
550 other.controller,
551 );
552 if sa.api == Some(ApiType::ManaReflected) {
553 colors.extend(get_reflectable_mana_colors(game, &sa));
554 } else {
555 colors = can_produce(6, Some(&sa), colors);
556 }
557 }
558
559 card_can_produce_color_mana(game, card_id, &colors)
560}
561
562pub fn get_valid_cards_to_target(game: &GameState, ability: &SpellAbility) -> Vec<CardId> {
563 let Some(tgt) = ability.target_restrictions.as_ref() else {
564 return Vec::new();
565 };
566 let player = ability
567 .targeting_player
568 .unwrap_or(ability.activating_player);
569
570 let zones = if tgt.tgt_zone.is_empty() {
571 vec![ZoneType::Battlefield]
572 } else {
573 tgt.tgt_zone.clone()
574 };
575
576 let mut candidates = Vec::new();
577 for zone in zones {
578 match zone {
579 ZoneType::Battlefield => {
580 candidates.extend(match &tgt.target_kind {
581 target_restrictions::TargetKind::Creature(filter) => {
582 target_restrictions::get_all_candidates_creature_filtered_for_restrictions(
583 game,
584 tgt,
585 filter.as_deref(),
586 player,
587 ability.source,
588 )
589 }
590 target_restrictions::TargetKind::Permanent(filter) => {
591 target_restrictions::get_all_battlefield_permanents_filtered_for_restrictions(
592 game,
593 tgt,
594 filter.as_deref(),
595 player,
596 ability.source,
597 )
598 }
599 target_restrictions::TargetKind::Any => {
600 target_restrictions::get_all_candidates_any_filtered_for_restrictions(
601 game,
602 tgt,
603 player,
604 ability.source,
605 )
606 }
607 _ => Vec::new(),
608 });
609 }
610 _ => {
611 if let target_restrictions::TargetKind::CardInZone {
612 filter,
613 zone: target_zone,
614 } = &tgt.target_kind
615 {
616 if *target_zone == zone {
617 candidates.extend(target_restrictions::get_valid_cards_in_zone_for_sa(
618 game,
619 zone,
620 player,
621 filter.as_deref(),
622 ability,
623 ));
624 }
625 }
626 }
627 }
628 }
629
630 for valid in &tgt.valid_tgts {
631 candidates = target_restrictions::apply_other_source_filter(
632 candidates,
633 Some(valid.as_str()),
634 ability.source,
635 );
636 }
637
638 candidates.retain(|&cid| {
639 !ability.target_chosen.contains(cid)
640 && target_restrictions::can_be_targeted_by_sa(
641 game,
642 cid,
643 ability
644 .targeting_player
645 .unwrap_or(ability.activating_player),
646 ability,
647 )
648 });
649
650 candidates.sort_unstable_by_key(|cid| cid.0);
651 candidates.dedup();
652 candidates
653}
654
655fn filter_valid_cards(
656 game: &GameState,
657 cards: Vec<CardId>,
658 valid: &str,
659 src: CardId,
660 controller: PlayerId,
661) -> Vec<CardId> {
662 if valid.is_empty() {
663 return cards;
664 }
665 let _ = controller;
666 CardLists::filter_as_list_with_source(game, &cards, valid, src)
667}
668
669fn color_long_name(color: Color) -> &'static str {
670 match color {
671 Color::White => "white",
672 Color::Blue => "blue",
673 Color::Black => "black",
674 Color::Red => "red",
675 Color::Green => "green",
676 }
677}
678
679fn color_short_name(color: &str) -> &'static str {
680 match color {
681 "white" | "White" => "W",
682 "blue" | "Blue" => "U",
683 "black" | "Black" => "B",
684 "red" | "Red" => "R",
685 "green" | "Green" => "G",
686 "colorless" | "Colorless" => "C",
687 _ => "",
688 }
689}
690
691fn find_sa_targeting_card(sa: &SpellAbility) -> Option<&SpellAbility> {
692 let mut current = Some(sa);
693 while let Some(node) = current {
694 if node.uses_targeting() && node.ir.radiance {
695 return Some(node);
696 }
697 current = node.get_sub_ability();
698 }
699 None
700}