//! Test harness: the thin adapter the course tests call. It translates each //! call into engine operations and holds no rules of its own. use crate::content; use crate::engine::{ self, puzzle, run_sequence, summon, Action, CardLibrary, Event, SpellGrid as Grid, World, END_OF_PHASE, }; pub use crate::engine::{AssembleError, CardDef, Half, Zone}; pub type Id = u32; pub type Player = u8; #[derive(Debug, Default, Clone)] pub struct GameOpts { pub decks: Option<[Vec; 2]>, } /// The events the tests read from `log()` (see course/harness.md). #[derive(Debug, Clone, PartialEq, Eq)] pub enum GameEvent { Summoned { id: Id }, Damaged { target: Id, amount: i32 }, Triggered { source: Id, trigger: String }, Destroyed { id: Id }, DeathPhase { round: u32 }, Drew { player: Player, id: Id }, Fatigue { player: Player }, ArmorGained { player: Player, amount: i32 }, ArmorLost { player: Player, amount: i32 }, StatsChanged { id: Id, attack: i32, health: i32 }, SpellCast { card: String }, } impl GameEvent { /// The course's view of an engine event; `None` for engine events the course log /// doesn't show. fn from_engine(event: &Event) -> Option { Some(match event.clone() { Event::Summoned { id, .. } => GameEvent::Summoned { id }, Event::Damaged { target, amount } => GameEvent::Damaged { target, amount }, Event::Triggered { source, on } => GameEvent::Triggered { source, trigger: on.label().to_string(), }, Event::Drew { player, id } => GameEvent::Drew { player, id }, Event::Fatigue { player } => GameEvent::Fatigue { player }, Event::ArmorGained { player, amount } => GameEvent::ArmorGained { player, amount }, Event::ArmorLost { player, amount } => GameEvent::ArmorLost { player, amount }, Event::StatsChanged { id, attack, health } => { GameEvent::StatsChanged { id, attack, health } } Event::SpellCast { card } => GameEvent::SpellCast { card }, Event::Destroyed { id, .. } => GameEvent::Destroyed { id }, Event::DeathPhase { round } => GameEvent::DeathPhase { round }, // A played minion's `Summoned` already shows it entering the board. Event::Played { .. } => return None, // SpellGrid writes the text on the grid; the course's log doesn't show the request. Event::TextSpawned { .. } => return None, }) } } /// One event as `scenarios.md` writes it, such as `Triggered{source: 3, trigger: "deathrattle"}`. impl std::fmt::Display for GameEvent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { GameEvent::Summoned { id } => write!(f, "Summoned{{id: {id}}}"), GameEvent::Damaged { target, amount } => { write!(f, "Damaged{{target: {target}, amount: {amount}}}") } GameEvent::Triggered { source, trigger } => { write!(f, "Triggered{{source: {source}, trigger: {trigger:?}}}") } GameEvent::Destroyed { id } => write!(f, "Destroyed{{id: {id}}}"), GameEvent::DeathPhase { round } => write!(f, "DeathPhase{{round: {round}}}"), GameEvent::Drew { player, id } => write!(f, "Drew{{player: {player}, id: {id}}}"), GameEvent::Fatigue { player } => write!(f, "Fatigue{{player: {player}}}"), GameEvent::ArmorGained { player, amount } => { write!(f, "ArmorGained{{player: {player}, amount: {amount}}}") } GameEvent::ArmorLost { player, amount } => { write!(f, "ArmorLost{{player: {player}, amount: {amount}}}") } GameEvent::StatsChanged { id, attack, health } => { write!( f, "StatsChanged{{id: {id}, attack: {attack}, health: {health}}}" ) } GameEvent::SpellCast { card } => write!(f, "SpellCast{{card: {card:?}}}"), } } } /// The log as a tree: one event per line, two spaces of indent per level of nesting. fn log_tree(world: &World, view: impl Fn(&Event) -> Option) -> String { world .log() .iter() .zip(world.log_depths()) .filter_map(|(event, &depth)| { let shown = view(event)?; Some(format!("{}{shown}\n", " ".repeat(depth as usize))) }) .collect() } pub struct Game { world: World, } pub fn new_game(opts: GameOpts) -> Game { let mut world = World::new(CardLibrary::new(content::cards())); if let Some(decks) = opts.decks { for (player, deck) in decks.iter().enumerate() { for card in deck { world.create(card, player as Player, Zone::Deck); } } } Game { world } } impl Game { /// Put `card` at the right end of `player`'s board. pub fn summon(&mut self, card: &str, player: Player) -> Id { summon(&mut self.world, card, player, None) } pub fn add_to_hand(&mut self, card: &str, player: Player) -> Id { self.world.create(card, player, Zone::Hand) } pub fn add_to_deck(&mut self, card: &str, player: Player) -> Id { self.world.create(card, player, Zone::Deck) } /// Play a card from hand, as one Sequence: it enters the right end of the board, then /// its Battlecry resolves. (The Module 1 Expansion Drop; required from Module 4.) pub fn play(&mut self, hand_id: Id) { run_sequence(&mut self.world, Action::Play { id: hand_id }); } pub fn zone_of(&self, id: Id) -> Zone { self.world.zone_of(id) } /// Board ids, left to right. pub fn board(&self, player: Player) -> Vec { self.world.zone(player, Zone::Board).to_vec() } pub fn hand(&self, player: Player) -> Vec { self.world.zone(player, Zone::Hand).to_vec() } pub fn order_of_play(&self, id: Id) -> u32 { self.world .order_of_play(id) .unwrap_or_else(|| panic!("entity {id} has not entered play")) } pub fn name_of(&self, id: Id) -> String { self.world.card_name(id).to_string() } // ---- Module 2 ---- pub fn hero(&self, player: Player) -> Id { self.world.hero(player) } /// Deal damage to one character, as one Sequence. pub fn deal_damage(&mut self, target: Id, amount: i32) { let targets = vec![target]; run_sequence(&mut self.world, Action::Damage { targets, amount }); } pub fn cast(&mut self, card: &str, player: Player, target: Option) { let card = card.to_string(); run_sequence( &mut self.world, Action::Cast { card, player, target, }, ); } pub fn health(&self, id: Id) -> i32 { self.world.health(id) } pub fn attack(&self, id: Id) -> i32 { self.world.attack(id) } pub fn armor(&self, player: Player) -> i32 { self.world.armor(player) } pub fn log(&self) -> Vec { self.world .log() .iter() .filter_map(GameEvent::from_engine) .collect() } pub fn clear_log(&mut self) { self.world.clear_log(); } /// The log as an indented tree (see course/harness.md). pub fn log_tree(&self) -> String { log_tree(&self.world, GameEvent::from_engine) } // ---- Module 3 ---- /// Mark `target` pending destroy, as one Sequence. It leaves play in the death step. pub fn destroy(&mut self, target: Id) { let targets = vec![target]; run_sequence(&mut self.world, Action::Destroy { targets }); } // ---- Module 5 ---- /// Add card definitions to this game, replacing any with the same name. pub fn register_cards(&mut self, defs: Vec) { for def in defs { self.world.register(def); } } /// Register `base` with `half` of its text replaced by the same half of `donor`, /// and return the new card's name, `" (Assembled)"`. pub fn assemble( &mut self, base: &str, donor: &str, half: Half, ) -> Result { let base = &self.world.card(base).def; let donor = &self.world.card(donor).def; let def = engine::assemble(base, donor, half)?; Ok(engine::register_assembled(&mut self.world, def)) } pub fn cost(&self, id: Id) -> i32 { self.world.cost(id) } /// Who makes choices (Assemble's base, donor and half): called with the kind of /// choice (`"base"`, `"donor"`, `"half"`) and the options; returns one of them. pub fn set_chooser(&mut self, chooser: impl FnMut(&str, &[String]) -> String + 'static) { self.world.set_chooser(Box::new(chooser)); } } // ======== Part II: Baba Is You ======== pub use crate::engine::puzzle::{Dir, LevelCard, LevelDef, LevelObject}; /// A Baba Is You level in play (Part II). pub struct Baba { level: puzzle::Level, } /// A level from a level definition; its text is parsed straight away. pub fn new_baba(level: LevelDef) -> Baba { Baba { level: puzzle::Level::new( &level, &content::words(), content::movement(), content::hazards(), ), } } impl Baba { // ---- Module 6 ---- /// Add an object (`"baba"`) or a piece of text (`"text:BABA"`) at (x, y). pub fn place(&mut self, kind: &str, x: i32, y: i32) -> Id { self.level.place(kind, x, y) } /// Every distinct rule the text spells, before resolution, normalised /// (`"BABA IS YOU"`, `"BABA IS NOT YOU"`) and sorted. pub fn rules(&self) -> Vec { normalised(self.level.rules()) } // ---- Module 7 ---- /// Every distinct rule in force after resolution, normalised and sorted. pub fn active_rules(&self) -> Vec { normalised(self.level.active_rules()) } /// Does object `id` have the property `prop` under the rules in force? pub fn has_property(&self, id: Id, prop: &str) -> bool { self.level.has_property(id, prop) } // ---- Module 8 ---- /// Move every `YOU` object one tile in `dir`, pushing what it pushes, then /// reparse the rules. Movement only: no conversion, hazard or win (Module 9). pub fn move_dir(&mut self, dir: Dir) { self.level.move_you(dir); } /// Where object `id` is, or `None` once it has been destroyed. pub fn pos_of(&self, id: Id) -> Option<(i32, i32)> { self.level.board().find(id).map(|o| (o.x, o.y)) } /// The kinds of the objects on tile (x, y), as placed (`"baba"`, `"text:ROCK"`), sorted. pub fn kinds_at(&self, x: i32, y: i32) -> Vec { let mut kinds: Vec = self .level .board() .at(x, y) .map(|o| o.kind.to_string()) .collect(); kinds.sort(); kinds } /// The kinds on tile (x, y), as `kinds_at`, but oldest object first. pub fn stack_at(&self, x: i32, y: i32) -> Vec { let board = self.level.board(); board.at(x, y).map(|o| o.kind.to_string()).collect() } // ---- Module 9 ---- /// Play one whole turn: movement, parse, conversion, parse, hazards, win /// (and a last parse, so the rules match the board). pub fn step(&mut self, dir: Dir) { self.level.step(dir); } /// Has a turn found a `YOU` object on a `WIN` one (or `WIN` itself)? Stays true. pub fn won(&self) -> bool { self.level.won() } /// Is no object `YOU`? pub fn lost(&self) -> bool { self.level.lost() } /// Put the level back as it was before the last `step` or `move_dir`. False, and /// no change, if there is none; repeated calls walk further back. pub fn undo(&mut self) -> bool { self.level.undo() } } fn normalised(rules: &[puzzle::Rule]) -> Vec { let mut rules: Vec = rules.iter().map(|r| r.to_string()).collect(); rules.sort(); rules.dedup(); rules } // ======== Part III: SpellGrid ======== /// A SpellGrid level in play (Part III): a Baba Is You level whose minions are cards. /// Every id is a grid id: a minion's is its grid object's (see `engine/spellgrid.rs`). pub struct SpellGrid { grid: Grid, } /// A SpellGrid level; `extra` cards are added to the course's cards, replacing any /// with the same name. pub fn new_spell_grid(level: LevelDef, extra: Vec) -> SpellGrid { let mut library = CardLibrary::new(content::cards()); for def in extra { library.register(def); } SpellGrid { grid: Grid::new( &level, library, &content::words(), content::movement(), content::hazards(), content::seams(), ), } } impl SpellGrid { // ---- Part II, as on `Baba` ---- pub fn place(&mut self, kind: &str, x: i32, y: i32) -> Id { self.grid.place(kind, x, y) } pub fn rules(&self) -> Vec { normalised(self.grid.level().rules()) } pub fn active_rules(&self) -> Vec { normalised(self.grid.level().active_rules()) } pub fn has_property(&self, id: Id, prop: &str) -> bool { self.grid.level().has_property(id, prop) } pub fn move_dir(&mut self, dir: Dir) { self.grid.move_dir(dir); } pub fn pos_of(&self, id: Id) -> Option<(i32, i32)> { self.grid.level().board().find(id).map(|o| (o.x, o.y)) } pub fn kinds_at(&self, x: i32, y: i32) -> Vec { let mut kinds: Vec = self .grid .level() .board() .at(x, y) .map(|o| o.kind.to_string()) .collect(); kinds.sort(); kinds } pub fn stack_at(&self, x: i32, y: i32) -> Vec { let board = self.grid.level().board(); board.at(x, y).map(|o| o.kind.to_string()).collect() } pub fn step(&mut self, dir: Dir) { self.grid.step(dir); } pub fn won(&self) -> bool { self.grid.level().won() } pub fn lost(&self) -> bool { self.grid.level().lost() } /// Not supported: Part I has state (health, hands, the log) that the level's /// snapshot doesn't cover. Panics. pub fn undo(&mut self) -> bool { panic!("undo is not supported in SpellGrid"); } // ---- Module 10 ---- /// Cast a spell from player 0's hand, as one Sequence. `target` is a minion's id. pub fn cast(&mut self, card: &str, target: Option) { self.grid.cast(card, target); } /// Play a minion from player 0's hand onto the empty tile (x, y); returns its id. pub fn play(&mut self, card: &str, x: i32, y: i32) -> Id { self.grid.play(card, x, y) } pub fn health(&self, id: Id) -> i32 { self.grid.world().health(self.grid.entity(id)) } pub fn hero(&self, player: Player) -> Id { self.grid.id_of(self.grid.world().hero(player)) } pub fn name_of(&self, id: Id) -> String { self.grid .world() .card_name(self.grid.entity(id)) .to_string() } /// The minion on tile (x, y), if any. pub fn minion_at(&self, x: i32, y: i32) -> Option { self.grid.minion_at(x, y) } /// The card names in player 0's hand, in order. pub fn hand(&self) -> Vec { let world = self.grid.world(); world .zone(0, Zone::Hand) .iter() .map(|&id| world.card_name(id).to_string()) .collect() } /// The course's view of an engine event, with every entity id as a grid id. fn grid_event(&self, event: &Event) -> Option { let id = |entity: Id| self.grid.id_of(entity); Some(match GameEvent::from_engine(event)? { GameEvent::Summoned { id: e } => GameEvent::Summoned { id: id(e) }, GameEvent::Destroyed { id: e } => GameEvent::Destroyed { id: id(e) }, GameEvent::Drew { player, id: e } => GameEvent::Drew { player, id: id(e) }, GameEvent::StatsChanged { id: e, attack, health, } => GameEvent::StatsChanged { id: id(e), attack, health, }, GameEvent::Damaged { target, amount } => GameEvent::Damaged { target: id(target), amount, }, GameEvent::Triggered { source, trigger } => GameEvent::Triggered { source: id(source), trigger, }, other => other, }) } /// Part I's log, with grid ids. pub fn log(&self) -> Vec { let log = self.grid.world().log(); log.iter().filter_map(|e| self.grid_event(e)).collect() } pub fn clear_log(&mut self) { self.grid.world_mut().clear_log(); } /// Part I's log as an indented tree, with grid ids (see course/harness.md). pub fn log_tree(&self) -> String { log_tree(self.grid.world(), |e| self.grid_event(e)) } /// Who makes Assemble's choices (`"base"`, `"donor"`, `"half"`). pub fn set_chooser(&mut self, chooser: impl FnMut(&str, &[String]) -> String + 'static) { self.grid.world_mut().set_chooser(Box::new(chooser)); } /// For the command line: why `cast` would panic, or `Ok` if it wouldn't. pub fn check_cast(&self, card: &str, target: Option) -> Result<(), String> { self.grid.check_cast(card, target) } /// For the command line: why `play` would panic, or `Ok` if it wouldn't. pub fn check_play(&self, card: &str, x: i32, y: i32) -> Result<(), String> { self.grid.check_play(card, x, y) } /// Test hook: the naive engine reads the auras before the dead leave play, the /// same end-of-Phase steps reversed. pub fn with_naive_aura_timing(&mut self) { let mut steps = END_OF_PHASE.to_vec(); steps.reverse(); self.grid.world_mut().set_end_of_phase(steps); } }