First levels v0.1.0
This commit is contained in:
35
src/levels/l01_minimum.rs
Normal file
35
src/levels/l01_minimum.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
//! Level 1 — the dungeon door. Write the smallest valid YAML.
|
||||
//!
|
||||
//! Paired design note: `l01.md`.
|
||||
|
||||
use serde_yaml::Value;
|
||||
|
||||
use super::{Generated, Level};
|
||||
|
||||
pub struct Minimum;
|
||||
|
||||
impl Level for Minimum {
|
||||
fn id(&self) -> u8 {
|
||||
1
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"The Dungeon Door"
|
||||
}
|
||||
|
||||
fn generate(&self, _seed: u64) -> Generated {
|
||||
// Canonical target: the null document. `---`, `~`, and `null` all
|
||||
// parse to `Value::Null`, so any of them passes via the semantic
|
||||
// short-circuit.
|
||||
let target_yaml = serde_yaml::to_string(&Value::Null).expect("serialise null");
|
||||
Generated {
|
||||
target_yaml,
|
||||
description:
|
||||
"Write the smallest possible valid YAML — a single empty document is enough."
|
||||
.to_string(),
|
||||
flavor:
|
||||
"A heavy door bars the way. A glyph above it asks only for the smallest valid offering."
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
81
src/levels/l02_kv.rs
Normal file
81
src/levels/l02_kv.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
//! Level 2 — key-value pairs. Map each direction to what lies that way.
|
||||
//!
|
||||
//! Paired design note: `l02.md`.
|
||||
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
use serde::Serialize;
|
||||
use serde_yaml::{Mapping, Value};
|
||||
|
||||
use crate::describe::Describer;
|
||||
|
||||
use super::{Generated, Level};
|
||||
|
||||
pub struct KeyValue;
|
||||
|
||||
const DIRECTIONS: &[&str] = &["left", "right", "straight", "back", "up", "down"];
|
||||
const FEATURES: &[&str] = &["door", "tunnel", "wall", "stairs", "pit", "altar"];
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DescCtx {
|
||||
pairs: Vec<DescPair>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DescPair {
|
||||
direction: String,
|
||||
feature: String,
|
||||
}
|
||||
|
||||
impl Level for KeyValue {
|
||||
fn id(&self) -> u8 {
|
||||
2
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Key-Value Pairs"
|
||||
}
|
||||
|
||||
fn generate(&self, seed: u64) -> Generated {
|
||||
// Seed XOR'd with a per-level constant so the same `current_seed`
|
||||
// produces different content per level.
|
||||
let mut rng = ChaCha8Rng::seed_from_u64(seed ^ 0x0000_0000_0000_0002);
|
||||
let n = rng.gen_range(2..=4);
|
||||
let directions: Vec<&'static str> =
|
||||
DIRECTIONS.choose_multiple(&mut rng, n).copied().collect();
|
||||
|
||||
let mut mapping = Mapping::new();
|
||||
let mut pairs = Vec::with_capacity(directions.len());
|
||||
for d in &directions {
|
||||
let f = *FEATURES.choose(&mut rng).expect("non-empty pool");
|
||||
mapping.insert(
|
||||
Value::String((*d).to_string()),
|
||||
Value::String(f.to_string()),
|
||||
);
|
||||
pairs.push(DescPair {
|
||||
direction: (*d).to_string(),
|
||||
feature: f.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let target_yaml =
|
||||
serde_yaml::to_string(&Value::Mapping(mapping)).expect("serialise mapping");
|
||||
|
||||
let mut d = Describer::new();
|
||||
d.register(
|
||||
"l02",
|
||||
"{% for p in pairs %}- {{ p.direction }} leads to a {{ p.feature }}\n{% endfor %}",
|
||||
)
|
||||
.expect("register template");
|
||||
let description = d
|
||||
.render("l02", &DescCtx { pairs })
|
||||
.expect("render template");
|
||||
|
||||
Generated {
|
||||
target_yaml,
|
||||
description,
|
||||
flavor: "You stand at a junction. Map what you see.".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//! Levels — hand-written Rust generators paired with design notes.
|
||||
//!
|
||||
//! Each level is implemented in `l<XX>_<name>.rs` and is the authoritative
|
||||
//! source of truth for what target YAML the player must reproduce and how
|
||||
//! the description is rendered.
|
||||
//!
|
||||
//! The paired `l<XX>.md` file is a **design note only**: it documents the
|
||||
//! intended scene and a minimal example of the target YAML. `.md` files
|
||||
//! are *not* loaded at runtime — there is no `include_str!` and no
|
||||
//! markdown parser. If a `.md` and its paired `.rs` ever disagree, the
|
||||
//! `.rs` wins.
|
||||
|
||||
pub mod l01_minimum;
|
||||
pub mod l02_kv;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Game-wide difficulty. Chosen once on the TierSelect screen; persisted
|
||||
/// in `Progress.tier`. Maps to a fixed passing threshold per level.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum Difficulty {
|
||||
Easy,
|
||||
Medium,
|
||||
Hard,
|
||||
}
|
||||
|
||||
impl Difficulty {
|
||||
pub fn threshold(self) -> f64 {
|
||||
match self {
|
||||
Self::Easy => 0.70,
|
||||
Self::Medium => 0.80,
|
||||
Self::Hard => 0.95,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Easy => "Easy (70%)",
|
||||
Self::Medium => "Medium (80%)",
|
||||
Self::Hard => "Hard (95%)",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What `Level::generate` returns: the canonical target YAML to grade
|
||||
/// against, the player-facing description (already rendered), and the
|
||||
/// dungeon flavor line.
|
||||
pub struct Generated {
|
||||
pub target_yaml: String,
|
||||
pub description: String,
|
||||
pub flavor: String,
|
||||
}
|
||||
|
||||
/// One level's generator. Implementations live in `l<XX>_<name>.rs`.
|
||||
pub trait Level {
|
||||
fn id(&self) -> u8;
|
||||
fn name(&self) -> &'static str;
|
||||
fn generate(&self, seed: u64) -> Generated;
|
||||
}
|
||||
|
||||
/// Ordered registry of all levels. `registry()[0]` is level 1.
|
||||
pub fn registry() -> Vec<Box<dyn Level>> {
|
||||
vec![
|
||||
Box::new(l01_minimum::Minimum),
|
||||
Box::new(l02_kv::KeyValue),
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user