1 Commits

Author SHA1 Message Date
424ef3e8fd Add level 10: Vault Ledger (dynamic values)
`vault:` with four entries — decimal int gold, decimal int silver,
fractional float experience, and an ISO-8601 date string. Description
hints at hex/octal/exponent equivalents the player can write — any
form parsing to the same numeric value passes.

All four values randomised per seed (ChaCha8Rng XOR'd with 0x..0A).

Not wired into levels::registry() yet — integration belongs to a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:58:25 +03:00
2 changed files with 102 additions and 0 deletions

101
src/levels/l10_dynamic.rs Normal file
View File

@@ -0,0 +1,101 @@
//! Level 10 — dynamic values. The vault ledger accepts numbers and
//! dates in many forms.
//!
//! Paired design note: `l10.md`.
//!
//! The target is canonical (decimal ints, decimal float, ISO date
//! string). The lesson is that the player can write equivalent values
//! in hex / octal / exponent forms — all parse to the same number.
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 Dynamic;
#[derive(Serialize)]
struct DescCtx {
gold_dec: i64,
gold_hex: String,
silver_dec: i64,
silver_oct: String,
experience: f64,
date: String,
}
impl Level for Dynamic {
fn id(&self) -> u8 {
10
}
fn name(&self) -> &'static str {
"Vault Ledger"
}
fn generate(&self, seed: u64) -> Generated {
let mut rng = ChaCha8Rng::seed_from_u64(seed ^ 0x0000_0000_0000_000A);
let gold = rng.gen_range(0x100..=0xFFFi64);
let silver = rng.gen_range(0o100..=0o777i64);
// Fractional float so the canonical serialisation keeps the `.5`.
let experience = rng.gen_range(10..=99) as f64 + 0.5;
let year = rng.gen_range(1100..=2050i64);
let month = rng.gen_range(1..=12i64);
let day = rng.gen_range(1..=28i64);
let date = format!("{year:04}-{month:02}-{day:02}");
let mut vault = Mapping::new();
vault.insert(Value::String("gold".to_string()), Value::from(gold));
vault.insert(Value::String("silver".to_string()), Value::from(silver));
vault.insert(
Value::String("experience".to_string()),
Value::from(experience),
);
vault.insert(
Value::String("date".to_string()),
Value::String(date.clone()),
);
let mut top = Mapping::new();
top.insert(Value::String("vault".to_string()), Value::Mapping(vault));
let target_yaml =
serde_yaml::to_string(&Value::Mapping(top)).expect("serialise mapping");
let mut d = Describer::new();
d.register(
"l10",
"The vault ledger demands its values:\n\
gold: {{ gold_dec }} (try hex: 0x{{ gold_hex }})\n\
silver: {{ silver_dec }} (try octal: 0o{{ silver_oct }})\n\
experience: {{ experience }} (any equivalent float form passes)\n\
date: {{ date }} (an ISO-8601 date string)\n\
💡 Any equivalent numeric form passes — what matters is the parsed value.",
)
.expect("register template");
let description = d
.render(
"l10",
&DescCtx {
gold_dec: gold,
gold_hex: format!("{:X}", gold),
silver_dec: silver,
silver_oct: format!("{:o}", silver),
experience,
date,
},
)
.expect("render template");
Generated {
target_yaml,
description,
flavor: "🪙 A vault ledger awaits in many ciphers.".to_string(),
}
}
}

View File

@@ -13,6 +13,7 @@
pub mod l01_minimum;
pub mod l02_kv;
pub mod l03_dict;
pub mod l10_dynamic;
use serde::{Deserialize, Serialize};