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}