Skip to main content

manabrew_engine/zone/
zone_store.rs

1use forge_foundation::ZoneType;
2
3use crate::game_rng::GameRng;
4use crate::ids::{CardId, PlayerId};
5
6use super::{Zone, ZoneKey};
7
8/// Zones allocated for each player at game creation.
9///
10/// This intentionally mirrors the previous `GameState::new` zone list. Zones
11/// outside this set, including `ZoneType::None`, are not stored zones and keep
12/// the old "Zone not found" behavior when accessed through `GameState::zone`.
13pub const STORED_ZONE_TYPES: [ZoneType; 16] = [
14    ZoneType::Hand,
15    ZoneType::Library,
16    ZoneType::Graveyard,
17    ZoneType::Battlefield,
18    ZoneType::Exile,
19    ZoneType::Command,
20    ZoneType::Sideboard,
21    ZoneType::SchemeDeck,
22    ZoneType::PlanarDeck,
23    ZoneType::AttractionDeck,
24    ZoneType::ContraptionDeck,
25    ZoneType::Junkyard,
26    ZoneType::Ante,
27    ZoneType::ExtraHand,
28    ZoneType::Subgame,
29    ZoneType::Stack,
30];
31
32/// Fixed-index store for per-player zones.
33///
34/// The old representation was `HashMap<ZoneKey, Zone>`. Most callers already
35/// route through `GameState::zone`, `zone_mut`, and `cards_in_zone`, so this
36/// store preserves that API while avoiding hash lookup on hot zone access.
37#[derive(Debug, Clone, Default)]
38pub struct ZoneStore {
39    zones: Vec<Zone>,
40    card_locations: Vec<Option<ZoneKey>>,
41    player_count: usize,
42}
43
44impl ZoneStore {
45    pub fn new(players: &[PlayerId]) -> Self {
46        let mut zones = Vec::with_capacity(players.len() * STORED_ZONE_TYPES.len());
47        for &pid in players {
48            for &zone_type in &STORED_ZONE_TYPES {
49                zones.push(Zone::new(zone_type, pid));
50            }
51        }
52        Self {
53            zones,
54            card_locations: Vec::new(),
55            player_count: players.len(),
56        }
57    }
58
59    pub fn get(&self, zone_type: ZoneType, owner: PlayerId) -> Option<&Zone> {
60        self.index(zone_type, owner)
61            .and_then(|index| self.zones.get(index))
62    }
63
64    pub fn get_mut(&mut self, zone_type: ZoneType, owner: PlayerId) -> Option<&mut Zone> {
65        self.index(zone_type, owner)
66            .and_then(|index| self.zones.get_mut(index))
67    }
68
69    pub fn remove_card(&mut self, zone_type: ZoneType, owner: PlayerId, card: CardId) -> bool {
70        let removed = self
71            .get_mut(zone_type, owner)
72            .expect("Zone not found")
73            .remove(card);
74        if removed {
75            self.clear_card_location(card, zone_type, owner);
76        }
77        removed
78    }
79
80    pub fn add_card_to_top(&mut self, zone_type: ZoneType, owner: PlayerId, card: CardId) {
81        self.get_mut(zone_type, owner)
82            .expect("Zone not found")
83            .add_to_top(card);
84        self.set_card_location(card, zone_type, owner);
85    }
86
87    pub fn add_card_to_bottom(&mut self, zone_type: ZoneType, owner: PlayerId, card: CardId) {
88        self.get_mut(zone_type, owner)
89            .expect("Zone not found")
90            .add_to_bottom(card);
91        self.set_card_location(card, zone_type, owner);
92    }
93
94    pub fn take_top_card(&mut self, zone_type: ZoneType, owner: PlayerId) -> Option<CardId> {
95        let card = self
96            .get_mut(zone_type, owner)
97            .expect("Zone not found")
98            .take_top()?;
99        self.clear_card_location(card, zone_type, owner);
100        Some(card)
101    }
102
103    pub fn reorder_card(
104        &mut self,
105        zone_type: ZoneType,
106        owner: PlayerId,
107        card: CardId,
108        index: usize,
109    ) {
110        self.get_mut(zone_type, owner)
111            .expect("Zone not found")
112            .reorder(card, index);
113    }
114
115    pub fn move_cards_to_top(&mut self, zone_type: ZoneType, owner: PlayerId, cards: &[CardId]) {
116        let zone = self.get_mut(zone_type, owner).expect("Zone not found");
117        for card in cards {
118            if let Some(pos) = zone.cards.iter().position(|&c| c == *card) {
119                zone.cards.remove(pos);
120            }
121        }
122        for card in cards {
123            zone.cards.push(*card);
124        }
125    }
126
127    pub fn move_cards_to_bottom(&mut self, zone_type: ZoneType, owner: PlayerId, cards: &[CardId]) {
128        let zone = self.get_mut(zone_type, owner).expect("Zone not found");
129        for card in cards {
130            if let Some(pos) = zone.cards.iter().position(|&c| c == *card) {
131                zone.cards.remove(pos);
132            }
133        }
134        for card in cards.iter().rev() {
135            zone.cards.insert(0, *card);
136        }
137    }
138
139    pub fn replace_cards(&mut self, zone_type: ZoneType, owner: PlayerId, cards: Vec<CardId>) {
140        let previous = {
141            let zone = self.get_mut(zone_type, owner).expect("Zone not found");
142            std::mem::replace(&mut zone.cards, cards)
143        };
144        for card in previous {
145            self.clear_card_location(card, zone_type, owner);
146        }
147        let current = self
148            .get(zone_type, owner)
149            .expect("Zone not found")
150            .cards
151            .clone();
152        for card in current {
153            self.set_card_location(card, zone_type, owner);
154        }
155    }
156
157    pub fn shuffle_cards(&mut self, zone_type: ZoneType, owner: PlayerId, rng: &mut dyn GameRng) {
158        self.get_mut(zone_type, owner)
159            .expect("Zone not found")
160            .shuffle(rng);
161    }
162
163    pub fn shuffle_cards_with_rand<R: rand::Rng + ?Sized>(
164        &mut self,
165        zone_type: ZoneType,
166        owner: PlayerId,
167        rng: &mut R,
168    ) {
169        use rand::seq::SliceRandom;
170
171        self.get_mut(zone_type, owner)
172            .expect("Zone not found")
173            .cards
174            .shuffle(rng);
175    }
176
177    pub fn save_lki(&mut self, zone_type: ZoneType, owner: PlayerId, card: CardId, from: ZoneType) {
178        self.get_mut(zone_type, owner)
179            .expect("Zone not found")
180            .save_lki(card, from);
181    }
182
183    pub fn card_location(&self, card: CardId) -> Option<ZoneKey> {
184        self.card_locations.get(card.index()).copied().flatten()
185    }
186
187    pub fn values_mut(&mut self) -> impl Iterator<Item = &mut Zone> {
188        self.zones.iter_mut()
189    }
190
191    pub fn iter(&self) -> impl Iterator<Item = (ZoneKey, &Zone)> {
192        self.zones
193            .iter()
194            .map(|zone| (ZoneKey::new(zone.zone_type, zone.owner), zone))
195    }
196
197    pub fn len(&self) -> usize {
198        self.zones.len()
199    }
200
201    pub fn is_empty(&self) -> bool {
202        self.zones.is_empty()
203    }
204
205    fn index(&self, zone_type: ZoneType, owner: PlayerId) -> Option<usize> {
206        if owner.index() >= self.player_count {
207            return None;
208        }
209        let zone_index = stored_zone_index(zone_type)?;
210        Some(owner.index() * STORED_ZONE_TYPES.len() + zone_index)
211    }
212
213    fn set_card_location(&mut self, card: CardId, zone_type: ZoneType, owner: PlayerId) {
214        self.ensure_card_location_slot(card);
215        self.card_locations[card.index()] = Some(ZoneKey::new(zone_type, owner));
216    }
217
218    fn clear_card_location(&mut self, card: CardId, zone_type: ZoneType, owner: PlayerId) {
219        let Some(slot) = self.card_locations.get_mut(card.index()) else {
220            return;
221        };
222        if *slot == Some(ZoneKey::new(zone_type, owner)) {
223            *slot = None;
224        }
225    }
226
227    fn ensure_card_location_slot(&mut self, card: CardId) {
228        let len = card.index() + 1;
229        if self.card_locations.len() < len {
230            self.card_locations.resize(len, None);
231        }
232    }
233}
234
235fn stored_zone_index(zone_type: ZoneType) -> Option<usize> {
236    match zone_type {
237        ZoneType::Hand => Some(0),
238        ZoneType::Library => Some(1),
239        ZoneType::Graveyard => Some(2),
240        ZoneType::Battlefield => Some(3),
241        ZoneType::Exile => Some(4),
242        ZoneType::Command => Some(5),
243        ZoneType::Sideboard => Some(6),
244        ZoneType::SchemeDeck => Some(7),
245        ZoneType::PlanarDeck => Some(8),
246        ZoneType::AttractionDeck => Some(9),
247        ZoneType::ContraptionDeck => Some(10),
248        ZoneType::Junkyard => Some(11),
249        ZoneType::Ante => Some(12),
250        ZoneType::ExtraHand => Some(13),
251        ZoneType::Subgame => Some(14),
252        ZoneType::Stack => Some(15),
253        ZoneType::Flashback | ZoneType::Merged | ZoneType::None => None,
254    }
255}