Skip to main content

manabrew_engine/ability/effects/
two_piles_effect.rs

1use forge_foundation::ZoneType;
2
3use super::{emit_zone_trigger, resolve_numeric_svar, EffectContext};
4use crate::ids::CardId;
5use crate::parsing::keys;
6
7/// `SP$ TwoPiles` — divide cards into two piles and an opponent chooses one.
8///
9/// Mirrors Java's `TwoPilesEffect.java` (simplified — auto-divides).
10/// - `NumCards$` — number of cards to reveal from library (default 5).
11/// - `Zone1$` — destination for the chosen pile (default Hand).
12/// - `Zone2$` — destination for the unchosen pile (default Graveyard).
13///
14/// # Card script examples
15/// ```text
16/// A:SP$ TwoPiles | NumCards$ 5 | Zone1$ Hand | Zone2$ Graveyard
17/// A:SP$ TwoPiles | NumCards$ 3 | Zone1$ Hand | Zone2$ Library
18/// ```
19/// Struct form of this effect so it can participate in the
20/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
21/// `TwoPilesEffect` class extending `SpellAbilityEffect`.
22#[manabrew_engine_macros::spell_effect(TwoPilesEffect)]
23fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
24    let controller = sa.activating_player;
25    let num_cards = resolve_numeric_svar(ctx.game, sa, keys::NUM_CARDS, 5).max(0) as usize;
26
27    let zone1 = sa.ir.zone1.unwrap_or(ZoneType::Hand);
28    let zone2 = sa.ir.zone2.unwrap_or(ZoneType::Graveyard);
29
30    // Get top N cards from library
31    let lib = ctx
32        .game
33        .cards_in_zone(ZoneType::Library, controller)
34        .to_vec();
35    if lib.is_empty() {
36        return;
37    }
38
39    let count = num_cards.min(lib.len());
40    // Library is bottom-to-top, so take from the end
41    let revealed: Vec<CardId> = lib[lib.len() - count..].to_vec();
42
43    if revealed.is_empty() {
44        return;
45    }
46
47    // Controller divides into two piles — simplified: they choose cards for pile 1
48    // via choose_cards_for_effect (min 0, max count-1 to ensure both piles have at least 1 if count>1)
49    let min_pile1 = if count > 1 { 1 } else { 0 };
50    let max_pile1 = if count > 1 { count - 1 } else { count };
51
52    let pile1: Vec<CardId> = ctx.agents[controller.index()]
53        .choose_cards_for_effect(controller, &revealed, min_pile1, max_pile1);
54    let pile2: Vec<CardId> = revealed
55        .iter()
56        .filter(|c| !pile1.contains(c))
57        .copied()
58        .collect();
59
60    // Opponent chooses which pile goes to zone1
61    let opponent = ctx.game.opponent_of(controller);
62    let pile1_names: Vec<String> = pile1
63        .iter()
64        .map(|&cid| ctx.game.card(cid).card_name.clone())
65        .collect();
66    let pile2_names: Vec<String> = pile2
67        .iter()
68        .map(|&cid| ctx.game.card(cid).card_name.clone())
69        .collect();
70
71    let prompt = format!(
72        "Choose a pile: Pile 1 ({}) or Pile 2 ({})?",
73        pile1_names.join(", "),
74        pile2_names.join(", "),
75    );
76    let choose_pile1 =
77        ctx.agents[opponent.index()].choose_optional_trigger(opponent, &prompt, sa.source, sa.api);
78
79    let (chosen_pile, unchosen_pile) = if choose_pile1 {
80        (pile1, pile2)
81    } else {
82        (pile2, pile1)
83    };
84
85    // Move chosen pile to zone1
86    for cid in chosen_pile {
87        if ctx.game.card(cid).zone == ZoneType::Library {
88            ctx.move_card(cid, zone1, controller);
89            emit_zone_trigger(ctx.trigger_handler, cid, ZoneType::Library, zone1);
90        }
91    }
92
93    // Move unchosen pile to zone2
94    for cid in unchosen_pile {
95        if ctx.game.card(cid).zone == ZoneType::Library {
96            ctx.move_card(cid, zone2, controller);
97            emit_zone_trigger(ctx.trigger_handler, cid, ZoneType::Library, zone2);
98        }
99    }
100}