First levels v0.1.0

This commit is contained in:
2026-05-21 17:12:23 +03:00
parent aa9cb6ea53
commit 19e39d220d
9 changed files with 749 additions and 22 deletions

View File

@@ -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"
);
}
}