diff --git a/src/levels/l11_adv_anchors.rs b/src/levels/l11_adv_anchors.rs new file mode 100644 index 0000000..4e258e7 --- /dev/null +++ b/src/levels/l11_adv_anchors.rs @@ -0,0 +1,141 @@ +//! Level 11 — advanced anchors. Anchor shapes inside a list, alias +//! them elsewhere. +//! +//! Paired design note: `l11.md`. +//! +//! Like L6, serde_yaml expands aliases on parse, so the emitted target +//! is the fully-inlined form. Players who use `&anchor` / `*alias` +//! produce the same `Value` and pass via the semantic short-circuit. + +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 AdvAnchors; + +const SHAPES: &[(&str, i64)] = &[ + ("triangle", 3), + ("square", 4), + ("pentagon", 5), + ("hexagon", 6), + ("heptagon", 7), + ("octagon", 8), +]; + +#[derive(Serialize)] +struct DescCtx { + shapes: Vec, + copies: Vec, +} + +#[derive(Serialize)] +struct ShapeDesc { + name: String, + sides: i64, + interior_angle_sum: i64, +} + +fn shape_value(name: &str, sides: i64) -> Value { + let mut m = Mapping::new(); + m.insert( + Value::String("name".to_string()), + Value::String(name.to_string()), + ); + m.insert(Value::String("sides".to_string()), Value::from(sides)); + m.insert( + Value::String("interior".to_string()), + Value::from((sides - 2) * 180), + ); + Value::Mapping(m) +} + +impl Level for AdvAnchors { + fn id(&self) -> u8 { + 11 + } + + fn name(&self) -> &'static str { + "Advanced Anchors" + } + + fn generate(&self, seed: u64) -> Generated { + let mut rng = ChaCha8Rng::seed_from_u64(seed ^ 0x0000_0000_0000_000B); + + let n = rng.gen_range(2..=3); + let picked: Vec<(&'static str, i64)> = SHAPES + .choose_multiple(&mut rng, n) + .copied() + .collect(); + + // The defining `shapes:` list. + let shapes_seq: Sequence = picked + .iter() + .map(|(name, sides)| shape_value(name, *sides)) + .collect(); + + // `copies:` — random selections with possible repetition. + let m = rng.gen_range(3..=4); + let mut copies_seq = Sequence::new(); + let mut copy_names = Vec::new(); + for _ in 0..m { + let (name, sides) = picked.choose(&mut rng).unwrap(); + copies_seq.push(shape_value(name, *sides)); + copy_names.push((*name).to_string()); + } + + let mut top = Mapping::new(); + top.insert( + Value::String("shapes".to_string()), + Value::Sequence(shapes_seq), + ); + top.insert( + Value::String("copies".to_string()), + Value::Sequence(copies_seq), + ); + + let target_yaml = + serde_yaml::to_string(&Value::Mapping(top)).expect("serialise mapping"); + + let shape_descs: Vec = picked + .iter() + .map(|(name, sides)| ShapeDesc { + name: (*name).to_string(), + sides: *sides, + interior_angle_sum: (sides - 2) * 180, + }) + .collect(); + + let mut d = Describer::new(); + d.register( + "l11", + "Shapes are defined once and reused.\n\ + Definitions:\n\ + {% for s in shapes %}- {{ s.name }}: sides={{ s.sides }}, interior={{ s.interior_angle_sum }}\n\ + {% endfor %}\n\ + Copies, in order: {% for c in copies %}{{ c }}{% if not loop.last %}, {% endif %}{% endfor %}\n\ + 💡 Anchor each shape in the list with `- &name`, then alias by `*name` in copies.", + ) + .expect("register template"); + let description = d + .render( + "l11", + &DescCtx { + shapes: shape_descs, + copies: copy_names, + }, + ) + .expect("render template"); + + Generated { + target_yaml, + description, + flavor: "🔺 The geometry chamber repeats its forms.".to_string(), + } + } +} diff --git a/src/levels/mod.rs b/src/levels/mod.rs index f3c78cc..dca5259 100644 --- a/src/levels/mod.rs +++ b/src/levels/mod.rs @@ -13,6 +13,7 @@ pub mod l01_minimum; pub mod l02_kv; pub mod l03_dict; +pub mod l11_adv_anchors; use serde::{Deserialize, Serialize};