1use std::collections::VecDeque;
2
3use forge_foundation::ZoneType;
4use serde::{Deserialize, Serialize};
5
6use crate::card::card_damage_map::CardDamageMap;
7use crate::card::card_zone_table::CardZoneTable;
8use crate::card::Card;
9use crate::ids::{CardId, PlayerId};
10use crate::phase::ExtraTurn;
11use crate::phase::TurnState;
12use crate::player::PlayerState;
13use crate::spellability::MagicStack;
14use crate::zone::{CostPaymentStack, Zone, ZoneKey, ZoneStore};
15
16pub struct TypeRegistry;
25
26static CREATURE_TYPES: std::sync::OnceLock<Vec<String>> = std::sync::OnceLock::new();
27
28impl TypeRegistry {
29 pub fn load(type_lists_content: &str) {
39 let _ = CREATURE_TYPES.set(Self::parse_creature_types(type_lists_content));
40 }
41
42 pub fn creature_types() -> &'static [String] {
47 CREATURE_TYPES.get().expect(
48 "TypeRegistry: creature types not loaded. \
49 Call TypeRegistry::load() with the contents of TypeLists.txt before starting a game.",
50 )
51 }
52
53 pub fn is_creature_type(creature_type: &str) -> bool {
58 CREATURE_TYPES.get().is_some_and(|types| {
59 types
60 .iter()
61 .any(|ty| ty.eq_ignore_ascii_case(creature_type))
62 })
63 }
64
65 fn parse_creature_types(content: &str) -> Vec<String> {
66 let mut in_creature_section = false;
67 let mut types = Vec::new();
68 for line in content.lines() {
69 let line = line.trim();
70 if line.is_empty() || line.starts_with('#') {
71 continue;
72 }
73 if line.starts_with('[') && line.ends_with(']') {
74 in_creature_section = &line[1..line.len() - 1] == "CreatureTypes";
75 continue;
76 }
77 if in_creature_section {
78 let singular = line.split(':').next().unwrap_or(line);
80 if !singular.is_empty() {
81 types.push(singular.to_string());
82 }
83 }
84 }
85 types
86 }
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct GameState {
93 pub cards: Vec<Card>,
95 pub players: Vec<PlayerState>,
96
97 #[serde(skip)]
99 zones: ZoneStore,
100
101 pub stack: MagicStack,
103
104 #[serde(skip)]
107 pub cost_payment_stack: CostPaymentStack,
108
109 pub is_night: bool,
111 pub day_night_started: bool,
112
113 pub turn: TurnState,
115
116 pub player_order: Vec<PlayerId>,
118
119 pub game_over: bool,
121 pub winner: Option<PlayerId>,
122
123 #[serde(skip)]
126 pub extra_turns: VecDeque<ExtraTurn>,
127
128 pub prevent_all_combat_damage: bool,
131
132 pub monarch: Option<PlayerId>,
134
135 pub initiative_holder: Option<PlayerId>,
137
138 pub end_turn_requested: bool,
140
141 pub end_combat_requested: bool,
143
144 pub extra_combat_phases: u32,
146
147 next_card_id: u32,
149
150 next_zone_timestamp: u64,
155 next_effect_timestamp: i64,
158 #[serde(skip)]
161 pub pending_damage_map: Option<CardDamageMap>,
162 #[serde(skip)]
164 pub pending_prevent_map: Option<CardDamageMap>,
165 #[serde(skip)]
168 pub pending_change_zone_table: Option<CardZoneTable>,
169
170 #[serde(skip)]
175 pub synced_token_scripts: std::collections::BTreeSet<String>,
176
177 #[serde(skip)]
181 pub last_state_battlefield: Vec<crate::lki::CardSnapshot>,
182
183 #[serde(skip)]
189 pub pre_sba_battlefield: Vec<CardId>,
190
191 #[serde(skip)]
194 pub last_sacrificed_card: Option<CardId>,
195}
196
197impl GameState {
198 pub fn new(player_names: &[&str], starting_life: i32) -> Self {
199 let mut players = Vec::new();
200 let mut player_order = Vec::new();
201
202 for (i, name) in player_names.iter().enumerate() {
203 let pid = PlayerId(i as u32);
204 players.push(PlayerState::new(pid, name.to_string(), starting_life));
205 player_order.push(pid);
206 }
207
208 let zones = ZoneStore::new(&player_order);
209
210 GameState {
211 cards: Vec::new(),
212 players,
213 zones,
214 stack: MagicStack::new(),
215 cost_payment_stack: CostPaymentStack::new(),
216 is_night: false,
217 day_night_started: false,
218 turn: TurnState::new(player_order[0], player_order.len() as u32),
219 player_order,
220 game_over: false,
221 winner: None,
222 extra_turns: VecDeque::new(),
223 prevent_all_combat_damage: false,
224 monarch: None,
225 initiative_holder: None,
226 end_turn_requested: false,
227 end_combat_requested: false,
228 extra_combat_phases: 0,
229 next_card_id: 0,
230 next_zone_timestamp: 0,
231 next_effect_timestamp: 1,
232 pending_damage_map: None,
233 pending_prevent_map: None,
234 pending_change_zone_table: None,
235 synced_token_scripts: std::collections::BTreeSet::new(),
236 last_state_battlefield: Vec::new(),
237 pre_sba_battlefield: Vec::new(),
238 last_sacrificed_card: None,
239 }
240 }
241
242 pub fn create_card(&mut self, mut card: Card) -> CardId {
244 let id = CardId(self.next_card_id);
245 self.next_card_id += 1;
246 card.id = id;
247 let bound_host = card.clone();
248 for trigger in &mut card.triggers {
249 trigger.bind_host_card_id(bound_host.id);
250 }
251 for static_ability in &mut card.static_abilities {
252 static_ability.base.set_host_card_id(bound_host.id);
253 }
254 for replacement_effect in &mut card.replacement_effects {
255 replacement_effect.base.set_host_card_id(bound_host.id);
256 }
257 self.cards.push(card);
258 id
259 }
260
261 pub fn card(&self, id: CardId) -> &Card {
264 &self.cards[id.index()]
265 }
266
267 pub fn card_mut(&mut self, id: CardId) -> &mut Card {
268 &mut self.cards[id.index()]
269 }
270
271 pub fn player(&self, id: PlayerId) -> &PlayerState {
272 &self.players[id.index()]
273 }
274
275 pub fn player_mut(&mut self, id: PlayerId) -> &mut PlayerState {
276 &mut self.players[id.index()]
277 }
278
279 pub fn zone(&self, zone_type: ZoneType, owner: PlayerId) -> &Zone {
280 self.zones.get(zone_type, owner).expect("Zone not found")
281 }
282
283 pub fn zone_mut(&mut self, zone_type: ZoneType, owner: PlayerId) -> &mut Zone {
284 self.zones
285 .get_mut(zone_type, owner)
286 .expect("Zone not found")
287 }
288
289 pub fn zone_store_snapshot(&self) -> ZoneStore {
290 self.zones.clone()
291 }
292
293 pub fn replace_zone_store(&mut self, zones: ZoneStore) {
294 self.zones = zones;
295 }
296
297 pub fn iter_zones(&self) -> impl Iterator<Item = (ZoneKey, &Zone)> {
298 self.zones.iter()
299 }
300
301 pub fn cards_in_all_zones(&self, zone_type: ZoneType) -> impl Iterator<Item = CardId> + '_ {
302 self.iter_zones()
303 .filter(move |(key, _)| key.zone_type == zone_type)
304 .flat_map(|(_, zone)| zone.cards.iter().copied())
305 }
306
307 pub fn card_zone_location(&self, card: CardId) -> Option<ZoneKey> {
308 self.zones.card_location(card)
309 }
310
311 pub fn card_zone(&self, card: CardId) -> Option<ZoneType> {
312 self.card_zone_location(card)
313 .map(|location| location.zone_type)
314 }
315
316 pub fn card_current_zone(&self, card: CardId) -> ZoneType {
317 self.card_zone(card).unwrap_or_else(|| self.card(card).zone)
318 }
319
320 pub fn card_is_in_zone(&self, card: CardId, zone: ZoneType) -> bool {
321 self.card_current_zone(card) == zone
322 }
323
324 pub fn card_zone_owner(&self, card: CardId) -> Option<PlayerId> {
325 self.card_zone_location(card).map(|location| location.owner)
326 }
327
328 pub fn card_zone_location_matches_card(&self, card: CardId) -> bool {
329 let card_ref = self.card(card);
330 match self.card_zone_location(card) {
331 Some(location) => {
332 location.zone_type == card_ref.zone && location.owner == card_ref.controller
333 }
334 None => card_ref.zone == ZoneType::None,
335 }
336 }
337
338 pub fn reset_zone_turn_tracking(&mut self) {
339 for zone in self.zones.values_mut() {
340 zone.reset_cards_added_this_turn();
341 }
342 }
343
344 pub fn reset_card_turn_tracking(&mut self) {
345 for card in &mut self.cards {
346 card.reset_activations_per_turn();
347 card.reset_ability_resolved_this_turn();
348 }
349 }
350
351 pub(crate) fn remove_card_from_zone(
352 &mut self,
353 zone_type: ZoneType,
354 owner: PlayerId,
355 card: CardId,
356 ) -> bool {
357 if std::env::var("FORGE_ZONE_TRACE").is_ok()
358 && self.cards[card.index()].card_name == "Mind Stone"
359 {
360 eprintln!(
361 "[zone-rust] T{} remove {:?} {} from {:?} owner={:?}",
362 self.turn.turn_number,
363 card,
364 self.cards[card.index()].card_name,
365 zone_type,
366 owner
367 );
368 }
369 self.zones.remove_card(zone_type, owner, card)
370 }
371
372 pub(crate) fn add_card_to_zone(&mut self, zone_type: ZoneType, owner: PlayerId, card: CardId) {
373 if std::env::var("FORGE_ZONE_TRACE").is_ok()
374 && self.cards[card.index()].card_name == "Mind Stone"
375 {
376 eprintln!(
377 "[zone-rust] T{} add {:?} {} -> {:?} owner={:?}",
378 self.turn.turn_number,
379 card,
380 self.cards[card.index()].card_name,
381 zone_type,
382 owner
383 );
384 }
385 self.zones.add_card_to_top(zone_type, owner, card);
386 }
387
388 pub(crate) fn add_card_to_zone_bottom(
389 &mut self,
390 zone_type: ZoneType,
391 owner: PlayerId,
392 card: CardId,
393 ) {
394 self.zones.add_card_to_bottom(zone_type, owner, card);
395 }
396
397 pub fn take_top_card_from_zone(
398 &mut self,
399 zone_type: ZoneType,
400 owner: PlayerId,
401 ) -> Option<CardId> {
402 self.zones.take_top_card(zone_type, owner)
403 }
404
405 pub fn take_top_cards_from_zone(
406 &mut self,
407 zone_type: ZoneType,
408 owner: PlayerId,
409 count: usize,
410 ) -> Vec<CardId> {
411 let mut cards = Vec::with_capacity(count);
412 for _ in 0..count {
413 let Some(card) = self.take_top_card_from_zone(zone_type, owner) else {
414 break;
415 };
416 cards.push(card);
417 }
418 cards.reverse();
419 cards
420 }
421
422 pub fn reorder_card_in_zone(
423 &mut self,
424 zone_type: ZoneType,
425 owner: PlayerId,
426 card: CardId,
427 index: usize,
428 ) {
429 self.zones.reorder_card(zone_type, owner, card, index);
430 }
431
432 pub fn move_cards_to_zone_top(
433 &mut self,
434 zone_type: ZoneType,
435 owner: PlayerId,
436 cards: &[CardId],
437 ) {
438 self.zones.move_cards_to_top(zone_type, owner, cards);
439 }
440
441 pub fn move_cards_to_zone_bottom(
442 &mut self,
443 zone_type: ZoneType,
444 owner: PlayerId,
445 cards: &[CardId],
446 ) {
447 self.zones.move_cards_to_bottom(zone_type, owner, cards);
448 }
449
450 pub fn replace_zone_cards(&mut self, zone_type: ZoneType, owner: PlayerId, cards: Vec<CardId>) {
451 self.zones.replace_cards(zone_type, owner, cards);
452 }
453
454 pub fn shuffle_zone_cards(
455 &mut self,
456 zone_type: ZoneType,
457 owner: PlayerId,
458 rng: &mut dyn crate::game_rng::GameRng,
459 ) {
460 self.zones.shuffle_cards(zone_type, owner, rng);
461 }
462
463 pub fn shuffle_zone_cards_with_rand<R: rand::Rng + ?Sized>(
464 &mut self,
465 zone_type: ZoneType,
466 owner: PlayerId,
467 rng: &mut R,
468 ) {
469 self.zones.shuffle_cards_with_rand(zone_type, owner, rng);
470 }
471
472 pub(crate) fn save_zone_lki(
473 &mut self,
474 zone_type: ZoneType,
475 owner: PlayerId,
476 card: CardId,
477 from: ZoneType,
478 ) {
479 self.zones.save_lki(zone_type, owner, card, from);
480 }
481
482 pub fn active_player(&self) -> PlayerId {
483 self.turn.active_player
484 }
485
486 pub fn is_day(&self) -> bool {
487 self.day_night_started && !self.is_night
488 }
489
490 pub fn is_neither_day_nor_night(&self) -> bool {
491 !self.day_night_started
492 }
493
494 pub fn next_player(&self, player: PlayerId) -> PlayerId {
495 let current_idx = self
496 .player_order
497 .iter()
498 .position(|&p| p == player)
499 .unwrap_or(0);
500 for i in 1..self.player_order.len() {
501 let next_idx = (current_idx + i) % self.player_order.len();
502 let next_pid = self.player_order[next_idx];
503 if self.player(next_pid).is_alive() {
504 return next_pid;
505 }
506 }
507 player
508 }
509
510 pub fn last_combat_turn_of(&self, player: PlayerId) -> Option<i32> {
519 if self.turn.active_player == player {
520 Some(self.turn.turn_number as i32)
521 } else {
522 None
523 }
524 }
525
526 pub fn opponent_of(&self, player: PlayerId) -> PlayerId {
527 for &pid in &self.player_order {
528 if pid != player && self.player(pid).is_alive() {
529 return pid;
530 }
531 }
532 player }
534
535 pub fn alive_players(&self) -> Vec<PlayerId> {
536 self.player_order
537 .iter()
538 .filter(|&&pid| self.player(pid).is_alive())
539 .copied()
540 .collect()
541 }
542
543 pub fn cards_in_zone(&self, zone_type: ZoneType, owner: PlayerId) -> &[CardId] {
545 &self.zone(zone_type, owner).cards
546 }
547
548 pub fn creatures_on_battlefield(&self, player: PlayerId) -> Vec<CardId> {
550 self.cards_in_zone(ZoneType::Battlefield, player)
551 .iter()
552 .filter(|&&cid| self.card(cid).is_creature())
553 .copied()
554 .collect()
555 }
556
557 pub fn assign_zone_timestamp(&mut self, card_id: CardId) -> u64 {
560 let ts = self.next_zone_timestamp;
561 self.next_zone_timestamp += 1;
562 self.cards[card_id.index()].zone_timestamp = ts;
563 ts
564 }
565
566 pub fn next_effect_timestamp(&mut self) -> i64 {
568 let ts = self.next_effect_timestamp;
569 self.next_effect_timestamp = self.next_effect_timestamp.saturating_add(1);
570 ts
571 }
572
573 pub fn ensure_pending_damage_maps(&mut self) {
575 if self.pending_damage_map.is_none() {
576 self.pending_damage_map = Some(CardDamageMap::default());
577 }
578 if self.pending_prevent_map.is_none() {
579 self.pending_prevent_map = Some(CardDamageMap::default());
580 }
581 }
582
583 pub fn clear_pending_damage_maps(&mut self) {
585 self.pending_damage_map = None;
586 self.pending_prevent_map = None;
587 }
588
589 pub fn ensure_pending_change_zone_table(&mut self) {
591 if self.pending_change_zone_table.is_none() {
592 self.pending_change_zone_table = Some(CardZoneTable::default());
593 }
594 }
595
596 pub fn clear_pending_change_zone_table(&mut self) {
598 self.pending_change_zone_table = None;
599 }
600
601 pub fn lands_on_battlefield(&self, player: PlayerId) -> Vec<CardId> {
603 self.cards_in_zone(ZoneType::Battlefield, player)
604 .iter()
605 .filter(|&&cid| self.card(cid).is_land())
606 .copied()
607 .collect()
608 }
609}
610
611#[cfg(test)]
612mod tests {
613 use super::*;
614 use forge_foundation::{CardTypeLine, ColorSet, ManaCost};
615
616 #[test]
617 fn create_game() {
618 let game = GameState::new(&["Alice", "Bob"], 20);
619 assert_eq!(game.players.len(), 2);
620 assert_eq!(game.player(PlayerId(0)).name, "Alice");
621 assert_eq!(game.player(PlayerId(1)).name, "Bob");
622 assert_eq!(game.player(PlayerId(0)).life, 20);
623 assert!(game.zone(ZoneType::Sideboard, PlayerId(0)).is_empty());
624 assert!(game.zone(ZoneType::AttractionDeck, PlayerId(0)).is_empty());
625 assert!(game.zone(ZoneType::ContraptionDeck, PlayerId(0)).is_empty());
626 }
627
628 #[test]
629 fn create_card_and_zone() {
630 let mut game = GameState::new(&["Alice", "Bob"], 20);
631 let card = Card::new(
632 CardId(0),
633 "Grizzly Bears".to_string(),
634 PlayerId(0),
635 CardTypeLine::parse("Creature Bear"),
636 ManaCost::parse("1 G"),
637 ColorSet::GREEN,
638 Some(2),
639 Some(2),
640 vec![],
641 vec![],
642 );
643 let cid = game.create_card(card);
644 game.add_card_to_zone(ZoneType::Library, PlayerId(0), cid);
645 game.card_mut(cid).zone = ZoneType::Library;
646 assert_eq!(game.zone(ZoneType::Library, PlayerId(0)).len(), 1);
647 assert_eq!(game.card_zone(cid), Some(ZoneType::Library));
648 }
649
650 #[test]
651 fn opponent_lookup() {
652 let game = GameState::new(&["Alice", "Bob"], 20);
653 assert_eq!(game.opponent_of(PlayerId(0)), PlayerId(1));
654 assert_eq!(game.opponent_of(PlayerId(1)), PlayerId(0));
655 }
656
657 #[test]
658 fn lki_snapshot_captures_battlefield_state() {
659 let mut game = GameState::new(&["Alice", "Bob"], 20);
660
661 let mut card = Card::new(
663 CardId(0),
664 "Grizzly Bears".to_string(),
665 PlayerId(0),
666 CardTypeLine::parse("Creature Bear"),
667 ManaCost::parse("1 G"),
668 ColorSet::GREEN,
669 Some(3),
670 Some(3),
671 vec![],
672 vec![],
673 );
674 card.zone = ZoneType::Battlefield;
675 let cid = game.create_card(card);
676
677 game.copy_last_state();
679
680 let snapshot = game.get_lki_snapshot(cid).expect("snapshot should exist");
682 assert_eq!(snapshot.power, 3);
683 assert_eq!(snapshot.toughness, 3);
684 assert_eq!(snapshot.card_name, "Grizzly Bears");
685
686 game.card_mut(cid).zone = ZoneType::Graveyard;
688 let snapshot = game
689 .get_lki_snapshot(cid)
690 .expect("snapshot should still exist");
691 assert_eq!(snapshot.power, 3);
692
693 game.copy_last_state();
696 assert!(
697 game.get_lki_snapshot(cid).is_some(),
698 "stale LKI should persist"
699 );
700 }
701}