Skip to main content

manabrew_engine/ability/effects/
flip_coin_effect.rs

1use super::EffectContext;
2use crate::agent::BinaryChoiceKind;
3use crate::spellability::SpellAbility;
4
5/// `SP$ FlipACoin` — flip a coin, resolve different abilities for win/lose.
6///
7/// Mirrors Java's `FlipCoinEffect.java`.
8/// - `NoCall` — if set, don't ask the player to call; just resolve HeadsSub$/TailsSub$.
9/// - `WinSubAbility$` / `LoseSubAbility$` — SVars for the sub-abilities to resolve.
10/// - `HeadsSubAbility$` / `TailsSubAbility$` — used with NoCall.
11///
12/// # Card script examples
13/// ```text
14/// A:SP$ FlipACoin | WinSubAbility$ Win | LoseSubAbility$ Lose
15/// A:SP$ FlipACoin | NoCall$ True | HeadsSubAbility$ Heads | TailsSubAbility$ Tails
16/// ```
17/// Struct form of this effect so it can participate in the
18/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
19/// `FlipCoinEffect` class extending `SpellAbilityEffect`.
20#[manabrew_engine_macros::spell_effect(FlipCoinEffect)]
21fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
22    let controller = sa.activating_player;
23    let no_call = sa.ir.no_call;
24
25    // Flip the coin (random)
26    let is_heads = ctx.rng.next_int(2) == 0;
27
28    let source_id = match sa.source {
29        Some(id) => id,
30        None => return,
31    };
32
33    if no_call {
34        // No-call mode: resolve HeadsSub$ or TailsSub$ directly
35        let sub_key = if is_heads {
36            "HeadsSubAbility"
37        } else {
38            "TailsSubAbility"
39        };
40        if let Some(sub_sa) = sa.get_additional_ability(sub_key).cloned() {
41            resolve_sub_chain(ctx, sub_sa);
42        }
43    } else {
44        // Call mode: player calls heads/tails
45        let _card_name = ctx.game.card(source_id).card_name.clone();
46        let called_heads = ctx.agents[controller.index()].choose_binary(
47            controller,
48            "Call the coin flip",
49            BinaryChoiceKind::HeadsOrTails,
50            None,
51            Some(source_id),
52            sa.api,
53        );
54        let won = called_heads == is_heads;
55
56        let sub_key = if won {
57            "WinSubAbility"
58        } else {
59            "LoseSubAbility"
60        };
61        if let Some(sub_sa) = sa.get_additional_ability(sub_key).cloned() {
62            resolve_sub_chain(ctx, sub_sa);
63        }
64    }
65}
66
67/// Flip `amount` coins for a player and return the number of wins.
68/// Mirrors Java `FlipCoinEffect.flipCoins(Player, SpellAbility, int)`.
69///
70/// If the SA has `FlipUntilYouLose$`, keeps flipping until a loss occurs
71/// (CR 705.3). The multiplier for coin-doubling effects (e.g. Krark's Thumb)
72/// is determined by counting the relevant keyword on the player.
73pub fn flip_coins(
74    ctx: &mut EffectContext,
75    flipper: crate::ids::PlayerId,
76    sa: &SpellAbility,
77    amount: i32,
78) -> i32 {
79    let flip_until_lose = sa.ir.flip_until_you_lose;
80
81    // Get flip multiplier (Krark's Thumb: "If you would flip a coin, instead
82    // flip two coins and ignore one.")
83    let multiplier = get_flip_multiplier(ctx, flipper);
84
85    let mut total_wins = 0;
86    let mut won = false;
87
88    loop {
89        for _ in 0..amount {
90            won = flip_single_coin(ctx, flipper, multiplier);
91            if won {
92                total_wins += 1;
93            }
94        }
95        // CR 705.3: repeat if FlipUntilYouLose and the last flip was a win
96        if !flip_until_lose || !won {
97            break;
98        }
99    }
100
101    total_wins
102}
103
104/// Flip a single coin, accounting for the multiplier (multiple flips, pick best).
105fn flip_single_coin(
106    ctx: &mut EffectContext,
107    _flipper: crate::ids::PlayerId,
108    multiplier: i32,
109) -> bool {
110    // With multiplier > 1, flip multiple coins and take the best result.
111    // (Krark's Thumb lets you flip two and pick one.)
112    let mut any_heads = false;
113    for _ in 0..multiplier {
114        let result = ctx.rng.next_int(2) == 0;
115        if result {
116            any_heads = true;
117        }
118    }
119    any_heads
120}
121
122/// Get the flip multiplier for a player. Each instance of "If you would flip
123/// a coin, instead flip two coins and ignore one." doubles the flips.
124/// Mirrors Java `FlipCoinEffect.getFlipMultiplier`.
125/// Mirrors Java `FlipCoinEffect.getFlipMultiplier(Player)`.
126pub fn get_flip_multiplier(ctx: &EffectContext, flipper: crate::ids::PlayerId) -> i32 {
127    let keyword = "If you would flip a coin, instead flip two coins and ignore one.";
128    let count = ctx
129        .game
130        .cards
131        .iter()
132        .filter(|c| {
133            c.zone == forge_foundation::ZoneType::Battlefield
134                && c.controller == flipper
135                && c.keywords.contains_string_ignore_case(keyword)
136        })
137        .count() as u32;
138    1i32 << count
139}
140
141fn resolve_sub_chain(ctx: &mut EffectContext, initial: SpellAbility) {
142    let mut cur_opt: Option<SpellAbility> = Some(initial);
143    while let Some(cur_sa) = cur_opt {
144        super::resolve_effect(ctx, &cur_sa);
145        cur_opt = cur_sa.sub_ability.map(|b| *b);
146        if ctx.game.game_over {
147            break;
148        }
149    }
150}