1 Commits

Author SHA1 Message Date
9f8e5caaa8 Add level 11: Advanced Anchors
`shapes:` list defines 2-3 polygons (name + sides + interior angle sum);
`copies:` is a list of 3-4 selections (with possible repetition) drawn
from the same pool. serde_yaml expands aliases on parse, so the target
is the inlined form; players using `- &name` anchors inside the list
and `*name` aliases in `copies:` produce the same Value and pass via
the semantic short-circuit.

All randomised per seed (ChaCha8Rng XOR'd with 0x..0B).

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:59:44 +03:00
3 changed files with 142 additions and 102 deletions

View File

@@ -1,101 +0,0 @@
//! 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

@@ -0,0 +1,141 @@
//! Level 11 — advanced anchors. Anchor shapes inside a list, alias
//! them elsewhere.
//!
//! Paired design note: `l11.md`.
//!
//! Like L6, serde_yaml expands aliases on parse, so the emitted target
//! is the fully-inlined form. Players who use `&anchor` / `*alias`
//! produce the same `Value` and pass via the semantic short-circuit.
use rand::seq::SliceRandom;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use serde::Serialize;
use serde_yaml::{Mapping, Sequence, Value};
use crate::describe::Describer;
use super::{Generated, Level};
pub struct AdvAnchors;
const SHAPES: &[(&str, i64)] = &[
("triangle", 3),
("square", 4),
("pentagon", 5),
("hexagon", 6),
("heptagon", 7),
("octagon", 8),
];
#[derive(Serialize)]
struct DescCtx {
shapes: Vec<ShapeDesc>,
copies: Vec<String>,
}
#[derive(Serialize)]
struct ShapeDesc {
name: String,
sides: i64,
interior_angle_sum: i64,
}
fn shape_value(name: &str, sides: i64) -> Value {
let mut m = Mapping::new();
m.insert(
Value::String("name".to_string()),
Value::String(name.to_string()),
);
m.insert(Value::String("sides".to_string()), Value::from(sides));
m.insert(
Value::String("interior".to_string()),
Value::from((sides - 2) * 180),
);
Value::Mapping(m)
}
impl Level for AdvAnchors {
fn id(&self) -> u8 {
11
}
fn name(&self) -> &'static str {
"Advanced Anchors"
}
fn generate(&self, seed: u64) -> Generated {
let mut rng = ChaCha8Rng::seed_from_u64(seed ^ 0x0000_0000_0000_000B);
let n = rng.gen_range(2..=3);
let picked: Vec<(&'static str, i64)> = SHAPES
.choose_multiple(&mut rng, n)
.copied()
.collect();
// The defining `shapes:` list.
let shapes_seq: Sequence = picked
.iter()
.map(|(name, sides)| shape_value(name, *sides))
.collect();
// `copies:` — random selections with possible repetition.
let m = rng.gen_range(3..=4);
let mut copies_seq = Sequence::new();
let mut copy_names = Vec::new();
for _ in 0..m {
let (name, sides) = picked.choose(&mut rng).unwrap();
copies_seq.push(shape_value(name, *sides));
copy_names.push((*name).to_string());
}
let mut top = Mapping::new();
top.insert(
Value::String("shapes".to_string()),
Value::Sequence(shapes_seq),
);
top.insert(
Value::String("copies".to_string()),
Value::Sequence(copies_seq),
);
let target_yaml =
serde_yaml::to_string(&Value::Mapping(top)).expect("serialise mapping");
let shape_descs: Vec<ShapeDesc> = picked
.iter()
.map(|(name, sides)| ShapeDesc {
name: (*name).to_string(),
sides: *sides,
interior_angle_sum: (sides - 2) * 180,
})
.collect();
let mut d = Describer::new();
d.register(
"l11",
"Shapes are defined once and reused.\n\
Definitions:\n\
{% for s in shapes %}- {{ s.name }}: sides={{ s.sides }}, interior={{ s.interior_angle_sum }}\n\
{% endfor %}\n\
Copies, in order: {% for c in copies %}{{ c }}{% if not loop.last %}, {% endif %}{% endfor %}\n\
💡 Anchor each shape in the list with `- &name`, then alias by `*name` in copies.",
)
.expect("register template");
let description = d
.render(
"l11",
&DescCtx {
shapes: shape_descs,
copies: copy_names,
},
)
.expect("render template");
Generated {
target_yaml,
description,
flavor: "🔺 The geometry chamber repeats its forms.".to_string(),
}
}
}

View File

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