//! Assemble: "Get a random minion. Change half its text." //! //! Card text is a list of clauses, so changing half of it is list surgery: the base //! card's `text[..split]` (top) or `text[split..]` (bottom) is replaced by the same //! half of a donor card. Name, cost and stats stay the base's. This works only //! because a card's behaviour is its data: the new card needs no new code. //! //! Hearthstone's wiki doesn't define "half". The course's `split` field is its //! definition: each card's data says where its top half ends. use std::fmt; use super::card::{well_formed, CardDef, PoolFilter}; use super::world::{EntityId, PlayerId, World, Zone}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Half { Top, Bottom, } impl Half { pub const BOTH: [Half; 2] = [Half::Top, Half::Bottom]; pub fn label(self) -> &'static str { match self { Half::Top => "top", Half::Bottom => "bottom", } } } /// Why an Assemble was refused. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AssembleFailure { /// The new text would equal the base's. Unchanged, /// The new text would not be well-formed (see `well_formed`). IllFormed, } /// Assembling `base` with `donor`'s `half` would leave the base's text unchanged, or ill-formed. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AssembleError { pub base: String, pub donor: String, pub half: Half, pub reason: AssembleFailure, } impl fmt::Display for AssembleError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let result = match self.reason { AssembleFailure::Unchanged => "leaves its text unchanged", AssembleFailure::IllFormed => "gives ill-formed text", }; write!( f, "assembling {:?} with the {} half of {:?} {result}", self.base, self.half.label(), self.donor ) } } impl std::error::Error for AssembleError {} /// The base card with `half` of its text replaced by the same half of `donor`. /// It keeps the base's type, cost and stats, and is named " (Assembled)". pub fn assemble(base: &CardDef, donor: &CardDef, half: Half) -> Result { let (base_top, base_bottom) = base.text.split_at(base.split); let (donor_top, donor_bottom) = donor.text.split_at(donor.split); let (top, bottom) = match half { Half::Top => (donor_top, base_bottom), Half::Bottom => (base_top, donor_bottom), }; let text = [top, bottom].concat(); let refuse = |reason| AssembleError { base: base.name.clone(), donor: donor.name.clone(), half, reason, }; if text == base.text { return Err(refuse(AssembleFailure::Unchanged)); } if !well_formed(&text, base.card_type) { return Err(refuse(AssembleFailure::IllFormed)); } Ok(CardDef { name: format!("{} (Assembled)", base.name), split: top.len(), text, ..base.clone() }) } /// Register an assembled card and return its name. If a different card already has /// the name (an earlier Assemble of the same base), a number is added: "(Assembled 2)". pub fn register_assembled(world: &mut World, mut def: CardDef) -> String { let stem = def.name.clone(); let mut n = 1; while world.has_card(&def.name) && world.card(&def.name).def != def { n += 1; def.name = format!("{} {n})", stem.trim_end_matches(')')); } let name = def.name.clone(); world.register_assembled(def); name } /// The `assemble` effect: the owner's chooser picks a base minion from the pool /// (filtered), a donor, and the half to replace. Only combinations that change the /// base's text and leave it well-formed are offered: a donor is offered if one of its /// halves gives such a text, and a base if it has such a donor. The Assembled minion /// goes to the owner's hand. Real Hearthstone picks the base at random and offers two /// donors; here every choice is the chooser's. pub fn assemble_from_pool( world: &mut World, player: PlayerId, filter: &PoolFilter, ) -> Option { // Each pool card, and whether the filter lets it be the base. let pool: Vec<(CardDef, bool)> = world .pool() .into_iter() .map(|c| { ( c.def.clone(), filter.trigger.map_or(true, |on| c.has_trigger(on)), ) }) .collect(); // Does assembling `base` with `donor`'s `half` give a new, well-formed text? let changes = |base: &CardDef, donor: &CardDef, half: Half| assemble(base, donor, half).is_ok(); let donors_for = |base: &CardDef| -> Vec { pool.iter() .map(|(d, _)| d) .filter(|d| d.name != base.name && Half::BOTH.iter().any(|&h| changes(base, d, h))) .map(|d| d.name.clone()) .collect() }; let bases: Vec = pool .iter() .filter(|(d, allowed)| *allowed && !donors_for(d).is_empty()) .map(|(d, _)| d.name.clone()) .collect(); if bases.is_empty() { return None; } let base_name = world.choose("base", &bases); let base = world.card(&base_name).def.clone(); let donor_name = world.choose("donor", &donors_for(&base)); let donor = world.card(&donor_name).def.clone(); let halves: Vec = Half::BOTH .iter() .filter(|&&h| changes(&base, &donor, h)) .map(|h| h.label().to_string()) .collect(); let half = world.choose("half", &halves); let half = if half == "top" { Half::Top } else { Half::Bottom }; let def = assemble(&base, &donor, half) .expect("only halves that give a new, well-formed text are offered"); let name = register_assembled(world, def); Some(world.create(&name, player, Zone::Hand)) }