pub mod config; pub mod describe; pub mod levels; pub mod progress; pub mod similarity; pub mod tui; #[cfg(test)] mod smoke { //! End-to-end wiring test with one hardcoded level (no `Level` trait yet). use super::*; use serde::Serialize; use serde_yaml::{Mapping, Value}; #[derive(Serialize)] struct JunctionCtx { directions: Vec, } #[derive(Serialize)] struct Direction { name: &'static str, feature: &'static str, } #[test] fn one_level_round_trip() { // (1) Describer renders prose for a fake "junction" level. let mut d = describe::Describer::new(); d.register( "junction", "You stand at a junction:\n\ {% for x in directions %}- {{ x.name }} leads to a {{ x.feature }}\n{% endfor %}", ) .unwrap(); let prose = d .render( "junction", &JunctionCtx { directions: vec![ Direction { name: "left", feature: "door" }, Direction { name: "right", feature: "tunnel" }, ], }, ) .unwrap(); assert!(prose.contains("left leads to a door")); assert!(prose.contains("right leads to a tunnel")); // (2) Canonical target via serde_yaml — what a generator will do. // A reordered candidate parses to the same Value, so the semantic // short-circuit must score it 1.0. let mut m = Mapping::new(); m.insert(Value::String("left".into()), Value::String("door".into())); m.insert(Value::String("right".into()), Value::String("tunnel".into())); let target = serde_yaml::to_string(&Value::Mapping(m)).unwrap(); let candidate = "right: tunnel\nleft: door\n"; assert_eq!( similarity::semantic_or_textual(&target, candidate), 1.0, "reordered keys should still be a perfect semantic match" ); // (3) Textually-different, semantically-different → ratio in (0, 1). let near_miss = similarity::similarity_ratio("a: 1\nb: 2\n", "a: 1\nb: 3\n"); assert!(near_miss > 0.0 && near_miss < 1.0); // (4) Progress round-trips through the same YAML pipeline that disk // save/load will use. let p = progress::Progress { nuggets: vec![(1, levels::Nugget::Silver), (2, levels::Nugget::Gold)], current_level: 3, current_seed: 0xCAFE, attempts: 1, }; let s = serde_yaml::to_string(&p).unwrap(); let loaded: progress::Progress = serde_yaml::from_str(&s).unwrap(); assert_eq!(loaded.nuggets, p.nuggets); assert_eq!(loaded.current_level, p.current_level); assert_eq!(loaded.current_seed, p.current_seed); } #[test] fn levels_generate_canonical_yaml() { let registry = levels::registry(); assert_eq!(registry.len(), 3); // Level 1: any null-equivalent passes via the semantic short-circuit. let g1 = registry[0].generate(0); let parsed: serde_yaml::Value = serde_yaml::from_str(&g1.target_yaml).unwrap(); assert!(parsed.is_null()); assert_eq!( similarity::semantic_or_textual(&g1.target_yaml, "---"), 1.0, "`---` should be accepted as the minimum YAML" ); assert_eq!( similarity::semantic_or_textual(&g1.target_yaml, "null"), 1.0 ); // Level 2: deterministic per seed, non-empty mapping. let g2 = registry[1].generate(42); let v2: serde_yaml::Value = serde_yaml::from_str(&g2.target_yaml).unwrap(); let m = v2.as_mapping().expect("level 2 produces a mapping"); assert!(!m.is_empty()); let g2_again = registry[1].generate(42); assert_eq!( g2.target_yaml, g2_again.target_yaml, "same seed should produce the same target" ); // Level 3: deterministic per seed; produces a mapping of mappings, // each inner mapping has a `type` key. let g3 = registry[2].generate(123); let v3: serde_yaml::Value = serde_yaml::from_str(&g3.target_yaml).unwrap(); let m3 = v3.as_mapping().expect("level 3 produces a mapping"); assert!(!m3.is_empty()); for (_dir, feature) in m3 { let inner = feature.as_mapping().expect("level 3 inner is a mapping"); assert!( inner.get(serde_yaml::Value::String("type".into())).is_some(), "each direction must carry a `type` key" ); } let g3_again = registry[2].generate(123); assert_eq!(g3.target_yaml, g3_again.target_yaml); } }