//! Level 5 — dictionaries AND lists. Each chamber keeps its own inventory. //! //! Paired design note: `l05.md`. use rand::seq::SliceRandom; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; use serde::Serialize; use serde_yaml::{Mapping, Sequence, Value}; use crate::describe::Describer; use super::{Generated, Level}; pub struct DictList; const CHAMBERS: &[&str] = &[ "armory", "pantry", "library", "vault", "kitchen", "cellar", ]; const ITEMS: &[&str] = &[ "sword", "shield", "bread", "water", "tome", "scroll", "gem", "coin", "dagger", "potion", ]; #[derive(Serialize)] struct DescCtx { sentences: Vec, } /// Pick "a" or "an" based on the first letter — keeps the prose reading /// naturally without giving away that chamber names are YAML keys. fn article(word: &str) -> &'static str { let first = word.chars().next().map(|c| c.to_ascii_lowercase()); if matches!(first, Some('a') | Some('e') | Some('i') | Some('o') | Some('u')) { "an" } else { "a" } } /// Join the item list as English: `a sword`, `a sword and a shield`, /// `a sword, a shield, and a potion` (Oxford comma for 3+). fn join_items(items: &[&str]) -> String { let parts: Vec = items .iter() .map(|i| format!("{} {}", article(i), i)) .collect(); match parts.as_slice() { [] => String::new(), [one] => one.clone(), [a, b] => format!("{a} and {b}"), rest => { let (last, head) = rest.split_last().unwrap(); format!("{}, and {}", head.join(", "), last) } } } impl Level for DictList { fn id(&self) -> u8 { 5 } fn name(&self) -> &'static str { "Chambers" } fn generate(&self, seed: u64) -> Generated { let mut rng = ChaCha8Rng::seed_from_u64(seed ^ 0x0000_0000_0000_0005); let n = rng.gen_range(2..=3); let chamber_names: Vec<&'static str> = CHAMBERS.choose_multiple(&mut rng, n).copied().collect(); let mut inner = Mapping::new(); let mut sentences = Vec::new(); for name in &chamber_names { let item_n = rng.gen_range(2..=3); let items: Vec<&'static str> = ITEMS.choose_multiple(&mut rng, item_n).copied().collect(); let seq: Sequence = items .iter() .map(|i| Value::String((*i).to_string())) .collect(); inner.insert(Value::String((*name).to_string()), Value::Sequence(seq)); let be = if items.len() == 1 { "is" } else { "are" }; sentences.push(format!( "There {be} {} inside {} {name}.", join_items(&items), article(name), )); } let mut top = Mapping::new(); top.insert( Value::String("chambers".to_string()), Value::Mapping(inner), ); let target_yaml = serde_yaml::to_string(&Value::Mapping(top)).expect("serialise mapping"); let mut d = Describer::new(); d.register( "l05", "Several chambers branch off, each with its own contents:\n\ {% for s in sentences %}\n {{ s }}{% endfor %}\n\n\ 💡 Wrap the whole tree under a `chambers:` key — a dict of lists.", ) .expect("register template"); let description = d .render("l05", &DescCtx { sentences }) .expect("render template"); Generated { target_yaml, description, flavor: "🏛 You enter a hall. Doors lead to many chambers.".to_string(), } } }