1use crate::ability::ability_ir::{DefinedRef, NumericParamIr};
2use crate::card::card_damage_history::TrackedEntity;
3use crate::card::filter_constants as fc;
4use crate::game::GameState;
5use crate::ids::{CardId, PlayerId};
6use crate::parsing::compare::compare_expr;
7use crate::spellability::SpellAbility;
8use forge_card_script::{
9 parse_script_svar_numeric_expression, ScriptSVarNumericExpression, ScriptSVarObjectRef,
10};
11
12fn parse_trigger_int_values(sa: &SpellAbility, key: &str) -> Vec<i32> {
13 crate::ability::ability_key::from_string(key)
14 .and_then(|ability_key| sa.get_triggering_value(ability_key))
15 .map(|raw| {
16 raw.split(',')
17 .filter_map(|part| part.trim().parse::<i32>().ok())
18 .collect::<Vec<_>>()
19 })
20 .unwrap_or_default()
21}
22
23fn paid_sacrificed_card(sa: &SpellAbility) -> Option<CardId> {
24 sa.paid_hash
25 .get(crate::cost::cost_sacrifice::HASH_CARDS)
26 .or_else(|| sa.paid_hash.get(crate::cost::cost_sacrifice::HASH_LKI))
27 .and_then(|ids| ids.first())
28 .and_then(|raw| raw.parse::<u32>().ok())
29 .map(CardId)
30}
31
32fn sacrificed_card_value(game: &GameState, sa: &SpellAbility, svar_expr: &str) -> i32 {
33 let Some(sac_id) = paid_sacrificed_card(sa).or(game.last_sacrificed_card) else {
34 return 0;
35 };
36 let sac_card = game.card(sac_id);
37 if svar_expr.ends_with("Power") {
38 sac_card
39 .lki_power
40 .unwrap_or(sac_card.base_power.unwrap_or(0))
41 } else if svar_expr.ends_with("Toughness") {
42 sac_card
43 .lki_toughness
44 .unwrap_or(sac_card.base_toughness.unwrap_or(0))
45 } else {
46 sac_card.mana_cost.cmc()
47 }
48}
49
50fn sacrificed_card_property_value(game: &GameState, sa: &SpellAbility, property: &str) -> i32 {
51 match property {
52 "CardPower" | "CardToughness" | "CardManaCost" => {
53 sacrificed_card_value(game, sa, &format!("Sacrificed${property}"))
54 }
55 _ => 0,
56 }
57}
58
59fn apply_simple_operator_chain(num: i32, operators: &str) -> i32 {
60 let mut value = num;
61 for op in operators.split('/') {
62 let op = op.trim();
63 if let Some(arg) = op.strip_prefix("Plus.") {
64 value += arg.parse::<i32>().unwrap_or(0);
65 } else if let Some(arg) = op.strip_prefix("Minus.") {
66 value -= arg.parse::<i32>().unwrap_or(0);
67 } else if let Some(arg) = op.strip_prefix("Times.") {
68 value *= arg.parse::<i32>().unwrap_or(1);
69 } else if let Some(arg) = op.strip_prefix("HalfUp") {
70 let _ = arg;
71 value = (value + 1) / 2;
72 } else if let Some(arg) = op.strip_prefix("HalfDown") {
73 let _ = arg;
74 value = ((value as f64) / 2.0).floor() as i32;
75 }
76 }
77 value
78}
79
80fn do_x_math(
81 num: i32,
82 operators: &str,
83 game: &GameState,
84 source_id: CardId,
85 controller: PlayerId,
86 sa: &SpellAbility,
87) -> i32 {
88 if operators.is_empty() {
89 return num;
90 }
91 let parts: Vec<&str> = operators.split('.').collect();
92 let op = parts.first().copied().unwrap_or("");
93 let secondary = parts.get(1).copied().map_or(0, |rhs| {
94 rhs.parse::<i32>()
95 .unwrap_or_else(|_| resolve_svar_expression(rhs, game, source_id, controller, sa))
96 });
97
98 if op.contains("Plus") {
99 num + secondary
100 } else if op.contains("NMinus") {
101 secondary - num
102 } else if op.contains("Minus") {
103 num - secondary
104 } else if op.contains("Twice") {
105 num * 2
106 } else if op.contains("Thrice") {
107 num * 3
108 } else if op.contains("HalfUp") {
109 ((num as f64) / 2.0).ceil() as i32
110 } else if op.contains("HalfDown") {
111 ((num as f64) / 2.0).floor() as i32
112 } else if op.contains("ThirdUp") {
113 ((num as f64) / 3.0).ceil() as i32
114 } else if op.contains("ThirdDown") {
115 ((num as f64) / 3.0).floor() as i32
116 } else if op.contains("Negative") {
117 -num
118 } else if op.contains("Times") {
119 num * secondary
120 } else if op.contains("Pow") {
121 (num as f64).powf(secondary as f64) as i32
122 } else if op.contains("DivideEvenlyUp") {
123 if secondary == 0 {
124 0
125 } else {
126 num / secondary + i32::from(num % secondary != 0)
127 }
128 } else if op.contains("DivideEvenlyDown") {
129 if secondary == 0 {
130 0
131 } else {
132 num / secondary
133 }
134 } else if op.contains("Mod") {
135 num % secondary
136 } else if op.contains("Abs") {
137 num.abs()
138 } else if op.contains("LimitMax") {
139 num.min(secondary)
140 } else if op.contains("LimitMin") {
141 num.max(secondary)
142 } else {
143 num
144 }
145}
146
147fn spell_ability_x_property(spell_ability: &SpellAbility, expr: &str, game: &GameState) -> i32 {
148 let Some(source_id) = spell_ability.source else {
149 return 0;
150 };
151 let source = game.card(source_id);
152 let parts: Vec<&str> = expr.split('/').collect();
153 let value = parts.first().copied().unwrap_or("");
154 let operators = parts.get(1).copied().unwrap_or("");
155
156 let base = match value {
157 "CardPower" => source.power(),
158 "CardToughness" => source.toughness(),
159 _ if value.starts_with("CardCounters.") => {
160 let counter_name = value.strip_prefix("CardCounters.").unwrap_or("");
161 if counter_name.eq_ignore_ascii_case("ALL") {
162 source.counters.values().copied().sum()
163 } else {
164 source.counter_count(&crate::ability::ability_utils::parse_counter_type(
165 counter_name,
166 ))
167 }
168 }
169 _ if value.starts_with("CardManaCost") => {
170 let mut cmc = source.mana_value();
171 if value.contains("LKI") && source.zone != forge_foundation::ZoneType::Stack {
172 cmc += spell_ability.x_mana_cost_paid as i32 * source.mana_cost.count_x() as i32;
173 }
174 cmc
175 }
176 _ => 0,
177 };
178
179 do_x_math(
180 base,
181 operators,
182 game,
183 source_id,
184 spell_ability.activating_player,
185 spell_ability,
186 )
187}
188
189fn card_x_property(
190 card_id: CardId,
191 expr: &str,
192 game: &GameState,
193 source_id: CardId,
194 controller: PlayerId,
195 sa: &SpellAbility,
196) -> i32 {
197 let card = game.card(card_id);
198 let parts: Vec<&str> = expr.split('/').collect();
199 let value = parts.first().copied().unwrap_or("");
200 let operators = parts.get(1).copied().unwrap_or("");
201
202 let base = match value {
203 "CardPower" => card.lki_power.unwrap_or_else(|| card.power()),
204 "CardBasePower" => card.base_power.unwrap_or(0),
205 "CardToughness" => card.lki_toughness.unwrap_or_else(|| card.toughness()),
206 "CardBaseToughness" => card.base_toughness.unwrap_or(0),
207 "CardSumPT" => {
208 card.lki_power.unwrap_or_else(|| card.power())
209 + card.lki_toughness.unwrap_or_else(|| card.toughness())
210 }
211 _ if value.starts_with("CardManaCost") || value == "ManaCost" => {
212 let mut cmc = card.mana_value();
213 if value.contains("LKI") && card.zone != forge_foundation::ZoneType::Stack {
214 cmc += sa.x_mana_cost_paid as i32 * card.mana_cost.count_x() as i32;
215 }
216 cmc
217 }
218 "Amount" | "Count" => 1,
219 _ if value.starts_with("CardCounters.") => {
220 let counter_name = value.strip_prefix("CardCounters.").unwrap_or("");
221 if counter_name.eq_ignore_ascii_case("ALL") {
222 card.counters.values().copied().sum()
223 } else {
224 card.counter_count(&crate::ability::ability_utils::parse_counter_type(
225 counter_name,
226 ))
227 }
228 }
229 _ => 0,
230 };
231
232 do_x_math(base, operators, game, source_id, controller, sa)
233}
234
235fn resolve_spell_ability_expr(expr: &str, game: &GameState, sa: &SpellAbility) -> Option<i32> {
236 let (defined, property) = expr.split_once('$')?;
237 resolve_spell_ability_property(defined, property, game, sa)
238}
239
240fn resolve_spell_ability_property(
241 defined: &str,
242 property: &str,
243 game: &GameState,
244 sa: &SpellAbility,
245) -> Option<i32> {
246 let spells = crate::ability::ability_utils::get_defined_spell_abilities(defined, sa, game);
247 if spells.is_empty() {
248 return None;
249 }
250 Some(
251 spells
252 .iter()
253 .map(|spell| spell_ability_x_property(spell, property, game))
254 .sum(),
255 )
256}
257
258fn resolve_card_list_expr(
259 expr: &str,
260 game: &GameState,
261 source_id: CardId,
262 controller: PlayerId,
263 sa: &SpellAbility,
264) -> Option<i32> {
265 let (defined, property) = expr.split_once('$')?;
266 resolve_card_list_property(defined, property, game, source_id, controller, sa)
267}
268
269fn resolve_card_list_property(
270 defined: &str,
271 property: &str,
272 game: &GameState,
273 source_id: CardId,
274 controller: PlayerId,
275 sa: &SpellAbility,
276) -> Option<i32> {
277 let cards = resolve_defined_cards_for_svar(defined, game, source_id, sa);
278 if cards.is_empty() {
279 return None;
280 }
281 if let Some(rest) = property.strip_prefix("Valid ") {
282 let (valid, operators) = rest.split_once('/').unwrap_or((rest, ""));
283 let num = cards
284 .into_iter()
285 .filter(|&cid| {
286 crate::ability::ability_utils::matches_valid_cards_for_sa(
287 game,
288 sa,
289 game.card(cid),
290 None,
291 valid,
292 )
293 })
294 .count() as i32;
295 return Some(do_x_math(num, operators, game, source_id, controller, sa));
296 }
297 Some(
298 cards
299 .into_iter()
300 .map(|cid| card_x_property(cid, property, game, source_id, controller, sa))
301 .sum(),
302 )
303}
304
305fn resolve_defined_cards_for_svar(
306 defined: &str,
307 game: &GameState,
308 source_id: CardId,
309 sa: &SpellAbility,
310) -> Vec<CardId> {
311 let defined_ref = DefinedRef::parse(defined);
312 match defined_ref {
313 DefinedRef::Targeted
314 | DefinedRef::TargetedCard
315 | DefinedRef::ThisTargetedCard
316 | DefinedRef::ParentTargeted => sa.target_chosen.all_target_cards(),
317 DefinedRef::TriggeredCard | DefinedRef::TriggeredCardLkiCopy => {
318 let cards = sa.get_triggering_cards(crate::ability::AbilityKey::Card);
319 if cards.is_empty() {
320 sa.trigger_source.into_iter().collect()
321 } else {
322 cards
323 }
324 }
325 DefinedRef::ReplacedCard => {
326 let cards = sa.get_triggering_cards(crate::ability::AbilityKey::ReplacedCard);
327 if cards.is_empty() {
328 sa.get_triggering_cards(crate::ability::AbilityKey::Card)
329 } else {
330 cards
331 }
332 }
333 DefinedRef::TriggeredNewCard | DefinedRef::TriggeredNewCardLkiCopy => {
334 let cards = sa.get_triggering_cards(crate::ability::AbilityKey::NewCard);
335 if cards.is_empty() {
336 sa.trigger_source.into_iter().collect()
337 } else {
338 cards
339 }
340 }
341 DefinedRef::TriggeredAttacker => {
342 sa.get_triggering_cards(crate::ability::AbilityKey::Attacker)
343 }
344 DefinedRef::TriggeredAttackers => {
345 sa.get_triggering_cards(crate::ability::AbilityKey::Attackers)
346 }
347 DefinedRef::TriggeredBlocker => {
348 sa.get_triggering_cards(crate::ability::AbilityKey::Blocker)
349 }
350 DefinedRef::TriggeredTarget
351 | DefinedRef::TriggeredTargetLkiCopy
352 | DefinedRef::TriggeredTargets => {
353 let cards = sa.get_triggering_cards(crate::ability::AbilityKey::TargetCard);
354 if cards.is_empty() {
355 sa.get_triggering_cards(crate::ability::AbilityKey::Target)
356 } else {
357 cards
358 }
359 }
360 DefinedRef::Explorer => sa.get_triggering_cards(crate::ability::AbilityKey::Explorer),
361 DefinedRef::Explored => sa.get_triggering_cards(crate::ability::AbilityKey::Explored),
362 DefinedRef::Discarded => sa.discarded_cost_cards.clone(),
363 DefinedRef::Sacrificed => paid_sacrificed_card(sa)
364 .or(game.last_sacrificed_card)
365 .into_iter()
366 .collect(),
367 DefinedRef::Remembered => game.card(source_id).remembered_cards.clone(),
368 DefinedRef::RememberedLki => {
369 let cards = sa
370 .trigger_objects
371 .get(&crate::ability::AbilityKey::RememberedLKI)
372 .map(cards_from_ability_value)
373 .unwrap_or_default();
374 if cards.is_empty() {
375 game.card(source_id).remembered_cards.clone()
376 } else {
377 cards
378 }
379 }
380 DefinedRef::DelayTriggerRememberedLki => sa
381 .trigger_objects
382 .get(&crate::ability::AbilityKey::RememberedLKI)
383 .map(cards_from_ability_value)
384 .unwrap_or_default(),
385 DefinedRef::DelayTriggerRemembered | DefinedRef::TriggerRemembered => sa
386 .trigger_remembered
387 .iter()
388 .flat_map(cards_from_ability_value)
389 .collect(),
390 DefinedRef::Imprinted => game.card(source_id).imprinted_cards.clone(),
391 _ => crate::ability::ability_utils::get_defined_cards(
392 game,
393 Some(source_id),
394 defined_ref.as_legacy_str(),
395 Some(sa.activating_player),
396 ),
397 }
398}
399
400fn cards_from_ability_value(value: &crate::event::AbilityValue) -> Vec<CardId> {
401 match value {
402 crate::event::AbilityValue::Card(cid) => vec![*cid],
403 crate::event::AbilityValue::Cards(cards) => cards.clone(),
404 _ => Vec::new(),
405 }
406}
407
408fn resolve_lowered_svar_expression(
409 expression: &ScriptSVarNumericExpression<'_>,
410 game: &GameState,
411 source_id: CardId,
412 controller: PlayerId,
413 sa: &SpellAbility,
414) -> Option<i32> {
415 match expression {
416 ScriptSVarNumericExpression::Number(value) => {
417 let mut parts = value.split('/');
418 let number = parts.next().unwrap_or("");
419 let operators = parts.next().unwrap_or("");
420 Some(do_x_math(
421 number.trim().parse::<i32>().unwrap_or(0),
422 operators,
423 game,
424 source_id,
425 controller,
426 sa,
427 ))
428 }
429 ScriptSVarNumericExpression::Count(raw) => Some(resolve_count_svar_for_sa(
430 raw, game, source_id, controller, sa,
431 )),
432 ScriptSVarNumericExpression::PlayerCount(raw) => Some(resolve_player_count_svar(
433 raw, game, source_id, controller, sa,
434 )),
435 ScriptSVarNumericExpression::TriggerCount(raw) => Some(resolve_trigger_count_svar(
436 raw, game, source_id, controller, sa,
437 )),
438 ScriptSVarNumericExpression::SVarReference { name, operators } => {
439 let raw = game.card(source_id).get_s_var(name)?;
440 let value = resolve_svar_expression(raw, game, source_id, controller, sa);
441 Some(do_x_math(value, operators, game, source_id, controller, sa))
442 }
443 ScriptSVarNumericExpression::Remembered { property } => {
444 Some(crate::ability::ability_utils::handle_paid(
445 game,
446 &game.card(source_id).remembered_cards,
447 property,
448 source_id,
449 ))
450 }
451 ScriptSVarNumericExpression::RememberedSize { operators } => Some(do_x_math(
452 game.card(source_id).remembered_cards.len() as i32,
453 operators,
454 game,
455 source_id,
456 controller,
457 sa,
458 )),
459 ScriptSVarNumericExpression::DiscardedValid { filter, times } => Some(
460 resolve_discarded_valid_svar(game, source_id, filter, *times),
461 ),
462 ScriptSVarNumericExpression::ObjectProperty { object, property } => match object {
463 ScriptSVarObjectRef::Sacrificed => {
464 Some(sacrificed_card_property_value(game, sa, property))
465 }
466 ScriptSVarObjectRef::TriggeredCard => {
467 crate::lki::resolve_triggered_card_lki_property(game, sa, property).or_else(|| {
468 resolve_card_list_property(
469 "TriggeredCard",
470 property,
471 game,
472 source_id,
473 controller,
474 sa,
475 )
476 })
477 }
478 ScriptSVarObjectRef::CardList(defined) => {
479 resolve_card_list_property(defined, property, game, source_id, controller, sa)
480 }
481 ScriptSVarObjectRef::PlayerList(defined) => {
482 resolve_direct_player_property(defined, property, game, source_id, controller, sa)
483 }
484 ScriptSVarObjectRef::SpellAbility(defined) => {
485 resolve_spell_ability_property(defined, property, game, sa)
486 }
487 ScriptSVarObjectRef::PaidHash(key) => {
488 resolve_paid_hash_property(key, property, game, source_id, sa)
489 }
490 ScriptSVarObjectRef::ReplaceCount => None,
491 ScriptSVarObjectRef::RuntimeValue(_) => None,
492 },
493 }
494}
495
496fn resolve_discarded_valid_svar(
497 game: &GameState,
498 source_id: CardId,
499 filter: &str,
500 times: i32,
501) -> i32 {
502 let remembered = &game.card(source_id).remembered_cards;
503 if remembered.is_empty() {
504 return 0;
505 }
506 for &rem_id in remembered {
507 let rem_card = game.card(rem_id);
508 let matches = !filter.contains("nonLand") || !rem_card.is_land();
509 if matches {
510 return times;
511 }
512 }
513 0
514}
515
516fn resolve_trigger_count_svar(
517 expr: &str,
518 game: &GameState,
519 source_id: CardId,
520 controller: PlayerId,
521 sa: &SpellAbility,
522) -> i32 {
523 let (prefix, rest) = expr.split_once('$').unwrap_or((expr, ""));
524 let mut parts = rest.split('/');
525 let key = parts.next().unwrap_or("");
526 let operators = parts.next().unwrap_or("");
527 let values = parse_trigger_int_values(sa, key.trim());
528 let count = if prefix.ends_with("Max") {
529 values.into_iter().max().unwrap_or(0)
530 } else {
531 values.into_iter().sum()
532 };
533 do_x_math(count, operators, game, source_id, controller, sa)
534}
535
536const MAX_SVAR_RESOLUTION_DEPTH: usize = 50;
537
538thread_local! {
539 static SVAR_RESOLUTION_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
540}
541
542pub(crate) fn resolve_svar_expression(
543 expr: &str,
544 game: &GameState,
545 source_id: CardId,
546 controller: PlayerId,
547 sa: &SpellAbility,
548) -> i32 {
549 let depth = SVAR_RESOLUTION_DEPTH.with(|d| d.get());
550 if depth >= MAX_SVAR_RESOLUTION_DEPTH {
551 eprintln!("SVar resolution exceeded depth limit, returning 0 for: {expr}");
552 return 0;
553 }
554 SVAR_RESOLUTION_DEPTH.with(|d| d.set(depth + 1));
555 let value = resolve_svar_expression_inner(expr, game, source_id, controller, sa);
556 SVAR_RESOLUTION_DEPTH.with(|d| d.set(depth));
557 value
558}
559
560fn resolve_svar_expression_inner(
561 expr: &str,
562 game: &GameState,
563 source_id: CardId,
564 controller: PlayerId,
565 sa: &SpellAbility,
566) -> i32 {
567 let expr = expr.trim();
568 if let Ok(n) = expr.parse::<i32>() {
569 return n;
570 }
571 if let Some(expression) = parse_script_svar_numeric_expression(expr) {
572 if let Some(value) =
573 resolve_lowered_svar_expression(&expression, game, source_id, controller, sa)
574 {
575 return value;
576 }
577 }
578 if expr.starts_with("TriggerCount$") || expr.starts_with("TriggerCountMax$") {
579 return resolve_trigger_count_svar(expr, game, source_id, controller, sa);
580 }
581 if expr.starts_with("Count$") {
582 return resolve_count_svar_for_sa(expr, game, source_id, controller, sa);
583 }
584 if expr.starts_with("PlayerCount") {
585 return resolve_player_count_svar(expr, game, source_id, controller, sa);
586 }
587 if let Some(property) = expr.strip_prefix("Remembered$") {
588 return crate::ability::ability_utils::handle_paid(
589 game,
590 &game.card(source_id).remembered_cards,
591 property,
592 source_id,
593 );
594 }
595 if let Some(rest) = expr.strip_prefix("RememberedSize") {
596 return do_x_math(
597 game.card(source_id).remembered_cards.len() as i32,
598 rest.strip_prefix('/').unwrap_or(""),
599 game,
600 source_id,
601 controller,
602 sa,
603 );
604 }
605 if let Some(value) = resolve_paid_hash_expr(expr, game, source_id, sa) {
606 return value;
607 }
608 if let Some(value) = resolve_spell_ability_expr(expr, game, sa) {
609 return value;
610 }
611 if let Some(value) = resolve_card_list_expr(expr, game, source_id, controller, sa) {
612 return value;
613 }
614 if let Some(value) = crate::lki::resolve_triggered_card_lki_svar(game, sa, expr) {
615 return value;
616 }
617 if let Some(value) = resolve_direct_player_expr(expr, game, source_id, controller, sa) {
618 return value;
619 }
620 if let Some(svar_expr) = game.card(source_id).get_s_var(expr) {
621 return resolve_svar_expression(svar_expr, game, source_id, controller, sa);
622 }
623 0
624}
625
626fn player_x_property(
627 player: PlayerId,
628 expr: &str,
629 game: &GameState,
630 source_id: CardId,
631 controller: PlayerId,
632 sa: &SpellAbility,
633) -> i32 {
634 let parts: Vec<&str> = expr.split('/').collect();
635 let value = parts.first().copied().unwrap_or("");
636 let operators = parts.get(1).copied().unwrap_or("");
637
638 let base = match value {
639 _ if value.starts_with("Valid") => {
640 let (zones, restrictions) = if let Some(rest) = value.strip_prefix("Valid ") {
641 (vec![forge_foundation::ZoneType::Battlefield], rest)
642 } else {
643 let mut parts = value.splitn(2, ' ');
644 let zone_part = parts
645 .next()
646 .unwrap_or("")
647 .strip_prefix("Valid")
648 .unwrap_or("");
649 let restrictions = parts.next().unwrap_or("");
650 let zones: Vec<_> = if zone_part.is_empty() {
651 vec![forge_foundation::ZoneType::Battlefield]
652 } else {
653 zone_part
654 .split(',')
655 .filter_map(crate::ability::ability_utils::parse_zone_type)
656 .collect()
657 };
658 (zones, restrictions)
659 };
660 let selector = crate::parsing::cached_compiled_selector(restrictions);
661 let source = game.card(source_id);
662 let context = crate::card::valid_filter::MatchContext::from_source(source)
668 .with_game(game)
669 .with_source_controller(player);
670 game.cards
671 .iter()
672 .filter(|card| {
673 zones.contains(&card.zone)
674 && crate::card::valid_filter::matches_valid_card_selector_with_context(
675 &selector, card, context,
676 )
677 })
678 .count() as i32
679 }
680 "CardsInHand" => game
681 .cards_in_zone(forge_foundation::ZoneType::Hand, player)
682 .len() as i32,
683 "CardsInLibrary" => game
684 .cards_in_zone(forge_foundation::ZoneType::Library, player)
685 .len() as i32,
686 "CardsInGraveyard" => game
687 .cards_in_zone(forge_foundation::ZoneType::Graveyard, player)
688 .len() as i32,
689 "CardsInPlay" => game
690 .cards_in_zone(forge_foundation::ZoneType::Battlefield, player)
691 .len() as i32,
692 "CreaturesInPlay" => game
693 .cards_in_zone(forge_foundation::ZoneType::Battlefield, player)
694 .iter()
695 .filter(|&&cid| game.card(cid).is_creature())
696 .count() as i32,
697 "StartingLife" => game.player(player).starting_life,
698 "LifeTotal" => game.player(player).life,
699 "LifeLostThisTurn" => game.player(player).life_lost_this_turn,
700 "LifeLostLastTurn" => game.player(player).life_lost_last_turn,
701 "LifeGainedThisTurn" => game.player(player).life_gained_this_turn,
702 "LifeGainedByTeamThisTurn" => game.player(player).life_gained_by_team_this_turn,
703 "LifeStartedThisTurnWith" => game.player(player).life_started_this_turn_with,
704 "Speed" => game.player(player).speed,
705 "TopOfLibraryCMC" => game
706 .cards_in_zone(forge_foundation::ZoneType::Library, player)
707 .last()
708 .map(|&cid| game.card(cid).mana_value())
709 .unwrap_or(0),
710 "LandsPlayed" => game.player(player).lands_played_this_turn,
711 "SpellsCastThisTurn" => game.player(player).spells_cast_this_turn,
712 "CardsDrawn" => game.player(player).drawn_this_turn,
713 "CardsDiscardedThisTurn" => game.player(player).discarded_this_turn,
714 "ExploredThisTurn" => game.player(player).explored_this_turn,
715 "AttackersDeclared" => game
716 .cards
717 .iter()
718 .filter(|card| {
719 card.controller == player && card.attacked_this_turn && card.is_creature()
720 })
721 .count() as i32,
722 "DamageToOppsThisTurn" => game.player(player).opponents_assigned_damage_this_turn,
723 "NonCombatDamageDealtThisTurn" => {
724 game.player(player).assigned_damage_this_turn
725 - game.player(player).assigned_combat_damage_this_turn
726 }
727 "PoisonCounters" => game.player(player).poison_counters,
728 "EnergyCounters" => game.player(player).energy_counters,
729 "ManaExpendedThisTurn" => game.player(player).mana_expended_this_turn,
730 "RingTemptedYou" => game.player(player).ring_level,
731 "OpponentsAttackedThisTurn" => {
732 let mut attacked = Vec::new();
733 for card in &game.cards {
734 if card.controller != player {
735 continue;
736 }
737 for entity in &card.damage_history.attacked_this_turn {
738 if let TrackedEntity::Player(pid) = entity {
739 if !attacked.contains(pid) {
740 attacked.push(*pid);
741 }
742 }
743 }
744 }
745 attacked.len() as i32
746 }
747 "OpponentsAttackedThisCombat" => {
748 game.player(player).attacked_players_this_combat.len() as i32
749 }
750 "BeenDealtCombatDamageSinceLastTurn" => {
751 i32::from(game.player(player).been_dealt_combat_damage_since_last_turn)
752 }
753 "AttractionsVisitedThisTurn" => game.player(player).attractions_visited_this_turn,
754 _ if value.starts_with("Counters.") => {
755 let counter_name = value.strip_prefix("Counters.").unwrap_or("");
756 if counter_name.eq_ignore_ascii_case("ALL") {
757 game.player(player).poison_counters
758 + game.player(player).energy_counters
759 + game.player(player).radiation_counters
760 } else if counter_name.eq_ignore_ascii_case("POISON") {
761 game.player(player).poison_counters
762 } else if counter_name.eq_ignore_ascii_case("ENERGY") {
763 game.player(player).energy_counters
764 } else if counter_name.eq_ignore_ascii_case("RADIATION") {
765 game.player(player).radiation_counters
766 } else {
767 0
768 }
769 }
770 _ if value.starts_with("HasProperty") => i32::from(crate::player::player_has_property(
771 player,
772 value.strip_prefix("HasProperty").unwrap_or(""),
773 game,
774 source_id,
775 controller,
776 sa,
777 )),
778 _ => 0,
779 };
780
781 do_x_math(base, operators, game, source_id, controller, sa)
782}
783
784pub fn player_condition_matches(
785 player: PlayerId,
786 property: &str,
787 game: &GameState,
788 source_id: CardId,
789 controller: PlayerId,
790 sa: &SpellAbility,
791) -> bool {
792 let Some(rest) = property.strip_prefix("Condition") else {
793 return false;
794 };
795 let Some((lhs, prop_expr)) = rest.split_once(' ') else {
796 return false;
797 };
798 let (cmp, rhs_expr) = if lhs.is_empty() {
799 ("GE", "1")
800 } else if lhs.len() >= 2 {
801 (&lhs[..2], &lhs[2..])
802 } else {
803 ("GE", "1")
804 };
805 let rhs = resolve_svar_expression(rhs_expr, game, source_id, controller, sa);
806 compare_expr(
807 player_x_property(player, prop_expr, game, source_id, controller, sa),
808 &format!("{cmp}{rhs}"),
809 )
810}
811
812fn resolve_direct_player_expr(
813 expr: &str,
814 game: &GameState,
815 source_id: CardId,
816 controller: PlayerId,
817 sa: &SpellAbility,
818) -> Option<i32> {
819 let (defined, property) = expr.split_once('$')?;
820 resolve_direct_player_property(defined, property, game, source_id, controller, sa)
821}
822
823fn resolve_direct_player_property(
824 defined: &str,
825 property: &str,
826 game: &GameState,
827 source_id: CardId,
828 controller: PlayerId,
829 sa: &SpellAbility,
830) -> Option<i32> {
831 let players = crate::ability::ability_utils::resolve_defined_players_with_sa(
832 defined, sa, controller, game,
833 );
834 if players.is_empty() {
835 return None;
836 }
837 Some(
838 players
839 .into_iter()
840 .map(|pid| player_x_property(pid, property, game, source_id, controller, sa))
841 .sum(),
842 )
843}
844
845fn resolve_player_count_svar(
846 expr: &str,
847 game: &GameState,
848 source_id: CardId,
849 controller: PlayerId,
850 sa: &SpellAbility,
851) -> i32 {
852 let Some((group, property_expr)) = expr.split_once('$') else {
853 return 0;
854 };
855 let kind = group.strip_prefix("PlayerCount").unwrap_or(group);
856 let mut property_parts = property_expr.splitn(2, '/');
857 let property = property_parts.next().unwrap_or("");
858 let operators = property_parts.next().unwrap_or("");
859 let players: Vec<PlayerId> = if kind.is_empty() || kind == "Players" {
860 game.alive_players()
861 } else if kind == "Opponents" {
862 game.alive_players()
863 .into_iter()
864 .filter(|&pid| crate::player::player_predicates::is_opponent_of(game, controller, pid))
865 .collect()
866 } else if kind == "Remembered" {
867 game.card(source_id).remembered_players.clone()
868 } else if kind.starts_with("PropertyYou") {
869 vec![controller]
870 } else if let Some(property) = kind.strip_prefix("Property") {
871 game.alive_players()
872 .into_iter()
873 .filter(|&pid| {
874 crate::player::player_has_property(pid, property, game, source_id, controller, sa)
875 })
876 .collect()
877 } else if let Some(defined) = kind.strip_prefix("Defined") {
878 crate::ability::ability_utils::resolve_defined_players_with_sa(
879 defined, sa, controller, game,
880 )
881 } else {
882 Vec::new()
883 };
884
885 if players.is_empty() {
886 return 0;
887 }
888
889 if property.eq_ignore_ascii_case("Amount") {
890 return do_x_math(
891 players.len() as i32,
892 operators,
893 game,
894 source_id,
895 controller,
896 sa,
897 );
898 }
899 if let Some(rest) = property.strip_prefix("Highest") {
900 return do_x_math(
901 players
902 .iter()
903 .map(|&pid| player_x_property(pid, rest, game, source_id, controller, sa))
904 .max()
905 .unwrap_or(0),
906 operators,
907 game,
908 source_id,
909 controller,
910 sa,
911 );
912 }
913 if let Some(rest) = property.strip_prefix("Lowest") {
914 return do_x_math(
915 players
916 .iter()
917 .map(|&pid| player_x_property(pid, rest, game, source_id, controller, sa))
918 .min()
919 .unwrap_or(0),
920 operators,
921 game,
922 source_id,
923 controller,
924 sa,
925 );
926 }
927 if property.eq_ignore_ascii_case("TiedForHighestLife") {
928 let max_life = players
929 .iter()
930 .map(|&pid| game.player(pid).life)
931 .max()
932 .unwrap_or(i32::MIN);
933 return do_x_math(
934 players
935 .iter()
936 .filter(|&&pid| game.player(pid).life == max_life)
937 .count() as i32,
938 operators,
939 game,
940 source_id,
941 controller,
942 sa,
943 );
944 }
945 if property.eq_ignore_ascii_case("TiedForLowestLife") {
946 let min_life = players
947 .iter()
948 .map(|&pid| game.player(pid).life)
949 .min()
950 .unwrap_or(i32::MAX);
951 return do_x_math(
952 players
953 .iter()
954 .filter(|&&pid| game.player(pid).life == min_life)
955 .count() as i32,
956 operators,
957 game,
958 source_id,
959 controller,
960 sa,
961 );
962 }
963 if let Some(raw_property) = property.strip_prefix("HasProperty") {
964 return do_x_math(
965 players
966 .into_iter()
967 .filter(|&pid| {
968 crate::player::player_has_property(
969 pid,
970 raw_property,
971 game,
972 source_id,
973 controller,
974 sa,
975 )
976 })
977 .count() as i32,
978 operators,
979 game,
980 source_id,
981 controller,
982 sa,
983 );
984 }
985 if let Some(rest) = property.strip_prefix("Condition") {
986 if let Some((lhs, prop_expr)) = rest.split_once(' ') {
987 let (cmp, rhs_expr) = if lhs.is_empty() {
988 ("GE", "1")
989 } else if lhs.len() >= 2 {
990 (&lhs[..2], &lhs[2..])
991 } else {
992 ("GE", "1")
993 };
994 let rhs = resolve_svar_expression(rhs_expr, game, source_id, controller, sa);
995 return do_x_math(
996 players
997 .into_iter()
998 .filter(|&pid| {
999 compare_expr(
1000 player_x_property(pid, prop_expr, game, source_id, controller, sa),
1001 &format!("{cmp}{rhs}"),
1002 )
1003 })
1004 .count() as i32,
1005 operators,
1006 game,
1007 source_id,
1008 controller,
1009 sa,
1010 );
1011 }
1012 }
1013
1014 do_x_math(
1015 players
1016 .into_iter()
1017 .map(|pid| player_x_property(pid, property, game, source_id, controller, sa))
1018 .sum(),
1019 operators,
1020 game,
1021 source_id,
1022 controller,
1023 sa,
1024 )
1025}
1026
1027pub fn resolve_numeric_svar(
1041 game: &GameState,
1042 sa: &SpellAbility,
1043 param_name: &str,
1044 default: i32,
1045) -> i32 {
1046 let Some(value) = sa.ir.semantic_numeric_params.get(param_name) else {
1047 return default;
1048 };
1049 resolve_semantic_numeric_value(game, sa, value, default)
1050}
1051
1052fn resolve_semantic_numeric_value(
1053 game: &GameState,
1054 sa: &SpellAbility,
1055 value: &NumericParamIr,
1056 default: i32,
1057) -> i32 {
1058 match value {
1059 NumericParamIr::Integer(value) => *value,
1060 NumericParamIr::Amount(amount) => amount.resolve_for_spell_ability(game, sa, default),
1061 NumericParamIr::SVarReference(names) => match names.as_slice() {
1062 [name] => resolve_numeric_value(game, sa, name, default),
1063 [] => default,
1064 _ => names
1065 .iter()
1066 .map(|name| resolve_numeric_value(game, sa, name, default))
1067 .sum(),
1068 },
1069 NumericParamIr::Raw(raw) => resolve_numeric_value(game, sa, raw, default),
1070 }
1071}
1072
1073pub fn resolve_numeric_value(
1076 game: &GameState,
1077 sa: &SpellAbility,
1078 raw_val: &str,
1079 default: i32,
1080) -> i32 {
1081 let val_str = raw_val.trim();
1082 if val_str.is_empty() {
1083 return default;
1084 }
1085
1086 if let Ok(n) = val_str.parse::<i32>() {
1088 return n;
1089 }
1090 if let Some(stripped) = val_str.strip_prefix('+') {
1092 if let Ok(n) = stripped.parse::<i32>() {
1093 return n;
1094 }
1095 }
1096
1097 let (sign, val_str) = if let Some(stripped) = val_str.strip_prefix('-') {
1099 (-1, stripped.trim())
1100 } else if let Some(stripped) = val_str.strip_prefix('+') {
1101 (1, stripped.trim())
1102 } else {
1103 (1, val_str)
1104 };
1105
1106 if let Some(source_id) = sa.source {
1107 if let Some(expression) = parse_script_svar_numeric_expression(val_str) {
1108 if let Some(value) = resolve_lowered_svar_expression(
1109 &expression,
1110 game,
1111 source_id,
1112 sa.activating_player,
1113 sa,
1114 ) {
1115 return sign * value;
1116 }
1117 }
1118 if let Some(value) =
1119 resolve_card_list_expr(val_str, game, source_id, sa.activating_player, sa)
1120 {
1121 return sign * value;
1122 }
1123 }
1124
1125 if val_str == "X" {
1127 if let Some(source_id) = sa.source {
1129 if let Some(svar_expr) = game.card(source_id).get_s_var("X") {
1130 if svar_expr.starts_with("Count$") {
1131 return sign
1132 * resolve_count_svar_for_sa(
1133 svar_expr,
1134 game,
1135 source_id,
1136 sa.activating_player,
1137 sa,
1138 );
1139 }
1140 if svar_expr.starts_with("PlayerCount") {
1141 return sign
1142 * resolve_player_count_svar(
1143 svar_expr,
1144 game,
1145 source_id,
1146 sa.activating_player,
1147 sa,
1148 );
1149 }
1150 if let Some(value) = resolve_paid_hash_expr(svar_expr, game, source_id, sa) {
1151 return sign * value;
1152 }
1153 if svar_expr.starts_with("TriggerCount$")
1154 || svar_expr.starts_with("TriggerCountMax$")
1155 {
1156 return sign
1157 * resolve_trigger_count_svar(
1158 svar_expr,
1159 game,
1160 source_id,
1161 sa.activating_player,
1162 sa,
1163 );
1164 }
1165 if let Some(expression) = parse_script_svar_numeric_expression(svar_expr) {
1166 if let Some(value) = resolve_lowered_svar_expression(
1167 &expression,
1168 game,
1169 source_id,
1170 sa.activating_player,
1171 sa,
1172 ) {
1173 return sign * value;
1174 }
1175 }
1176 if let Some(value) = resolve_spell_ability_expr(svar_expr, game, sa) {
1177 return sign * value;
1178 }
1179 if let Some(value) =
1180 resolve_card_list_expr(svar_expr, game, source_id, sa.activating_player, sa)
1181 {
1182 return sign * value;
1183 }
1184 if let Some(value) =
1187 crate::lki::resolve_triggered_card_lki_svar(game, sa, svar_expr)
1188 {
1189 return sign * value;
1190 }
1191 if let Some(value) =
1192 resolve_direct_player_expr(svar_expr, game, source_id, sa.activating_player, sa)
1193 {
1194 return sign * value;
1195 }
1196 return sign * evaluate_svar(svar_expr, sa);
1197 }
1198 }
1199 return sign * sa.x_mana_cost_paid as i32;
1201 }
1202
1203 if let Some(source_id) = sa.source {
1205 if let Some(svar_expr) = game.card(source_id).get_s_var(val_str.trim()) {
1206 if svar_expr.starts_with("Count$") {
1208 return sign
1209 * resolve_count_svar_for_sa(
1210 svar_expr,
1211 game,
1212 source_id,
1213 sa.activating_player,
1214 sa,
1215 );
1216 }
1217 if svar_expr.starts_with("PlayerCount") {
1218 return sign
1219 * resolve_player_count_svar(
1220 svar_expr,
1221 game,
1222 source_id,
1223 sa.activating_player,
1224 sa,
1225 );
1226 }
1227 if let Some(value) = resolve_paid_hash_expr(svar_expr, game, source_id, sa) {
1228 return sign * value;
1229 }
1230 if let Some(expression) = parse_script_svar_numeric_expression(svar_expr) {
1231 if let Some(value) = resolve_lowered_svar_expression(
1232 &expression,
1233 game,
1234 source_id,
1235 sa.activating_player,
1236 sa,
1237 ) {
1238 return sign * value;
1239 }
1240 }
1241 if let Some(value) = resolve_spell_ability_expr(svar_expr, game, sa) {
1242 return sign * value;
1243 }
1244 if let Some(value) =
1245 resolve_card_list_expr(svar_expr, game, source_id, sa.activating_player, sa)
1246 {
1247 return sign * value;
1248 }
1249 if let Some(value) = crate::lki::resolve_triggered_card_lki_svar(game, sa, svar_expr) {
1252 return sign * value;
1253 }
1254 let eval = evaluate_svar(svar_expr, sa);
1258 if eval != 0 || svar_expr.starts_with("Number$") || svar_expr.starts_with("Count$") {
1259 return sign * eval;
1260 }
1261 if let Some(value) =
1262 resolve_direct_player_expr(svar_expr, game, source_id, sa.activating_player, sa)
1263 {
1264 return sign * value;
1265 }
1266 return sign * eval;
1267 }
1268 }
1269
1270 default
1271}
1272
1273fn resolve_paid_hash_expr(
1274 expr: &str,
1275 game: &GameState,
1276 source_id: CardId,
1277 sa: &SpellAbility,
1278) -> Option<i32> {
1279 let (paid_key, property) = expr.split_once('$')?;
1280 resolve_paid_hash_property(paid_key, property, game, source_id, sa)
1281}
1282
1283fn resolve_paid_hash_property(
1284 paid_key: &str,
1285 property: &str,
1286 game: &GameState,
1287 source_id: CardId,
1288 sa: &SpellAbility,
1289) -> Option<i32> {
1290 let paid_values = sa.paid_hash.get(paid_key)?;
1291 let paid_cards: Vec<CardId> = paid_values
1292 .iter()
1293 .filter_map(|value| {
1294 let raw = value.strip_prefix("Card#").unwrap_or(value);
1295 raw.parse::<u32>().ok().map(CardId)
1296 })
1297 .filter(|cid| cid.index() < game.cards.len())
1298 .collect();
1299
1300 if property.starts_with("TapPowerValue") {
1301 return Some(
1302 paid_cards
1303 .iter()
1304 .map(|&cid| crate::cost::cost_tap_type::tap_power_value(game, cid, Some(sa)))
1305 .sum(),
1306 );
1307 }
1308
1309 Some(crate::ability::ability_utils::handle_paid(
1310 game,
1311 &paid_cards,
1312 property,
1313 source_id,
1314 ))
1315}
1316
1317pub fn evaluate_svar(expr: &str, sa: &SpellAbility) -> i32 {
1321 if let Some(rest) = expr
1323 .strip_prefix("Count$xPaid")
1324 .or_else(|| expr.strip_prefix("Count$XPaid"))
1325 {
1326 let operators = rest.strip_prefix('/').unwrap_or(rest);
1327 return apply_simple_operator_chain(sa.x_mana_cost_paid as i32, operators);
1328 }
1329 if expr == "Count$Converge" || expr == "Count$Sunburst" {
1331 return 0; }
1333 if expr == "Count$TriggerRememberAmount" {
1334 return sa.trigger_remembered_amount;
1335 }
1336 if let Some(rest) = expr.strip_prefix("TriggerCount$") {
1337 let (key, operators) = rest.split_once('/').unwrap_or((rest, ""));
1338 let values = parse_trigger_int_values(sa, key.trim());
1339 let count = values.into_iter().sum::<i32>();
1340 return apply_simple_operator_chain(count, operators);
1341 }
1342 if let Some(rest) = expr.strip_prefix("TriggerCountMax$") {
1343 let (key, operators) = rest.split_once('/').unwrap_or((rest, ""));
1344 let count = parse_trigger_int_values(sa, key.trim())
1345 .into_iter()
1346 .max()
1347 .unwrap_or(0);
1348 return apply_simple_operator_chain(count, operators);
1349 }
1350 if expr == "TriggerCount$Result" {
1351 return trigger_result_values(sa).into_iter().sum();
1352 }
1353 if expr == "TriggerCountMax$Result" {
1354 return trigger_result_values(sa).into_iter().max().unwrap_or(0);
1355 }
1356 if expr == "TriggerCount$Amount" {
1359 return sa.trigger_remembered_amount.max(1);
1360 }
1361 if expr == "Count$KickedCount" {
1363 return sa.kick_count as i32;
1364 }
1365 if let Some(rest) = expr.strip_prefix("Count$Kicked.") {
1367 let parts: Vec<&str> = rest.splitn(2, '.').collect();
1368 if parts.len() == 2 {
1369 let kicked_val = parts[0].parse::<i32>().unwrap_or(0);
1370 let normal_val = parts[1].parse::<i32>().unwrap_or(0);
1371 return if sa.kicked { kicked_val } else { normal_val };
1372 }
1373 }
1374 if let Some(rest) = expr.strip_prefix("Number$") {
1376 return rest.trim().parse::<i32>().unwrap_or(0);
1377 }
1378 expr.parse::<i32>().unwrap_or(0)
1380}
1381
1382fn trigger_result_values(sa: &SpellAbility) -> Vec<i32> {
1383 sa.trigger_objects
1384 .get(&crate::ability::AbilityKey::Result)
1385 .map(|raw| {
1386 raw.split(',')
1387 .filter_map(|part| part.trim().parse::<i32>().ok())
1388 .collect::<Vec<_>>()
1389 })
1390 .unwrap_or_default()
1391}
1392
1393pub fn resolve_count_svar(
1397 expr: &str,
1398 game: &GameState,
1399 source_id: CardId,
1400 controller: PlayerId,
1401) -> i32 {
1402 resolve_count_svar_for_sa(
1403 expr,
1404 game,
1405 source_id,
1406 controller,
1407 &crate::spellability::SpellAbility::new_empty(Some(source_id), controller),
1408 )
1409}
1410
1411pub fn resolve_cost_amount_svar(
1418 game: &GameState,
1419 source: &crate::card::Card,
1420 name: &str,
1421 caster: PlayerId,
1422) -> i32 {
1423 if let Ok(n) = name.parse::<i32>() {
1424 return n;
1425 }
1426 let Some(expr) = source.get_s_var(name) else {
1427 return 0;
1428 };
1429 evaluate_cost_amount_count_expr(game, source, expr, caster)
1430}
1431
1432fn evaluate_cost_amount_count_expr(
1433 game: &GameState,
1434 source: &crate::card::Card,
1435 expr: &str,
1436 caster: PlayerId,
1437) -> i32 {
1438 use crate::card::Card;
1439 use forge_foundation::ZoneType;
1440 if expr == "Count$xPaid" || expr == "Count$XPaid" {
1441 return source
1442 .svars
1443 .get("XPaid")
1444 .and_then(|s| s.parse::<i32>().ok())
1445 .unwrap_or(0);
1446 }
1447 if let Some(counter_name) = expr.strip_prefix("Count$CardCounters.") {
1448 let counter_type = crate::ability::ability_utils::parse_counter_type(counter_name);
1449 return source.counter_count(&counter_type);
1450 }
1451 if let Some(rest) = expr.strip_prefix("Count$ThisTurnCast_") {
1452 if rest.contains("YouCtrl") || rest.contains("YouOwn") {
1453 return game.player(source.controller).spells_cast_this_turn;
1454 }
1455 return game.player(caster).spells_cast_this_turn;
1456 }
1457 if expr == "Count$YourLifeTotal" {
1458 return game.player(source.controller).life;
1459 }
1460 if let Some(rest) = expr.strip_prefix("Count$Valid ") {
1461 let (filter, aggregator) = rest.split_once('$').unwrap_or((rest, ""));
1462 let selector = crate::parsing::cached_compiled_selector(filter);
1463 let matches: Vec<&Card> = game
1464 .cards
1465 .iter()
1466 .filter(|c| c.zone == ZoneType::Battlefield)
1467 .filter(|c| {
1468 crate::card::valid_filter::matches_valid_card_selector_in_game(
1469 &selector, c, source, game,
1470 )
1471 })
1472 .collect();
1473 return match aggregator {
1474 "" | "Amount" => matches.len() as i32,
1475 "GreatestCardManaCost" => matches.iter().map(|c| c.mana_cost.cmc()).max().unwrap_or(0),
1476 _ => 0,
1477 };
1478 }
1479 if expr.contains("Graveyard") && expr.contains("YouCtrl") {
1480 return game
1481 .cards_in_zone(ZoneType::Graveyard, source.controller)
1482 .len() as i32;
1483 }
1484 expr.strip_prefix("Count$")
1485 .and_then(|s| s.parse::<i32>().ok())
1486 .unwrap_or(0)
1487}
1488
1489pub fn resolve_count_svar_for_sa(
1490 expr: &str,
1491 game: &GameState,
1492 source_id: CardId,
1493 controller: PlayerId,
1494 sa: &SpellAbility,
1495) -> i32 {
1496 use forge_foundation::ZoneType;
1497
1498 if let Some(rest) = expr
1499 .strip_prefix("Count$xPaid")
1500 .or_else(|| expr.strip_prefix("Count$XPaid"))
1501 {
1502 let operators = rest.strip_prefix('/').unwrap_or(rest);
1503 return do_x_math(
1504 sa.x_mana_cost_paid as i32,
1505 operators,
1506 game,
1507 source_id,
1508 controller,
1509 sa,
1510 );
1511 }
1512 if let Some(operators) = expr.strip_prefix("Count$CastTotalManaSpent") {
1513 let operators = operators.strip_prefix('/').unwrap_or(operators);
1514 return do_x_math(
1515 game.card(source_id).paying_mana_to_cast.len() as i32,
1516 operators,
1517 game,
1518 source_id,
1519 controller,
1520 sa,
1521 );
1522 }
1523
1524 if expr == "Count$TriggerRememberAmount" {
1525 return sa.trigger_remembered_amount;
1526 }
1527 if expr == "Count$ChosenNumber" {
1528 return game.card(source_id).chosen_number.unwrap_or(0);
1529 }
1530 if expr == "TriggerCount$Result" {
1531 return trigger_result_values(sa).into_iter().sum();
1532 }
1533 if expr == "TriggerCountMax$Result" {
1534 return trigger_result_values(sa).into_iter().max().unwrap_or(0);
1535 }
1536
1537 if expr == "Count$Converge" || expr == "Count$Sunburst" {
1538 return game.card(source_id).sunburst_count();
1539 }
1540
1541 if let Some(operators) = expr.strip_prefix("Count$FinalChapterNr") {
1542 let operators = operators.strip_prefix('/').unwrap_or(operators);
1543 return do_x_math(
1544 game.card(source_id).get_final_chapter_nr(),
1545 operators,
1546 game,
1547 source_id,
1548 controller,
1549 sa,
1550 );
1551 }
1552
1553 if expr == "Count$YourSpeed" {
1554 return game.player(controller).speed;
1555 }
1556
1557 if let Some(operators) = expr.strip_prefix("Count$YourLifeTotal") {
1558 let operators = operators.strip_prefix('/').unwrap_or(operators);
1559 return do_x_math(
1560 game.player(controller).life,
1561 operators,
1562 game,
1563 source_id,
1564 controller,
1565 sa,
1566 );
1567 }
1568
1569 if let Some(operators) = expr.strip_prefix("Count$YouDrewThisTurn") {
1570 let operators = operators.strip_prefix('/').unwrap_or(operators);
1571 return do_x_math(
1572 game.player(controller).drawn_this_turn,
1573 operators,
1574 game,
1575 source_id,
1576 controller,
1577 sa,
1578 );
1579 }
1580
1581 if let Some(operators) = expr.strip_prefix("Count$OppGreatestLifeTotal") {
1582 let operators = operators.strip_prefix('/').unwrap_or(operators);
1583 let highest_life = game
1584 .alive_players()
1585 .into_iter()
1586 .filter(|&pid| crate::player::player_predicates::is_opponent_of(game, controller, pid))
1587 .map(|pid| game.player(pid).life)
1588 .max()
1589 .unwrap_or(0);
1590 return do_x_math(highest_life, operators, game, source_id, controller, sa);
1591 }
1592
1593 if let Some(rest) = expr.strip_prefix("Count$Metalcraft.") {
1595 let parts: Vec<&str> = rest.splitn(2, '.').collect();
1596 if parts.len() == 2 {
1597 let yes = parts[0].parse::<i32>().unwrap_or(1);
1598 let no = parts[1].parse::<i32>().unwrap_or(0);
1599 return if game.player_has_metalcraft(controller) {
1600 yes
1601 } else {
1602 no
1603 };
1604 }
1605 }
1606
1607 if let Some(rest) = expr.strip_prefix("Count$MaxSpeed.") {
1608 let parts: Vec<&str> = rest.splitn(2, '.').collect();
1609 if parts.len() == 2 {
1610 let yes = parts[0].parse::<i32>().unwrap_or(1);
1611 let no = parts[1].parse::<i32>().unwrap_or(0);
1612 return if game.player(controller).speed == 4 {
1613 yes
1614 } else {
1615 no
1616 };
1617 }
1618 }
1619
1620 if expr == "Count$AttackersDeclared" {
1621 return game
1622 .cards
1623 .iter()
1624 .filter(|card| {
1625 card.controller == controller && card.attacked_this_turn && card.is_creature()
1626 })
1627 .count() as i32;
1628 }
1629
1630 if expr == "Count$TopOfLibraryCMC" {
1631 return game
1632 .cards_in_zone(ZoneType::Library, controller)
1633 .last()
1634 .map(|&cid| game.card(cid).mana_value())
1635 .unwrap_or(0);
1636 }
1637
1638 if let Some(rest) = expr.strip_prefix("Count$OptionalGenericCostPaid.") {
1639 let parts: Vec<&str> = rest.splitn(2, '.').collect();
1640 if parts.len() == 2 {
1641 let paid_val = parts[0].parse::<i32>().unwrap_or(1);
1642 let unpaid_val = parts[1].parse::<i32>().unwrap_or(0);
1643 return if sa.optional_generic_cost_paid {
1644 paid_val
1645 } else {
1646 unpaid_val
1647 };
1648 }
1649 }
1650
1651 if expr == "Count$KickedCount" {
1652 return sa.kick_count as i32;
1653 }
1654 if let Some(rest) = expr.strip_prefix("Count$Kicked.") {
1655 let parts: Vec<&str> = rest.splitn(2, '.').collect();
1656 if parts.len() == 2 {
1657 let chosen = if sa.kicked { parts[0] } else { parts[1] };
1658 return resolve_svar_expression(chosen, game, source_id, controller, sa);
1659 }
1660 }
1661
1662 if let Some(rest) = expr.strip_prefix("Count$UrzaLands.") {
1665 let parts: Vec<&str> = rest.splitn(2, '.').collect();
1666 if parts.len() == 2 {
1667 let chosen = if crate::player::player_predicates::has_urza_lands(game, controller) {
1668 parts[0]
1669 } else {
1670 parts[1]
1671 };
1672 return resolve_svar_expression(chosen, game, source_id, controller, sa);
1673 }
1674 }
1675
1676 if let Some(rest) = expr.strip_prefix("Count$PromisedGift.") {
1678 let parts: Vec<&str> = rest.splitn(2, '.').collect();
1679 if parts.len() == 2 {
1680 let promised_val = parts[0].parse::<i32>().unwrap_or(1);
1681 let not_promised_val = parts[1].parse::<i32>().unwrap_or(0);
1682 return if game.card(source_id).promised_gift.is_some() {
1683 promised_val
1684 } else {
1685 not_promised_val
1686 };
1687 }
1688 }
1689 if expr == "Count$PromisedGift" {
1690 return if game.card(source_id).promised_gift.is_some() {
1691 1
1692 } else {
1693 0
1694 };
1695 }
1696
1697 if let Some(rest) = expr.strip_prefix("Count$Valid") {
1703 let (rest, operators) = rest.split_once('/').unwrap_or((rest, ""));
1704 let mut parts = rest.trim_start().splitn(2, ' ');
1705 let zone_part = parts.next().unwrap_or("").trim();
1706 let restrictions = parts.next().unwrap_or("").trim();
1707 let (restrictions, aggregator) = restrictions.split_once('$').unwrap_or((restrictions, ""));
1708 if !restrictions.is_empty() {
1709 let zones: Vec<ZoneType> = if zone_part.is_empty() {
1710 vec![ZoneType::Battlefield]
1711 } else {
1712 zone_part
1713 .split(',')
1714 .filter_map(crate::ability::ability_utils::parse_zone_type)
1715 .collect()
1716 };
1717 if !zones.is_empty() {
1718 let source = game.card(source_id);
1719 let selector = crate::parsing::cached_compiled_selector(restrictions);
1720 let targeted_players: Vec<crate::ids::PlayerId> =
1722 sa.target_chosen.target_player.into_iter().collect();
1723 let targeted_cards: Vec<crate::ids::CardId> =
1724 sa.target_chosen.target_card.into_iter().collect();
1725 let ctx = crate::card::valid_filter::MatchContext::from_source(source)
1726 .with_game(game)
1727 .with_targets(&targeted_cards, &targeted_players)
1728 .with_spell_ability(sa);
1729 let matches: Vec<&crate::card::Card> = game
1730 .cards
1731 .iter()
1732 .filter(|card| {
1733 zones.contains(&card.zone)
1734 && crate::card::valid_filter::matches_valid_card_selector_with_context(
1735 &selector, card, ctx,
1736 )
1737 })
1738 .collect();
1739 let count = match aggregator {
1740 "" | "Amount" => matches.len() as i32,
1741 "GreatestCardManaCost" => {
1742 matches.iter().map(|c| c.mana_cost.cmc()).max().unwrap_or(0)
1743 }
1744 _ => 0,
1745 };
1746 return do_x_math(count, operators, game, source_id, controller, sa);
1747 }
1748 }
1749 }
1750
1751 if let Some(filter_str) = expr.strip_prefix("Count$Valid ") {
1755 let (filter_str, operators) = filter_str.split_once('/').unwrap_or((filter_str, ""));
1756 let (filter_str, greatest_power) =
1758 if let Some(base) = filter_str.strip_suffix("$GreatestCardPower") {
1759 (base, true)
1760 } else {
1761 (filter_str, false)
1762 };
1763
1764 let count_distinct_colors = filter_str.ends_with("$Colors");
1767 let filter_str = if count_distinct_colors {
1768 filter_str.trim_end_matches("$Colors")
1769 } else {
1770 filter_str
1771 };
1772
1773 let (filter_str, multiplier) = crate::parsing::strip_times_multiplier(filter_str);
1775
1776 let battlefield = game.cards_in_zone(ZoneType::Battlefield, controller);
1777 let opp = game.opponent_of(controller);
1779 let opp_battlefield = game.cards_in_zone(ZoneType::Battlefield, opp);
1780
1781 let has_you_ctrl =
1782 filter_str.contains(fc::YOU_CTRL) || filter_str.contains(fc::YOU_CONTROL);
1783
1784 let cards_to_check: Vec<CardId> = if has_you_ctrl {
1785 battlefield.to_vec()
1786 } else {
1787 battlefield
1788 .iter()
1789 .chain(opp_battlefield.iter())
1790 .copied()
1791 .collect()
1792 };
1793
1794 let source = game.card(source_id);
1795 let selector = crate::parsing::cached_compiled_selector(filter_str);
1796 if greatest_power {
1797 let mut max_power = 0;
1799 for &cid in &cards_to_check {
1800 let card = game.card(cid);
1801 if crate::card::valid_filter::matches_valid_card_selector_in_game(
1802 &selector, card, source, game,
1803 ) {
1804 max_power = max_power.max(card.power());
1805 }
1806 }
1807 return do_x_math(max_power, operators, game, source_id, controller, sa);
1808 } else if count_distinct_colors {
1809 let mut mask: u8 = 0;
1810 for &cid in &cards_to_check {
1811 let card = game.card(cid);
1812 if crate::card::valid_filter::matches_valid_card_selector_in_game(
1813 &selector, card, source, game,
1814 ) {
1815 mask |= card.color.mask();
1816 }
1817 }
1818 return do_x_math(
1819 (mask.count_ones() as i32) * multiplier,
1820 operators,
1821 game,
1822 source_id,
1823 controller,
1824 sa,
1825 );
1826 } else {
1827 let mut count = 0;
1828 for &cid in &cards_to_check {
1829 let card = game.card(cid);
1830 if crate::card::valid_filter::matches_valid_card_selector_in_game(
1831 &selector, card, source, game,
1832 ) {
1833 count += 1;
1834 }
1835 }
1836 return do_x_math(
1837 count * multiplier,
1838 operators,
1839 game,
1840 source_id,
1841 controller,
1842 sa,
1843 );
1844 }
1845 }
1846
1847 if let Some(color_str) = expr.strip_prefix("Count$Devotion.") {
1849 let color_mask: u16 = match color_str.to_uppercase().as_str() {
1850 "W" | "WHITE" => forge_foundation::ManaAtom::WHITE,
1851 "U" | "BLUE" => forge_foundation::ManaAtom::BLUE,
1852 "B" | "BLACK" => forge_foundation::ManaAtom::BLACK,
1853 "R" | "RED" => forge_foundation::ManaAtom::RED,
1854 "G" | "GREEN" => forge_foundation::ManaAtom::GREEN,
1855 _ => 0,
1856 };
1857 if color_mask != 0 {
1858 let battlefield = game.cards_in_zone(ZoneType::Battlefield, controller);
1859 let mut count = 0i32;
1860 for &cid in battlefield {
1861 let card = game.card(cid);
1862 for shard in card.mana_cost.shards() {
1863 if (shard.shard() & color_mask) != 0 {
1864 count += 1;
1865 }
1866 }
1867 }
1868 return count;
1869 }
1870 }
1871
1872 if let Some(rest) = expr.strip_prefix("Count$Compare ") {
1875 let parts: Vec<&str> = rest.splitn(2, ' ').collect();
1876 if parts.len() == 2 {
1877 let svar_name = parts[0];
1878 let cond_parts: Vec<&str> = parts[1].splitn(3, '.').collect();
1879 if cond_parts.len() == 3 {
1880 let svar_val = if let Some(svar_expr) = game.card(source_id).get_s_var(svar_name) {
1882 if svar_expr.starts_with("Count$") || svar_expr.starts_with("PlayerCount") {
1883 resolve_svar_expression(svar_expr, game, source_id, controller, sa)
1884 } else {
1885 svar_expr.parse::<i32>().unwrap_or(0)
1886 }
1887 } else {
1888 svar_name.parse::<i32>().unwrap_or(0)
1889 };
1890
1891 let cond = cond_parts[0];
1893 let result = compare_expr(svar_val, cond);
1894
1895 let resolve_branch = |raw: &str| {
1896 raw.parse::<i32>().unwrap_or_else(|_| {
1897 if let Some(svar_expr) = game.card(source_id).get_s_var(raw) {
1898 resolve_svar_expression(svar_expr, game, source_id, controller, sa)
1899 } else {
1900 resolve_svar_expression(raw, game, source_id, controller, sa)
1901 }
1902 })
1903 };
1904 let if_true = resolve_branch(cond_parts[1]);
1905 let if_false = resolve_branch(cond_parts[2]);
1906 return if result { if_true } else { if_false };
1907 }
1908 }
1909 }
1910
1911 if let Some(operators) = expr.strip_prefix("Count$ColorsColorIdentity") {
1912 let operators = operators.strip_prefix('/').unwrap_or(operators);
1913 let count = game
1914 .player_commander_color_identity(game.card(source_id).controller)
1915 .len() as i32;
1916 return do_x_math(count, operators, game, source_id, controller, sa);
1917 }
1918
1919 if expr == "Count$CardPower" {
1921 return game.card(source_id).power();
1922 }
1923 if expr == "Count$CardToughness" {
1925 return game.card(source_id).toughness();
1926 }
1927 if let Some(operators) = expr.strip_prefix("Count$YourTurns") {
1928 let operators = operators.strip_prefix('/').unwrap_or(operators);
1929 return do_x_math(
1930 game.player(controller).statistics.turns_played,
1931 operators,
1932 game,
1933 source_id,
1934 controller,
1935 sa,
1936 );
1937 }
1938 if let Some(counter_type) = expr.strip_prefix("Count$CardCounters.") {
1940 let ct = crate::ability::effects::parse_counter_type(counter_type);
1941 return *game.card(source_id).counters.get(&ct).unwrap_or(&0);
1942 }
1943
1944 if expr == "Count$TotalDamageDoneByThisTurn" {
1946 return game.card(source_id).total_damage_done_this_turn;
1947 }
1948
1949 if let Some(rest) = expr
1954 .strip_prefix("Count$CardsInYour")
1955 .or_else(|| expr.strip_prefix("Count$InYour"))
1956 {
1957 let zone = match rest {
1958 "Hand" => Some(ZoneType::Hand),
1959 "Yard" | "Graveyard" => Some(ZoneType::Graveyard),
1960 "Library" => Some(ZoneType::Library),
1961 "Exile" => Some(ZoneType::Exile),
1962 "Battlefield" => Some(ZoneType::Battlefield),
1963 _ => None,
1964 };
1965 if let Some(zone) = zone {
1966 return game.cards_in_zone(zone, controller).len() as i32;
1967 }
1968 }
1969
1970 if let Some(rest) = expr.strip_prefix("Count$RememberedNumber") {
1971 let operators = rest.strip_prefix('/').unwrap_or(rest);
1972 let count = game.card(source_id).remembered_cmc.iter().sum();
1973 return do_x_math(count, operators, game, source_id, controller, sa);
1974 }
1975
1976 if let Some(rest) = expr.strip_prefix("Count$RememberedSize") {
1979 let operators = rest.strip_prefix('/').unwrap_or(rest);
1980 let card = game.card(source_id);
1981 let count =
1982 card.remembered_cards.len() + card.remembered_players.len() + card.remembered_cmc.len();
1983 return do_x_math(count as i32, operators, game, source_id, controller, sa);
1984 }
1985
1986 expr.parse::<i32>().unwrap_or_else(|_| {
1987 eprintln!("Unrecognized Count expression, returning 0 for: {expr}");
1988 0
1989 })
1990}
1991
1992#[allow(dead_code)]
1994fn valid_card_matches_with_source(
1995 filter: &str,
1996 card: &crate::card::Card,
1997 controller: PlayerId,
1998 source_id: CardId,
1999 chosen_type: Option<&str>,
2000) -> bool {
2001 let parts: Vec<&str> = filter.split('.').collect();
2002 let base_type = parts.first().copied().unwrap_or("");
2003
2004 let type_ok = match base_type {
2006 fc::CREATURE => card.is_creature(),
2007 fc::LAND => card.is_land(),
2008 fc::ARTIFACT => card.type_line.is_artifact(),
2009 fc::ENCHANTMENT => card.type_line.is_enchantment(),
2010 fc::PLANESWALKER => card.type_line.is_planeswalker(),
2011 fc::PERMANENT | fc::CARD => true,
2012 _ => card.type_line.has_subtype(base_type),
2014 };
2015 if !type_ok {
2016 return false;
2017 }
2018
2019 for &dot_qual in &parts[1..] {
2021 for sub_qual in dot_qual.split('+') {
2022 let sub_qual = sub_qual.trim();
2023 if sub_qual.eq_ignore_ascii_case(fc::YOU_CTRL)
2024 || sub_qual.eq_ignore_ascii_case(fc::YOU_CONTROL)
2025 {
2026 if card.controller != controller {
2027 return false;
2028 }
2029 } else if sub_qual.eq_ignore_ascii_case(fc::SELF_REF) {
2030 if card.id != source_id {
2031 return false;
2032 }
2033 } else if sub_qual.eq_ignore_ascii_case(fc::OTHER) {
2034 if card.id == source_id {
2035 return false;
2036 }
2037 } else if sub_qual.eq_ignore_ascii_case("ChosenType") {
2038 match chosen_type {
2041 Some(ct)
2042 if card.type_line.has_subtype(ct) || card.has_keyword("Changeling") => {}
2043 _ => return false,
2044 }
2045 } else if sub_qual.starts_with("counters_") {
2046 if !check_counter_qualifier(card, sub_qual) {
2048 return false;
2049 }
2050 }
2051 }
2052 }
2053 true
2054}
2055
2056#[allow(dead_code)]
2058fn check_counter_qualifier(card: &crate::card::Card, qual: &str) -> bool {
2059 let rest = match qual.strip_prefix("counters_") {
2060 Some(r) => r,
2061 None => return true,
2062 };
2063 let parts: Vec<&str> = rest.splitn(2, '_').collect();
2065 if parts.len() != 2 {
2066 return true;
2067 }
2068 let cond = parts[0];
2069 let counter_type = crate::ability::effects::parse_counter_type(parts[1]);
2070 let count = *card.counters.get(&counter_type).unwrap_or(&0);
2071
2072 compare_expr(count, cond)
2073}
2074
2075#[cfg(test)]
2076mod tests {
2077 use forge_foundation::{CardTypeLine, ColorSet, ManaCost};
2078
2079 use super::resolve_numeric_svar;
2080 use crate::card::Card;
2081 use crate::game::GameState;
2082 use crate::ids::{CardId, PlayerId};
2083 use crate::spellability::SpellAbility;
2084
2085 #[test]
2086 fn resolves_player_count_defined_life_total_twice() {
2087 let mut game = GameState::new(&["A", "B"], 20);
2088 let p0 = PlayerId(0);
2089 let p1 = PlayerId(1);
2090 game.player_mut(p1).life = 7;
2091
2092 let mut host = Card::new(
2093 CardId(0),
2094 "Host".to_string(),
2095 p0,
2096 CardTypeLine::parse("Creature"),
2097 ManaCost::parse(""),
2098 ColorSet::COLORLESS,
2099 Some(1),
2100 Some(1),
2101 vec![],
2102 vec![],
2103 );
2104 host.svars.insert(
2105 "X".to_string(),
2106 "PlayerCountDefinedTriggeredAttackedTarget$LifeTotal/Twice".to_string(),
2107 );
2108 let host_id = game.create_card(host);
2109
2110 let mut sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ GainLife | LifeAmount$ X");
2111 sa.set_triggering_object(crate::ability::AbilityKey::AttackedTarget, p1);
2112
2113 assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 14);
2114 }
2115
2116 #[test]
2117 fn resolves_player_count_highest_life_total() {
2118 let mut game = GameState::new(&["A", "B"], 20);
2119 let p0 = PlayerId(0);
2120 let p1 = PlayerId(1);
2121 game.player_mut(p0).life = 11;
2122 game.player_mut(p1).life = 17;
2123
2124 let mut host = Card::new(
2125 CardId(0),
2126 "Host".to_string(),
2127 p0,
2128 CardTypeLine::parse("Creature"),
2129 ManaCost::parse(""),
2130 ColorSet::COLORLESS,
2131 Some(1),
2132 Some(1),
2133 vec![],
2134 vec![],
2135 );
2136 host.svars.insert(
2137 "X".to_string(),
2138 "PlayerCountPlayers$HighestLifeTotal".to_string(),
2139 );
2140 let host_id = game.create_card(host);
2141
2142 let sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ GainLife | LifeAmount$ X");
2143 assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 17);
2144 }
2145
2146 #[test]
2147 fn resolves_triggered_target_life_total_half_up() {
2148 let mut game = GameState::new(&["A", "B"], 20);
2149 let p0 = PlayerId(0);
2150 let p1 = PlayerId(1);
2151 game.player_mut(p1).life = 9;
2152
2153 let mut host = Card::new(
2154 CardId(0),
2155 "Host".to_string(),
2156 p0,
2157 CardTypeLine::parse("Creature"),
2158 ManaCost::parse(""),
2159 ColorSet::COLORLESS,
2160 Some(1),
2161 Some(1),
2162 vec![],
2163 vec![],
2164 );
2165 host.svars.insert(
2166 "X".to_string(),
2167 "TriggeredTarget$LifeTotal/HalfUp".to_string(),
2168 );
2169 let host_id = game.create_card(host);
2170
2171 let mut sa = SpellAbility::new_simple(
2172 Some(host_id),
2173 p0,
2174 "DB$ LoseLife | Defined$ TriggeredTarget | LifeAmount$ X",
2175 );
2176 sa.set_triggering_object(crate::ability::AbilityKey::TargetPlayer, p1);
2177
2178 assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 5);
2179 }
2180
2181 #[test]
2182 fn resolves_player_count_minus_remembered_amount() {
2183 let mut game = GameState::new(&["A", "B"], 20);
2184 let p0 = PlayerId(0);
2185 let p1 = PlayerId(1);
2186
2187 let remembered = Card::new(
2188 CardId(1),
2189 "Remembered".to_string(),
2190 p1,
2191 CardTypeLine::parse("Creature"),
2192 ManaCost::parse(""),
2193 ColorSet::COLORLESS,
2194 Some(1),
2195 Some(1),
2196 vec![],
2197 vec![],
2198 );
2199 let remembered_id = game.create_card(remembered);
2200
2201 let mut host = Card::new(
2202 CardId(0),
2203 "Host".to_string(),
2204 p0,
2205 CardTypeLine::parse("Creature"),
2206 ManaCost::parse(""),
2207 ColorSet::COLORLESS,
2208 Some(1),
2209 Some(1),
2210 vec![],
2211 vec![],
2212 );
2213 host.svars.insert(
2214 "X".to_string(),
2215 "PlayerCountOpponents$Amount/Minus.Remembered$Amount".to_string(),
2216 );
2217 let host_id = game.create_card(host);
2218 game.card_mut(host_id).add_remembered_card(remembered_id);
2219
2220 let sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ Draw | NumCards$ X");
2221 assert_eq!(resolve_numeric_svar(&game, &sa, "NumCards", -1), 0);
2222 }
2223
2224 #[test]
2225 fn resolves_player_count_minus_empty_remembered_amount() {
2226 let mut game = GameState::new(&["A", "B"], 20);
2227 let p0 = PlayerId(0);
2228
2229 let mut host = Card::new(
2230 CardId(0),
2231 "Host".to_string(),
2232 p0,
2233 CardTypeLine::parse("Creature"),
2234 ManaCost::parse(""),
2235 ColorSet::COLORLESS,
2236 Some(1),
2237 Some(1),
2238 vec![],
2239 vec![],
2240 );
2241 host.svars.insert(
2242 "X".to_string(),
2243 "PlayerCountOpponents$Amount/Minus.Remembered$Amount".to_string(),
2244 );
2245 let host_id = game.create_card(host);
2246
2247 let sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ Draw | NumCards$ X");
2248 assert_eq!(resolve_numeric_svar(&game, &sa, "NumCards", -1), 1);
2249 }
2250
2251 #[test]
2252 fn resolves_player_count_remembered_life_lost_this_turn() {
2253 let mut game = GameState::new(&["A", "B"], 20);
2254 let p0 = PlayerId(0);
2255 let p1 = PlayerId(1);
2256
2257 game.player_mut(p1).life_lost_this_turn = 11;
2258
2259 let mut host = Card::new(
2260 CardId(0),
2261 "Host".to_string(),
2262 p0,
2263 CardTypeLine::parse("Creature"),
2264 ManaCost::parse(""),
2265 ColorSet::COLORLESS,
2266 Some(1),
2267 Some(1),
2268 vec![],
2269 vec![],
2270 );
2271 host.svars.insert(
2272 "X".to_string(),
2273 "PlayerCountRemembered$LifeLostThisTurn".to_string(),
2274 );
2275 let host_id = game.create_card(host);
2276 game.card_mut(host_id).add_remembered_player(p1);
2277
2278 let sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ LoseLife | LifeAmount$ X");
2279 assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", -1), 11);
2280 }
2281
2282 #[test]
2283 fn resolves_triggered_spell_ability_card_mana_cost_lki() {
2284 let mut game = GameState::new(&["A", "B"], 20);
2285 let p0 = PlayerId(0);
2286 let p1 = PlayerId(1);
2287
2288 let mut host = Card::new(
2289 CardId(0),
2290 "Host".to_string(),
2291 p0,
2292 CardTypeLine::parse("Creature"),
2293 ManaCost::parse(""),
2294 ColorSet::COLORLESS,
2295 Some(1),
2296 Some(1),
2297 vec![],
2298 vec![],
2299 );
2300 host.svars.insert(
2301 "X".to_string(),
2302 "TriggeredSpellAbility$CardManaCostLKI".to_string(),
2303 );
2304 let host_id = game.create_card(host);
2305
2306 let mut spell_card = Card::new(
2307 CardId(1),
2308 "Big Spell".to_string(),
2309 p1,
2310 CardTypeLine::parse("Sorcery"),
2311 ManaCost::parse("X U"),
2312 ColorSet::BLUE,
2313 None,
2314 None,
2315 vec![],
2316 vec![],
2317 );
2318 spell_card.set_zone(forge_foundation::ZoneType::Graveyard);
2319 let spell_id = game.create_card(spell_card);
2320
2321 let mut triggered_sa =
2322 SpellAbility::new_simple(Some(spell_id), p1, "SP$ DealDamage | NumDmg$ 1");
2323 triggered_sa.x_mana_cost_paid = 4;
2324
2325 let mut sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ GainLife | LifeAmount$ X");
2326 sa.set_triggering_spell_ability("SpellAbility", triggered_sa);
2327
2328 assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 5);
2329 }
2330
2331 #[test]
2332 fn resolves_count_your_speed_and_max_speed() {
2333 let mut game = GameState::new(&["A", "B"], 20);
2334 let p0 = PlayerId(0);
2335 game.player_mut(p0).speed = 4;
2336
2337 let mut host = Card::new(
2338 CardId(0),
2339 "Host".to_string(),
2340 p0,
2341 CardTypeLine::parse("Creature"),
2342 ManaCost::parse(""),
2343 ColorSet::COLORLESS,
2344 Some(1),
2345 Some(1),
2346 vec![],
2347 vec![],
2348 );
2349 host.svars
2350 .insert("X".to_string(), "Count$YourSpeed".to_string());
2351 host.svars
2352 .insert("Y".to_string(), "Count$MaxSpeed.2.1".to_string());
2353 let host_id = game.create_card(host);
2354
2355 let sa = SpellAbility::new_simple(
2356 Some(host_id),
2357 p0,
2358 "DB$ GainLife | LifeAmount$ X | NumCards$ Y",
2359 );
2360 assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 4);
2361 assert_eq!(resolve_numeric_svar(&game, &sa, "NumCards", 0), 2);
2362 }
2363
2364 #[test]
2365 fn resolves_attackers_declared_and_life_lost_last_turn() {
2366 let mut game = GameState::new(&["A", "B"], 20);
2367 let p0 = PlayerId(0);
2368
2369 let mut attacker = Card::new(
2370 CardId(0),
2371 "Attacker".to_string(),
2372 p0,
2373 CardTypeLine::parse("Creature"),
2374 ManaCost::parse("1 R"),
2375 ColorSet::RED,
2376 Some(2),
2377 Some(2),
2378 vec![],
2379 vec![],
2380 );
2381 attacker.attacked_this_turn = true;
2382 game.create_card(attacker);
2383
2384 game.player_mut(p0).life_lost_this_turn = 3;
2385 game.player_mut(p0).new_turn();
2386
2387 let mut host = Card::new(
2388 CardId(1),
2389 "Host".to_string(),
2390 p0,
2391 CardTypeLine::parse("Creature"),
2392 ManaCost::parse(""),
2393 ColorSet::COLORLESS,
2394 Some(1),
2395 Some(1),
2396 vec![],
2397 vec![],
2398 );
2399 host.svars
2400 .insert("X".to_string(), "Count$AttackersDeclared".to_string());
2401 host.svars.insert(
2402 "Y".to_string(),
2403 "PlayerCountPropertyYou$LifeLostLastTurn".to_string(),
2404 );
2405 let host_id = game.create_card(host);
2406
2407 let sa = SpellAbility::new_simple(
2408 Some(host_id),
2409 p0,
2410 "DB$ GainLife | LifeAmount$ X | NumCards$ Y",
2411 );
2412 assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 1);
2413 assert_eq!(resolve_numeric_svar(&game, &sa, "NumCards", 0), 3);
2414 }
2415
2416 #[test]
2417 fn resolves_top_of_library_cmc() {
2418 let mut game = GameState::new(&["A", "B"], 20);
2419 let p0 = PlayerId(0);
2420
2421 let top = Card::new(
2422 CardId(0),
2423 "Top".to_string(),
2424 p0,
2425 CardTypeLine::parse("Sorcery"),
2426 ManaCost::parse("2 U"),
2427 ColorSet::BLUE,
2428 None,
2429 None,
2430 vec![],
2431 vec![],
2432 );
2433 let top_id = game.create_card(top);
2434 game.move_card(top_id, forge_foundation::ZoneType::Library, p0);
2435
2436 let mut host = Card::new(
2437 CardId(1),
2438 "Host".to_string(),
2439 p0,
2440 CardTypeLine::parse("Creature"),
2441 ManaCost::parse(""),
2442 ColorSet::COLORLESS,
2443 Some(1),
2444 Some(1),
2445 vec![],
2446 vec![],
2447 );
2448 host.svars
2449 .insert("X".to_string(), "Count$TopOfLibraryCMC".to_string());
2450 let host_id = game.create_card(host);
2451
2452 let sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ GainLife | LifeAmount$ X");
2453 assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 3);
2454 }
2455
2456 #[test]
2457 fn resolves_player_property_counters_for_discard_damage_and_combat() {
2458 let mut game = GameState::new(&["A", "B"], 20);
2459 let p0 = PlayerId(0);
2460 let p1 = PlayerId(1);
2461 game.player_mut(p0).discarded_this_turn = 2;
2462 game.player_mut(p0).explored_this_turn = 1;
2463 game.player_mut(p0).opponents_assigned_damage_this_turn = 4;
2464 game.player_mut(p0).assigned_damage_this_turn = 7;
2465 game.player_mut(p0).assigned_combat_damage_this_turn = 2;
2466 game.player_mut(p0).attacked_players_this_combat.push(p1);
2467 game.player_mut(p0).been_dealt_combat_damage_since_last_turn = true;
2468
2469 let mut host = Card::new(
2470 CardId(0),
2471 "Host".to_string(),
2472 p0,
2473 CardTypeLine::parse("Creature"),
2474 ManaCost::parse(""),
2475 ColorSet::COLORLESS,
2476 Some(1),
2477 Some(1),
2478 vec![],
2479 vec![],
2480 );
2481 host.svars.insert(
2482 "A".to_string(),
2483 "PlayerCountPropertyYou$CardsDiscardedThisTurn".to_string(),
2484 );
2485 host.svars.insert(
2486 "B".to_string(),
2487 "PlayerCountPropertyYou$ExploredThisTurn".to_string(),
2488 );
2489 host.svars.insert(
2490 "C".to_string(),
2491 "PlayerCountPropertyYou$DamageToOppsThisTurn".to_string(),
2492 );
2493 host.svars.insert(
2494 "D".to_string(),
2495 "PlayerCountPropertyYou$NonCombatDamageDealtThisTurn".to_string(),
2496 );
2497 host.svars.insert(
2498 "E".to_string(),
2499 "PlayerCountPropertyYou$OpponentsAttackedThisCombat".to_string(),
2500 );
2501 host.svars.insert(
2502 "F".to_string(),
2503 "PlayerCountPropertyYou$BeenDealtCombatDamageSinceLastTurn".to_string(),
2504 );
2505 let host_id = game.create_card(host);
2506
2507 let sa = SpellAbility::new_simple(
2508 Some(host_id),
2509 p0,
2510 "DB$ GainLife | LifeAmount$ A | NumCards$ B",
2511 );
2512 assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 2);
2513 assert_eq!(resolve_numeric_svar(&game, &sa, "NumCards", 0), 1);
2514 assert_eq!(
2515 super::resolve_svar_expression(
2516 game.card(host_id).get_s_var("C").unwrap(),
2517 &game,
2518 host_id,
2519 p0,
2520 &sa,
2521 ),
2522 4
2523 );
2524 assert_eq!(
2525 super::resolve_svar_expression(
2526 game.card(host_id).get_s_var("D").unwrap(),
2527 &game,
2528 host_id,
2529 p0,
2530 &sa,
2531 ),
2532 5
2533 );
2534 assert_eq!(
2535 super::resolve_svar_expression(
2536 game.card(host_id).get_s_var("E").unwrap(),
2537 &game,
2538 host_id,
2539 p0,
2540 &sa,
2541 ),
2542 1
2543 );
2544 assert_eq!(
2545 super::resolve_svar_expression(
2546 game.card(host_id).get_s_var("F").unwrap(),
2547 &game,
2548 host_id,
2549 p0,
2550 &sa,
2551 ),
2552 1
2553 );
2554 }
2555
2556 #[test]
2557 fn resolves_trigger_result_sum_and_max_from_trigger_objects() {
2558 let mut game = GameState::new(&["A", "B"], 20);
2559 let p0 = PlayerId(0);
2560
2561 let mut host = Card::new(
2562 CardId(0),
2563 "Host".to_string(),
2564 p0,
2565 CardTypeLine::parse("Creature"),
2566 ManaCost::parse(""),
2567 ColorSet::COLORLESS,
2568 Some(1),
2569 Some(1),
2570 vec![],
2571 vec![],
2572 );
2573 host.svars
2574 .insert("Sum".to_string(), "TriggerCount$Result".to_string());
2575 host.svars
2576 .insert("Max".to_string(), "TriggerCountMax$Result".to_string());
2577 let host_id = game.create_card(host);
2578
2579 let mut sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ Draw | NumCards$ Sum");
2580 sa.set_triggering_object(crate::ability::AbilityKey::Result, "4,11,7");
2581
2582 assert_eq!(
2583 super::resolve_svar_expression(
2584 game.card(host_id).get_s_var("Sum").unwrap(),
2585 &game,
2586 host_id,
2587 p0,
2588 &sa,
2589 ),
2590 22
2591 );
2592 assert_eq!(
2593 super::resolve_svar_expression(
2594 game.card(host_id).get_s_var("Max").unwrap(),
2595 &game,
2596 host_id,
2597 p0,
2598 &sa,
2599 ),
2600 11
2601 );
2602 }
2603}