Skip to main content

manabrew_engine/ability/effects/
rearrange_top_of_library_effect.rs

1use forge_foundation::ZoneType;
2
3use super::{resolve_defined_player, resolve_numeric_svar, EffectContext};
4use crate::event::RunParams;
5use crate::parsing::keys;
6use crate::trigger::TriggerType;
7
8/// Mirrors the `RearrangeTopOfLibrary` API used by cards like Ponder.
9///
10/// `SP$ RearrangeTopOfLibrary | Defined$ You | NumCards$ N | MayShuffle$ True`
11/// The activating player looks at the top N cards and puts them back in any order.
12/// With `MayShuffle$ True`, the player may choose to shuffle instead.
13///
14/// The agent decides the order via `choose_reorder_library`.
15/// The default implementation (PassAgent) keeps the existing order.
16/// Struct form of this effect so it can participate in the
17/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
18/// `RearrangeTopOfLibraryEffect` class extending `SpellAbilityEffect`.
19#[manabrew_engine_macros::spell_effect(RearrangeTopOfLibraryEffect)]
20fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
21    let num = resolve_numeric_svar(ctx.game, sa, keys::NUM_CARDS, 3).max(0) as usize;
22    let may_shuffle = sa.ir.may_shuffle;
23
24    let target = sa
25        .defined()
26        .and_then(|defined| resolve_defined_player(defined, sa.activating_player, ctx.game))
27        .unwrap_or(sa.activating_player);
28
29    let lib_len = ctx.game.cards_in_zone(ZoneType::Library, target).len();
30    if lib_len == 0 {
31        return;
32    }
33
34    let count = num.min(lib_len);
35
36    // Take top N cards (last `count` elements).
37    let mut top_n = ctx
38        .game
39        .take_top_cards_from_zone(ZoneType::Library, target, count);
40
41    // Reverse to present in top-first order, matching Java's getTopXCardsFromLibrary
42    // which returns [top, 2nd, 3rd, ...]. Rust's split_off gives [3rd, 2nd, top].
43    // The agent convention (shared with Java) is: last element = will go on top.
44    // Java's moveToLibrary(card, 0) loop reverses the returned list (first card
45    // ends up deepest, last card on top). Rust's push loop does the same (last
46    // element pushed = end of Vec = top). By reversing the input, both agents
47    // see the same card order, and "keep original" produces the same result.
48    top_n.reverse();
49
50    // Let the agent see the cards before reordering.
51    ctx.agents[sa.activating_player.index()].snapshot_state(ctx.game, ctx.mana_pools);
52
53    // Ask the agent to reorder the cards.
54    let reordered = ctx.agents[sa.activating_player.index()].choose_reorder_library(
55        ctx.game,
56        sa.activating_player,
57        &top_n,
58    );
59
60    // Validate: use reordered if it contains exactly the same cards.
61    let put_back =
62        if reordered.len() == top_n.len() && top_n.iter().all(|id| reordered.contains(id)) {
63            reordered
64        } else {
65            top_n
66        };
67
68    // Put cards back on top (append to end = top of library).
69    // Convention: last element in put_back = top of library, matching Java's
70    // moveToLibrary(card, 0) loop where the last card iterated ends up on top.
71    for &id in &put_back {
72        ctx.game.add_card_to_zone(ZoneType::Library, target, id);
73    }
74
75    // Handle optional shuffle.
76    if may_shuffle {
77        let source_name = sa.source.map(|cid| ctx.game.card(cid).card_name.as_str());
78        let wants_shuffle = ctx.agents[sa.activating_player.index()].confirm_action(
79            sa.activating_player,
80            None,
81            "Do you want to shuffle the library?",
82            &[],
83            sa.source,
84            Some(crate::ability::api_type::ApiType::RearrangeTopOfLibrary),
85        );
86        if wants_shuffle {
87            ctx.game
88                .shuffle_zone_cards(ZoneType::Library, target, ctx.rng);
89            ctx.trigger_handler.run_trigger(
90                TriggerType::Shuffled,
91                RunParams {
92                    player: Some(target),
93                    ..Default::default()
94                },
95                false,
96            );
97        }
98    }
99}