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 = if filter.contains("nonLand") {
509 !rem_card.is_land()
510 } else if filter == "Card" {
511 true
512 } else {
513 true
514 };
515 if matches {
516 return times;
517 }
518 }
519 0
520}
521
522fn resolve_trigger_count_svar(
523 expr: &str,
524 game: &GameState,
525 source_id: CardId,
526 controller: PlayerId,
527 sa: &SpellAbility,
528) -> i32 {
529 let (prefix, rest) = expr.split_once('$').unwrap_or((expr, ""));
530 let mut parts = rest.split('/');
531 let key = parts.next().unwrap_or("");
532 let operators = parts.next().unwrap_or("");
533 let values = parse_trigger_int_values(sa, key.trim());
534 let count = if prefix.ends_with("Max") {
535 values.into_iter().max().unwrap_or(0)
536 } else {
537 values.into_iter().sum()
538 };
539 do_x_math(count, operators, game, source_id, controller, sa)
540}
541
542const MAX_SVAR_RESOLUTION_DEPTH: usize = 50;
543
544thread_local! {
545 static SVAR_RESOLUTION_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
546}
547
548pub(crate) fn resolve_svar_expression(
549 expr: &str,
550 game: &GameState,
551 source_id: CardId,
552 controller: PlayerId,
553 sa: &SpellAbility,
554) -> i32 {
555 let depth = SVAR_RESOLUTION_DEPTH.with(|d| d.get());
556 if depth >= MAX_SVAR_RESOLUTION_DEPTH {
557 eprintln!("SVar resolution exceeded depth limit, returning 0 for: {expr}");
558 return 0;
559 }
560 SVAR_RESOLUTION_DEPTH.with(|d| d.set(depth + 1));
561 let value = resolve_svar_expression_inner(expr, game, source_id, controller, sa);
562 SVAR_RESOLUTION_DEPTH.with(|d| d.set(depth));
563 value
564}
565
566fn resolve_svar_expression_inner(
567 expr: &str,
568 game: &GameState,
569 source_id: CardId,
570 controller: PlayerId,
571 sa: &SpellAbility,
572) -> i32 {
573 let expr = expr.trim();
574 if let Ok(n) = expr.parse::<i32>() {
575 return n;
576 }
577 if let Some(expression) = parse_script_svar_numeric_expression(expr) {
578 if let Some(value) =
579 resolve_lowered_svar_expression(&expression, game, source_id, controller, sa)
580 {
581 return value;
582 }
583 }
584 if expr.starts_with("TriggerCount$") || expr.starts_with("TriggerCountMax$") {
585 return resolve_trigger_count_svar(expr, game, source_id, controller, sa);
586 }
587 if expr.starts_with("Count$") {
588 return resolve_count_svar_for_sa(expr, game, source_id, controller, sa);
589 }
590 if expr.starts_with("PlayerCount") {
591 return resolve_player_count_svar(expr, game, source_id, controller, sa);
592 }
593 if let Some(property) = expr.strip_prefix("Remembered$") {
594 return crate::ability::ability_utils::handle_paid(
595 game,
596 &game.card(source_id).remembered_cards,
597 property,
598 source_id,
599 );
600 }
601 if let Some(rest) = expr.strip_prefix("RememberedSize") {
602 return do_x_math(
603 game.card(source_id).remembered_cards.len() as i32,
604 rest.strip_prefix('/').unwrap_or(""),
605 game,
606 source_id,
607 controller,
608 sa,
609 );
610 }
611 if let Some(value) = resolve_paid_hash_expr(expr, game, source_id, sa) {
612 return value;
613 }
614 if let Some(value) = resolve_spell_ability_expr(expr, game, sa) {
615 return value;
616 }
617 if let Some(value) = resolve_card_list_expr(expr, game, source_id, controller, sa) {
618 return value;
619 }
620 if let Some(value) = crate::lki::resolve_triggered_card_lki_svar(game, sa, expr) {
621 return value;
622 }
623 if let Some(value) = resolve_direct_player_expr(expr, game, source_id, controller, sa) {
624 return value;
625 }
626 if let Some(svar_expr) = game.card(source_id).get_s_var(expr) {
627 return resolve_svar_expression(svar_expr, game, source_id, controller, sa);
628 }
629 0
630}
631
632fn player_x_property(
633 player: PlayerId,
634 expr: &str,
635 game: &GameState,
636 source_id: CardId,
637 controller: PlayerId,
638 sa: &SpellAbility,
639) -> i32 {
640 let parts: Vec<&str> = expr.split('/').collect();
641 let value = parts.first().copied().unwrap_or("");
642 let operators = parts.get(1).copied().unwrap_or("");
643
644 let base = match value {
645 _ if value.starts_with("Valid") => {
646 let (zones, restrictions) = if let Some(rest) = value.strip_prefix("Valid ") {
647 (vec![forge_foundation::ZoneType::Battlefield], rest)
648 } else {
649 let mut parts = value.splitn(2, ' ');
650 let zone_part = parts
651 .next()
652 .unwrap_or("")
653 .strip_prefix("Valid")
654 .unwrap_or("");
655 let restrictions = parts.next().unwrap_or("");
656 let zones: Vec<_> = if zone_part.is_empty() {
657 vec![forge_foundation::ZoneType::Battlefield]
658 } else {
659 zone_part
660 .split(',')
661 .filter_map(crate::ability::ability_utils::parse_zone_type)
662 .collect()
663 };
664 (zones, restrictions)
665 };
666 let selector = crate::parsing::cached_compiled_selector(restrictions);
667 let source = game.card(source_id);
668 let context = crate::card::valid_filter::MatchContext::from_source(source)
674 .with_game(game)
675 .with_source_controller(player);
676 game.cards
677 .iter()
678 .filter(|card| {
679 zones.contains(&card.zone)
680 && crate::card::valid_filter::matches_valid_card_selector_with_context(
681 &selector, card, context,
682 )
683 })
684 .count() as i32
685 }
686 "CardsInHand" => game
687 .cards_in_zone(forge_foundation::ZoneType::Hand, player)
688 .len() as i32,
689 "CardsInLibrary" => game
690 .cards_in_zone(forge_foundation::ZoneType::Library, player)
691 .len() as i32,
692 "CardsInGraveyard" => game
693 .cards_in_zone(forge_foundation::ZoneType::Graveyard, player)
694 .len() as i32,
695 "CardsInPlay" => game
696 .cards_in_zone(forge_foundation::ZoneType::Battlefield, player)
697 .len() as i32,
698 "CreaturesInPlay" => game
699 .cards_in_zone(forge_foundation::ZoneType::Battlefield, player)
700 .iter()
701 .filter(|&&cid| game.card(cid).is_creature())
702 .count() as i32,
703 "StartingLife" => game.player(player).starting_life,
704 "LifeTotal" => game.player(player).life,
705 "LifeLostThisTurn" => game.player(player).life_lost_this_turn,
706 "LifeLostLastTurn" => game.player(player).life_lost_last_turn,
707 "LifeGainedThisTurn" => game.player(player).life_gained_this_turn,
708 "LifeGainedByTeamThisTurn" => game.player(player).life_gained_by_team_this_turn,
709 "LifeStartedThisTurnWith" => game.player(player).life_started_this_turn_with,
710 "Speed" => game.player(player).speed,
711 "TopOfLibraryCMC" => game
712 .cards_in_zone(forge_foundation::ZoneType::Library, player)
713 .last()
714 .map(|&cid| game.card(cid).mana_value())
715 .unwrap_or(0),
716 "LandsPlayed" => game.player(player).lands_played_this_turn,
717 "SpellsCastThisTurn" => game.player(player).spells_cast_this_turn,
718 "CardsDrawn" => game.player(player).drawn_this_turn,
719 "CardsDiscardedThisTurn" => game.player(player).discarded_this_turn,
720 "ExploredThisTurn" => game.player(player).explored_this_turn,
721 "AttackersDeclared" => game
722 .cards
723 .iter()
724 .filter(|card| {
725 card.controller == player && card.attacked_this_turn && card.is_creature()
726 })
727 .count() as i32,
728 "DamageToOppsThisTurn" => game.player(player).opponents_assigned_damage_this_turn,
729 "NonCombatDamageDealtThisTurn" => {
730 game.player(player).assigned_damage_this_turn
731 - game.player(player).assigned_combat_damage_this_turn
732 }
733 "PoisonCounters" => game.player(player).poison_counters,
734 "EnergyCounters" => game.player(player).energy_counters,
735 "ManaExpendedThisTurn" => game.player(player).mana_expended_this_turn,
736 "RingTemptedYou" => game.player(player).ring_level,
737 "OpponentsAttackedThisTurn" => {
738 let mut attacked = Vec::new();
739 for card in &game.cards {
740 if card.controller != player {
741 continue;
742 }
743 for entity in &card.damage_history.attacked_this_turn {
744 if let TrackedEntity::Player(pid) = entity {
745 if !attacked.contains(pid) {
746 attacked.push(*pid);
747 }
748 }
749 }
750 }
751 attacked.len() as i32
752 }
753 "OpponentsAttackedThisCombat" => {
754 game.player(player).attacked_players_this_combat.len() as i32
755 }
756 "BeenDealtCombatDamageSinceLastTurn" => {
757 i32::from(game.player(player).been_dealt_combat_damage_since_last_turn)
758 }
759 "AttractionsVisitedThisTurn" => game.player(player).attractions_visited_this_turn,
760 _ if value.starts_with("Counters.") => {
761 let counter_name = value.strip_prefix("Counters.").unwrap_or("");
762 if counter_name.eq_ignore_ascii_case("ALL") {
763 game.player(player).poison_counters
764 + game.player(player).energy_counters
765 + game.player(player).radiation_counters
766 } else if counter_name.eq_ignore_ascii_case("POISON") {
767 game.player(player).poison_counters
768 } else if counter_name.eq_ignore_ascii_case("ENERGY") {
769 game.player(player).energy_counters
770 } else if counter_name.eq_ignore_ascii_case("RADIATION") {
771 game.player(player).radiation_counters
772 } else {
773 0
774 }
775 }
776 _ if value.starts_with("HasProperty") => i32::from(crate::player::player_has_property(
777 player,
778 value.strip_prefix("HasProperty").unwrap_or(""),
779 game,
780 source_id,
781 controller,
782 sa,
783 )),
784 _ => 0,
785 };
786
787 do_x_math(base, operators, game, source_id, controller, sa)
788}
789
790pub fn player_condition_matches(
791 player: PlayerId,
792 property: &str,
793 game: &GameState,
794 source_id: CardId,
795 controller: PlayerId,
796 sa: &SpellAbility,
797) -> bool {
798 let Some(rest) = property.strip_prefix("Condition") else {
799 return false;
800 };
801 let Some((lhs, prop_expr)) = rest.split_once(' ') else {
802 return false;
803 };
804 let (cmp, rhs_expr) = if lhs.is_empty() {
805 ("GE", "1")
806 } else if lhs.len() >= 2 {
807 (&lhs[..2], &lhs[2..])
808 } else {
809 ("GE", "1")
810 };
811 let rhs = resolve_svar_expression(rhs_expr, game, source_id, controller, sa);
812 compare_expr(
813 player_x_property(player, prop_expr, game, source_id, controller, sa),
814 &format!("{cmp}{rhs}"),
815 )
816}
817
818fn resolve_direct_player_expr(
819 expr: &str,
820 game: &GameState,
821 source_id: CardId,
822 controller: PlayerId,
823 sa: &SpellAbility,
824) -> Option<i32> {
825 let (defined, property) = expr.split_once('$')?;
826 resolve_direct_player_property(defined, property, game, source_id, controller, sa)
827}
828
829fn resolve_direct_player_property(
830 defined: &str,
831 property: &str,
832 game: &GameState,
833 source_id: CardId,
834 controller: PlayerId,
835 sa: &SpellAbility,
836) -> Option<i32> {
837 let players = crate::ability::ability_utils::resolve_defined_players_with_sa(
838 defined, sa, controller, game,
839 );
840 if players.is_empty() {
841 return None;
842 }
843 Some(
844 players
845 .into_iter()
846 .map(|pid| player_x_property(pid, property, game, source_id, controller, sa))
847 .sum(),
848 )
849}
850
851fn resolve_player_count_svar(
852 expr: &str,
853 game: &GameState,
854 source_id: CardId,
855 controller: PlayerId,
856 sa: &SpellAbility,
857) -> i32 {
858 let Some((group, property_expr)) = expr.split_once('$') else {
859 return 0;
860 };
861 let kind = group.strip_prefix("PlayerCount").unwrap_or(group);
862 let mut property_parts = property_expr.splitn(2, '/');
863 let property = property_parts.next().unwrap_or("");
864 let operators = property_parts.next().unwrap_or("");
865 let players: Vec<PlayerId> = if kind.is_empty() || kind == "Players" {
866 game.alive_players()
867 } else if kind == "Opponents" {
868 game.alive_players()
869 .into_iter()
870 .filter(|&pid| crate::player::player_predicates::is_opponent_of(game, controller, pid))
871 .collect()
872 } else if kind == "Remembered" {
873 game.card(source_id).remembered_players.clone()
874 } else if kind.starts_with("PropertyYou") {
875 vec![controller]
876 } else if let Some(property) = kind.strip_prefix("Property") {
877 game.alive_players()
878 .into_iter()
879 .filter(|&pid| {
880 crate::player::player_has_property(pid, property, game, source_id, controller, sa)
881 })
882 .collect()
883 } else if let Some(defined) = kind.strip_prefix("Defined") {
884 crate::ability::ability_utils::resolve_defined_players_with_sa(
885 defined, sa, controller, game,
886 )
887 } else {
888 Vec::new()
889 };
890
891 if players.is_empty() {
892 return 0;
893 }
894
895 if property.eq_ignore_ascii_case("Amount") {
896 return do_x_math(
897 players.len() as i32,
898 operators,
899 game,
900 source_id,
901 controller,
902 sa,
903 );
904 }
905 if let Some(rest) = property.strip_prefix("Highest") {
906 return do_x_math(
907 players
908 .iter()
909 .map(|&pid| player_x_property(pid, rest, game, source_id, controller, sa))
910 .max()
911 .unwrap_or(0),
912 operators,
913 game,
914 source_id,
915 controller,
916 sa,
917 );
918 }
919 if let Some(rest) = property.strip_prefix("Lowest") {
920 return do_x_math(
921 players
922 .iter()
923 .map(|&pid| player_x_property(pid, rest, game, source_id, controller, sa))
924 .min()
925 .unwrap_or(0),
926 operators,
927 game,
928 source_id,
929 controller,
930 sa,
931 );
932 }
933 if property.eq_ignore_ascii_case("TiedForHighestLife") {
934 let max_life = players
935 .iter()
936 .map(|&pid| game.player(pid).life)
937 .max()
938 .unwrap_or(i32::MIN);
939 return do_x_math(
940 players
941 .iter()
942 .filter(|&&pid| game.player(pid).life == max_life)
943 .count() as i32,
944 operators,
945 game,
946 source_id,
947 controller,
948 sa,
949 );
950 }
951 if property.eq_ignore_ascii_case("TiedForLowestLife") {
952 let min_life = players
953 .iter()
954 .map(|&pid| game.player(pid).life)
955 .min()
956 .unwrap_or(i32::MAX);
957 return do_x_math(
958 players
959 .iter()
960 .filter(|&&pid| game.player(pid).life == min_life)
961 .count() as i32,
962 operators,
963 game,
964 source_id,
965 controller,
966 sa,
967 );
968 }
969 if let Some(raw_property) = property.strip_prefix("HasProperty") {
970 return do_x_math(
971 players
972 .into_iter()
973 .filter(|&pid| {
974 crate::player::player_has_property(
975 pid,
976 raw_property,
977 game,
978 source_id,
979 controller,
980 sa,
981 )
982 })
983 .count() as i32,
984 operators,
985 game,
986 source_id,
987 controller,
988 sa,
989 );
990 }
991 if let Some(rest) = property.strip_prefix("Condition") {
992 if let Some((lhs, prop_expr)) = rest.split_once(' ') {
993 let (cmp, rhs_expr) = if lhs.is_empty() {
994 ("GE", "1")
995 } else if lhs.len() >= 2 {
996 (&lhs[..2], &lhs[2..])
997 } else {
998 ("GE", "1")
999 };
1000 let rhs = resolve_svar_expression(rhs_expr, game, source_id, controller, sa);
1001 return do_x_math(
1002 players
1003 .into_iter()
1004 .filter(|&pid| {
1005 compare_expr(
1006 player_x_property(pid, prop_expr, game, source_id, controller, sa),
1007 &format!("{cmp}{rhs}"),
1008 )
1009 })
1010 .count() as i32,
1011 operators,
1012 game,
1013 source_id,
1014 controller,
1015 sa,
1016 );
1017 }
1018 }
1019
1020 do_x_math(
1021 players
1022 .into_iter()
1023 .map(|pid| player_x_property(pid, property, game, source_id, controller, sa))
1024 .sum(),
1025 operators,
1026 game,
1027 source_id,
1028 controller,
1029 sa,
1030 )
1031}
1032
1033pub fn resolve_numeric_svar(
1047 game: &GameState,
1048 sa: &SpellAbility,
1049 param_name: &str,
1050 default: i32,
1051) -> i32 {
1052 let Some(value) = sa.ir.semantic_numeric_params.get(param_name) else {
1053 return default;
1054 };
1055 resolve_semantic_numeric_value(game, sa, value, default)
1056}
1057
1058fn resolve_semantic_numeric_value(
1059 game: &GameState,
1060 sa: &SpellAbility,
1061 value: &NumericParamIr,
1062 default: i32,
1063) -> i32 {
1064 match value {
1065 NumericParamIr::Integer(value) => *value,
1066 NumericParamIr::Amount(amount) => amount.resolve_for_spell_ability(game, sa, default),
1067 NumericParamIr::SVarReference(names) => match names.as_slice() {
1068 [name] => resolve_numeric_value(game, sa, name, default),
1069 [] => default,
1070 _ => names
1071 .iter()
1072 .map(|name| resolve_numeric_value(game, sa, name, default))
1073 .sum(),
1074 },
1075 NumericParamIr::Raw(raw) => resolve_numeric_value(game, sa, raw, default),
1076 }
1077}
1078
1079pub fn resolve_numeric_value(
1082 game: &GameState,
1083 sa: &SpellAbility,
1084 raw_val: &str,
1085 default: i32,
1086) -> i32 {
1087 let val_str = raw_val.trim();
1088 if val_str.is_empty() {
1089 return default;
1090 }
1091
1092 if let Ok(n) = val_str.parse::<i32>() {
1094 return n;
1095 }
1096 if let Some(stripped) = val_str.strip_prefix('+') {
1098 if let Ok(n) = stripped.parse::<i32>() {
1099 return n;
1100 }
1101 }
1102
1103 let (sign, val_str) = if let Some(stripped) = val_str.strip_prefix('-') {
1105 (-1, stripped.trim())
1106 } else if let Some(stripped) = val_str.strip_prefix('+') {
1107 (1, stripped.trim())
1108 } else {
1109 (1, val_str)
1110 };
1111
1112 if let Some(source_id) = sa.source {
1113 if let Some(expression) = parse_script_svar_numeric_expression(val_str) {
1114 if let Some(value) = resolve_lowered_svar_expression(
1115 &expression,
1116 game,
1117 source_id,
1118 sa.activating_player,
1119 sa,
1120 ) {
1121 return sign * value;
1122 }
1123 }
1124 if let Some(value) =
1125 resolve_card_list_expr(val_str, game, source_id, sa.activating_player, sa)
1126 {
1127 return sign * value;
1128 }
1129 }
1130
1131 if val_str == "X" {
1133 if let Some(source_id) = sa.source {
1135 if let Some(svar_expr) = game.card(source_id).get_s_var("X") {
1136 if svar_expr.starts_with("Count$") {
1137 return sign
1138 * resolve_count_svar_for_sa(
1139 svar_expr,
1140 game,
1141 source_id,
1142 sa.activating_player,
1143 sa,
1144 );
1145 }
1146 if svar_expr.starts_with("PlayerCount") {
1147 return sign
1148 * resolve_player_count_svar(
1149 svar_expr,
1150 game,
1151 source_id,
1152 sa.activating_player,
1153 sa,
1154 );
1155 }
1156 if let Some(value) = resolve_paid_hash_expr(svar_expr, game, source_id, sa) {
1157 return sign * value;
1158 }
1159 if svar_expr.starts_with("TriggerCount$")
1160 || svar_expr.starts_with("TriggerCountMax$")
1161 {
1162 return sign
1163 * resolve_trigger_count_svar(
1164 svar_expr,
1165 game,
1166 source_id,
1167 sa.activating_player,
1168 sa,
1169 );
1170 }
1171 if let Some(expression) = parse_script_svar_numeric_expression(svar_expr) {
1172 if let Some(value) = resolve_lowered_svar_expression(
1173 &expression,
1174 game,
1175 source_id,
1176 sa.activating_player,
1177 sa,
1178 ) {
1179 return sign * value;
1180 }
1181 }
1182 if let Some(value) = resolve_spell_ability_expr(svar_expr, game, sa) {
1183 return sign * value;
1184 }
1185 if let Some(value) =
1186 resolve_card_list_expr(svar_expr, game, source_id, sa.activating_player, sa)
1187 {
1188 return sign * value;
1189 }
1190 if let Some(value) =
1193 crate::lki::resolve_triggered_card_lki_svar(game, sa, svar_expr)
1194 {
1195 return sign * value;
1196 }
1197 if let Some(value) =
1198 resolve_direct_player_expr(svar_expr, game, source_id, sa.activating_player, sa)
1199 {
1200 return sign * value;
1201 }
1202 return sign * evaluate_svar(svar_expr, sa);
1203 }
1204 }
1205 return sign * sa.x_mana_cost_paid as i32;
1207 }
1208
1209 if let Some(source_id) = sa.source {
1211 if let Some(svar_expr) = game.card(source_id).get_s_var(val_str.trim()) {
1212 if svar_expr.starts_with("Count$") {
1214 return sign
1215 * resolve_count_svar_for_sa(
1216 svar_expr,
1217 game,
1218 source_id,
1219 sa.activating_player,
1220 sa,
1221 );
1222 }
1223 if svar_expr.starts_with("PlayerCount") {
1224 return sign
1225 * resolve_player_count_svar(
1226 svar_expr,
1227 game,
1228 source_id,
1229 sa.activating_player,
1230 sa,
1231 );
1232 }
1233 if let Some(value) = resolve_paid_hash_expr(svar_expr, game, source_id, sa) {
1234 return sign * value;
1235 }
1236 if let Some(expression) = parse_script_svar_numeric_expression(svar_expr) {
1237 if let Some(value) = resolve_lowered_svar_expression(
1238 &expression,
1239 game,
1240 source_id,
1241 sa.activating_player,
1242 sa,
1243 ) {
1244 return sign * value;
1245 }
1246 }
1247 if let Some(value) = resolve_spell_ability_expr(svar_expr, game, sa) {
1248 return sign * value;
1249 }
1250 if let Some(value) =
1251 resolve_card_list_expr(svar_expr, game, source_id, sa.activating_player, sa)
1252 {
1253 return sign * value;
1254 }
1255 if let Some(value) = crate::lki::resolve_triggered_card_lki_svar(game, sa, svar_expr) {
1258 return sign * value;
1259 }
1260 let eval = evaluate_svar(svar_expr, sa);
1264 if eval != 0 || svar_expr.starts_with("Number$") || svar_expr.starts_with("Count$") {
1265 return sign * eval;
1266 }
1267 if let Some(value) =
1268 resolve_direct_player_expr(svar_expr, game, source_id, sa.activating_player, sa)
1269 {
1270 return sign * value;
1271 }
1272 return sign * eval;
1273 }
1274 }
1275
1276 default
1277}
1278
1279fn resolve_paid_hash_expr(
1280 expr: &str,
1281 game: &GameState,
1282 source_id: CardId,
1283 sa: &SpellAbility,
1284) -> Option<i32> {
1285 let (paid_key, property) = expr.split_once('$')?;
1286 resolve_paid_hash_property(paid_key, property, game, source_id, sa)
1287}
1288
1289fn resolve_paid_hash_property(
1290 paid_key: &str,
1291 property: &str,
1292 game: &GameState,
1293 source_id: CardId,
1294 sa: &SpellAbility,
1295) -> Option<i32> {
1296 let paid_values = sa.paid_hash.get(paid_key)?;
1297 let paid_cards: Vec<CardId> = paid_values
1298 .iter()
1299 .filter_map(|value| {
1300 let raw = value.strip_prefix("Card#").unwrap_or(value);
1301 raw.parse::<u32>().ok().map(CardId)
1302 })
1303 .filter(|cid| cid.index() < game.cards.len())
1304 .collect();
1305
1306 if property.starts_with("TapPowerValue") {
1307 return Some(
1308 paid_cards
1309 .iter()
1310 .map(|&cid| crate::cost::cost_tap_type::tap_power_value(game, cid, Some(sa)))
1311 .sum(),
1312 );
1313 }
1314
1315 Some(crate::ability::ability_utils::handle_paid(
1316 game,
1317 &paid_cards,
1318 property,
1319 source_id,
1320 ))
1321}
1322
1323pub fn evaluate_svar(expr: &str, sa: &SpellAbility) -> i32 {
1327 if let Some(rest) = expr
1329 .strip_prefix("Count$xPaid")
1330 .or_else(|| expr.strip_prefix("Count$XPaid"))
1331 {
1332 let operators = rest.strip_prefix('/').unwrap_or(rest);
1333 return apply_simple_operator_chain(sa.x_mana_cost_paid as i32, operators);
1334 }
1335 if expr == "Count$Converge" || expr == "Count$Sunburst" {
1337 return 0; }
1339 if expr == "Count$TriggerRememberAmount" {
1340 return sa.trigger_remembered_amount;
1341 }
1342 if let Some(rest) = expr.strip_prefix("TriggerCount$") {
1343 let (key, operators) = rest.split_once('/').unwrap_or((rest, ""));
1344 let values = parse_trigger_int_values(sa, key.trim());
1345 let count = values.into_iter().sum::<i32>();
1346 return apply_simple_operator_chain(count, operators);
1347 }
1348 if let Some(rest) = expr.strip_prefix("TriggerCountMax$") {
1349 let (key, operators) = rest.split_once('/').unwrap_or((rest, ""));
1350 let count = parse_trigger_int_values(sa, key.trim())
1351 .into_iter()
1352 .max()
1353 .unwrap_or(0);
1354 return apply_simple_operator_chain(count, operators);
1355 }
1356 if expr == "TriggerCount$Result" {
1357 return trigger_result_values(sa).into_iter().sum();
1358 }
1359 if expr == "TriggerCountMax$Result" {
1360 return trigger_result_values(sa).into_iter().max().unwrap_or(0);
1361 }
1362 if expr == "TriggerCount$Amount" {
1365 return sa.trigger_remembered_amount.max(1);
1366 }
1367 if expr == "Count$KickedCount" {
1369 return sa.kick_count as i32;
1370 }
1371 if let Some(rest) = expr.strip_prefix("Count$Kicked.") {
1373 let parts: Vec<&str> = rest.splitn(2, '.').collect();
1374 if parts.len() == 2 {
1375 let kicked_val = parts[0].parse::<i32>().unwrap_or(0);
1376 let normal_val = parts[1].parse::<i32>().unwrap_or(0);
1377 return if sa.kicked { kicked_val } else { normal_val };
1378 }
1379 }
1380 if let Some(rest) = expr.strip_prefix("Number$") {
1382 return rest.trim().parse::<i32>().unwrap_or(0);
1383 }
1384 expr.parse::<i32>().unwrap_or(0)
1386}
1387
1388fn trigger_result_values(sa: &SpellAbility) -> Vec<i32> {
1389 sa.trigger_objects
1390 .get(&crate::ability::AbilityKey::Result)
1391 .map(|raw| {
1392 raw.split(',')
1393 .filter_map(|part| part.trim().parse::<i32>().ok())
1394 .collect::<Vec<_>>()
1395 })
1396 .unwrap_or_default()
1397}
1398
1399pub fn resolve_count_svar(
1403 expr: &str,
1404 game: &GameState,
1405 source_id: CardId,
1406 controller: PlayerId,
1407) -> i32 {
1408 resolve_count_svar_for_sa(
1409 expr,
1410 game,
1411 source_id,
1412 controller,
1413 &crate::spellability::SpellAbility::new_empty(Some(source_id), controller),
1414 )
1415}
1416
1417pub fn resolve_cost_amount_svar(
1424 game: &GameState,
1425 source: &crate::card::Card,
1426 name: &str,
1427 caster: PlayerId,
1428) -> i32 {
1429 if let Ok(n) = name.parse::<i32>() {
1430 return n;
1431 }
1432 let Some(expr) = source.get_s_var(name) else {
1433 return 0;
1434 };
1435 evaluate_cost_amount_count_expr(game, source, expr, caster)
1436}
1437
1438fn evaluate_cost_amount_count_expr(
1439 game: &GameState,
1440 source: &crate::card::Card,
1441 expr: &str,
1442 caster: PlayerId,
1443) -> i32 {
1444 use crate::card::Card;
1445 use forge_foundation::ZoneType;
1446 if expr == "Count$xPaid" || expr == "Count$XPaid" {
1447 return source
1448 .svars
1449 .get("XPaid")
1450 .and_then(|s| s.parse::<i32>().ok())
1451 .unwrap_or(0);
1452 }
1453 if let Some(counter_name) = expr.strip_prefix("Count$CardCounters.") {
1454 let counter_type = crate::ability::ability_utils::parse_counter_type(counter_name);
1455 return source.counter_count(&counter_type);
1456 }
1457 if let Some(rest) = expr.strip_prefix("Count$ThisTurnCast_") {
1458 if rest.contains("YouCtrl") || rest.contains("YouOwn") {
1459 return game.player(source.controller).spells_cast_this_turn;
1460 }
1461 return game.player(caster).spells_cast_this_turn;
1462 }
1463 if expr == "Count$YourLifeTotal" {
1464 return game.player(source.controller).life;
1465 }
1466 if let Some(rest) = expr.strip_prefix("Count$Valid ") {
1467 let (filter, aggregator) = rest.split_once('$').unwrap_or((rest, ""));
1468 let selector = crate::parsing::cached_compiled_selector(filter);
1469 let matches: Vec<&Card> = game
1470 .cards
1471 .iter()
1472 .filter(|c| c.zone == ZoneType::Battlefield)
1473 .filter(|c| {
1474 crate::card::valid_filter::matches_valid_card_selector_in_game(
1475 &selector, c, source, game,
1476 )
1477 })
1478 .collect();
1479 return match aggregator {
1480 "" | "Amount" => matches.len() as i32,
1481 "GreatestCardManaCost" => matches.iter().map(|c| c.mana_cost.cmc()).max().unwrap_or(0),
1482 _ => 0,
1483 };
1484 }
1485 if expr.contains("Graveyard") && expr.contains("YouCtrl") {
1486 return game
1487 .cards_in_zone(ZoneType::Graveyard, source.controller)
1488 .len() as i32;
1489 }
1490 expr.strip_prefix("Count$")
1491 .and_then(|s| s.parse::<i32>().ok())
1492 .unwrap_or(0)
1493}
1494
1495pub fn resolve_count_svar_for_sa(
1496 expr: &str,
1497 game: &GameState,
1498 source_id: CardId,
1499 controller: PlayerId,
1500 sa: &SpellAbility,
1501) -> i32 {
1502 use forge_foundation::ZoneType;
1503
1504 if let Some(rest) = expr
1505 .strip_prefix("Count$xPaid")
1506 .or_else(|| expr.strip_prefix("Count$XPaid"))
1507 {
1508 let operators = rest.strip_prefix('/').unwrap_or(rest);
1509 return do_x_math(
1510 sa.x_mana_cost_paid as i32,
1511 operators,
1512 game,
1513 source_id,
1514 controller,
1515 sa,
1516 );
1517 }
1518 if let Some(operators) = expr.strip_prefix("Count$CastTotalManaSpent") {
1519 let operators = operators.strip_prefix('/').unwrap_or(operators);
1520 return do_x_math(
1521 game.card(source_id).paying_mana_to_cast.len() as i32,
1522 operators,
1523 game,
1524 source_id,
1525 controller,
1526 sa,
1527 );
1528 }
1529
1530 if expr == "Count$TriggerRememberAmount" {
1531 return sa.trigger_remembered_amount;
1532 }
1533 if expr == "Count$ChosenNumber" {
1534 return game.card(source_id).chosen_number.unwrap_or(0);
1535 }
1536 if expr == "TriggerCount$Result" {
1537 return trigger_result_values(sa).into_iter().sum();
1538 }
1539 if expr == "TriggerCountMax$Result" {
1540 return trigger_result_values(sa).into_iter().max().unwrap_or(0);
1541 }
1542
1543 if expr == "Count$Converge" || expr == "Count$Sunburst" {
1544 return game.card(source_id).sunburst_count();
1545 }
1546
1547 if let Some(operators) = expr.strip_prefix("Count$FinalChapterNr") {
1548 let operators = operators.strip_prefix('/').unwrap_or(operators);
1549 return do_x_math(
1550 game.card(source_id).get_final_chapter_nr(),
1551 operators,
1552 game,
1553 source_id,
1554 controller,
1555 sa,
1556 );
1557 }
1558
1559 if expr == "Count$YourSpeed" {
1560 return game.player(controller).speed;
1561 }
1562
1563 if let Some(operators) = expr.strip_prefix("Count$YourLifeTotal") {
1564 let operators = operators.strip_prefix('/').unwrap_or(operators);
1565 return do_x_math(
1566 game.player(controller).life,
1567 operators,
1568 game,
1569 source_id,
1570 controller,
1571 sa,
1572 );
1573 }
1574
1575 if let Some(operators) = expr.strip_prefix("Count$YouDrewThisTurn") {
1576 let operators = operators.strip_prefix('/').unwrap_or(operators);
1577 return do_x_math(
1578 game.player(controller).drawn_this_turn,
1579 operators,
1580 game,
1581 source_id,
1582 controller,
1583 sa,
1584 );
1585 }
1586
1587 if let Some(operators) = expr.strip_prefix("Count$OppGreatestLifeTotal") {
1588 let operators = operators.strip_prefix('/').unwrap_or(operators);
1589 let highest_life = game
1590 .alive_players()
1591 .into_iter()
1592 .filter(|&pid| crate::player::player_predicates::is_opponent_of(game, controller, pid))
1593 .map(|pid| game.player(pid).life)
1594 .max()
1595 .unwrap_or(0);
1596 return do_x_math(highest_life, operators, game, source_id, controller, sa);
1597 }
1598
1599 if let Some(rest) = expr.strip_prefix("Count$Metalcraft.") {
1601 let parts: Vec<&str> = rest.splitn(2, '.').collect();
1602 if parts.len() == 2 {
1603 let yes = parts[0].parse::<i32>().unwrap_or(1);
1604 let no = parts[1].parse::<i32>().unwrap_or(0);
1605 return if game.player_has_metalcraft(controller) {
1606 yes
1607 } else {
1608 no
1609 };
1610 }
1611 }
1612
1613 if let Some(rest) = expr.strip_prefix("Count$MaxSpeed.") {
1614 let parts: Vec<&str> = rest.splitn(2, '.').collect();
1615 if parts.len() == 2 {
1616 let yes = parts[0].parse::<i32>().unwrap_or(1);
1617 let no = parts[1].parse::<i32>().unwrap_or(0);
1618 return if game.player(controller).speed == 4 {
1619 yes
1620 } else {
1621 no
1622 };
1623 }
1624 }
1625
1626 if expr == "Count$AttackersDeclared" {
1627 return game
1628 .cards
1629 .iter()
1630 .filter(|card| {
1631 card.controller == controller && card.attacked_this_turn && card.is_creature()
1632 })
1633 .count() as i32;
1634 }
1635
1636 if expr == "Count$TopOfLibraryCMC" {
1637 return game
1638 .cards_in_zone(ZoneType::Library, controller)
1639 .last()
1640 .map(|&cid| game.card(cid).mana_value())
1641 .unwrap_or(0);
1642 }
1643
1644 if let Some(rest) = expr.strip_prefix("Count$OptionalGenericCostPaid.") {
1645 let parts: Vec<&str> = rest.splitn(2, '.').collect();
1646 if parts.len() == 2 {
1647 let paid_val = parts[0].parse::<i32>().unwrap_or(1);
1648 let unpaid_val = parts[1].parse::<i32>().unwrap_or(0);
1649 return if sa.optional_generic_cost_paid {
1650 paid_val
1651 } else {
1652 unpaid_val
1653 };
1654 }
1655 }
1656
1657 if expr == "Count$KickedCount" {
1658 return sa.kick_count as i32;
1659 }
1660 if let Some(rest) = expr.strip_prefix("Count$Kicked.") {
1661 let parts: Vec<&str> = rest.splitn(2, '.').collect();
1662 if parts.len() == 2 {
1663 let chosen = if sa.kicked { parts[0] } else { parts[1] };
1664 return resolve_svar_expression(chosen, game, source_id, controller, sa);
1665 }
1666 }
1667
1668 if let Some(rest) = expr.strip_prefix("Count$UrzaLands.") {
1671 let parts: Vec<&str> = rest.splitn(2, '.').collect();
1672 if parts.len() == 2 {
1673 let chosen = if crate::player::player_predicates::has_urza_lands(game, controller) {
1674 parts[0]
1675 } else {
1676 parts[1]
1677 };
1678 return resolve_svar_expression(chosen, game, source_id, controller, sa);
1679 }
1680 }
1681
1682 if let Some(rest) = expr.strip_prefix("Count$PromisedGift.") {
1684 let parts: Vec<&str> = rest.splitn(2, '.').collect();
1685 if parts.len() == 2 {
1686 let promised_val = parts[0].parse::<i32>().unwrap_or(1);
1687 let not_promised_val = parts[1].parse::<i32>().unwrap_or(0);
1688 return if game.card(source_id).promised_gift.is_some() {
1689 promised_val
1690 } else {
1691 not_promised_val
1692 };
1693 }
1694 }
1695 if expr == "Count$PromisedGift" {
1696 return if game.card(source_id).promised_gift.is_some() {
1697 1
1698 } else {
1699 0
1700 };
1701 }
1702
1703 if let Some(rest) = expr.strip_prefix("Count$Valid") {
1709 let (rest, operators) = rest.split_once('/').unwrap_or((rest, ""));
1710 let mut parts = rest.trim_start().splitn(2, ' ');
1711 let zone_part = parts.next().unwrap_or("").trim();
1712 let restrictions = parts.next().unwrap_or("").trim();
1713 let (restrictions, aggregator) = restrictions.split_once('$').unwrap_or((restrictions, ""));
1714 if !restrictions.is_empty() {
1715 let zones: Vec<ZoneType> = if zone_part.is_empty() {
1716 vec![ZoneType::Battlefield]
1717 } else {
1718 zone_part
1719 .split(',')
1720 .filter_map(crate::ability::ability_utils::parse_zone_type)
1721 .collect()
1722 };
1723 if !zones.is_empty() {
1724 let source = game.card(source_id);
1725 let selector = crate::parsing::cached_compiled_selector(restrictions);
1726 let targeted_players: Vec<crate::ids::PlayerId> =
1728 sa.target_chosen.target_player.into_iter().collect();
1729 let targeted_cards: Vec<crate::ids::CardId> =
1730 sa.target_chosen.target_card.into_iter().collect();
1731 let ctx = crate::card::valid_filter::MatchContext::from_source(source)
1732 .with_game(game)
1733 .with_targets(&targeted_cards, &targeted_players)
1734 .with_spell_ability(sa);
1735 let matches: Vec<&crate::card::Card> = game
1736 .cards
1737 .iter()
1738 .filter(|card| {
1739 zones.contains(&card.zone)
1740 && crate::card::valid_filter::matches_valid_card_selector_with_context(
1741 &selector, card, ctx,
1742 )
1743 })
1744 .collect();
1745 let count = match aggregator {
1746 "" | "Amount" => matches.len() as i32,
1747 "GreatestCardManaCost" => {
1748 matches.iter().map(|c| c.mana_cost.cmc()).max().unwrap_or(0)
1749 }
1750 _ => 0,
1751 };
1752 return do_x_math(count, operators, game, source_id, controller, sa);
1753 }
1754 }
1755 }
1756
1757 if let Some(filter_str) = expr.strip_prefix("Count$Valid ") {
1761 let (filter_str, operators) = filter_str.split_once('/').unwrap_or((filter_str, ""));
1762 let (filter_str, greatest_power) =
1764 if let Some(base) = filter_str.strip_suffix("$GreatestCardPower") {
1765 (base, true)
1766 } else {
1767 (filter_str, false)
1768 };
1769
1770 let count_distinct_colors = filter_str.ends_with("$Colors");
1773 let filter_str = if count_distinct_colors {
1774 filter_str.trim_end_matches("$Colors")
1775 } else {
1776 filter_str
1777 };
1778
1779 let (filter_str, multiplier) = crate::parsing::strip_times_multiplier(filter_str);
1781
1782 let battlefield = game.cards_in_zone(ZoneType::Battlefield, controller);
1783 let opp = game.opponent_of(controller);
1785 let opp_battlefield = game.cards_in_zone(ZoneType::Battlefield, opp);
1786
1787 let has_you_ctrl =
1788 filter_str.contains(fc::YOU_CTRL) || filter_str.contains(fc::YOU_CONTROL);
1789
1790 let cards_to_check: Vec<CardId> = if has_you_ctrl {
1791 battlefield.to_vec()
1792 } else {
1793 battlefield
1794 .iter()
1795 .chain(opp_battlefield.iter())
1796 .copied()
1797 .collect()
1798 };
1799
1800 let source = game.card(source_id);
1801 let selector = crate::parsing::cached_compiled_selector(filter_str);
1802 if greatest_power {
1803 let mut max_power = 0;
1805 for &cid in &cards_to_check {
1806 let card = game.card(cid);
1807 if crate::card::valid_filter::matches_valid_card_selector_in_game(
1808 &selector, card, source, game,
1809 ) {
1810 max_power = max_power.max(card.power());
1811 }
1812 }
1813 return do_x_math(max_power, operators, game, source_id, controller, sa);
1814 } else if count_distinct_colors {
1815 let mut mask: u8 = 0;
1816 for &cid in &cards_to_check {
1817 let card = game.card(cid);
1818 if crate::card::valid_filter::matches_valid_card_selector_in_game(
1819 &selector, card, source, game,
1820 ) {
1821 mask |= card.color.mask();
1822 }
1823 }
1824 return do_x_math(
1825 (mask.count_ones() as i32) * multiplier,
1826 operators,
1827 game,
1828 source_id,
1829 controller,
1830 sa,
1831 );
1832 } else {
1833 let mut count = 0;
1834 for &cid in &cards_to_check {
1835 let card = game.card(cid);
1836 if crate::card::valid_filter::matches_valid_card_selector_in_game(
1837 &selector, card, source, game,
1838 ) {
1839 count += 1;
1840 }
1841 }
1842 return do_x_math(
1843 count * multiplier,
1844 operators,
1845 game,
1846 source_id,
1847 controller,
1848 sa,
1849 );
1850 }
1851 }
1852
1853 if let Some(color_str) = expr.strip_prefix("Count$Devotion.") {
1855 let color_mask: u16 = match color_str.to_uppercase().as_str() {
1856 "W" | "WHITE" => forge_foundation::ManaAtom::WHITE,
1857 "U" | "BLUE" => forge_foundation::ManaAtom::BLUE,
1858 "B" | "BLACK" => forge_foundation::ManaAtom::BLACK,
1859 "R" | "RED" => forge_foundation::ManaAtom::RED,
1860 "G" | "GREEN" => forge_foundation::ManaAtom::GREEN,
1861 _ => 0,
1862 };
1863 if color_mask != 0 {
1864 let battlefield = game.cards_in_zone(ZoneType::Battlefield, controller);
1865 let mut count = 0i32;
1866 for &cid in battlefield {
1867 let card = game.card(cid);
1868 for shard in card.mana_cost.shards() {
1869 if (shard.shard() & color_mask) != 0 {
1870 count += 1;
1871 }
1872 }
1873 }
1874 return count;
1875 }
1876 }
1877
1878 if let Some(rest) = expr.strip_prefix("Count$Compare ") {
1881 let parts: Vec<&str> = rest.splitn(2, ' ').collect();
1882 if parts.len() == 2 {
1883 let svar_name = parts[0];
1884 let cond_parts: Vec<&str> = parts[1].splitn(3, '.').collect();
1885 if cond_parts.len() == 3 {
1886 let svar_val = if let Some(svar_expr) = game.card(source_id).get_s_var(svar_name) {
1888 if svar_expr.starts_with("Count$") || svar_expr.starts_with("PlayerCount") {
1889 resolve_svar_expression(svar_expr, game, source_id, controller, sa)
1890 } else {
1891 svar_expr.parse::<i32>().unwrap_or(0)
1892 }
1893 } else {
1894 svar_name.parse::<i32>().unwrap_or(0)
1895 };
1896
1897 let cond = cond_parts[0];
1899 let result = compare_expr(svar_val, cond);
1900
1901 let resolve_branch = |raw: &str| {
1902 raw.parse::<i32>().unwrap_or_else(|_| {
1903 if let Some(svar_expr) = game.card(source_id).get_s_var(raw) {
1904 resolve_svar_expression(svar_expr, game, source_id, controller, sa)
1905 } else {
1906 resolve_svar_expression(raw, game, source_id, controller, sa)
1907 }
1908 })
1909 };
1910 let if_true = resolve_branch(cond_parts[1]);
1911 let if_false = resolve_branch(cond_parts[2]);
1912 return if result { if_true } else { if_false };
1913 }
1914 }
1915 }
1916
1917 if let Some(operators) = expr.strip_prefix("Count$ColorsColorIdentity") {
1918 let operators = operators.strip_prefix('/').unwrap_or(operators);
1919 let count = game
1920 .player_commander_color_identity(game.card(source_id).controller)
1921 .len() as i32;
1922 return do_x_math(count, operators, game, source_id, controller, sa);
1923 }
1924
1925 if expr == "Count$CardPower" {
1927 return game.card(source_id).power();
1928 }
1929 if expr == "Count$CardToughness" {
1931 return game.card(source_id).toughness();
1932 }
1933 if let Some(operators) = expr.strip_prefix("Count$YourTurns") {
1934 let operators = operators.strip_prefix('/').unwrap_or(operators);
1935 return do_x_math(
1936 game.player(controller).statistics.turns_played,
1937 operators,
1938 game,
1939 source_id,
1940 controller,
1941 sa,
1942 );
1943 }
1944 if let Some(counter_type) = expr.strip_prefix("Count$CardCounters.") {
1946 let ct = crate::ability::effects::parse_counter_type(counter_type);
1947 return *game.card(source_id).counters.get(&ct).unwrap_or(&0);
1948 }
1949
1950 if expr == "Count$TotalDamageDoneByThisTurn" {
1952 return game.card(source_id).total_damage_done_this_turn;
1953 }
1954
1955 if let Some(rest) = expr
1960 .strip_prefix("Count$CardsInYour")
1961 .or_else(|| expr.strip_prefix("Count$InYour"))
1962 {
1963 let zone = match rest {
1964 "Hand" => Some(ZoneType::Hand),
1965 "Yard" | "Graveyard" => Some(ZoneType::Graveyard),
1966 "Library" => Some(ZoneType::Library),
1967 "Exile" => Some(ZoneType::Exile),
1968 "Battlefield" => Some(ZoneType::Battlefield),
1969 _ => None,
1970 };
1971 if let Some(zone) = zone {
1972 return game.cards_in_zone(zone, controller).len() as i32;
1973 }
1974 }
1975
1976 if let Some(rest) = expr.strip_prefix("Count$RememberedNumber") {
1977 let operators = rest.strip_prefix('/').unwrap_or(rest);
1978 let count = game.card(source_id).remembered_cmc.iter().sum();
1979 return do_x_math(count, operators, game, source_id, controller, sa);
1980 }
1981
1982 if let Some(rest) = expr.strip_prefix("Count$RememberedSize") {
1985 let operators = rest.strip_prefix('/').unwrap_or(rest);
1986 let card = game.card(source_id);
1987 let count =
1988 card.remembered_cards.len() + card.remembered_players.len() + card.remembered_cmc.len();
1989 return do_x_math(count as i32, operators, game, source_id, controller, sa);
1990 }
1991
1992 expr.parse::<i32>().unwrap_or_else(|_| {
1993 eprintln!("Unrecognized Count expression, returning 0 for: {expr}");
1994 0
1995 })
1996}
1997
1998#[allow(dead_code)]
2000fn valid_card_matches_with_source(
2001 filter: &str,
2002 card: &crate::card::Card,
2003 controller: PlayerId,
2004 source_id: CardId,
2005 chosen_type: Option<&str>,
2006) -> bool {
2007 let parts: Vec<&str> = filter.split('.').collect();
2008 let base_type = parts.first().copied().unwrap_or("");
2009
2010 let type_ok = match base_type {
2012 fc::CREATURE => card.is_creature(),
2013 fc::LAND => card.is_land(),
2014 fc::ARTIFACT => card.type_line.is_artifact(),
2015 fc::ENCHANTMENT => card.type_line.is_enchantment(),
2016 fc::PLANESWALKER => card.type_line.is_planeswalker(),
2017 fc::PERMANENT | fc::CARD => true,
2018 _ => card.type_line.has_subtype(base_type),
2020 };
2021 if !type_ok {
2022 return false;
2023 }
2024
2025 for &dot_qual in &parts[1..] {
2027 for sub_qual in dot_qual.split('+') {
2028 let sub_qual = sub_qual.trim();
2029 if sub_qual.eq_ignore_ascii_case(fc::YOU_CTRL)
2030 || sub_qual.eq_ignore_ascii_case(fc::YOU_CONTROL)
2031 {
2032 if card.controller != controller {
2033 return false;
2034 }
2035 } else if sub_qual.eq_ignore_ascii_case(fc::SELF_REF) {
2036 if card.id != source_id {
2037 return false;
2038 }
2039 } else if sub_qual.eq_ignore_ascii_case(fc::OTHER) {
2040 if card.id == source_id {
2041 return false;
2042 }
2043 } else if sub_qual.eq_ignore_ascii_case("ChosenType") {
2044 match chosen_type {
2047 Some(ct)
2048 if card.type_line.has_subtype(ct) || card.has_keyword("Changeling") => {}
2049 _ => return false,
2050 }
2051 } else if sub_qual.starts_with("counters_") {
2052 if !check_counter_qualifier(card, sub_qual) {
2054 return false;
2055 }
2056 }
2057 }
2058 }
2059 true
2060}
2061
2062#[allow(dead_code)]
2064fn check_counter_qualifier(card: &crate::card::Card, qual: &str) -> bool {
2065 let rest = match qual.strip_prefix("counters_") {
2066 Some(r) => r,
2067 None => return true,
2068 };
2069 let parts: Vec<&str> = rest.splitn(2, '_').collect();
2071 if parts.len() != 2 {
2072 return true;
2073 }
2074 let cond = parts[0];
2075 let counter_type = crate::ability::effects::parse_counter_type(parts[1]);
2076 let count = *card.counters.get(&counter_type).unwrap_or(&0);
2077
2078 compare_expr(count, cond)
2079}
2080
2081#[cfg(test)]
2082mod tests {
2083 use forge_foundation::{CardTypeLine, ColorSet, ManaCost};
2084
2085 use super::resolve_numeric_svar;
2086 use crate::card::Card;
2087 use crate::game::GameState;
2088 use crate::ids::{CardId, PlayerId};
2089 use crate::spellability::SpellAbility;
2090
2091 #[test]
2092 fn resolves_player_count_defined_life_total_twice() {
2093 let mut game = GameState::new(&["A", "B"], 20);
2094 let p0 = PlayerId(0);
2095 let p1 = PlayerId(1);
2096 game.player_mut(p1).life = 7;
2097
2098 let mut host = Card::new(
2099 CardId(0),
2100 "Host".to_string(),
2101 p0,
2102 CardTypeLine::parse("Creature"),
2103 ManaCost::parse(""),
2104 ColorSet::COLORLESS,
2105 Some(1),
2106 Some(1),
2107 vec![],
2108 vec![],
2109 );
2110 host.svars.insert(
2111 "X".to_string(),
2112 "PlayerCountDefinedTriggeredAttackedTarget$LifeTotal/Twice".to_string(),
2113 );
2114 let host_id = game.create_card(host);
2115
2116 let mut sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ GainLife | LifeAmount$ X");
2117 sa.set_triggering_object(crate::ability::AbilityKey::AttackedTarget, p1);
2118
2119 assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 14);
2120 }
2121
2122 #[test]
2123 fn resolves_player_count_highest_life_total() {
2124 let mut game = GameState::new(&["A", "B"], 20);
2125 let p0 = PlayerId(0);
2126 let p1 = PlayerId(1);
2127 game.player_mut(p0).life = 11;
2128 game.player_mut(p1).life = 17;
2129
2130 let mut host = Card::new(
2131 CardId(0),
2132 "Host".to_string(),
2133 p0,
2134 CardTypeLine::parse("Creature"),
2135 ManaCost::parse(""),
2136 ColorSet::COLORLESS,
2137 Some(1),
2138 Some(1),
2139 vec![],
2140 vec![],
2141 );
2142 host.svars.insert(
2143 "X".to_string(),
2144 "PlayerCountPlayers$HighestLifeTotal".to_string(),
2145 );
2146 let host_id = game.create_card(host);
2147
2148 let sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ GainLife | LifeAmount$ X");
2149 assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 17);
2150 }
2151
2152 #[test]
2153 fn resolves_triggered_target_life_total_half_up() {
2154 let mut game = GameState::new(&["A", "B"], 20);
2155 let p0 = PlayerId(0);
2156 let p1 = PlayerId(1);
2157 game.player_mut(p1).life = 9;
2158
2159 let mut host = Card::new(
2160 CardId(0),
2161 "Host".to_string(),
2162 p0,
2163 CardTypeLine::parse("Creature"),
2164 ManaCost::parse(""),
2165 ColorSet::COLORLESS,
2166 Some(1),
2167 Some(1),
2168 vec![],
2169 vec![],
2170 );
2171 host.svars.insert(
2172 "X".to_string(),
2173 "TriggeredTarget$LifeTotal/HalfUp".to_string(),
2174 );
2175 let host_id = game.create_card(host);
2176
2177 let mut sa = SpellAbility::new_simple(
2178 Some(host_id),
2179 p0,
2180 "DB$ LoseLife | Defined$ TriggeredTarget | LifeAmount$ X",
2181 );
2182 sa.set_triggering_object(crate::ability::AbilityKey::TargetPlayer, p1);
2183
2184 assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 5);
2185 }
2186
2187 #[test]
2188 fn resolves_player_count_minus_remembered_amount() {
2189 let mut game = GameState::new(&["A", "B"], 20);
2190 let p0 = PlayerId(0);
2191 let p1 = PlayerId(1);
2192
2193 let remembered = Card::new(
2194 CardId(1),
2195 "Remembered".to_string(),
2196 p1,
2197 CardTypeLine::parse("Creature"),
2198 ManaCost::parse(""),
2199 ColorSet::COLORLESS,
2200 Some(1),
2201 Some(1),
2202 vec![],
2203 vec![],
2204 );
2205 let remembered_id = game.create_card(remembered);
2206
2207 let mut host = Card::new(
2208 CardId(0),
2209 "Host".to_string(),
2210 p0,
2211 CardTypeLine::parse("Creature"),
2212 ManaCost::parse(""),
2213 ColorSet::COLORLESS,
2214 Some(1),
2215 Some(1),
2216 vec![],
2217 vec![],
2218 );
2219 host.svars.insert(
2220 "X".to_string(),
2221 "PlayerCountOpponents$Amount/Minus.Remembered$Amount".to_string(),
2222 );
2223 let host_id = game.create_card(host);
2224 game.card_mut(host_id).add_remembered_card(remembered_id);
2225
2226 let sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ Draw | NumCards$ X");
2227 assert_eq!(resolve_numeric_svar(&game, &sa, "NumCards", -1), 0);
2228 }
2229
2230 #[test]
2231 fn resolves_player_count_minus_empty_remembered_amount() {
2232 let mut game = GameState::new(&["A", "B"], 20);
2233 let p0 = PlayerId(0);
2234
2235 let mut host = Card::new(
2236 CardId(0),
2237 "Host".to_string(),
2238 p0,
2239 CardTypeLine::parse("Creature"),
2240 ManaCost::parse(""),
2241 ColorSet::COLORLESS,
2242 Some(1),
2243 Some(1),
2244 vec![],
2245 vec![],
2246 );
2247 host.svars.insert(
2248 "X".to_string(),
2249 "PlayerCountOpponents$Amount/Minus.Remembered$Amount".to_string(),
2250 );
2251 let host_id = game.create_card(host);
2252
2253 let sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ Draw | NumCards$ X");
2254 assert_eq!(resolve_numeric_svar(&game, &sa, "NumCards", -1), 1);
2255 }
2256
2257 #[test]
2258 fn resolves_player_count_remembered_life_lost_this_turn() {
2259 let mut game = GameState::new(&["A", "B"], 20);
2260 let p0 = PlayerId(0);
2261 let p1 = PlayerId(1);
2262
2263 game.player_mut(p1).life_lost_this_turn = 11;
2264
2265 let mut host = Card::new(
2266 CardId(0),
2267 "Host".to_string(),
2268 p0,
2269 CardTypeLine::parse("Creature"),
2270 ManaCost::parse(""),
2271 ColorSet::COLORLESS,
2272 Some(1),
2273 Some(1),
2274 vec![],
2275 vec![],
2276 );
2277 host.svars.insert(
2278 "X".to_string(),
2279 "PlayerCountRemembered$LifeLostThisTurn".to_string(),
2280 );
2281 let host_id = game.create_card(host);
2282 game.card_mut(host_id).add_remembered_player(p1);
2283
2284 let sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ LoseLife | LifeAmount$ X");
2285 assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", -1), 11);
2286 }
2287
2288 #[test]
2289 fn resolves_triggered_spell_ability_card_mana_cost_lki() {
2290 let mut game = GameState::new(&["A", "B"], 20);
2291 let p0 = PlayerId(0);
2292 let p1 = PlayerId(1);
2293
2294 let mut host = Card::new(
2295 CardId(0),
2296 "Host".to_string(),
2297 p0,
2298 CardTypeLine::parse("Creature"),
2299 ManaCost::parse(""),
2300 ColorSet::COLORLESS,
2301 Some(1),
2302 Some(1),
2303 vec![],
2304 vec![],
2305 );
2306 host.svars.insert(
2307 "X".to_string(),
2308 "TriggeredSpellAbility$CardManaCostLKI".to_string(),
2309 );
2310 let host_id = game.create_card(host);
2311
2312 let mut spell_card = Card::new(
2313 CardId(1),
2314 "Big Spell".to_string(),
2315 p1,
2316 CardTypeLine::parse("Sorcery"),
2317 ManaCost::parse("X U"),
2318 ColorSet::BLUE,
2319 None,
2320 None,
2321 vec![],
2322 vec![],
2323 );
2324 spell_card.set_zone(forge_foundation::ZoneType::Graveyard);
2325 let spell_id = game.create_card(spell_card);
2326
2327 let mut triggered_sa =
2328 SpellAbility::new_simple(Some(spell_id), p1, "SP$ DealDamage | NumDmg$ 1");
2329 triggered_sa.x_mana_cost_paid = 4;
2330
2331 let mut sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ GainLife | LifeAmount$ X");
2332 sa.set_triggering_spell_ability("SpellAbility", triggered_sa);
2333
2334 assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 5);
2335 }
2336
2337 #[test]
2338 fn resolves_count_your_speed_and_max_speed() {
2339 let mut game = GameState::new(&["A", "B"], 20);
2340 let p0 = PlayerId(0);
2341 game.player_mut(p0).speed = 4;
2342
2343 let mut host = Card::new(
2344 CardId(0),
2345 "Host".to_string(),
2346 p0,
2347 CardTypeLine::parse("Creature"),
2348 ManaCost::parse(""),
2349 ColorSet::COLORLESS,
2350 Some(1),
2351 Some(1),
2352 vec![],
2353 vec![],
2354 );
2355 host.svars
2356 .insert("X".to_string(), "Count$YourSpeed".to_string());
2357 host.svars
2358 .insert("Y".to_string(), "Count$MaxSpeed.2.1".to_string());
2359 let host_id = game.create_card(host);
2360
2361 let sa = SpellAbility::new_simple(
2362 Some(host_id),
2363 p0,
2364 "DB$ GainLife | LifeAmount$ X | NumCards$ Y",
2365 );
2366 assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 4);
2367 assert_eq!(resolve_numeric_svar(&game, &sa, "NumCards", 0), 2);
2368 }
2369
2370 #[test]
2371 fn resolves_attackers_declared_and_life_lost_last_turn() {
2372 let mut game = GameState::new(&["A", "B"], 20);
2373 let p0 = PlayerId(0);
2374
2375 let mut attacker = Card::new(
2376 CardId(0),
2377 "Attacker".to_string(),
2378 p0,
2379 CardTypeLine::parse("Creature"),
2380 ManaCost::parse("1 R"),
2381 ColorSet::RED,
2382 Some(2),
2383 Some(2),
2384 vec![],
2385 vec![],
2386 );
2387 attacker.attacked_this_turn = true;
2388 game.create_card(attacker);
2389
2390 game.player_mut(p0).life_lost_this_turn = 3;
2391 game.player_mut(p0).new_turn();
2392
2393 let mut host = Card::new(
2394 CardId(1),
2395 "Host".to_string(),
2396 p0,
2397 CardTypeLine::parse("Creature"),
2398 ManaCost::parse(""),
2399 ColorSet::COLORLESS,
2400 Some(1),
2401 Some(1),
2402 vec![],
2403 vec![],
2404 );
2405 host.svars
2406 .insert("X".to_string(), "Count$AttackersDeclared".to_string());
2407 host.svars.insert(
2408 "Y".to_string(),
2409 "PlayerCountPropertyYou$LifeLostLastTurn".to_string(),
2410 );
2411 let host_id = game.create_card(host);
2412
2413 let sa = SpellAbility::new_simple(
2414 Some(host_id),
2415 p0,
2416 "DB$ GainLife | LifeAmount$ X | NumCards$ Y",
2417 );
2418 assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 1);
2419 assert_eq!(resolve_numeric_svar(&game, &sa, "NumCards", 0), 3);
2420 }
2421
2422 #[test]
2423 fn resolves_top_of_library_cmc() {
2424 let mut game = GameState::new(&["A", "B"], 20);
2425 let p0 = PlayerId(0);
2426
2427 let top = Card::new(
2428 CardId(0),
2429 "Top".to_string(),
2430 p0,
2431 CardTypeLine::parse("Sorcery"),
2432 ManaCost::parse("2 U"),
2433 ColorSet::BLUE,
2434 None,
2435 None,
2436 vec![],
2437 vec![],
2438 );
2439 let top_id = game.create_card(top);
2440 game.move_card(top_id, forge_foundation::ZoneType::Library, p0);
2441
2442 let mut host = Card::new(
2443 CardId(1),
2444 "Host".to_string(),
2445 p0,
2446 CardTypeLine::parse("Creature"),
2447 ManaCost::parse(""),
2448 ColorSet::COLORLESS,
2449 Some(1),
2450 Some(1),
2451 vec![],
2452 vec![],
2453 );
2454 host.svars
2455 .insert("X".to_string(), "Count$TopOfLibraryCMC".to_string());
2456 let host_id = game.create_card(host);
2457
2458 let sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ GainLife | LifeAmount$ X");
2459 assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 3);
2460 }
2461
2462 #[test]
2463 fn resolves_player_property_counters_for_discard_damage_and_combat() {
2464 let mut game = GameState::new(&["A", "B"], 20);
2465 let p0 = PlayerId(0);
2466 let p1 = PlayerId(1);
2467 game.player_mut(p0).discarded_this_turn = 2;
2468 game.player_mut(p0).explored_this_turn = 1;
2469 game.player_mut(p0).opponents_assigned_damage_this_turn = 4;
2470 game.player_mut(p0).assigned_damage_this_turn = 7;
2471 game.player_mut(p0).assigned_combat_damage_this_turn = 2;
2472 game.player_mut(p0).attacked_players_this_combat.push(p1);
2473 game.player_mut(p0).been_dealt_combat_damage_since_last_turn = true;
2474
2475 let mut host = Card::new(
2476 CardId(0),
2477 "Host".to_string(),
2478 p0,
2479 CardTypeLine::parse("Creature"),
2480 ManaCost::parse(""),
2481 ColorSet::COLORLESS,
2482 Some(1),
2483 Some(1),
2484 vec![],
2485 vec![],
2486 );
2487 host.svars.insert(
2488 "A".to_string(),
2489 "PlayerCountPropertyYou$CardsDiscardedThisTurn".to_string(),
2490 );
2491 host.svars.insert(
2492 "B".to_string(),
2493 "PlayerCountPropertyYou$ExploredThisTurn".to_string(),
2494 );
2495 host.svars.insert(
2496 "C".to_string(),
2497 "PlayerCountPropertyYou$DamageToOppsThisTurn".to_string(),
2498 );
2499 host.svars.insert(
2500 "D".to_string(),
2501 "PlayerCountPropertyYou$NonCombatDamageDealtThisTurn".to_string(),
2502 );
2503 host.svars.insert(
2504 "E".to_string(),
2505 "PlayerCountPropertyYou$OpponentsAttackedThisCombat".to_string(),
2506 );
2507 host.svars.insert(
2508 "F".to_string(),
2509 "PlayerCountPropertyYou$BeenDealtCombatDamageSinceLastTurn".to_string(),
2510 );
2511 let host_id = game.create_card(host);
2512
2513 let sa = SpellAbility::new_simple(
2514 Some(host_id),
2515 p0,
2516 "DB$ GainLife | LifeAmount$ A | NumCards$ B",
2517 );
2518 assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 2);
2519 assert_eq!(resolve_numeric_svar(&game, &sa, "NumCards", 0), 1);
2520 assert_eq!(
2521 super::resolve_svar_expression(
2522 game.card(host_id).get_s_var("C").unwrap(),
2523 &game,
2524 host_id,
2525 p0,
2526 &sa,
2527 ),
2528 4
2529 );
2530 assert_eq!(
2531 super::resolve_svar_expression(
2532 game.card(host_id).get_s_var("D").unwrap(),
2533 &game,
2534 host_id,
2535 p0,
2536 &sa,
2537 ),
2538 5
2539 );
2540 assert_eq!(
2541 super::resolve_svar_expression(
2542 game.card(host_id).get_s_var("E").unwrap(),
2543 &game,
2544 host_id,
2545 p0,
2546 &sa,
2547 ),
2548 1
2549 );
2550 assert_eq!(
2551 super::resolve_svar_expression(
2552 game.card(host_id).get_s_var("F").unwrap(),
2553 &game,
2554 host_id,
2555 p0,
2556 &sa,
2557 ),
2558 1
2559 );
2560 }
2561
2562 #[test]
2563 fn resolves_trigger_result_sum_and_max_from_trigger_objects() {
2564 let mut game = GameState::new(&["A", "B"], 20);
2565 let p0 = PlayerId(0);
2566
2567 let mut host = Card::new(
2568 CardId(0),
2569 "Host".to_string(),
2570 p0,
2571 CardTypeLine::parse("Creature"),
2572 ManaCost::parse(""),
2573 ColorSet::COLORLESS,
2574 Some(1),
2575 Some(1),
2576 vec![],
2577 vec![],
2578 );
2579 host.svars
2580 .insert("Sum".to_string(), "TriggerCount$Result".to_string());
2581 host.svars
2582 .insert("Max".to_string(), "TriggerCountMax$Result".to_string());
2583 let host_id = game.create_card(host);
2584
2585 let mut sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ Draw | NumCards$ Sum");
2586 sa.set_triggering_object(crate::ability::AbilityKey::Result, "4,11,7");
2587
2588 assert_eq!(
2589 super::resolve_svar_expression(
2590 game.card(host_id).get_s_var("Sum").unwrap(),
2591 &game,
2592 host_id,
2593 p0,
2594 &sa,
2595 ),
2596 22
2597 );
2598 assert_eq!(
2599 super::resolve_svar_expression(
2600 game.card(host_id).get_s_var("Max").unwrap(),
2601 &game,
2602 host_id,
2603 p0,
2604 &sa,
2605 ),
2606 11
2607 );
2608 }
2609}