diff --git a/src/levels/l05_dict_list.rs b/src/levels/l05_dict_list.rs index f44f53b..bd2828f 100644 --- a/src/levels/l05_dict_list.rs +++ b/src/levels/l05_dict_list.rs @@ -23,13 +23,36 @@ const ITEMS: &[&str] = &[ #[derive(Serialize)] struct DescCtx { - chambers: Vec, + sentences: Vec, } -#[derive(Serialize)] -struct ChamberDesc { - name: String, - items: 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 { @@ -48,7 +71,7 @@ impl Level for DictList { CHAMBERS.choose_multiple(&mut rng, n).copied().collect(); let mut inner = Mapping::new(); - let mut desc_chambers = Vec::new(); + let mut sentences = Vec::new(); for name in &chamber_names { let item_n = rng.gen_range(2..=3); let items: Vec<&'static str> = @@ -58,10 +81,13 @@ impl Level for DictList { .map(|i| Value::String((*i).to_string())) .collect(); inner.insert(Value::String((*name).to_string()), Value::Sequence(seq)); - desc_chambers.push(ChamberDesc { - name: (*name).to_string(), - items: items.iter().map(|s| s.to_string()).collect(), - }); + + 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(); @@ -76,18 +102,13 @@ impl Level for DictList { let mut d = Describer::new(); d.register( "l05", - "Several chambers branch off, each with its own inventory:\n\ - {% for c in chambers %}\n{{ c.name }}:{% for it in c.items %}\n - {{ it }}{% endfor %}\n{% endfor %}\n\ + "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 { - chambers: desc_chambers, - }, - ) + .render("l05", &DescCtx { sentences }) .expect("render template"); Generated {