Skip to main content

manabrew_engine/zone/
mod.rs

1//! Zone module — game zones for cards.
2//!
3//! Mirrors Java's `forge.game.zone` package.
4
5pub mod cost_payment_stack;
6pub mod magic_stack;
7pub mod player_zone;
8pub mod player_zone_battlefield;
9pub mod zone_store;
10pub mod zone_type;
11
12use forge_foundation::ZoneType;
13use serde::{Deserialize, Serialize};
14
15use crate::ids::{CardId, PlayerId};
16
17// Re-exports
18pub use cost_payment_stack::CostPaymentStack;
19pub use player_zone::PlayerZone;
20pub use player_zone_battlefield::PlayerZoneBattlefield;
21pub use zone_store::ZoneStore;
22
23/// A game zone owned by a specific player.
24/// Each player has their own Hand, Library, Graveyard, etc.
25/// Battlefield and Stack are shared but cards still track their controller.
26///
27/// Mirrors Java's `Zone.java`.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Zone {
30    pub zone_type: ZoneType,
31    pub owner: PlayerId,
32    pub cards: Vec<CardId>,
33    /// Cards added this turn, keyed by their origin zone.
34    #[serde(skip)]
35    pub cards_added_this_turn: Vec<(ZoneType, CardId)>,
36    /// Cards added last turn, keyed by their origin zone.
37    #[serde(skip)]
38    pub cards_added_last_turn: Vec<(ZoneType, CardId)>,
39
40    // ── Battlefield-specific fields (mirrors Java's PlayerZoneBattlefield) ──
41    /// Cards that have been melded (combined into a single permanent).
42    /// Only meaningful when `zone_type == Battlefield`.
43    #[serde(default)]
44    pub melded_cards: Vec<CardId>,
45
46    /// Whether entering-the-battlefield triggers are active.
47    /// Mirrors Java's `PlayerZoneBattlefield.trigger` field.
48    /// Only meaningful when `zone_type == Battlefield`.
49    #[serde(default)]
50    pub triggers_enabled: bool,
51}
52
53impl Zone {
54    pub fn new(zone_type: ZoneType, owner: PlayerId) -> Self {
55        Zone {
56            zone_type,
57            owner,
58            cards: Vec::new(),
59            cards_added_this_turn: Vec::new(),
60            cards_added_last_turn: Vec::new(),
61            melded_cards: Vec::new(),
62            triggers_enabled: zone_type == ZoneType::Battlefield,
63        }
64    }
65
66    pub fn add(&mut self, card: CardId) {
67        self.cards.push(card);
68    }
69
70    pub fn add_to_top(&mut self, card: CardId) {
71        self.cards.push(card);
72    }
73
74    pub fn add_to_bottom(&mut self, card: CardId) {
75        self.cards.insert(0, card);
76    }
77
78    pub fn remove(&mut self, card: CardId) -> bool {
79        if let Some(pos) = self.cards.iter().position(|&c| c == card) {
80            self.cards.remove(pos);
81            true
82        } else {
83            false
84        }
85    }
86
87    /// Remove all cards from the zone.
88    /// Mirrors Java's `Zone.removeAllCards()`.
89    pub fn remove_all_cards(&mut self) {
90        self.cards.clear();
91    }
92
93    /// Reorder a card to a specific index.
94    /// Mirrors Java's `Zone.reorder()`.
95    pub fn reorder(&mut self, card: CardId, index: usize) {
96        if let Some(pos) = self.cards.iter().position(|&c| c == card) {
97            self.cards.remove(pos);
98            let idx = index.min(self.cards.len());
99            self.cards.insert(idx, card);
100        }
101    }
102
103    pub fn contains(&self, card: CardId) -> bool {
104        self.cards.contains(&card)
105    }
106
107    /// Check if this zone is the given type.
108    /// Mirrors Java's `Zone.is()`.
109    pub fn is(&self, zone_type: ZoneType) -> bool {
110        self.zone_type == zone_type
111    }
112
113    /// Number of cards in the zone.
114    /// Mirrors Java's `Zone.size()`.
115    pub fn size(&self) -> usize {
116        self.cards.len()
117    }
118
119    pub fn len(&self) -> usize {
120        self.cards.len()
121    }
122
123    pub fn is_empty(&self) -> bool {
124        self.cards.is_empty()
125    }
126
127    /// Get the card at the given index.
128    /// Mirrors Java's `Zone.get()`.
129    pub fn get(&self, index: usize) -> Option<CardId> {
130        self.cards.get(index).copied()
131    }
132
133    /// Take the top card (last element = top of library).
134    pub fn take_top(&mut self) -> Option<CardId> {
135        self.cards.pop()
136    }
137
138    /// Peek at the top card without removing it.
139    pub fn peek_top(&self) -> Option<CardId> {
140        self.cards.last().copied()
141    }
142
143    /// Reset turn tracking: move this-turn data to last-turn.
144    /// Mirrors Java's `Zone.resetCardsAddedThisTurn()`.
145    pub fn reset_cards_added_this_turn(&mut self) {
146        self.cards_added_last_turn = std::mem::take(&mut self.cards_added_this_turn);
147    }
148
149    /// Provides an iterator over the cards in this zone.
150    /// Mirrors Java's `Zone.iterator()`.
151    pub fn iterator(&self) -> impl Iterator<Item = &CardId> {
152        self.cards.iter()
153    }
154
155    /// Shuffle the cards in this zone using the provided RNG.
156    /// Mirrors Java's `Zone.shuffle()`.
157    ///
158    /// All game randomness must flow through the game's RNG for
159    /// deterministic replay and parity testing.
160    pub fn shuffle(&mut self, rng: &mut dyn crate::game_rng::GameRng) {
161        rng.shuffle_cards(&mut self.cards);
162    }
163
164    // ── Battlefield-specific methods ────────────────────────────────
165
166    /// Add a card to the melded cards list.
167    /// Mirrors Java's `PlayerZoneBattlefield.addToMelded()`.
168    pub fn add_to_melded(&mut self, card: CardId) {
169        self.melded_cards.push(card);
170    }
171
172    /// Remove a card from the melded cards list.
173    /// Mirrors Java's `PlayerZoneBattlefield.removeFromMelded()`.
174    pub fn remove_from_melded(&mut self, card: CardId) {
175        if let Some(pos) = self.melded_cards.iter().position(|&c| c == card) {
176            self.melded_cards.remove(pos);
177        }
178    }
179
180    // ── LKI tracking ────────────────────────────────────────────────
181
182    /// Get the cards in this zone.
183    /// The `_filter` parameter is ignored for non-battlefield zones (they always
184    /// return the full list). Battlefield filtering is handled by
185    /// `PlayerZoneBattlefield::get_cards()`.
186    /// Mirrors Java's `Zone.getCards(boolean)`.
187    pub fn get_cards(&self, _filter: bool) -> &[CardId] {
188        &self.cards
189    }
190
191    /// Get cards that were added to this zone this turn from the given origin zone.
192    /// Mirrors Java's `Zone.getCardsAddedThisTurn(ZoneType)`.
193    pub fn get_cards_added_this_turn(&self, origin: ZoneType) -> Vec<CardId> {
194        self.cards_added_this_turn
195            .iter()
196            .filter(|(z, _)| *z == origin)
197            .map(|(_, c)| *c)
198            .collect()
199    }
200
201    /// Get cards that were added to this zone last turn from the given origin zone.
202    /// Mirrors Java's `Zone.getCardsAddedLastTurn(ZoneType)`.
203    pub fn get_cards_added_last_turn(&self, origin: ZoneType) -> Vec<CardId> {
204        self.cards_added_last_turn
205            .iter()
206            .filter(|(z, _)| *z == origin)
207            .map(|(_, c)| *c)
208            .collect()
209    }
210
211    /// Check whether a specific card was added to this zone this turn from
212    /// the given origin zone.
213    /// Mirrors Java's `Zone.isCardAddedThisTurn(Card, ZoneType)`.
214    pub fn is_card_added_this_turn(&self, card: CardId, origin: ZoneType) -> bool {
215        self.cards_added_this_turn
216            .iter()
217            .any(|(z, c)| *z == origin && *c == card)
218    }
219
220    /// Create a shallow copy of this zone for last-known-information purposes.
221    /// The returned zone has the same type, owner, and card list.
222    /// Mirrors Java's `Zone.getLKICopy()`.
223    pub fn get_lki_copy(&self) -> Zone {
224        Zone {
225            zone_type: self.zone_type,
226            owner: self.owner,
227            cards: self.cards.clone(),
228            cards_added_this_turn: Vec::new(),
229            cards_added_last_turn: Vec::new(),
230            melded_cards: Vec::new(),
231            triggers_enabled: self.triggers_enabled,
232        }
233    }
234
235    /// Save last-known-information for a card entering this zone.
236    /// Mirrors Java's `Zone.saveLKI()`.
237    pub fn save_lki(&mut self, card: CardId, origin_zone: ZoneType) {
238        if origin_zone == self.zone_type {
239            return;
240        }
241        self.cards_added_this_turn.push((origin_zone, card));
242    }
243}
244
245/// Key for looking up a zone: (zone_type, owner).
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
247pub struct ZoneKey {
248    pub zone_type: ZoneType,
249    pub owner: PlayerId,
250}
251
252impl ZoneKey {
253    pub fn new(zone_type: ZoneType, owner: PlayerId) -> Self {
254        ZoneKey { zone_type, owner }
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn zone_add_remove() {
264        let mut z = Zone::new(ZoneType::Hand, PlayerId(0));
265        z.add(CardId(1));
266        z.add(CardId(2));
267        assert_eq!(z.size(), 2);
268        assert!(z.contains(CardId(1)));
269        z.remove(CardId(1));
270        assert_eq!(z.size(), 1);
271        assert!(!z.contains(CardId(1)));
272    }
273
274    #[test]
275    fn zone_reorder() {
276        let mut z = Zone::new(ZoneType::Library, PlayerId(0));
277        z.add(CardId(1));
278        z.add(CardId(2));
279        z.add(CardId(3));
280        z.reorder(CardId(3), 0);
281        assert_eq!(z.get(0), Some(CardId(3)));
282    }
283
284    #[test]
285    fn zone_is() {
286        let z = Zone::new(ZoneType::Graveyard, PlayerId(0));
287        assert!(z.is(ZoneType::Graveyard));
288        assert!(!z.is(ZoneType::Hand));
289    }
290
291    #[test]
292    fn reset_cards_added() {
293        let mut z = Zone::new(ZoneType::Battlefield, PlayerId(0));
294        z.save_lki(CardId(1), ZoneType::Hand);
295        assert_eq!(z.cards_added_this_turn.len(), 1);
296        z.reset_cards_added_this_turn();
297        assert!(z.cards_added_this_turn.is_empty());
298        assert_eq!(z.cards_added_last_turn.len(), 1);
299    }
300}