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