First levels v0.1.0
This commit is contained in:
109
src/lib.rs
109
src/lib.rs
@@ -3,3 +3,112 @@ 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 {
|
||||
tier: Some(levels::Difficulty::Medium),
|
||||
completed: vec![1, 2],
|
||||
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.tier, p.tier);
|
||||
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(), 2);
|
||||
|
||||
// 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user