//! 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, run_sequence, summon, Action, CardLibrary, Event, World}; 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 }, 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::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, }) } } 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(); } // ---- 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)); } }