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