//! 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))
}