Skip to main content

manabrew_engine/svar/
mod.rs

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            // Mirror Java `AbilityUtils.playerXProperty` (`AbilityUtils.java:
663            // 3380, 3389`): pass the iterated `player` as the `YouCtrl`
664            // controller so per-opponent counts (e.g. Beza's
665            // `PlayerCountOpponents$HighestValid Land.YouCtrl`) actually scope
666            // to that opponent's permanents, not the source's.
667            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
1027/// Resolve a numeric parameter from a SpellAbility, expanding SVar references.
1028///
1029/// This is the main entry point for effect resolution — call it whenever you
1030/// need to convert a param value (which might be a literal int, "X", or an
1031/// SVar reference) into an integer.
1032///
1033/// **Examples:**
1034/// - `"NumDmg" -> "3"` → returns 3
1035/// - `"NumDmg" -> "X"` → returns `sa.x_mana_cost_paid` or evaluates the "X" SVar
1036/// - `"NumDmg" -> "AFLifeLost"` → looks up SVar "AFLifeLost" and evaluates it
1037///
1038/// **param_name**: The param key on the ability IR (e.g. "NumDmg", "LifeAmount")
1039/// **default**: The value to return if the param is missing or empty
1040pub 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
1073/// Resolve a raw numeric DSL value using the same semantics as
1074/// [`resolve_numeric_svar`], without first looking it up in `sa.params`.
1075pub 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    // Try direct integer parse first
1087    if let Ok(n) = val_str.parse::<i32>() {
1088        return n;
1089    }
1090    // Try with leading + sign (e.g. "+3")
1091    if let Some(stripped) = val_str.strip_prefix('+') {
1092        if let Ok(n) = stripped.parse::<i32>() {
1093            return n;
1094        }
1095    }
1096
1097    // Support signed SVar references like "-X" / "+X".
1098    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    // Check if it's the X mana cost value directly
1126    if val_str == "X" {
1127        // First check if there's an SVar named "X" on the source card
1128        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                // Must run before resolve_direct_player_expr, which can
1185                // greedily match some object-property expression prefixes.
1186                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        // Otherwise use x_mana_cost_paid directly
1200        return sign * sa.x_mana_cost_paid as i32;
1201    }
1202
1203    // It's an SVar reference — look it up on the source card
1204    if let Some(source_id) = sa.source {
1205        if let Some(svar_expr) = game.card(source_id).get_s_var(val_str.trim()) {
1206            // Game-aware SVar resolution for patterns that need GameState.
1207            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            // Must be checked before resolve_direct_player_expr, which
1250            // would incorrectly match "TriggeredCard" as a player definition.
1251            if let Some(value) = crate::lki::resolve_triggered_card_lki_svar(game, sa, svar_expr) {
1252                return sign * value;
1253            }
1254            // evaluate_svar handles Number$N, Count$Kicked, TriggerCount, etc.
1255            // Must run before resolve_direct_player_expr which greedily matches
1256            // any foo$bar pattern via the resolve_defined_players fallback.
1257            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
1317/// Evaluate a simple SVar expression.
1318/// Supports `Count$Kicked.A.B` (returns A if kicked, B otherwise)
1319/// and `Count$KickedCount` (returns the multikicker count).
1320pub fn evaluate_svar(expr: &str, sa: &SpellAbility) -> i32 {
1321    // X mana cost — return the value of X paid when casting
1322    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    // Converge/Sunburst — handled in resolve_numeric_svar (needs GameState)
1330    if expr == "Count$Converge" || expr == "Count$Sunburst" {
1331        return 0; // Fallback; game-aware path in resolve_numeric_svar handles this
1332    }
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    // TriggerCount$Amount — number of objects that matched the trigger event.
1357    // For per-event triggers (ChangesZoneAll batched as individual fires), this is 1.
1358    if expr == "TriggerCount$Amount" {
1359        return sa.trigger_remembered_amount.max(1);
1360    }
1361    // Count$KickedCount — return the multikicker count (for Multikicker effects)
1362    if expr == "Count$KickedCount" {
1363        return sa.kick_count as i32;
1364    }
1365    // Count$Kicked.X.Y — if kicked return X, else return Y
1366    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    // Number$N — literal numeric SVar (e.g. "Number$2" set by LoseLife for AFLifeLost)
1375    if let Some(rest) = expr.strip_prefix("Number$") {
1376        return rest.trim().parse::<i32>().unwrap_or(0);
1377    }
1378    // Fallback: try parsing as integer
1379    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
1393/// Resolve a Count$ SVar expression that requires game state access.
1394/// Handles patterns like `Count$Valid Forest.YouCtrl`, `Count$Converge`,
1395/// `Count$CardPower`, etc.
1396pub 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
1411/// Resolve a cost-adjustment `Amount$ <ident>` slot. Tries a direct integer
1412/// parse first, then looks up `ident` on the host's SVar table and routes
1413/// through the subset of `Count$…` patterns relevant for cost adjustment.
1414/// Mirrors Java `AbilityUtils.calculateAmount(host, name, staticAbility)`
1415/// for the static-ability path. Owned by `svar/` so cost callers don't
1416/// reimplement SVar walking.
1417pub 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    // Count$Metalcraft.A.B — return A if controller has 3+ artifacts, else B.
1594    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    // Count$UrzaLands.A.B — return A when the controller has all three Urza
1663    // lands, else B.
1664    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    // Count$PromisedGift.A.B — return A when gift promised, else B.
1677    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    // Count$Valid<Zone[,Zone...]> <restrictions>
1698    // Examples:
1699    // - Count$ValidHand Card.YouOwn
1700    // - Count$ValidGraveyard Card
1701    // - Count$ValidBattlefield Creature.YouCtrl
1702    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                // Thread targets through so `TargetedPlayerOwn` etc. resolve.
1721                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    // Count$Valid TYPE.QUALIFIERS — count permanents matching filter
1752    // Count$Valid TYPE.QUALIFIERS/Times.N — count × N multiplier
1753    // Count$Valid TYPE.QUALIFIERS$GreatestCardPower — greatest power among matching creatures
1754    if let Some(filter_str) = expr.strip_prefix("Count$Valid ") {
1755        let (filter_str, operators) = filter_str.split_once('/').unwrap_or((filter_str, ""));
1756        // Check for $GreatestCardPower suffix
1757        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        // Check for $Colors suffix — return distinct colors among matching
1765        // permanents (e.g. Faeburrow Elder: Count$Valid Permanent.YouCtrl$Colors).
1766        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        // Check for /Times.N multiplier suffix (e.g. "Enchantment.Other/Times.2")
1774        let (filter_str, multiplier) = crate::parsing::strip_times_multiplier(filter_str);
1775
1776        let battlefield = game.cards_in_zone(ZoneType::Battlefield, controller);
1777        // Also check opponent's battlefield for non-YouCtrl filters
1778        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            // Return the greatest power among matching creatures
1798            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    // Count$Devotion.COLOR — count mana symbols of a color among permanents you control.
1848    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    // Count$Compare SVAR OPTHRESHOLD.IFTRUE.IFFALSE
1873    // e.g. Count$Compare Y GE1.3.1  → if Y >= 1 then 3 else 1
1874    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                // Resolve the referenced SVar
1881                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                // Parse operator + threshold from cond_parts[0], e.g. "GE1"
1892                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    // Count$CardPower — power of the source card
1920    if expr == "Count$CardPower" {
1921        return game.card(source_id).power();
1922    }
1923    // Count$CardToughness
1924    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    // Count$CardCounters.TYPE
1939    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    // Count$TotalDamageDoneByThisTurn — total damage dealt by the source card this turn.
1945    if expr == "Count$TotalDamageDoneByThisTurn" {
1946        return game.card(source_id).total_damage_done_this_turn;
1947    }
1948
1949    // Count$InYour<Zone> / Count$CardsInYour<Zone> — zone size for the SA's
1950    // controller (e.g. `Count$CardsInYourHand` returns the hand size of the
1951    // ability's "you"). Mirrors Java `AbilityUtils.getCardListForXCount`'s
1952    // `InYour<Zone>` substring branch (`AbilityUtils.java:3718`).
1953    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    // Count$RememberedSize — mirrors Java `Card.getRememberedCount()`
1977    // (cards + players + integers).
1978    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/// Check if a card matches a validity filter string like "Forest.YouCtrl".
1993#[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    // Check base type
2005    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        // Subtypes (Forest, Island, Goblin, etc.)
2013        _ => card.type_line.has_subtype(base_type),
2014    };
2015    if !type_ok {
2016        return false;
2017    }
2018
2019    // Check qualifiers (split by '.' and '+')
2020    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                // Card must have the source card's chosen creature type as a subtype.
2039                // Changeling means all creature types — always matches.
2040                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                // Parse "counters_GE1_P1P1", "counters_EQ0_P1P1", etc.
2047                if !check_counter_qualifier(card, sub_qual) {
2048                    return false;
2049                }
2050            }
2051        }
2052    }
2053    true
2054}
2055
2056/// Check a counter qualifier like "counters_GE1_P1P1".
2057#[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    // Split into OP+THRESHOLD and COUNTER_TYPE, e.g. "GE1_P1P1"
2064    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}