//! A level in play: the board, the words content provides, and the rules compiled //! from them. Every change to the board recompiles the rules from scratch: parse, //! then resolve. Every question about what an object is goes through `has_property`. //! //! A move (Module 8) is planned for each `YOU` object, then committed; the rules are //! reparsed once, after all of them. //! //! A turn (Module 9) runs the phases of `TURN` in order: movement, parse, //! conversion, parse, content's hazards, the win check, parse. //! //! Undo (Module 9): before each move or turn the level saves the board (every //! object's id, kind and position, oldest first, and the id counter) and `won`. //! The rules are not saved: undo restores the board and reparses. //! //! Module 10: what happens to the objects a turn destroys is a destroy function the //! caller may pass in (`step_with`, `move_you_with`). The default, `remove_all`, //! removes them. SpellGrid passes one that sends minions to Part I's Death Creation //! Step instead. SpellGrid doesn't offer undo: Part I has state the level's //! snapshot doesn't cover. //! //! Module 11: a rule may carry conditions (`X ON Z IS P`). They are not //! decided when the rules are compiled. Every question about an object //! (`has_property`, conversion) asks content's `ConditionCheck`s against the board //! as it is at that moment, and uses only the rules whose conditions hold for that //! object. use std::collections::BTreeSet; use super::condition::ConditionChecks; use super::grid::{Board, Kind, LevelDef, ObjectId}; use super::movement::{Dir, Plan, Planner, Response}; use super::parse::{parse, Rule}; use super::resolve::resolve; use super::turn::{Hazard, Phase, TURN}; use super::word::{Lexicon, Role, WordDef}; #[derive(Debug, Clone)] pub struct Level { board: Board, lexicon: Lexicon, responses: Vec, hazards: Vec, conditions: ConditionChecks, rules: Vec, active: Vec, won: bool, /// What `undo` puts back: one entry per move or turn, the latest last (Module 9). history: Vec, } /// The level before a move or a turn: the whole board (its objects, oldest first, /// and its id counter) and `won`. #[derive(Debug, Clone)] struct Snapshot { board: Board, won: bool, } /// The property that makes an object move when the player moves. pub const YOU: &str = "YOU"; /// The property that wins the level when a `YOU` object is on it, or is it. pub const WIN: &str = "WIN"; /// The property that puts an object on the other float level: only objects on /// the same level share a tile for hazards and for winning. pub const FLOAT: &str = "FLOAT"; /// What to do with objects a move or a turn destroys (Module 10). It may change the /// board, and reparse, before it returns. An id may appear more than once. pub type Destroy<'a> = dyn FnMut(&mut Level, &[ObjectId]) + 'a; /// The default destroy function: take every object off the board. pub fn remove_all(level: &mut Level, ids: &[ObjectId]) { for &id in ids { level.board.remove(id); } } impl Level { /// A level from its definition, the words content provides, content's movement /// responses and hazards, each in the order they are to run, and content's /// condition checks, by word. Panics if a word with role `Condition` has no check. pub fn new( def: &LevelDef, words: &[WordDef], responses: Vec, hazards: Vec, conditions: ConditionChecks, ) -> Level { for w in words { if w.role == Role::Condition && !conditions.contains_key(&w.name.to_uppercase()) { panic!("the condition word {} has no condition check", w.name); } } let mut level = Level { board: Board::new(def.width, def.height), lexicon: Lexicon::new(words), responses, hazards, conditions, rules: vec![], active: vec![], won: false, history: vec![], }; for o in &def.objects { level.board.place(Kind::parse(&o.kind), o.x, o.y); } level.reparse(); level } pub fn place(&mut self, kind: &str, x: i32, y: i32) -> ObjectId { let id = self.board.place(Kind::parse(kind), x, y); self.history.clear(); self.reparse(); id } pub fn board(&self) -> &Board { &self.board } /// The board, to change directly (Module 10: SpellGrid writes text and removes /// minions). Nothing is reparsed until `reparse` is called. pub fn board_mut(&mut self) -> &mut Board { &mut self.board } /// The movement responses, in the order they run. pub fn responses(&self) -> &[Response] { &self.responses } /// Every rule the text spells, as parsed: negated rules and duplicates included. pub fn rules(&self) -> &[Rule] { &self.rules } /// The rules in force after the resolution pass. pub fn active_rules(&self) -> &[Rule] { &self.active } /// Does object `id` have `property`? Yes if an active rule that applies to it /// (`applies_to`) gives it the property and no active negated rule that applies /// to it takes it away. A destroyed object has no properties. pub fn has_property(&self, id: ObjectId, property: &str) -> bool { if self.board.find(id).is_none() { return false; } let about_it: Vec<&Rule> = self .active .iter() .filter(|r| r.complement == property && self.applies_to(r, id)) .collect(); about_it.iter().any(|r| !r.negated) && !about_it.iter().any(|r| r.negated) } /// Does the noun `word`, as the subject of a rule, name object `id`? pub fn names(&self, word: &str, id: ObjectId) -> bool { self.lexicon.names(word, &self.board.get(id).kind) } /// Does `rule` apply to object `id` now? Its subject names the object, and each /// of its conditions holds for the object on the board as it is (Module 11). fn applies_to(&self, rule: &Rule, id: ObjectId) -> bool { self.names(&rule.subject, id) && rule .conditions .iter() .all(|c| (self.conditions[&c.word])(self, id, &c.nouns)) } /// The other objects on object `id`'s tile that are on its float level (both /// `FLOAT` or both not), oldest first. Hazards and the win check ask this. pub fn sharing(&self, id: ObjectId) -> Vec { let o = self.board.get(id); let floats = self.has_property(id, FLOAT); self.board .at(o.x, o.y) .filter(|other| other.id != id && self.has_property(other.id, FLOAT) == floats) .map(|other| other.id) .collect() } /// Has a turn's win check found a `YOU` object on a `WIN` one? Stays true. pub fn won(&self) -> bool { self.won } /// Is there no `YOU` object left? pub fn lost(&self) -> bool { !self.board.objects().any(|o| self.has_property(o.id, YOU)) } /// Move every `YOU` object one tile in `dir`, then reparse (Module 8). Movement /// only: no conversion, hazard or win. pub fn move_you(&mut self, dir: Dir) { self.move_you_with(dir, &mut remove_all); } /// `move_you`, with destroyed objects going to `destroy` (Module 10). pub fn move_you_with(&mut self, dir: Dir, destroy: &mut Destroy) { self.save(); self.movement(dir, destroy); self.reparse(); } /// Play one turn: the phases of `TURN`, in order. pub fn step(&mut self, dir: Dir) { self.step_with(dir, &mut remove_all); } /// `step`, with destroyed objects going to `destroy` (Module 10). pub fn step_with(&mut self, dir: Dir, destroy: &mut Destroy) { self.save(); for phase in TURN { match phase { Phase::Movement => self.movement(dir, destroy), Phase::Parse => self.reparse(), Phase::Conversion => self.convert(destroy), Phase::Hazards => self.run_hazards(destroy), Phase::Win => self.check_win(), } } } /// Put the level back as it was before the last move or turn (Module 9), and /// reparse. False, and no change, if there is none; `place` forgets them all. pub fn undo(&mut self) -> bool { let Some(last) = self.history.pop() else { return false; }; self.board = last.board; self.won = last.won; self.reparse(); true } fn save(&mut self) { let snapshot = Snapshot { board: self.board.clone(), won: self.won, }; self.history.push(snapshot); } /// The `YOU` objects move one at a time, the one furthest ahead in `dir` first /// (oldest first on a tie); each one's move is planned in full, then committed, /// before the next is planned. Every move uses the rules in force before the /// first. /// /// An object steps at most once in a movement phase. A `YOU` object that an /// earlier move already carried along (pushed, or moved by any other response) /// doesn't start a move of its own, and no later move can make it step again. fn movement(&mut self, dir: Dir, destroy: &mut Destroy) { let (dx, dy) = dir.delta(); let mut you: Vec<_> = self .board .objects() .filter(|o| self.has_property(o.id, YOU)) .map(|o| (-(o.x * dx + o.y * dy), o.id)) .collect(); you.sort(); let mut stepped = BTreeSet::new(); for (_, id) in you { if self.board.find(id).is_none() { continue; // destroyed by an earlier move } if stepped.contains(&id) { continue; // already moved, by an earlier move } let mut planner = Planner::new(self, dir, &stepped); if planner.try_move(id) { let plan = planner.into_plan(); self.commit(&plan, dir, destroy); stepped.extend(plan.moving.difference(&plan.destroyed)); } } } /// Destroy, then move what is still on the board (a destroy function may remove /// more than it was given). fn commit(&mut self, plan: &Plan, dir: Dir, destroy: &mut Destroy) { if !plan.destroyed.is_empty() { let ids: Vec = plan.destroyed.iter().copied().collect(); destroy(self, &ids); } for &id in plan.moving.difference(&plan.destroyed) { if self.board.find(id).is_some() { self.board.shift(id, dir.delta()); } } } /// Every object named by a conversion `X IS Y` (`Y` a noun) becomes a `Y` /// where it stands, keeping its id; with several `Y`s it becomes the first, /// and each other `Y` is a new object on its tile. An `X IS X` naming the /// object blocks this. An object named by `X IS NOT X` is deleted, and still /// makes its `Y`s, as new objects: the negation cancelled `X IS X`. /// /// Negation is settled per object, as in `has_property`: an object doesn't /// become a `Y` if any active `... IS NOT Y` names it, whatever noun gave it the /// conversion (`ALL IS Y` with `X IS NOT Y` leaves every X an X). /// /// Every object is decided from the board as movement left it, before any /// changes, so `X IS Y` and `Y IS X` swap the Xs and the Ys. Only the rules that /// apply to the object are used: their conditions are checked on that board too. fn convert(&mut self, destroy: &mut Destroy) { let mut changes = vec![]; for o in self.board.objects() { let about_it: Vec<&Rule> = self .active .iter() .filter(|r| self.applies_to(r, o.id)) .collect(); let deleted = about_it.iter().any(|r| r.negated && r.is_identity()); let locked = about_it.iter().any(|r| !r.negated && r.is_identity()); let mut into: Vec = vec![]; if !locked { for r in about_it.iter().filter(|r| !r.negated && r.is_conversion()) { let negated = |n: &&Rule| n.negated && n.complement == r.complement; if about_it.iter().any(negated) { continue; } match self.lexicon.thing_named(&r.complement) { Some(kind) if kind != o.kind && !into.contains(&kind) => into.push(kind), _ => {} } } } if deleted || !into.is_empty() { changes.push((o.id, (o.x, o.y), deleted, into)); } } let mut deleted_ids = vec![]; for (id, (x, y), deleted, into) in changes { let mut into = into.into_iter(); if deleted { deleted_ids.push(id); } else if let Some(first) = into.next() { self.board.set_kind(id, first); } for kind in into { self.board.place(kind, x, y); } } if !deleted_ids.is_empty() { destroy(self, &deleted_ids); } } /// Run content's hazards in order. Each one's victims are destroyed together /// before the next runs; no reparse until the turn's last phase. fn run_hazards(&mut self, destroy: &mut Destroy) { for i in 0..self.hazards.len() { let hazard = self.hazards[i]; let victims = hazard(self); if !victims.is_empty() { destroy(self, &victims); } } } /// The level is won if any `YOU` object is `WIN`, or shares its tile with a /// `WIN` object on its float level. fn check_win(&mut self) { let wins = |id: ObjectId| self.has_property(id, WIN); let won = self .board .objects() .filter(|o| self.has_property(o.id, YOU)) .any(|o| wins(o.id) || self.sharing(o.id).into_iter().any(wins)); self.won |= won; } /// Recompile the rules from the board: parse, then resolve. pub fn reparse(&mut self) { self.rules = parse(&self.board, &self.lexicon); self.active = resolve(&self.rules); } }