Files
yamlabyrinth/src/lib.rs

131 lines
4.7 KiB
Rust

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<Direction>,
}
#[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);
}
}