Skip to main content

manabrew_engine/ability/effects/
balance_effect.rs

1use forge_foundation::ZoneType;
2
3use super::{emit_zone_trigger, matches_valid_cards_for_sa, EffectContext};
4use crate::event::RunParams;
5use crate::trigger::TriggerType;
6
7/// `SP$ Balance` — equalize resources across all players.
8///
9/// Mirrors Java's `BalanceEffect.java`.
10///
11/// # Params
12/// - `Valid` — card filter (default "Card")
13/// - `Zone` — zone to balance (default Battlefield)
14///
15/// Each player with more than the minimum count sacrifices/discards down to the minimum.
16/// Struct form of this effect so it can participate in the
17/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
18/// `BalanceEffect` class extending `SpellAbilityEffect`.
19#[manabrew_engine_macros::spell_effect(BalanceEffect)]
20fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
21    let filter = sa
22        .ir
23        .valid_filter_text
24        .clone()
25        .unwrap_or_else(|| "Card".to_string());
26    let filter_selector = sa.ir.valid_filter_selector.clone();
27
28    let zone = sa.ir.zone.unwrap_or(ZoneType::Battlefield);
29
30    let player_order = ctx.game.player_order.clone();
31
32    // Count valid cards per player
33    let counts: Vec<(crate::ids::PlayerId, usize)> = player_order
34        .iter()
35        .map(|&pid| {
36            let count = ctx
37                .game
38                .cards_in_zone(zone, pid)
39                .to_vec()
40                .iter()
41                .filter(|&&cid| {
42                    matches_valid_cards_for_sa(
43                        ctx.game,
44                        sa,
45                        ctx.game.card(cid),
46                        filter_selector.as_ref(),
47                        &filter,
48                    )
49                })
50                .count();
51            (pid, count)
52        })
53        .collect();
54
55    let min_count = counts.iter().map(|(_, c)| *c).min().unwrap_or(0);
56
57    // Each player with excess must sacrifice/discard down
58    for &(pid, count) in &counts {
59        if count <= min_count {
60            continue;
61        }
62        let excess = count - min_count;
63
64        match zone {
65            ZoneType::Battlefield => {
66                // Sacrifice excess cards
67                for _ in 0..excess {
68                    let valid: Vec<_> = ctx
69                        .game
70                        .cards_in_zone(ZoneType::Battlefield, pid)
71                        .to_vec()
72                        .into_iter()
73                        .filter(|&cid| {
74                            matches_valid_cards_for_sa(
75                                ctx.game,
76                                sa,
77                                ctx.game.card(cid),
78                                filter_selector.as_ref(),
79                                &filter,
80                            )
81                        })
82                        .collect();
83
84                    if valid.is_empty() {
85                        break;
86                    }
87
88                    ctx.agents[pid.index()].snapshot_state(ctx.game, ctx.mana_pools);
89                    if let Some(card_id) =
90                        ctx.agents[pid.index()].choose_sacrifice(pid, &valid, sa.source)
91                    {
92                        if ctx.game.card(card_id).zone == ZoneType::Battlefield {
93                            let owner = ctx.game.card(card_id).owner;
94                            ctx.trigger_handler.run_trigger(
95                                TriggerType::Sacrificed,
96                                RunParams {
97                                    card: Some(card_id),
98                                    player: Some(pid),
99                                    ..Default::default()
100                                },
101                                false,
102                            );
103                            ctx.move_card(card_id, ZoneType::Graveyard, owner);
104                            emit_zone_trigger(
105                                ctx.trigger_handler,
106                                card_id,
107                                ZoneType::Battlefield,
108                                ZoneType::Graveyard,
109                            );
110                        }
111                    }
112                }
113            }
114            ZoneType::Hand => {
115                // Discard excess cards
116                let hand: Vec<_> = ctx.game.cards_in_zone(ZoneType::Hand, pid).to_vec();
117                ctx.agents[pid.index()].snapshot_state(ctx.game, ctx.mana_pools);
118                let to_discard = ctx.agents[pid.index()].choose_discard(pid, &hand, excess);
119
120                for &card_id in to_discard.iter().take(excess) {
121                    if ctx.game.card(card_id).zone == ZoneType::Hand {
122                        ctx.game.player_record_discard(pid, 1);
123                        ctx.game.card_mut(card_id).set_discarded(true);
124                        let owner = ctx.game.card(card_id).owner;
125                        ctx.move_card(card_id, ZoneType::Graveyard, owner);
126                        emit_zone_trigger(
127                            ctx.trigger_handler,
128                            card_id,
129                            ZoneType::Hand,
130                            ZoneType::Graveyard,
131                        );
132                        ctx.trigger_handler.run_trigger(
133                            TriggerType::Discarded,
134                            RunParams {
135                                card: Some(card_id),
136                                player: Some(pid),
137                                ..Default::default()
138                            },
139                            false,
140                        );
141                    }
142                }
143            }
144            _ => {
145                // Other zones: not commonly balanced, skip
146            }
147        }
148    }
149}