//! Part III: SpellGrid, the two engines in one level. //! //! Part I (the card engine) and Part II (the grid engine, `puzzle`) don't use each //! other. This file is the only place that knows both, and they meet at four seams: //! //! 1. Rules become modifiers. After every reparse, the rules in force that join a //! trigger word to a multiplier word (`DEATHRATTLE IS DOUBLE`) are compiled into //! auras, and handed to Part I with `World::set_rule_auras`. A negated one //! (`DEATHRATTLE IS NOT DOUBLE`) becomes a cap. Which words are which is content's //! data (`SeamWords`), not code here. //! 2. Hazards mark, the death step removes. The grid engine hands the objects a turn //! destroys to `CardSide::destroy`. A minion among them is marked pending destroy, //! in one Sequence, and Part I's Death Creation Step removes it. //! 3. Grid changes reparse. While a Sequence runs, the grid doesn't change. When it //! ends, the text its effects asked for (`TextSpawned` events) is written, the //! minions that died (`Destroyed` events) leave the grid, and the rules are parsed //! again, before anything else happens. //! 4. Summoned minions enter the grid. In the same place, before the dead leave: a //! minion that a minion's ability summoned (`Summoned` events with a `by`), whose card //! has a noun, becomes an object of that noun on its summoner's tile. One summoned by //! a spell, or with no noun, is in play but not on the grid. //! //! One id space: a minion's id is its grid object's id. Every other card (a hero, a //! card in hand) gets an id from the same counter that no object will have. use std::collections::{HashMap, HashSet}; use serde::Deserialize; use super::aura::aura_update_other; use super::card::{Aura, CardLibrary, CardType, Effect, Stacking, Target, TriggerOn}; use super::event::Event; use super::puzzle::{Dir, Hazard, Kind, Level, LevelDef, ObjectId, Response, Rule, WordDef}; use super::queue::{run_sequence, Action}; use super::world::{EntityId, World, Zone}; /// A multiplier word's effect: `"DOUBLE": { "times": 2, "stacking": "max" }`. #[derive(Debug, Clone, Copy, Deserialize)] pub struct Multiplier { pub times: u32, pub stacking: Stacking, } /// The words that cross the seam, from content: nouns that name a trigger /// (`"DEATHRATTLE": "deathrattle"`) and properties that multiply one. #[derive(Debug, Clone, Default, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SeamWords { pub trigger_words: HashMap, pub multiplier_words: HashMap, } /// Seam 1: the auras the rules in force give. `T IS M` (T a trigger word, M a /// multiplier word) multiplies T's triggers; `T IS NOT M` caps them at one activation, /// so it cancels a card's multiplier too, as `X IS NOT Y` cancels every `X IS Y`. pub fn rule_auras(rules: &[Rule], words: &SeamWords) -> Vec { let mut auras = Vec::new(); for rule in rules { let (Some(&trigger), Some(&m)) = ( words.trigger_words.get(&rule.subject), words.multiplier_words.get(&rule.complement), ) else { continue; }; auras.push(if rule.negated { Aura::TriggerCap { trigger, times: 1 } } else { Aura::TriggerMultiplier { trigger, times: m.times, stacking: m.stacking, } }); } auras } /// A SpellGrid level in play: a grid and a game, kept in step. Ids are grid ids (see above). pub struct SpellGrid { level: Level, side: CardSide, } /// Part I's half: the game, and which entity is which id. Kept apart from the level so /// that the level can lend itself to the destroy function while this half changes. struct CardSide { world: World, /// Every entity's id. ids: HashMap, /// The entity behind each id (a card's old id still finds it after it is played). entities: HashMap, seam_words: SeamWords, } impl SpellGrid { /// The level's objects; its `cards` as minions in play for player 0, each a grid /// object named by the card's noun; its `hand` in player 0's hand. pub fn new( def: &LevelDef, library: CardLibrary, words: &[WordDef], responses: Vec, hazards: Vec, seam_words: SeamWords, ) -> SpellGrid { let mut level = Level::new(def, words, responses, hazards); let mut side = CardSide { world: World::new(library), ids: HashMap::new(), entities: HashMap::new(), seam_words, }; for c in &def.cards { let kind = side.noun_of(&c.card); let id = level.board_mut().place(kind, c.x, c.y); let entity = side.world.create(&c.card, 0, Zone::Board); side.link(entity, id); } // Every minion is in play: one Aura Update, as for a summon. aura_update_other(&mut side.world); for card in &def.hand { side.world.create(card, 0, Zone::Hand); } side.link_the_rest(&mut level); side.grid_changed(&mut level); SpellGrid { level, side } } pub fn level(&self) -> &Level { &self.level } pub fn world(&self) -> &World { &self.side.world } pub fn world_mut(&mut self) -> &mut World { &mut self.side.world } // ---- Ids ---- /// The id of entity `entity`. pub fn id_of(&self, entity: EntityId) -> ObjectId { self.side.ids[&entity] } /// The entity behind `id`. Panics if `id` is not a card's. pub fn entity(&self, id: ObjectId) -> EntityId { self.side.entity(id) } /// The id of the minion on tile (x, y), if there is one. pub fn minion_at(&self, x: i32, y: i32) -> Option { self.level .board() .at(x, y) .map(|o| o.id) .find(|id| self.side.entities.contains_key(id)) } // ---- What the player does ---- /// Add an object or a piece of text, then reparse. pub fn place(&mut self, kind: &str, x: i32, y: i32) -> ObjectId { let id = self.level.place(kind, x, y); self.side.grid_changed(&mut self.level); id } /// One whole turn (Module 9); what it destroys goes through seam 2. pub fn step(&mut self, dir: Dir) { let side = &mut self.side; self.level .step_with(dir, &mut |level, ids| side.destroy(level, ids)); self.side.grid_changed(&mut self.level); } /// Movement only (Module 8), then a reparse. pub fn move_dir(&mut self, dir: Dir) { let side = &mut self.side; self.level .move_you_with(dir, &mut |level, ids| side.destroy(level, ids)); self.side.grid_changed(&mut self.level); } /// Why `cast(card, target)` can't be done now, or `Ok` if it can: the card must be a /// spell in player 0's hand, and a spell that hits a chosen minion needs a target /// minion on the grid. Nothing changes either way. pub fn check_cast(&self, card: &str, target: Option) -> Result<(), String> { self.side.check_hand(card, CardType::Spell)?; let chooses = self.side.world.card(card).on_cast.iter().any(|e| { matches!( e, Effect::DealDamage { target: Target::Chosen, .. } | Effect::GainAttack { target: Target::Chosen, .. } | Effect::Destroy { target: Target::Chosen } ) }); match target { None if chooses => Err(format!("\"{card}\" needs a target minion")), None => Ok(()), Some(id) => { // Every id in `entities` was issued by the board, so `find` may ask about it. let card = self.side.entities.contains_key(&id); if card && self.level.board().find(id).is_some() { Ok(()) } else { Err(format!("{id} is not a minion on the grid")) } } } } /// Why `play(card, x, y)` can't be done now, or `Ok` if it can. pub fn check_play(&self, card: &str, x: i32, y: i32) -> Result<(), String> { self.side.check_hand(card, CardType::Minion)?; if self.side.world.card(card).def.noun.is_none() { return Err(format!( "\"{card}\" has no noun: it can't stand on the grid" )); } if !self.level.board().in_bounds(x, y) { return Err(format!("({x}, {y}) is off the grid")); } if self.level.board().at(x, y).next().is_some() { return Err(format!("({x}, {y}) is not empty")); } Ok(()) } /// Cast a spell from player 0's hand, as one Sequence. `target` is a minion's id. /// Panics if `check_cast` says no. pub fn cast(&mut self, card: &str, target: Option) { if let Err(problem) = self.check_cast(card, target) { panic!("{problem}"); } let in_hand = self.side.in_hand(card).expect("checked"); self.side.world.move_to(in_hand, Zone::Graveyard); let target = target.map(|id| self.side.entity(id)); let action = Action::Cast { card: card.to_string(), player: 0, target, }; self.side.sequence(&mut self.level, action); } /// Play a minion from player 0's hand onto the empty tile (x, y), as one Sequence. /// Returns its new id. Panics if `check_play` says no. pub fn play(&mut self, card: &str, x: i32, y: i32) -> ObjectId { if let Err(problem) = self.check_play(card, x, y) { panic!("{problem}"); } let in_hand = self.side.in_hand(card).expect("checked"); let kind = self.side.noun_of(card); let id = self.level.board_mut().place(kind, x, y); self.side.link(in_hand, id); self.side .sequence(&mut self.level, Action::Play { id: in_hand }); id } } impl CardSide { fn entity(&self, id: ObjectId) -> EntityId { *self .entities .get(&id) .unwrap_or_else(|| panic!("{id} is not a card")) } fn link(&mut self, entity: EntityId, id: ObjectId) { self.ids.insert(entity, id); self.entities.insert(id, entity); } /// Give every entity that has no id one that no object will have. fn link_the_rest(&mut self, level: &mut Level) { for entity in 0..self.world.entity_count() { if !self.ids.contains_key(&entity) { let id = level.board_mut().reserve_id(); self.link(entity, id); } } } fn noun_of(&self, card: &str) -> Kind { let def = &self.world.card(card).def; match (&def.noun, def.card_type) { (Some(noun), CardType::Minion) => Kind::parse(&noun.to_lowercase()), _ => panic!("{card:?} is not a minion with a noun"), } } fn in_hand(&self, card: &str) -> Option { self.world .zone(0, Zone::Hand) .iter() .copied() .find(|&id| self.world.card_name(id) == card) } fn check_hand(&self, card: &str, card_type: CardType) -> Result<(), String> { if self.in_hand(card).is_none() { return Err(format!("\"{card}\" is not in the hand")); } if self.world.card(card).def.card_type != card_type { let kind = match card_type { CardType::Minion => "minion", CardType::Spell => "spell", }; return Err(format!("\"{card}\" is not a {kind}")); } Ok(()) } /// Seam 2. Objects that aren't cards leave the board. Minions are marked pending /// destroy, in one Sequence, under the rules as they stand now. fn destroy(&mut self, level: &mut Level, ids: &[ObjectId]) { let mut seen = HashSet::new(); let mut minions = Vec::new(); for &id in ids { if !seen.insert(id) || level.board().find(id).is_none() { continue; } match self.entities.get(&id) { Some(&entity) => minions.push(entity), None => level.board_mut().remove(id), } } if minions.is_empty() { return; } self.sync_rules(level); self.sequence(level, Action::Destroy { targets: minions }); } /// Run one Sequence in Part I, then seams 3 and 4: apply its grid changes and reparse. fn sequence(&mut self, level: &mut Level, action: Action) { let from = self.world.log().len(); run_sequence(&mut self.world, action); let events = self.world.log()[from..].to_vec(); // Text first, while every minion that wrote it is still on its tile. A minion // with no id (summoned by this Sequence) is not on the grid yet: nowhere to write. for event in &events { if let Event::TextSpawned { source, word } = event { let tile = self .ids .get(source) .and_then(|&id| level.board().find(id)) .map(|o| (o.x, o.y)); if let Some((x, y)) = tile { let text = Kind::Text { word: word.clone() }; level.board_mut().place(text, x, y); } } } // Seam 4: summoned minions, in summon order, on their summoner's tile. The dead are // still on the grid here, so a Deathrattle's summoner is found where it died; a // minion summoned earlier in the list is already placed, so it can be a summoner too. for event in &events { let Event::Summoned { id, by: Some(by) } = *event else { continue; }; let card = self.world.card_name(id).to_string(); if self.ids.contains_key(&id) || self.world.card(&card).def.noun.is_none() { continue; } let tile = self .ids .get(&by) .and_then(|&object| level.board().find(object)) .map(|o| (o.x, o.y)); if let Some((x, y)) = tile { let object = level.board_mut().place(self.noun_of(&card), x, y); self.link(id, object); } } for event in &events { if let Event::Destroyed { id, .. } = event { if let Some(&object) = self.ids.get(id) { level.board_mut().remove(object); } } } self.link_the_rest(level); self.grid_changed(level); } /// The grid changed: reparse it, then seam 1. fn grid_changed(&mut self, level: &mut Level) { level.reparse(); self.sync_rules(level); } /// Seam 1: compile the rules in force into auras for Part I, then run an Aura Update. fn sync_rules(&mut self, level: &Level) { let auras = rule_auras(level.active_rules(), &self.seam_words); self.world.set_rule_auras(auras); aura_update_other(&mut self.world); } }