Skip to main content

manabrew_engine/zone/
magic_stack.rs

1//! MagicStack — the game stack for spells and abilities.
2//!
3//! Mirrors Java's `MagicStack.java` from `forge.game.zone`.
4//! Spells and abilities are pushed onto the stack and resolve LIFO.
5
6use forge_foundation::ZoneType;
7use serde::{Deserialize, Serialize};
8
9use crate::ids::{CardId, PlayerId};
10use crate::spellability::SpellAbility;
11
12// ── StackEntry (mirrors Java's SpellAbilityStackInstance) ────────────
13
14/// An entry on the game stack (spell or ability waiting to resolve).
15/// Mirrors Java's `SpellAbilityStackInstance` which wraps a `SpellAbility`.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct StackEntry {
18    pub id: u32,
19    /// The spell ability with its full sub-ability chain and targets.
20    pub spell_ability: SpellAbility,
21    /// True while a spell has been announced and moved to the stack, but its
22    /// modes/targets/costs/payment have not finished yet. Pending entries are
23    /// visible for casting prompts but must never resolve or receive priority.
24    #[serde(default)]
25    pub is_pending_cast: bool,
26    /// Whether this is a creature spell (goes to battlefield on resolve).
27    pub is_creature_spell: bool,
28    /// Whether this is a non-creature permanent spell.
29    pub is_permanent_spell: bool,
30    /// The zone the spell was cast from (for Flashback exile-on-resolve).
31    pub cast_from_zone: Option<ZoneType>,
32    /// If this is an optional trigger, the player who decides whether to
33    /// accept or decline.  Mirrors Java's WrappedAbility `decider` field.
34    #[serde(default)]
35    pub optional_trigger_decider: Option<PlayerId>,
36    /// Description text shown to the deciding player for optional triggers.
37    #[serde(default)]
38    pub optional_trigger_description: Option<String>,
39    /// Source card name for optional trigger prompts.
40    #[serde(default)]
41    pub optional_trigger_source_name: Option<String>,
42}
43
44impl StackEntry {
45    /// Get the next unique ID for a stack entry.
46    /// Mirrors Java's `SpellAbilityStackInstance.nextId()`.
47    pub fn next_id() -> u64 {
48        use std::sync::atomic::{AtomicU64, Ordering};
49        static COUNTER: AtomicU64 = AtomicU64::new(1);
50        COUNTER.fetch_add(1, Ordering::Relaxed)
51    }
52
53    /// Update a target card in this stack entry's spell ability.
54    /// Mirrors Java's `SpellAbilityStackInstance.updateTarget(Card, Card)`.
55    pub fn update_target(&mut self, old: CardId, new: CardId) {
56        self.spell_ability.update_target(old, new);
57    }
58
59    /// Set a triggering object on this stack entry's spell ability.
60    /// Mirrors Java's `SpellAbilityStackInstance.setTriggeringObject(String, Object)`.
61    pub fn set_triggering_object<K: crate::spellability::TriggerKeyInput>(
62        &mut self,
63        key: K,
64        value: &str,
65    ) {
66        self.spell_ability.set_triggering_object(key, value);
67    }
68
69    /// Update a triggering object in this stack entry's spell ability.
70    /// Mirrors Java's `SpellAbilityStackInstance.updateTriggeringObject(String, Object)`.
71    pub fn update_triggering_object<K: crate::spellability::TriggerKeyInput>(
72        &mut self,
73        key: K,
74        value: &str,
75    ) {
76        self.spell_ability.update_triggering_object(key, value);
77    }
78}
79
80/// The game stack. Spells and abilities are added to the top and resolve LIFO.
81/// Mirrors Java's `MagicStack` class.
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct MagicStack {
84    entries: Vec<StackEntry>,
85    next_id: u32,
86
87    /// Whether the stack is frozen (during declare attackers/blockers).
88    /// While frozen, new non-mana abilities are queued in `frozen_stack`.
89    #[serde(default)]
90    frozen: bool,
91
92    /// Entries queued while the stack is frozen (combat declarations).
93    #[serde(default)]
94    frozen_stack: Vec<StackEntry>,
95
96    /// Whether the stack is currently resolving an entry.
97    #[serde(default)]
98    resolving: bool,
99
100    /// The card currently being resolved (if any).
101    #[serde(default)]
102    cur_resolving_card: Option<CardId>,
103
104    /// Short-lived LKI cache of entries that were removed from the stack
105    /// during the current resolution cycle (e.g. by Counter effects). Used
106    /// by `find_by_id` so sub-abilities like An Offer You Can't Refuse's
107    /// "its controller creates two Treasure tokens" can still look up the
108    /// countered spell's controller after it's left the stack.
109    #[serde(default, skip)]
110    recently_removed: Vec<StackEntry>,
111
112    /// Cards (by ID) of spells cast this turn — for storm count, etc.
113    #[serde(default)]
114    this_turn_cast: Vec<CardId>,
115
116    /// Cards cast last turn (rotated from this_turn_cast on turn change).
117    #[serde(default)]
118    last_turn_cast: Vec<CardId>,
119
120    /// Abilities activated this turn.
121    #[serde(default)]
122    this_turn_activated: Vec<CardId>,
123
124    /// Maximum distinct sources that have been on the stack simultaneously.
125    #[serde(default)]
126    max_distinct_sources: usize,
127
128    /// Undo stack — tracks undoable spell abilities and their owner.
129    #[serde(default)]
130    undo_stack: Vec<UndoEntry>,
131
132    /// Player who owns the current undo stack.
133    #[serde(default)]
134    undo_stack_owner: Option<PlayerId>,
135
136    /// Simultaneous stack entries waiting to be added (triggers that fire
137    /// at the same time and need ordering by the active player).
138    #[serde(default)]
139    simultaneous_entries: Vec<StackEntry>,
140
141    /// Cast commands keyed by card name — callbacks to run when a spell resolves.
142    #[serde(default)]
143    cast_commands: std::collections::HashMap<String, Vec<String>>,
144}
145
146/// An undo entry tracking a spell that can be undone.
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct UndoEntry {
149    pub source_card: Option<CardId>,
150    pub activating_player: PlayerId,
151}
152
153impl MagicStack {
154    pub fn new() -> Self {
155        MagicStack {
156            entries: Vec::new(),
157            next_id: 0,
158            frozen: false,
159            frozen_stack: Vec::new(),
160            resolving: false,
161            cur_resolving_card: None,
162            this_turn_cast: Vec::new(),
163            last_turn_cast: Vec::new(),
164            this_turn_activated: Vec::new(),
165            max_distinct_sources: 0,
166            undo_stack: Vec::new(),
167            undo_stack_owner: None,
168            simultaneous_entries: Vec::new(),
169            cast_commands: std::collections::HashMap::new(),
170            recently_removed: Vec::new(),
171        }
172    }
173
174    pub fn push(&mut self, mut entry: StackEntry) -> u32 {
175        let id = self.next_id;
176        self.next_id += 1;
177        entry.is_pending_cast = false;
178        entry.id = id;
179        self.entries.push(entry);
180        self.update_max_distinct_sources();
181        id
182    }
183
184    pub fn begin_pending_cast(&mut self, mut entry: StackEntry) -> u32 {
185        let id = self.next_id;
186        self.next_id += 1;
187        entry.id = id;
188        entry.is_pending_cast = true;
189        self.entries.push(entry);
190        self.update_max_distinct_sources();
191        id
192    }
193
194    pub fn complete_pending_cast(&mut self, id: u32, mut entry: StackEntry) -> Option<&StackEntry> {
195        let pending = self
196            .entries
197            .iter_mut()
198            .find(|existing| existing.id == id && existing.is_pending_cast)?;
199        entry.id = id;
200        entry.is_pending_cast = false;
201        *pending = entry;
202        self.update_max_distinct_sources();
203        self.entries.iter().find(|existing| existing.id == id)
204    }
205
206    pub fn remove_pending_cast(&mut self, id: u32) -> Option<StackEntry> {
207        let index = self
208            .entries
209            .iter()
210            .position(|entry| entry.id == id && entry.is_pending_cast)?;
211        let entry = self.entries.remove(index);
212        self.update_max_distinct_sources();
213        Some(entry)
214    }
215
216    fn update_max_distinct_sources(&mut self) {
217        let distinct: std::collections::HashSet<_> = self
218            .entries
219            .iter()
220            .filter_map(|e| e.spell_ability.source)
221            .collect();
222        if distinct.len() > self.max_distinct_sources {
223            self.max_distinct_sources = distinct.len();
224        }
225    }
226
227    pub fn pop(&mut self) -> Option<StackEntry> {
228        self.entries.pop()
229    }
230
231    pub fn peek(&self) -> Option<&StackEntry> {
232        self.entries.last()
233    }
234
235    pub fn is_empty(&self) -> bool {
236        self.entries.is_empty()
237    }
238
239    pub fn len(&self) -> usize {
240        self.entries.len()
241    }
242
243    pub fn iter(&self) -> impl Iterator<Item = &StackEntry> {
244        self.entries.iter()
245    }
246
247    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut StackEntry> {
248        self.entries.iter_mut()
249    }
250
251    /// Find a stack entry by ID without removing it. Also searches the
252    /// short-lived LKI cache populated by Counter effects so that follow-up
253    /// sub-abilities can still resolve `Defined$ TargetedController` after
254    /// the target spell has been pulled off the stack.
255    pub fn find_by_id(&self, id: u32) -> Option<&StackEntry> {
256        self.entries
257            .iter()
258            .find(|e| e.id == id)
259            .or_else(|| self.recently_removed.iter().find(|e| e.id == id))
260    }
261
262    /// Remove and return the stack entry with the given ID (for Counter effects).
263    /// The removed entry is kept in a short-lived LKI cache so follow-up
264    /// sub-abilities can still observe its metadata (see `find_by_id`).
265    pub fn remove_by_id(&mut self, id: u32) -> Option<StackEntry> {
266        if let Some(pos) = self.entries.iter().position(|e| e.id == id) {
267            let entry = self.entries.remove(pos);
268            self.recently_removed.push(entry.clone());
269            Some(entry)
270        } else {
271            None
272        }
273    }
274
275    /// Clear the LKI cache of recently-removed entries. Called at the end of
276    /// each stack-resolution cycle so the cache doesn't leak state.
277    pub fn clear_recently_removed(&mut self) {
278        self.recently_removed.clear();
279    }
280
281    /// Find a stack entry by its source card ID (for Ward — finding the targeting spell).
282    pub fn find_by_source_card(&self, card_id: CardId) -> Option<&StackEntry> {
283        self.entries
284            .iter()
285            .find(|e| e.spell_ability.source == Some(card_id))
286    }
287
288    /// Number of items on the stack. Mirrors Java's `MagicStack.size()`.
289    pub fn size(&self) -> usize {
290        self.entries.len()
291    }
292
293    /// Add a stack entry. Mirrors Java's `MagicStack.add()`.
294    pub fn add(&mut self, entry: StackEntry) -> u32 {
295        self.push(entry)
296    }
297
298    /// Remove a specific entry by ID. Mirrors Java's `MagicStack.remove()`.
299    pub fn remove(&mut self, id: u32) -> Option<StackEntry> {
300        self.remove_by_id(id)
301    }
302
303    /// Clear all entries from the stack. Mirrors Java's underlying `clear()`.
304    pub fn clear(&mut self) {
305        self.entries.clear();
306    }
307
308    /// Reset the stack completely. Mirrors Java's `MagicStack.reset()`.
309    pub fn reset(&mut self) {
310        self.entries.clear();
311        self.next_id = 0;
312        self.frozen = false;
313        self.frozen_stack.clear();
314        self.resolving = false;
315        self.cur_resolving_card = None;
316        self.last_turn_cast.clear();
317        self.this_turn_cast.clear();
318        self.simultaneous_entries.clear();
319        self.undo_stack.clear();
320        self.undo_stack_owner = None;
321        self.cast_commands.clear();
322    }
323
324    /// Peek at the top ability. Mirrors Java's `MagicStack.peekAbility()`.
325    pub fn peek_ability(&self) -> Option<&SpellAbility> {
326        self.entries.last().map(|e| &e.spell_ability)
327    }
328
329    /// Check if any entry has the given source card.
330    /// Mirrors Java's `MagicStack.hasSourceOnStack()`.
331    pub fn has_source_on_stack(&self, card_id: CardId) -> bool {
332        self.entries
333            .iter()
334            .any(|e| e.spell_ability.source == Some(card_id))
335    }
336
337    /// Check if the top entry has legal targeting (at least one target chosen).
338    /// Mirrors Java's `MagicStack.hasLegalTargeting()`.
339    pub fn has_legal_targeting(&self) -> bool {
340        match self.entries.last() {
341            Some(entry) => {
342                let tc = &entry.spell_ability.target_chosen;
343                // No targeting required = always legal
344                if entry.spell_ability.target_restrictions.is_none() {
345                    return true;
346                }
347                // Has at least one target chosen
348                tc.target_card.is_some()
349                    || tc.target_player.is_some()
350                    || tc.target_stack_entry.is_some()
351            }
352            None => false,
353        }
354    }
355
356    /// Remove all entries controlled by the given player.
357    /// Mirrors Java's `MagicStack.removeInstancesControlledBy()`.
358    pub fn remove_instances_controlled_by(&mut self, player: PlayerId) {
359        self.entries
360            .retain(|e| e.spell_ability.activating_player != player);
361    }
362
363    /// Forward iterator. Mirrors Java's `MagicStack.iterator()`.
364    pub fn iterator(&self) -> impl Iterator<Item = &StackEntry> {
365        self.entries.iter()
366    }
367
368    /// Reverse iterator (top to bottom). Mirrors Java's `MagicStack.reverseIterator()`.
369    pub fn reverse_iterator(&self) -> impl Iterator<Item = &StackEntry> {
370        self.entries.iter().rev()
371    }
372
373    // ── Frozen stack (for declare attackers/blockers) ────────────────
374
375    /// Whether the stack is currently frozen.
376    pub fn is_frozen(&self) -> bool {
377        self.frozen
378    }
379
380    /// Freeze the stack. While frozen, new entries go to the frozen queue.
381    /// Mirrors Java's `MagicStack.freezeStack()`.
382    pub fn freeze_stack(&mut self) {
383        self.frozen = true;
384    }
385
386    /// Add an entry and unfreeze, flushing any queued frozen entries.
387    /// Mirrors Java's `MagicStack.addAndUnfreeze()`.
388    pub fn add_and_unfreeze(&mut self, entry: StackEntry) -> u32 {
389        let id = self.push(entry);
390        self.unfreeze_stack();
391        id
392    }
393
394    /// Unfreeze the stack and flush all frozen entries onto the real stack.
395    /// Mirrors Java's `MagicStack.unfreezeStack()`.
396    pub fn unfreeze_stack(&mut self) {
397        self.frozen = false;
398        // Move frozen entries onto the real stack
399        let frozen = std::mem::take(&mut self.frozen_stack);
400        for entry in frozen.into_iter().rev() {
401            self.push(entry);
402        }
403    }
404
405    /// Clear all frozen entries without processing them.
406    /// Mirrors Java's `MagicStack.clearFrozen()`.
407    pub fn clear_frozen(&mut self) {
408        self.frozen = false;
409        self.frozen_stack.clear();
410    }
411
412    // ── Resolution state ─────────────────────────────────────────────
413
414    /// Whether the stack is currently resolving.
415    pub fn is_resolving(&self) -> bool {
416        self.resolving
417    }
418
419    pub fn set_resolving(&mut self, resolving: bool) {
420        self.resolving = resolving;
421    }
422
423    pub fn set_cur_resolving_card(&mut self, card: Option<CardId>) {
424        self.cur_resolving_card = card;
425    }
426
427    pub fn cur_resolving_card(&self) -> Option<CardId> {
428        self.cur_resolving_card
429    }
430
431    // ── Undo stack ───────────────────────────────────────────────────
432
433    /// Check if undo is available for the given player.
434    /// Mirrors Java's `MagicStack.canUndo()`.
435    pub fn can_undo(&self, player: PlayerId) -> bool {
436        self.undo_stack_owner == Some(player) && !self.undo_stack.is_empty()
437    }
438
439    /// Undo the last undoable action. Returns true if successful.
440    /// Mirrors Java's `MagicStack.undo()`.
441    pub fn undo(&mut self) -> bool {
442        if self.undo_stack.is_empty() {
443            return false;
444        }
445        self.undo_stack.pop();
446        if self.undo_stack.is_empty() {
447            self.undo_stack_owner = None;
448        }
449        true
450    }
451
452    /// Clear the undo stack entirely.
453    /// Mirrors Java's `MagicStack.clearUndoStack()`.
454    pub fn clear_undo_stack(&mut self) {
455        self.undo_stack.clear();
456        self.undo_stack_owner = None;
457    }
458
459    /// Remove undo entries whose source matches the given card.
460    /// Mirrors Java's `MagicStack.filterUndoStackByHost()`.
461    pub fn filter_undo_stack_by_host(&mut self, card_id: CardId) {
462        self.undo_stack.retain(|e| e.source_card != Some(card_id));
463        if self.undo_stack.is_empty() {
464            self.undo_stack_owner = None;
465        }
466    }
467
468    /// Record an undoable action on the undo stack.
469    pub fn record_undoable(&mut self, source: Option<CardId>, player: PlayerId) {
470        self.undo_stack_owner = Some(player);
471        self.undo_stack.push(UndoEntry {
472            source_card: source,
473            activating_player: player,
474        });
475    }
476
477    // ── Simultaneous stack entries (triggers) ────────────────────────
478
479    /// Check if there are simultaneous stack entries waiting to be added.
480    /// Mirrors Java's `MagicStack.hasSimultaneousStackEntries()`.
481    pub fn has_simultaneous_stack_entries(&self) -> bool {
482        !self.simultaneous_entries.is_empty()
483    }
484
485    /// Clear all simultaneous stack entries.
486    /// Mirrors Java's `MagicStack.clearSimultaneousStack()`.
487    pub fn clear_simultaneous_stack(&mut self) {
488        self.simultaneous_entries.clear();
489    }
490
491    /// Queue a simultaneous stack entry (trigger) for later addition.
492    /// Mirrors Java's `MagicStack.addSimultaneousStackEntry()`.
493    pub fn add_simultaneous_stack_entry(&mut self, entry: StackEntry) {
494        self.simultaneous_entries.push(entry);
495    }
496
497    /// Move all queued simultaneous entries onto the real stack.
498    /// Returns true if any were added.
499    /// Mirrors Java's `MagicStack.addAllTriggeredAbilitiesToStack()`.
500    pub fn add_all_triggered_abilities_to_stack(&mut self) -> bool {
501        if self.simultaneous_entries.is_empty() {
502            return false;
503        }
504        let entries = std::mem::take(&mut self.simultaneous_entries);
505        for entry in entries {
506            self.push(entry);
507        }
508        true
509    }
510
511    /// Check if there's a state trigger waiting in the simultaneous queue.
512    /// Mirrors Java's `MagicStack.hasStateTrigger()`.
513    pub fn has_state_trigger(&self) -> bool {
514        self.simultaneous_entries
515            .iter()
516            .any(|e| e.spell_ability.is_trigger)
517    }
518
519    /// Check if a specific trigger id already exists in pending/active stack entries.
520    /// Mirrors Java's `MagicStack.hasStateTrigger(triggerId)` behavior.
521    pub fn has_state_trigger_id(&self, trigger_id: u32) -> bool {
522        let matches = |e: &StackEntry| {
523            e.spell_ability.is_trigger && e.spell_ability.source_trigger_id == Some(trigger_id)
524        };
525        self.entries.iter().any(matches)
526            || self.frozen_stack.iter().any(matches)
527            || self.simultaneous_entries.iter().any(matches)
528    }
529
530    // ── Cast commands ────────────────────────────────────────────────
531
532    /// Register a command to run when a spell with the given key resolves.
533    /// Mirrors Java's `MagicStack.addCastCommand()`.
534    pub fn add_cast_command(&mut self, key: &str, command: String) {
535        self.cast_commands
536            .entry(key.to_string())
537            .or_default()
538            .push(command);
539    }
540
541    /// Take and return any cast commands registered for the given key.
542    pub fn take_cast_commands(&mut self, key: &str) -> Vec<String> {
543        self.cast_commands.remove(key).unwrap_or_default()
544    }
545
546    // ── Stack resolution ─────────────────────────────────────────────
547
548    /// Pop the top entry and begin resolution.
549    /// Sets `resolving` state and tracks the resolving card.
550    /// The actual effect resolution is driven by `GameLoop::resolve_stack()`
551    /// which calls this to get the entry, then resolves it with full game context.
552    /// Mirrors Java's `MagicStack.resolveStack()`.
553    pub fn resolve_stack(&mut self) -> Option<StackEntry> {
554        if self
555            .entries
556            .last()
557            .map(|entry| entry.is_pending_cast)
558            .unwrap_or(false)
559        {
560            return None;
561        }
562        let entry = self.entries.pop()?;
563        self.resolving = true;
564        self.cur_resolving_card = entry.spell_ability.source;
565        Some(entry)
566    }
567
568    /// Mark resolution as complete.
569    pub fn finish_resolving(&mut self) {
570        self.resolving = false;
571        self.cur_resolving_card = None;
572    }
573
574    // ── Turn tracking ────────────────────────────────────────────────
575
576    /// Called when a new turn begins. Rotates cast/activated tracking.
577    /// Mirrors Java's `MagicStack.onNextTurn()`.
578    pub fn on_next_turn(&mut self) {
579        self.last_turn_cast = std::mem::take(&mut self.this_turn_cast);
580        self.this_turn_activated.clear();
581    }
582
583    /// Record that a spell was cast this turn (for storm count, etc.).
584    pub fn record_spell_cast(&mut self, card_id: CardId) {
585        self.this_turn_cast.push(card_id);
586    }
587
588    /// Get the number of spells cast this turn (storm count).
589    pub fn spells_cast_this_turn(&self) -> usize {
590        self.this_turn_cast.len()
591    }
592
593    /// Get the list of spells cast this turn.
594    pub fn get_spells_cast_this_turn(&self) -> &[CardId] {
595        &self.this_turn_cast
596    }
597
598    /// Get the list of spells cast last turn.
599    pub fn get_spells_cast_last_turn(&self) -> &[CardId] {
600        &self.last_turn_cast
601    }
602
603    /// Track an ability activation this turn.
604    /// Mirrors Java's `MagicStack.addAbilityActivatedThisTurn()`.
605    pub fn add_ability_activated_this_turn(&mut self, sa: &SpellAbility) {
606        if let Some(source) = sa.source {
607            self.this_turn_activated.push(source);
608        }
609    }
610
611    /// Reset max distinct sources counter.
612    /// Mirrors Java's `MagicStack.resetMaxDistinctSources()`.
613    pub fn reset_max_distinct_sources(&mut self) {
614        self.max_distinct_sources = 0;
615    }
616
617    /// Get the max distinct sources seen on the stack this turn.
618    pub fn get_max_distinct_sources(&self) -> usize {
619        self.max_distinct_sources
620    }
621
622    /// Find a stack entry whose spell ability ID matches the given stack entry ID.
623    /// Mirrors Java's `MagicStack.getInstanceMatchingSpellAbilityID()`.
624    pub fn get_instance_matching_spell_ability_id(&self, id: u32) -> Option<&StackEntry> {
625        self.entries.iter().find(|si| si.id == id)
626    }
627
628    /// Find a spell on the stack whose host card matches the given card.
629    /// Returns the spell ability if found.
630    /// Mirrors Java's `MagicStack.getSpellMatchingHost()`.
631    pub fn get_spell_matching_host(&self, host: CardId) -> Option<&SpellAbility> {
632        for si in &self.entries {
633            if si.spell_ability.is_spell && si.spell_ability.source == Some(host) {
634                return Some(&si.spell_ability);
635            }
636        }
637        None
638    }
639}
640
641impl Default for MagicStack {
642    fn default() -> Self {
643        Self::new()
644    }
645}