diff --git a/src/lib.rs b/src/lib.rs index 3c5f54b..d71b4d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -81,82 +81,13 @@ mod smoke { assert_eq!(loaded.current_seed, p.current_seed); } - #[test] - fn levels_generate_canonical_yaml() { - let registry = levels::registry(); - assert_eq!(registry.len(), 11, "all 11 levels must be registered"); - - // Invariants every level must satisfy, regardless of how it generates. - for (i, level) in registry.iter().enumerate() { - let want_id = i as u8 + 1; - assert_eq!( - level.id(), - want_id, - "level at registry index {i} must report id {want_id}" - ); - - let seed = 0x5EED_0000 + i as u64; - let g = level.generate(seed); - - // The target must be valid YAML. - serde_yaml::from_str::(&g.target_yaml) - .unwrap_or_else(|e| panic!("level {want_id} target is not valid YAML: {e}")); - - // Generation is deterministic for a given seed. - let again = level.generate(seed); - assert_eq!( - g.target_yaml, again.target_yaml, - "level {want_id} must be deterministic for a given seed" - ); - - // The canonical target must be a perfect match against itself. - assert_eq!( - similarity::semantic_or_textual(&g.target_yaml, &g.target_yaml), - 1.0, - "level {want_id} target must score 1.0 against itself" - ); - } - - // 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: non-empty mapping. - let g2 = registry[1].generate(42); - let v2: serde_yaml::Value = serde_yaml::from_str(&g2.target_yaml).unwrap(); - assert!(!v2 - .as_mapping() - .expect("level 2 produces a mapping") - .is_empty()); - - // Level 3: 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" - ); - } - } - - // ---- Per-level shape checks ------------------------------------- + // ---- Per-level tests -------------------------------------------- // - // One test per level. Each pins the *shape* its design note promises - // and, where it is the whole lesson, the semantic forgiveness. The - // generic invariants (valid YAML, determinism, self-similarity, id - // ordering) are covered once for every level by - // `levels_generate_canonical_yaml` above. + // 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. @@ -165,20 +96,103 @@ mod smoke { .unwrap_or_else(|| panic!("expected key `{key}` in {v:?}")) } - /// Generate level `index` (0-based) with the given seed. - fn gen_level(index: usize, seed: u64) -> levels::Generated { - levels::registry()[index].generate(seed) - } - /// 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(&gen_level(3, 4).target_yaml); + 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"); @@ -187,7 +201,7 @@ mod smoke { #[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(&gen_level(4, 5).target_yaml); + let v = parse(&checked_level(4, 5).target_yaml); let chambers = field(&v, "chambers") .as_mapping() .expect("chambers is a dict"); @@ -202,7 +216,7 @@ mod smoke { 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(&gen_level(5, 6).target_yaml); + 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}`"); @@ -217,7 +231,7 @@ mod smoke { #[test] fn level_07_floor_map_nests_maps_and_lists() { // `floor:` int + `rooms:` dict; each room nests two lists. - let v = parse(&gen_level(6, 7).target_yaml); + 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"); @@ -236,7 +250,7 @@ mod smoke { 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(&gen_level(7, 8).target_yaml); + 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"); @@ -252,7 +266,7 @@ mod smoke { 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(&gen_level(8, 9).target_yaml); + let v = parse(&checked_level(8, 9).target_yaml); let defaults = field(&v, "door_defaults"); for door in ["north_door", "south_door"] { assert_eq!( @@ -269,7 +283,7 @@ mod smoke { 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 = gen_level(9, 10); + 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"); @@ -291,7 +305,7 @@ mod smoke { 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(&gen_level(10, 11).target_yaml); + 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 {