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 expr == "Count$YourSpeed" {
1548        return game.player(controller).speed;
1549    }
1550
1551    if let Some(operators) = expr.strip_prefix("Count$YourLifeTotal") {
1552        let operators = operators.strip_prefix('/').unwrap_or(operators);
1553        return do_x_math(
1554            game.player(controller).life,
1555            operators,
1556            game,
1557            source_id,
1558            controller,
1559            sa,
1560        );
1561    }
1562
1563    if let Some(operators) = expr.strip_prefix("Count$YouDrewThisTurn") {
1564        let operators = operators.strip_prefix('/').unwrap_or(operators);
1565        return do_x_math(
1566            game.player(controller).drawn_this_turn,
1567            operators,
1568            game,
1569            source_id,
1570            controller,
1571            sa,
1572        );
1573    }
1574
1575    if let Some(operators) = expr.strip_prefix("Count$OppGreatestLifeTotal") {
1576        let operators = operators.strip_prefix('/').unwrap_or(operators);
1577        let highest_life = game
1578            .alive_players()
1579            .into_iter()
1580            .filter(|&pid| crate::player::player_predicates::is_opponent_of(game, controller, pid))
1581            .map(|pid| game.player(pid).life)
1582            .max()
1583            .unwrap_or(0);
1584        return do_x_math(highest_life, operators, game, source_id, controller, sa);
1585    }
1586
1587    // Count$Metalcraft.A.B — return A if controller has 3+ artifacts, else B.
1588    if let Some(rest) = expr.strip_prefix("Count$Metalcraft.") {
1589        let parts: Vec<&str> = rest.splitn(2, '.').collect();
1590        if parts.len() == 2 {
1591            let yes = parts[0].parse::<i32>().unwrap_or(1);
1592            let no = parts[1].parse::<i32>().unwrap_or(0);
1593            return if game.player_has_metalcraft(controller) {
1594                yes
1595            } else {
1596                no
1597            };
1598        }
1599    }
1600
1601    if let Some(rest) = expr.strip_prefix("Count$MaxSpeed.") {
1602        let parts: Vec<&str> = rest.splitn(2, '.').collect();
1603        if parts.len() == 2 {
1604            let yes = parts[0].parse::<i32>().unwrap_or(1);
1605            let no = parts[1].parse::<i32>().unwrap_or(0);
1606            return if game.player(controller).speed == 4 {
1607                yes
1608            } else {
1609                no
1610            };
1611        }
1612    }
1613
1614    if expr == "Count$AttackersDeclared" {
1615        return game
1616            .cards
1617            .iter()
1618            .filter(|card| {
1619                card.controller == controller && card.attacked_this_turn && card.is_creature()
1620            })
1621            .count() as i32;
1622    }
1623
1624    if expr == "Count$TopOfLibraryCMC" {
1625        return game
1626            .cards_in_zone(ZoneType::Library, controller)
1627            .last()
1628            .map(|&cid| game.card(cid).mana_value())
1629            .unwrap_or(0);
1630    }
1631
1632    if let Some(rest) = expr.strip_prefix("Count$OptionalGenericCostPaid.") {
1633        let parts: Vec<&str> = rest.splitn(2, '.').collect();
1634        if parts.len() == 2 {
1635            let paid_val = parts[0].parse::<i32>().unwrap_or(1);
1636            let unpaid_val = parts[1].parse::<i32>().unwrap_or(0);
1637            return if sa.optional_generic_cost_paid {
1638                paid_val
1639            } else {
1640                unpaid_val
1641            };
1642        }
1643    }
1644
1645    if expr == "Count$KickedCount" {
1646        return sa.kick_count as i32;
1647    }
1648    if let Some(rest) = expr.strip_prefix("Count$Kicked.") {
1649        let parts: Vec<&str> = rest.splitn(2, '.').collect();
1650        if parts.len() == 2 {
1651            let chosen = if sa.kicked { parts[0] } else { parts[1] };
1652            return resolve_svar_expression(chosen, game, source_id, controller, sa);
1653        }
1654    }
1655
1656    // Count$UrzaLands.A.B — return A when the controller has all three Urza
1657    // lands, else B.
1658    if let Some(rest) = expr.strip_prefix("Count$UrzaLands.") {
1659        let parts: Vec<&str> = rest.splitn(2, '.').collect();
1660        if parts.len() == 2 {
1661            let chosen = if crate::player::player_predicates::has_urza_lands(game, controller) {
1662                parts[0]
1663            } else {
1664                parts[1]
1665            };
1666            return resolve_svar_expression(chosen, game, source_id, controller, sa);
1667        }
1668    }
1669
1670    // Count$PromisedGift.A.B — return A when gift promised, else B.
1671    if let Some(rest) = expr.strip_prefix("Count$PromisedGift.") {
1672        let parts: Vec<&str> = rest.splitn(2, '.').collect();
1673        if parts.len() == 2 {
1674            let promised_val = parts[0].parse::<i32>().unwrap_or(1);
1675            let not_promised_val = parts[1].parse::<i32>().unwrap_or(0);
1676            return if game.card(source_id).promised_gift.is_some() {
1677                promised_val
1678            } else {
1679                not_promised_val
1680            };
1681        }
1682    }
1683    if expr == "Count$PromisedGift" {
1684        return if game.card(source_id).promised_gift.is_some() {
1685            1
1686        } else {
1687            0
1688        };
1689    }
1690
1691    // Count$Valid<Zone[,Zone...]> <restrictions>
1692    // Examples:
1693    // - Count$ValidHand Card.YouOwn
1694    // - Count$ValidGraveyard Card
1695    // - Count$ValidBattlefield Creature.YouCtrl
1696    if let Some(rest) = expr.strip_prefix("Count$Valid") {
1697        let (rest, operators) = rest.split_once('/').unwrap_or((rest, ""));
1698        let mut parts = rest.trim_start().splitn(2, ' ');
1699        let zone_part = parts.next().unwrap_or("").trim();
1700        let restrictions = parts.next().unwrap_or("").trim();
1701        let (restrictions, aggregator) = restrictions.split_once('$').unwrap_or((restrictions, ""));
1702        if !restrictions.is_empty() {
1703            let zones: Vec<ZoneType> = if zone_part.is_empty() {
1704                vec![ZoneType::Battlefield]
1705            } else {
1706                zone_part
1707                    .split(',')
1708                    .filter_map(crate::ability::ability_utils::parse_zone_type)
1709                    .collect()
1710            };
1711            if !zones.is_empty() {
1712                let source = game.card(source_id);
1713                let selector = crate::parsing::cached_compiled_selector(restrictions);
1714                // Thread targets through so `TargetedPlayerOwn` etc. resolve.
1715                let targeted_players: Vec<crate::ids::PlayerId> =
1716                    sa.target_chosen.target_player.into_iter().collect();
1717                let targeted_cards: Vec<crate::ids::CardId> =
1718                    sa.target_chosen.target_card.into_iter().collect();
1719                let ctx = crate::card::valid_filter::MatchContext::from_source(source)
1720                    .with_game(game)
1721                    .with_targets(&targeted_cards, &targeted_players)
1722                    .with_spell_ability(sa);
1723                let matches: Vec<&crate::card::Card> = game
1724                    .cards
1725                    .iter()
1726                    .filter(|card| {
1727                        zones.contains(&card.zone)
1728                            && crate::card::valid_filter::matches_valid_card_selector_with_context(
1729                                &selector, card, ctx,
1730                            )
1731                    })
1732                    .collect();
1733                let count = match aggregator {
1734                    "" | "Amount" => matches.len() as i32,
1735                    "GreatestCardManaCost" => {
1736                        matches.iter().map(|c| c.mana_cost.cmc()).max().unwrap_or(0)
1737                    }
1738                    _ => 0,
1739                };
1740                return do_x_math(count, operators, game, source_id, controller, sa);
1741            }
1742        }
1743    }
1744
1745    // Count$Valid TYPE.QUALIFIERS — count permanents matching filter
1746    // Count$Valid TYPE.QUALIFIERS/Times.N — count × N multiplier
1747    // Count$Valid TYPE.QUALIFIERS$GreatestCardPower — greatest power among matching creatures
1748    if let Some(filter_str) = expr.strip_prefix("Count$Valid ") {
1749        let (filter_str, operators) = filter_str.split_once('/').unwrap_or((filter_str, ""));
1750        // Check for $GreatestCardPower suffix
1751        let (filter_str, greatest_power) =
1752            if let Some(base) = filter_str.strip_suffix("$GreatestCardPower") {
1753                (base, true)
1754            } else {
1755                (filter_str, false)
1756            };
1757
1758        // Check for $Colors suffix — return distinct colors among matching
1759        // permanents (e.g. Faeburrow Elder: Count$Valid Permanent.YouCtrl$Colors).
1760        let count_distinct_colors = filter_str.ends_with("$Colors");
1761        let filter_str = if count_distinct_colors {
1762            filter_str.trim_end_matches("$Colors")
1763        } else {
1764            filter_str
1765        };
1766
1767        // Check for /Times.N multiplier suffix (e.g. "Enchantment.Other/Times.2")
1768        let (filter_str, multiplier) = crate::parsing::strip_times_multiplier(filter_str);
1769
1770        let battlefield = game.cards_in_zone(ZoneType::Battlefield, controller);
1771        // Also check opponent's battlefield for non-YouCtrl filters
1772        let opp = game.opponent_of(controller);
1773        let opp_battlefield = game.cards_in_zone(ZoneType::Battlefield, opp);
1774
1775        let has_you_ctrl =
1776            filter_str.contains(fc::YOU_CTRL) || filter_str.contains(fc::YOU_CONTROL);
1777
1778        let cards_to_check: Vec<CardId> = if has_you_ctrl {
1779            battlefield.to_vec()
1780        } else {
1781            battlefield
1782                .iter()
1783                .chain(opp_battlefield.iter())
1784                .copied()
1785                .collect()
1786        };
1787
1788        let source = game.card(source_id);
1789        let selector = crate::parsing::cached_compiled_selector(filter_str);
1790        if greatest_power {
1791            // Return the greatest power among matching creatures
1792            let mut max_power = 0;
1793            for &cid in &cards_to_check {
1794                let card = game.card(cid);
1795                if crate::card::valid_filter::matches_valid_card_selector_in_game(
1796                    &selector, card, source, game,
1797                ) {
1798                    max_power = max_power.max(card.power());
1799                }
1800            }
1801            return do_x_math(max_power, operators, game, source_id, controller, sa);
1802        } else if count_distinct_colors {
1803            let mut mask: u8 = 0;
1804            for &cid in &cards_to_check {
1805                let card = game.card(cid);
1806                if crate::card::valid_filter::matches_valid_card_selector_in_game(
1807                    &selector, card, source, game,
1808                ) {
1809                    mask |= card.color.mask();
1810                }
1811            }
1812            return do_x_math(
1813                (mask.count_ones() as i32) * multiplier,
1814                operators,
1815                game,
1816                source_id,
1817                controller,
1818                sa,
1819            );
1820        } else {
1821            let mut count = 0;
1822            for &cid in &cards_to_check {
1823                let card = game.card(cid);
1824                if crate::card::valid_filter::matches_valid_card_selector_in_game(
1825                    &selector, card, source, game,
1826                ) {
1827                    count += 1;
1828                }
1829            }
1830            return do_x_math(
1831                count * multiplier,
1832                operators,
1833                game,
1834                source_id,
1835                controller,
1836                sa,
1837            );
1838        }
1839    }
1840
1841    // Count$Devotion.COLOR — count mana symbols of a color among permanents you control.
1842    if let Some(color_str) = expr.strip_prefix("Count$Devotion.") {
1843        let color_mask: u16 = match color_str.to_uppercase().as_str() {
1844            "W" | "WHITE" => forge_foundation::ManaAtom::WHITE,
1845            "U" | "BLUE" => forge_foundation::ManaAtom::BLUE,
1846            "B" | "BLACK" => forge_foundation::ManaAtom::BLACK,
1847            "R" | "RED" => forge_foundation::ManaAtom::RED,
1848            "G" | "GREEN" => forge_foundation::ManaAtom::GREEN,
1849            _ => 0,
1850        };
1851        if color_mask != 0 {
1852            let battlefield = game.cards_in_zone(ZoneType::Battlefield, controller);
1853            let mut count = 0i32;
1854            for &cid in battlefield {
1855                let card = game.card(cid);
1856                for shard in card.mana_cost.shards() {
1857                    if (shard.shard() & color_mask) != 0 {
1858                        count += 1;
1859                    }
1860                }
1861            }
1862            return count;
1863        }
1864    }
1865
1866    // Count$Compare SVAR OPTHRESHOLD.IFTRUE.IFFALSE
1867    // e.g. Count$Compare Y GE1.3.1  → if Y >= 1 then 3 else 1
1868    if let Some(rest) = expr.strip_prefix("Count$Compare ") {
1869        let parts: Vec<&str> = rest.splitn(2, ' ').collect();
1870        if parts.len() == 2 {
1871            let svar_name = parts[0];
1872            let cond_parts: Vec<&str> = parts[1].splitn(3, '.').collect();
1873            if cond_parts.len() == 3 {
1874                // Resolve the referenced SVar
1875                let svar_val = if let Some(svar_expr) = game.card(source_id).get_s_var(svar_name) {
1876                    if svar_expr.starts_with("Count$") || svar_expr.starts_with("PlayerCount") {
1877                        resolve_svar_expression(svar_expr, game, source_id, controller, sa)
1878                    } else {
1879                        svar_expr.parse::<i32>().unwrap_or(0)
1880                    }
1881                } else {
1882                    svar_name.parse::<i32>().unwrap_or(0)
1883                };
1884
1885                // Parse operator + threshold from cond_parts[0], e.g. "GE1"
1886                let cond = cond_parts[0];
1887                let result = compare_expr(svar_val, cond);
1888
1889                let resolve_branch = |raw: &str| {
1890                    raw.parse::<i32>().unwrap_or_else(|_| {
1891                        if let Some(svar_expr) = game.card(source_id).get_s_var(raw) {
1892                            resolve_svar_expression(svar_expr, game, source_id, controller, sa)
1893                        } else {
1894                            resolve_svar_expression(raw, game, source_id, controller, sa)
1895                        }
1896                    })
1897                };
1898                let if_true = resolve_branch(cond_parts[1]);
1899                let if_false = resolve_branch(cond_parts[2]);
1900                return if result { if_true } else { if_false };
1901            }
1902        }
1903    }
1904
1905    if let Some(operators) = expr.strip_prefix("Count$ColorsColorIdentity") {
1906        let operators = operators.strip_prefix('/').unwrap_or(operators);
1907        let count = game
1908            .player_commander_color_identity(game.card(source_id).controller)
1909            .len() as i32;
1910        return do_x_math(count, operators, game, source_id, controller, sa);
1911    }
1912
1913    // Count$CardPower — power of the source card
1914    if expr == "Count$CardPower" {
1915        return game.card(source_id).power();
1916    }
1917    // Count$CardToughness
1918    if expr == "Count$CardToughness" {
1919        return game.card(source_id).toughness();
1920    }
1921    if let Some(operators) = expr.strip_prefix("Count$YourTurns") {
1922        let operators = operators.strip_prefix('/').unwrap_or(operators);
1923        return do_x_math(
1924            game.player(controller).statistics.turns_played,
1925            operators,
1926            game,
1927            source_id,
1928            controller,
1929            sa,
1930        );
1931    }
1932    // Count$CardCounters.TYPE
1933    if let Some(counter_type) = expr.strip_prefix("Count$CardCounters.") {
1934        let ct = crate::ability::effects::parse_counter_type(counter_type);
1935        return *game.card(source_id).counters.get(&ct).unwrap_or(&0);
1936    }
1937
1938    // Count$TotalDamageDoneByThisTurn — total damage dealt by the source card this turn.
1939    if expr == "Count$TotalDamageDoneByThisTurn" {
1940        return game.card(source_id).total_damage_done_this_turn;
1941    }
1942
1943    // Count$InYour<Zone> / Count$CardsInYour<Zone> — zone size for the SA's
1944    // controller (e.g. `Count$CardsInYourHand` returns the hand size of the
1945    // ability's "you"). Mirrors Java `AbilityUtils.getCardListForXCount`'s
1946    // `InYour<Zone>` substring branch (`AbilityUtils.java:3718`).
1947    if let Some(rest) = expr
1948        .strip_prefix("Count$CardsInYour")
1949        .or_else(|| expr.strip_prefix("Count$InYour"))
1950    {
1951        let zone = match rest {
1952            "Hand" => Some(ZoneType::Hand),
1953            "Yard" | "Graveyard" => Some(ZoneType::Graveyard),
1954            "Library" => Some(ZoneType::Library),
1955            "Exile" => Some(ZoneType::Exile),
1956            "Battlefield" => Some(ZoneType::Battlefield),
1957            _ => None,
1958        };
1959        if let Some(zone) = zone {
1960            return game.cards_in_zone(zone, controller).len() as i32;
1961        }
1962    }
1963
1964    // Count$RememberedSize — mirrors Java `Card.getRememberedCount()`
1965    // (cards + players + integers).
1966    if let Some(rest) = expr.strip_prefix("Count$RememberedSize") {
1967        let operators = rest.strip_prefix('/').unwrap_or(rest);
1968        let card = game.card(source_id);
1969        let count =
1970            card.remembered_cards.len() + card.remembered_players.len() + card.remembered_cmc.len();
1971        return do_x_math(count as i32, operators, game, source_id, controller, sa);
1972    }
1973
1974    expr.parse::<i32>().unwrap_or_else(|_| {
1975        eprintln!("Unrecognized Count expression, returning 0 for: {expr}");
1976        0
1977    })
1978}
1979
1980/// Check if a card matches a validity filter string like "Forest.YouCtrl".
1981#[allow(dead_code)]
1982fn valid_card_matches_with_source(
1983    filter: &str,
1984    card: &crate::card::Card,
1985    controller: PlayerId,
1986    source_id: CardId,
1987    chosen_type: Option<&str>,
1988) -> bool {
1989    let parts: Vec<&str> = filter.split('.').collect();
1990    let base_type = parts.first().copied().unwrap_or("");
1991
1992    // Check base type
1993    let type_ok = match base_type {
1994        fc::CREATURE => card.is_creature(),
1995        fc::LAND => card.is_land(),
1996        fc::ARTIFACT => card.type_line.is_artifact(),
1997        fc::ENCHANTMENT => card.type_line.is_enchantment(),
1998        fc::PLANESWALKER => card.type_line.is_planeswalker(),
1999        fc::PERMANENT | fc::CARD => true,
2000        // Subtypes (Forest, Island, Goblin, etc.)
2001        _ => card.type_line.has_subtype(base_type),
2002    };
2003    if !type_ok {
2004        return false;
2005    }
2006
2007    // Check qualifiers (split by '.' and '+')
2008    for &dot_qual in &parts[1..] {
2009        for sub_qual in dot_qual.split('+') {
2010            let sub_qual = sub_qual.trim();
2011            if sub_qual.eq_ignore_ascii_case(fc::YOU_CTRL)
2012                || sub_qual.eq_ignore_ascii_case(fc::YOU_CONTROL)
2013            {
2014                if card.controller != controller {
2015                    return false;
2016                }
2017            } else if sub_qual.eq_ignore_ascii_case(fc::SELF_REF) {
2018                if card.id != source_id {
2019                    return false;
2020                }
2021            } else if sub_qual.eq_ignore_ascii_case(fc::OTHER) {
2022                if card.id == source_id {
2023                    return false;
2024                }
2025            } else if sub_qual.eq_ignore_ascii_case("ChosenType") {
2026                // Card must have the source card's chosen creature type as a subtype.
2027                // Changeling means all creature types — always matches.
2028                match chosen_type {
2029                    Some(ct)
2030                        if card.type_line.has_subtype(ct) || card.has_keyword("Changeling") => {}
2031                    _ => return false,
2032                }
2033            } else if sub_qual.starts_with("counters_") {
2034                // Parse "counters_GE1_P1P1", "counters_EQ0_P1P1", etc.
2035                if !check_counter_qualifier(card, sub_qual) {
2036                    return false;
2037                }
2038            }
2039        }
2040    }
2041    true
2042}
2043
2044/// Check a counter qualifier like "counters_GE1_P1P1".
2045#[allow(dead_code)]
2046fn check_counter_qualifier(card: &crate::card::Card, qual: &str) -> bool {
2047    let rest = match qual.strip_prefix("counters_") {
2048        Some(r) => r,
2049        None => return true,
2050    };
2051    // Split into OP+THRESHOLD and COUNTER_TYPE, e.g. "GE1_P1P1"
2052    let parts: Vec<&str> = rest.splitn(2, '_').collect();
2053    if parts.len() != 2 {
2054        return true;
2055    }
2056    let cond = parts[0];
2057    let counter_type = crate::ability::effects::parse_counter_type(parts[1]);
2058    let count = *card.counters.get(&counter_type).unwrap_or(&0);
2059
2060    compare_expr(count, cond)
2061}
2062
2063#[cfg(test)]
2064mod tests {
2065    use forge_foundation::{CardTypeLine, ColorSet, ManaCost};
2066
2067    use super::resolve_numeric_svar;
2068    use crate::card::Card;
2069    use crate::game::GameState;
2070    use crate::ids::{CardId, PlayerId};
2071    use crate::spellability::SpellAbility;
2072
2073    #[test]
2074    fn resolves_player_count_defined_life_total_twice() {
2075        let mut game = GameState::new(&["A", "B"], 20);
2076        let p0 = PlayerId(0);
2077        let p1 = PlayerId(1);
2078        game.player_mut(p1).life = 7;
2079
2080        let mut host = Card::new(
2081            CardId(0),
2082            "Host".to_string(),
2083            p0,
2084            CardTypeLine::parse("Creature"),
2085            ManaCost::parse(""),
2086            ColorSet::COLORLESS,
2087            Some(1),
2088            Some(1),
2089            vec![],
2090            vec![],
2091        );
2092        host.svars.insert(
2093            "X".to_string(),
2094            "PlayerCountDefinedTriggeredAttackedTarget$LifeTotal/Twice".to_string(),
2095        );
2096        let host_id = game.create_card(host);
2097
2098        let mut sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ GainLife | LifeAmount$ X");
2099        sa.set_triggering_object(crate::ability::AbilityKey::AttackedTarget, p1);
2100
2101        assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 14);
2102    }
2103
2104    #[test]
2105    fn resolves_player_count_highest_life_total() {
2106        let mut game = GameState::new(&["A", "B"], 20);
2107        let p0 = PlayerId(0);
2108        let p1 = PlayerId(1);
2109        game.player_mut(p0).life = 11;
2110        game.player_mut(p1).life = 17;
2111
2112        let mut host = Card::new(
2113            CardId(0),
2114            "Host".to_string(),
2115            p0,
2116            CardTypeLine::parse("Creature"),
2117            ManaCost::parse(""),
2118            ColorSet::COLORLESS,
2119            Some(1),
2120            Some(1),
2121            vec![],
2122            vec![],
2123        );
2124        host.svars.insert(
2125            "X".to_string(),
2126            "PlayerCountPlayers$HighestLifeTotal".to_string(),
2127        );
2128        let host_id = game.create_card(host);
2129
2130        let sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ GainLife | LifeAmount$ X");
2131        assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 17);
2132    }
2133
2134    #[test]
2135    fn resolves_triggered_target_life_total_half_up() {
2136        let mut game = GameState::new(&["A", "B"], 20);
2137        let p0 = PlayerId(0);
2138        let p1 = PlayerId(1);
2139        game.player_mut(p1).life = 9;
2140
2141        let mut host = Card::new(
2142            CardId(0),
2143            "Host".to_string(),
2144            p0,
2145            CardTypeLine::parse("Creature"),
2146            ManaCost::parse(""),
2147            ColorSet::COLORLESS,
2148            Some(1),
2149            Some(1),
2150            vec![],
2151            vec![],
2152        );
2153        host.svars.insert(
2154            "X".to_string(),
2155            "TriggeredTarget$LifeTotal/HalfUp".to_string(),
2156        );
2157        let host_id = game.create_card(host);
2158
2159        let mut sa = SpellAbility::new_simple(
2160            Some(host_id),
2161            p0,
2162            "DB$ LoseLife | Defined$ TriggeredTarget | LifeAmount$ X",
2163        );
2164        sa.set_triggering_object(crate::ability::AbilityKey::TargetPlayer, p1);
2165
2166        assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 5);
2167    }
2168
2169    #[test]
2170    fn resolves_player_count_minus_remembered_amount() {
2171        let mut game = GameState::new(&["A", "B"], 20);
2172        let p0 = PlayerId(0);
2173        let p1 = PlayerId(1);
2174
2175        let remembered = Card::new(
2176            CardId(1),
2177            "Remembered".to_string(),
2178            p1,
2179            CardTypeLine::parse("Creature"),
2180            ManaCost::parse(""),
2181            ColorSet::COLORLESS,
2182            Some(1),
2183            Some(1),
2184            vec![],
2185            vec![],
2186        );
2187        let remembered_id = game.create_card(remembered);
2188
2189        let mut host = Card::new(
2190            CardId(0),
2191            "Host".to_string(),
2192            p0,
2193            CardTypeLine::parse("Creature"),
2194            ManaCost::parse(""),
2195            ColorSet::COLORLESS,
2196            Some(1),
2197            Some(1),
2198            vec![],
2199            vec![],
2200        );
2201        host.svars.insert(
2202            "X".to_string(),
2203            "PlayerCountOpponents$Amount/Minus.Remembered$Amount".to_string(),
2204        );
2205        let host_id = game.create_card(host);
2206        game.card_mut(host_id).add_remembered_card(remembered_id);
2207
2208        let sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ Draw | NumCards$ X");
2209        assert_eq!(resolve_numeric_svar(&game, &sa, "NumCards", -1), 0);
2210    }
2211
2212    #[test]
2213    fn resolves_player_count_minus_empty_remembered_amount() {
2214        let mut game = GameState::new(&["A", "B"], 20);
2215        let p0 = PlayerId(0);
2216
2217        let mut host = Card::new(
2218            CardId(0),
2219            "Host".to_string(),
2220            p0,
2221            CardTypeLine::parse("Creature"),
2222            ManaCost::parse(""),
2223            ColorSet::COLORLESS,
2224            Some(1),
2225            Some(1),
2226            vec![],
2227            vec![],
2228        );
2229        host.svars.insert(
2230            "X".to_string(),
2231            "PlayerCountOpponents$Amount/Minus.Remembered$Amount".to_string(),
2232        );
2233        let host_id = game.create_card(host);
2234
2235        let sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ Draw | NumCards$ X");
2236        assert_eq!(resolve_numeric_svar(&game, &sa, "NumCards", -1), 1);
2237    }
2238
2239    #[test]
2240    fn resolves_player_count_remembered_life_lost_this_turn() {
2241        let mut game = GameState::new(&["A", "B"], 20);
2242        let p0 = PlayerId(0);
2243        let p1 = PlayerId(1);
2244
2245        game.player_mut(p1).life_lost_this_turn = 11;
2246
2247        let mut host = Card::new(
2248            CardId(0),
2249            "Host".to_string(),
2250            p0,
2251            CardTypeLine::parse("Creature"),
2252            ManaCost::parse(""),
2253            ColorSet::COLORLESS,
2254            Some(1),
2255            Some(1),
2256            vec![],
2257            vec![],
2258        );
2259        host.svars.insert(
2260            "X".to_string(),
2261            "PlayerCountRemembered$LifeLostThisTurn".to_string(),
2262        );
2263        let host_id = game.create_card(host);
2264        game.card_mut(host_id).add_remembered_player(p1);
2265
2266        let sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ LoseLife | LifeAmount$ X");
2267        assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", -1), 11);
2268    }
2269
2270    #[test]
2271    fn resolves_triggered_spell_ability_card_mana_cost_lki() {
2272        let mut game = GameState::new(&["A", "B"], 20);
2273        let p0 = PlayerId(0);
2274        let p1 = PlayerId(1);
2275
2276        let mut host = Card::new(
2277            CardId(0),
2278            "Host".to_string(),
2279            p0,
2280            CardTypeLine::parse("Creature"),
2281            ManaCost::parse(""),
2282            ColorSet::COLORLESS,
2283            Some(1),
2284            Some(1),
2285            vec![],
2286            vec![],
2287        );
2288        host.svars.insert(
2289            "X".to_string(),
2290            "TriggeredSpellAbility$CardManaCostLKI".to_string(),
2291        );
2292        let host_id = game.create_card(host);
2293
2294        let mut spell_card = Card::new(
2295            CardId(1),
2296            "Big Spell".to_string(),
2297            p1,
2298            CardTypeLine::parse("Sorcery"),
2299            ManaCost::parse("X U"),
2300            ColorSet::BLUE,
2301            None,
2302            None,
2303            vec![],
2304            vec![],
2305        );
2306        spell_card.set_zone(forge_foundation::ZoneType::Graveyard);
2307        let spell_id = game.create_card(spell_card);
2308
2309        let mut triggered_sa =
2310            SpellAbility::new_simple(Some(spell_id), p1, "SP$ DealDamage | NumDmg$ 1");
2311        triggered_sa.x_mana_cost_paid = 4;
2312
2313        let mut sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ GainLife | LifeAmount$ X");
2314        sa.set_triggering_spell_ability("SpellAbility", triggered_sa);
2315
2316        assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 5);
2317    }
2318
2319    #[test]
2320    fn resolves_count_your_speed_and_max_speed() {
2321        let mut game = GameState::new(&["A", "B"], 20);
2322        let p0 = PlayerId(0);
2323        game.player_mut(p0).speed = 4;
2324
2325        let mut host = Card::new(
2326            CardId(0),
2327            "Host".to_string(),
2328            p0,
2329            CardTypeLine::parse("Creature"),
2330            ManaCost::parse(""),
2331            ColorSet::COLORLESS,
2332            Some(1),
2333            Some(1),
2334            vec![],
2335            vec![],
2336        );
2337        host.svars
2338            .insert("X".to_string(), "Count$YourSpeed".to_string());
2339        host.svars
2340            .insert("Y".to_string(), "Count$MaxSpeed.2.1".to_string());
2341        let host_id = game.create_card(host);
2342
2343        let sa = SpellAbility::new_simple(
2344            Some(host_id),
2345            p0,
2346            "DB$ GainLife | LifeAmount$ X | NumCards$ Y",
2347        );
2348        assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 4);
2349        assert_eq!(resolve_numeric_svar(&game, &sa, "NumCards", 0), 2);
2350    }
2351
2352    #[test]
2353    fn resolves_attackers_declared_and_life_lost_last_turn() {
2354        let mut game = GameState::new(&["A", "B"], 20);
2355        let p0 = PlayerId(0);
2356
2357        let mut attacker = Card::new(
2358            CardId(0),
2359            "Attacker".to_string(),
2360            p0,
2361            CardTypeLine::parse("Creature"),
2362            ManaCost::parse("1 R"),
2363            ColorSet::RED,
2364            Some(2),
2365            Some(2),
2366            vec![],
2367            vec![],
2368        );
2369        attacker.attacked_this_turn = true;
2370        game.create_card(attacker);
2371
2372        game.player_mut(p0).life_lost_this_turn = 3;
2373        game.player_mut(p0).new_turn();
2374
2375        let mut host = Card::new(
2376            CardId(1),
2377            "Host".to_string(),
2378            p0,
2379            CardTypeLine::parse("Creature"),
2380            ManaCost::parse(""),
2381            ColorSet::COLORLESS,
2382            Some(1),
2383            Some(1),
2384            vec![],
2385            vec![],
2386        );
2387        host.svars
2388            .insert("X".to_string(), "Count$AttackersDeclared".to_string());
2389        host.svars.insert(
2390            "Y".to_string(),
2391            "PlayerCountPropertyYou$LifeLostLastTurn".to_string(),
2392        );
2393        let host_id = game.create_card(host);
2394
2395        let sa = SpellAbility::new_simple(
2396            Some(host_id),
2397            p0,
2398            "DB$ GainLife | LifeAmount$ X | NumCards$ Y",
2399        );
2400        assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 1);
2401        assert_eq!(resolve_numeric_svar(&game, &sa, "NumCards", 0), 3);
2402    }
2403
2404    #[test]
2405    fn resolves_top_of_library_cmc() {
2406        let mut game = GameState::new(&["A", "B"], 20);
2407        let p0 = PlayerId(0);
2408
2409        let top = Card::new(
2410            CardId(0),
2411            "Top".to_string(),
2412            p0,
2413            CardTypeLine::parse("Sorcery"),
2414            ManaCost::parse("2 U"),
2415            ColorSet::BLUE,
2416            None,
2417            None,
2418            vec![],
2419            vec![],
2420        );
2421        let top_id = game.create_card(top);
2422        game.move_card(top_id, forge_foundation::ZoneType::Library, p0);
2423
2424        let mut host = Card::new(
2425            CardId(1),
2426            "Host".to_string(),
2427            p0,
2428            CardTypeLine::parse("Creature"),
2429            ManaCost::parse(""),
2430            ColorSet::COLORLESS,
2431            Some(1),
2432            Some(1),
2433            vec![],
2434            vec![],
2435        );
2436        host.svars
2437            .insert("X".to_string(), "Count$TopOfLibraryCMC".to_string());
2438        let host_id = game.create_card(host);
2439
2440        let sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ GainLife | LifeAmount$ X");
2441        assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 3);
2442    }
2443
2444    #[test]
2445    fn resolves_player_property_counters_for_discard_damage_and_combat() {
2446        let mut game = GameState::new(&["A", "B"], 20);
2447        let p0 = PlayerId(0);
2448        let p1 = PlayerId(1);
2449        game.player_mut(p0).discarded_this_turn = 2;
2450        game.player_mut(p0).explored_this_turn = 1;
2451        game.player_mut(p0).opponents_assigned_damage_this_turn = 4;
2452        game.player_mut(p0).assigned_damage_this_turn = 7;
2453        game.player_mut(p0).assigned_combat_damage_this_turn = 2;
2454        game.player_mut(p0).attacked_players_this_combat.push(p1);
2455        game.player_mut(p0).been_dealt_combat_damage_since_last_turn = true;
2456
2457        let mut host = Card::new(
2458            CardId(0),
2459            "Host".to_string(),
2460            p0,
2461            CardTypeLine::parse("Creature"),
2462            ManaCost::parse(""),
2463            ColorSet::COLORLESS,
2464            Some(1),
2465            Some(1),
2466            vec![],
2467            vec![],
2468        );
2469        host.svars.insert(
2470            "A".to_string(),
2471            "PlayerCountPropertyYou$CardsDiscardedThisTurn".to_string(),
2472        );
2473        host.svars.insert(
2474            "B".to_string(),
2475            "PlayerCountPropertyYou$ExploredThisTurn".to_string(),
2476        );
2477        host.svars.insert(
2478            "C".to_string(),
2479            "PlayerCountPropertyYou$DamageToOppsThisTurn".to_string(),
2480        );
2481        host.svars.insert(
2482            "D".to_string(),
2483            "PlayerCountPropertyYou$NonCombatDamageDealtThisTurn".to_string(),
2484        );
2485        host.svars.insert(
2486            "E".to_string(),
2487            "PlayerCountPropertyYou$OpponentsAttackedThisCombat".to_string(),
2488        );
2489        host.svars.insert(
2490            "F".to_string(),
2491            "PlayerCountPropertyYou$BeenDealtCombatDamageSinceLastTurn".to_string(),
2492        );
2493        let host_id = game.create_card(host);
2494
2495        let sa = SpellAbility::new_simple(
2496            Some(host_id),
2497            p0,
2498            "DB$ GainLife | LifeAmount$ A | NumCards$ B",
2499        );
2500        assert_eq!(resolve_numeric_svar(&game, &sa, "LifeAmount", 0), 2);
2501        assert_eq!(resolve_numeric_svar(&game, &sa, "NumCards", 0), 1);
2502        assert_eq!(
2503            super::resolve_svar_expression(
2504                game.card(host_id).get_s_var("C").unwrap(),
2505                &game,
2506                host_id,
2507                p0,
2508                &sa,
2509            ),
2510            4
2511        );
2512        assert_eq!(
2513            super::resolve_svar_expression(
2514                game.card(host_id).get_s_var("D").unwrap(),
2515                &game,
2516                host_id,
2517                p0,
2518                &sa,
2519            ),
2520            5
2521        );
2522        assert_eq!(
2523            super::resolve_svar_expression(
2524                game.card(host_id).get_s_var("E").unwrap(),
2525                &game,
2526                host_id,
2527                p0,
2528                &sa,
2529            ),
2530            1
2531        );
2532        assert_eq!(
2533            super::resolve_svar_expression(
2534                game.card(host_id).get_s_var("F").unwrap(),
2535                &game,
2536                host_id,
2537                p0,
2538                &sa,
2539            ),
2540            1
2541        );
2542    }
2543
2544    #[test]
2545    fn resolves_trigger_result_sum_and_max_from_trigger_objects() {
2546        let mut game = GameState::new(&["A", "B"], 20);
2547        let p0 = PlayerId(0);
2548
2549        let mut host = Card::new(
2550            CardId(0),
2551            "Host".to_string(),
2552            p0,
2553            CardTypeLine::parse("Creature"),
2554            ManaCost::parse(""),
2555            ColorSet::COLORLESS,
2556            Some(1),
2557            Some(1),
2558            vec![],
2559            vec![],
2560        );
2561        host.svars
2562            .insert("Sum".to_string(), "TriggerCount$Result".to_string());
2563        host.svars
2564            .insert("Max".to_string(), "TriggerCountMax$Result".to_string());
2565        let host_id = game.create_card(host);
2566
2567        let mut sa = SpellAbility::new_simple(Some(host_id), p0, "DB$ Draw | NumCards$ Sum");
2568        sa.set_triggering_object(crate::ability::AbilityKey::Result, "4,11,7");
2569
2570        assert_eq!(
2571            super::resolve_svar_expression(
2572                game.card(host_id).get_s_var("Sum").unwrap(),
2573                &game,
2574                host_id,
2575                p0,
2576                &sa,
2577            ),
2578            22
2579        );
2580        assert_eq!(
2581            super::resolve_svar_expression(
2582                game.card(host_id).get_s_var("Max").unwrap(),
2583                &game,
2584                host_id,
2585                p0,
2586                &sa,
2587            ),
2588            11
2589        );
2590    }
2591}