diff --git a/src/levels/l09_operators.rs b/src/levels/l09_operators.rs new file mode 100644 index 0000000..8bfe705 --- /dev/null +++ b/src/levels/l09_operators.rs @@ -0,0 +1,135 @@ +//! Level 9 — special operators. The merge key (`<<`) lets a door +//! inherit a template and override one field. +//! +//! Paired design note: `l09.md`. +//! +//! `serde_yaml` treats `<<` as a literal mapping key (it does NOT +//! perform YAML 1.1 merge-key resolution). The target therefore carries +//! a `<<` key whose value is the defaults dict, exactly as the player's +//! `<<: *defaults` parses. + +use rand::seq::SliceRandom; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; +use serde::Serialize; +use serde_yaml::{Mapping, Value}; + +use crate::describe::Describer; + +use super::{Generated, Level}; + +pub struct Operators; + +const MATERIALS: &[&str] = &["oak", "iron", "stone", "silver", "bone"]; + +#[derive(Serialize)] +struct DescCtx { + default_material: String, + default_locked: bool, + north_locked: bool, + south_material: String, +} + +impl Level for Operators { + fn id(&self) -> u8 { + 9 + } + + fn name(&self) -> &'static str { + "Merge Keys" + } + + fn generate(&self, seed: u64) -> Generated { + let mut rng = ChaCha8Rng::seed_from_u64(seed ^ 0x0000_0000_0000_0009); + let default_material = (*MATERIALS.choose(&mut rng).unwrap()).to_string(); + let default_locked = rng.gen_bool(0.5); + // The north door overrides locked; choose the opposite so the + // override is meaningful. + let north_locked = !default_locked; + // The south door overrides material; pick anything but default. + let other_materials: Vec<&str> = MATERIALS + .iter() + .filter(|m| **m != default_material) + .copied() + .collect(); + let south_material = (*other_materials.choose(&mut rng).unwrap()).to_string(); + + // Build the defaults mapping (referenced by all doors). + let mut defaults = Mapping::new(); + defaults.insert( + Value::String("material".to_string()), + Value::String(default_material.clone()), + ); + defaults.insert( + Value::String("locked".to_string()), + Value::Bool(default_locked), + ); + + let mut north_door = Mapping::new(); + north_door.insert( + Value::String("<<".to_string()), + Value::Mapping(defaults.clone()), + ); + north_door.insert( + Value::String("locked".to_string()), + Value::Bool(north_locked), + ); + + let mut south_door = Mapping::new(); + south_door.insert( + Value::String("<<".to_string()), + Value::Mapping(defaults.clone()), + ); + south_door.insert( + Value::String("material".to_string()), + Value::String(south_material.clone()), + ); + + let mut top = Mapping::new(); + top.insert( + Value::String("door_defaults".to_string()), + Value::Mapping(defaults), + ); + top.insert( + Value::String("north_door".to_string()), + Value::Mapping(north_door), + ); + top.insert( + Value::String("south_door".to_string()), + Value::Mapping(south_door), + ); + + let target_yaml = + serde_yaml::to_string(&Value::Mapping(top)).expect("serialise mapping"); + + let mut d = Describer::new(); + d.register( + "l09", + "Two doors share a template:\n\ + default material: {{ default_material }}\n\ + default locked: {{ default_locked }}\n\ + \n\ + north_door overrides locked → {{ north_locked }}\n\ + south_door overrides material → {{ south_material }}\n\ + 💡 Anchor the defaults (`door_defaults: &name`) and merge with `<<: *name` in each door.", + ) + .expect("register template"); + let description = d + .render( + "l09", + &DescCtx { + default_material, + default_locked, + north_locked, + south_material, + }, + ) + .expect("render template"); + + Generated { + target_yaml, + description, + flavor: "🚪 Two doors echo a single template.".to_string(), + } + } +} diff --git a/src/levels/mod.rs b/src/levels/mod.rs index 3a0a2fa..71087eb 100644 --- a/src/levels/mod.rs +++ b/src/levels/mod.rs @@ -18,6 +18,7 @@ pub mod l05_dict_list; pub mod l06_anchors; pub mod l07_complex; pub mod l08_tags; +pub mod l09_operators; use serde::{Deserialize, Serialize};