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); } // ---- Per-level tests -------------------------------------------- // // One test per level. `checked_level` runs the invariants every // level must satisfy (correct id, valid & deterministic canonical // YAML, self-similarity 1.0); each test then pins the *shape* its // design note promises and, where it is the whole lesson, the // semantic forgiveness. /// Look up a string key in a YAML mapping, panicking with a clear /// message if it is missing — keeps the per-level assertions terse. fn field<'a>(v: &'a Value, key: &str) -> &'a Value { v.get(key) .unwrap_or_else(|| panic!("expected key `{key}` in {v:?}")) } /// Parse a target YAML string, asserting it is well-formed. fn parse(yaml: &str) -> Value { serde_yaml::from_str(yaml).expect("target is valid YAML") } /// Generate level `index` (0-based) with `seed`, assert the invariants /// every level must satisfy, and return its output for shape checks: /// - the level reports `id == index + 1`, /// - the target is valid YAML, /// - generation is deterministic for a given seed, /// - the canonical target scores 1.0 against itself. fn checked_level(index: usize, seed: u64) -> levels::Generated { let registry = levels::registry(); let level = ®istry[index]; let want_id = index as u8 + 1; assert_eq!( level.id(), want_id, "registry index {index} must hold level {want_id}" ); let g = level.generate(seed); serde_yaml::from_str::(&g.target_yaml) .unwrap_or_else(|e| panic!("level {want_id} target is not valid YAML: {e}")); let again = level.generate(seed); assert_eq!( g.target_yaml, again.target_yaml, "level {want_id} must be deterministic for a given seed" ); assert_eq!( similarity::semantic_or_textual(&g.target_yaml, &g.target_yaml), 1.0, "level {want_id} target must score 1.0 against itself" ); g } #[test] fn registry_lists_all_eleven_levels() { assert_eq!( levels::registry().len(), 11, "all 11 levels must be registered" ); } #[test] fn level_01_minimum_is_a_null_document() { // Any null-equivalent passes via the semantic short-circuit. let g = checked_level(0, 0); assert!(parse(&g.target_yaml).is_null(), "the minimum YAML is null"); assert_eq!( similarity::semantic_or_textual(&g.target_yaml, "---"), 1.0, "`---` should be accepted as the minimum YAML" ); assert_eq!( similarity::semantic_or_textual(&g.target_yaml, "null"), 1.0, "`null` should be accepted as the minimum YAML" ); } #[test] fn level_02_key_value_is_a_non_empty_mapping() { let g = checked_level(1, 42); let v = parse(&g.target_yaml); assert!( !v.as_mapping() .expect("level 2 produces a mapping") .is_empty(), "level 2 yields a non-empty mapping" ); } #[test] fn level_03_dict_nests_typed_mappings() { // A mapping of mappings; each inner mapping carries a `type` key. let g = checked_level(2, 123); let v = parse(&g.target_yaml); let m = v.as_mapping().expect("level 3 produces a mapping"); assert!(!m.is_empty(), "at least one direction"); for (_dir, feature) in m { feature.as_mapping().expect("level 3 inner is a mapping"); assert!( feature.get("type").is_some(), "each direction must carry a `type` key" ); } } #[test] fn level_04_chest_is_a_list() { // `chest:` is a list of 3–5 item strings. let v = parse(&checked_level(3, 4).target_yaml); let chest = field(&v, "chest").as_sequence().expect("chest is a list"); assert!((3..=5).contains(&chest.len()), "chest holds 3–5 items"); assert!(chest.iter().all(Value::is_string), "every item is a string"); } #[test] fn level_05_chambers_is_a_dict_of_lists() { // `chambers:` is a dict of 2–3 lists, 2–3 items each. let v = parse(&checked_level(4, 5).target_yaml); let chambers = field(&v, "chambers") .as_mapping() .expect("chambers is a dict"); assert!((2..=3).contains(&chambers.len()), "2–3 chambers"); for (_name, items) in chambers { let items = items.as_sequence().expect("each chamber holds a list"); assert!((2..=3).contains(&items.len()), "2–3 items per chamber"); } } #[test] fn level_06_trap_repeats_in_every_room() { // `trap:` is defined once; every room repeats it verbatim, so a // player using `&anchor`/`*alias` parses to the same Value. let v = parse(&checked_level(5, 6).target_yaml); let trap = field(&v, "trap"); for key in ["type", "depth", "spikes"] { assert!(trap.get(key).is_some(), "trap carries `{key}`"); } let rooms = field(&v, "rooms").as_mapping().expect("rooms is a dict"); assert!(!rooms.is_empty(), "at least one room"); for (_room, payload) in rooms { assert_eq!(payload, trap, "every room repeats the trap definition"); } } #[test] fn level_07_floor_map_nests_maps_and_lists() { // `floor:` int + `rooms:` dict; each room nests two lists. let v = parse(&checked_level(6, 7).target_yaml); assert!(field(&v, "floor").is_i64(), "floor is an integer"); let rooms = field(&v, "rooms").as_mapping().expect("rooms is a dict"); assert!(!rooms.is_empty(), "at least one room"); for (_name, room) in rooms { assert!(field(room, "type").is_string(), "room.type is a string"); assert!(field(room, "locked").is_bool(), "room.locked is a bool"); assert!(field(room, "exits").is_sequence(), "room.exits is a list"); assert!( field(room, "contents").is_sequence(), "room.contents is a list" ); } } #[test] fn level_08_scroll_keeps_explicit_types() { // Explicit types: a multi-line string, a float, and a digit-only // string that must NOT collapse to an integer. let v = parse(&checked_level(7, 8).target_yaml); let scroll = field(&v, "scroll").as_str().expect("scroll is a string"); assert!(scroll.contains('\n'), "scroll preserves its newlines"); assert!(field(&v, "weight").is_f64(), "weight is a float"); let title = field(&v, "title"); assert!(title.is_string(), "title stays a string, not an int"); assert!( title.as_str().unwrap().chars().all(|c| c.is_ascii_digit()), "title is digit-only — the point of the `!!str` lesson" ); } #[test] fn level_09_doors_merge_shared_defaults() { // Merge keys: each door carries a literal `<<` whose value is the // shared defaults dict, plus its own override. let v = parse(&checked_level(8, 9).target_yaml); let defaults = field(&v, "door_defaults"); for door in ["north_door", "south_door"] { assert_eq!( field(field(&v, door), "<<"), defaults, "{door} merges the shared defaults via `<<`" ); } assert!(field(field(&v, "north_door"), "locked").is_bool()); assert!(field(field(&v, "south_door"), "material").is_string()); } #[test] fn level_10_ledger_forgives_numeric_forms() { // The ledger: int gold/silver, float experience, ISO date. The // lesson is numeric forgiveness, so hex must score a perfect match. let g = checked_level(9, 10); let v = parse(&g.target_yaml); let vault = field(&v, "vault"); let gold = field(vault, "gold").as_i64().expect("gold is an integer"); assert!(field(vault, "silver").is_i64(), "silver is an integer"); assert!(field(vault, "experience").is_f64(), "experience is a float"); assert!(field(vault, "date").is_string(), "date is an ISO string"); let hex_candidate = g .target_yaml .replace(&format!("gold: {gold}"), &format!("gold: 0x{gold:X}")); assert_ne!(hex_candidate, g.target_yaml, "hex rewrite must apply"); assert_eq!( similarity::semantic_or_textual(&g.target_yaml, &hex_candidate), 1.0, "writing gold in hex must still score a perfect match" ); } #[test] fn level_11_copies_alias_defined_shapes() { // `shapes:` defines polygons; `copies:` reuses them, so each copy // is structurally identical to one of the definitions. let v = parse(&checked_level(10, 11).target_yaml); let shapes = field(&v, "shapes").as_sequence().expect("shapes is a list"); assert!((2..=3).contains(&shapes.len()), "2–3 defined shapes"); for s in shapes { assert!(field(s, "name").is_string(), "shape.name is a string"); let sides = field(s, "sides").as_i64().expect("shape.sides is an int"); assert_eq!( field(s, "interior").as_i64().expect("shape.interior is an int"), (sides - 2) * 180, "interior angle sum follows (n-2)·180" ); } let copies = field(&v, "copies").as_sequence().expect("copies is a list"); assert!((3..=4).contains(&copies.len()), "3–4 copies"); for c in copies { assert!(shapes.contains(c), "every copy aliases a defined shape"); } } }