131 lines
3.8 KiB
Rust
131 lines
3.8 KiB
Rust
//! Scenario file format and runner scaffold (MetricsAndScenarios §2,
|
|
//! GameKernel K17). The format parses fully; execution against a game
|
|
//! aggregate is wired up in T08 — until then `run` reports Unimplemented.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::BTreeMap;
|
|
|
|
/// One scenario file (`scenarios/<game>/<slug>.yaml`).
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ScenarioFile {
|
|
pub scenario: String,
|
|
#[serde(default)]
|
|
pub description: String,
|
|
/// Numbered rule IDs this scenario covers (feeds M-D1-COV / AM-1).
|
|
pub covers: Vec<String>,
|
|
/// Marks scenarios encoding a PROVISIONAL U-item default
|
|
/// (GroundRules §Underdetermined): a ground-game ruling flips the
|
|
/// scenario, not the kernel.
|
|
#[serde(default)]
|
|
pub provisional: bool,
|
|
pub seed: u64,
|
|
pub setup: Setup,
|
|
pub commands: Vec<CommandStep>,
|
|
pub expect: Expect,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct Setup {
|
|
pub players: u8,
|
|
/// Named setup preset from the game spec (e.g. "standard-3p").
|
|
pub preset: String,
|
|
/// Explicit state overrides applied after the preset (dot-paths).
|
|
#[serde(default)]
|
|
pub patch: BTreeMap<String, serde_yaml::Value>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct CommandStep {
|
|
pub actor: String,
|
|
pub cmd: String,
|
|
#[serde(default)]
|
|
pub args: BTreeMap<String, serde_yaml::Value>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct Expect {
|
|
/// Ordered subsequence of event kinds that must occur.
|
|
#[serde(default)]
|
|
pub events: Vec<BTreeMap<String, serde_yaml::Value>>,
|
|
/// Partial end-state assertions: dot-path → expected value.
|
|
#[serde(default)]
|
|
pub state: BTreeMap<String, serde_yaml::Value>,
|
|
/// Indices into `commands` that must be rejected.
|
|
#[serde(default)]
|
|
pub rejects: Vec<usize>,
|
|
/// Optional full-state golden hash.
|
|
#[serde(default)]
|
|
pub state_hash: Option<String>,
|
|
}
|
|
|
|
impl ScenarioFile {
|
|
pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml::Error> {
|
|
serde_yaml::from_str(yaml)
|
|
}
|
|
}
|
|
|
|
/// Result of executing one scenario (twice, per the K8 double-run rule).
|
|
#[derive(Debug)]
|
|
pub enum RunOutcome {
|
|
Passed {
|
|
covers: Vec<String>,
|
|
},
|
|
Failed {
|
|
reason: String,
|
|
},
|
|
/// Scaffold state: parsing works, execution lands in T08.
|
|
Unimplemented,
|
|
}
|
|
|
|
/// Runner scaffold. T08 replaces the body with: build aggregate from
|
|
/// setup, apply commands via validate/fold, check expectations, run twice
|
|
/// and compare hashes, emit a replay bundle on failure.
|
|
pub fn run(_scenario: &ScenarioFile) -> RunOutcome {
|
|
RunOutcome::Unimplemented
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
const SAMPLE: &str = r#"
|
|
scenario: ground/smoke-setup
|
|
description: Parses the documented scenario shape end to end.
|
|
covers: [GR-S02, GR-S03]
|
|
provisional: false
|
|
seed: 42
|
|
setup:
|
|
players: 3
|
|
preset: standard-3p
|
|
patch:
|
|
"players.0.stress": 4
|
|
commands:
|
|
- actor: P1
|
|
cmd: select_action
|
|
args: { action: ATTACK, target: P2 }
|
|
expect:
|
|
state:
|
|
"round": 1
|
|
rejects: []
|
|
"#;
|
|
|
|
#[test]
|
|
fn scenario_format_roundtrips() {
|
|
let parsed = ScenarioFile::from_yaml(SAMPLE).unwrap();
|
|
assert_eq!(parsed.scenario, "ground/smoke-setup");
|
|
assert_eq!(parsed.covers, vec!["GR-S02", "GR-S03"]);
|
|
assert_eq!(parsed.setup.players, 3);
|
|
assert_eq!(parsed.commands.len(), 1);
|
|
assert!(matches!(run(&parsed), RunOutcome::Unimplemented));
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_fields_are_rejected() {
|
|
let bad = SAMPLE.replace("seed: 42", "seed: 42\ntypo_field: 1");
|
|
assert!(ScenarioFile::from_yaml(&bad).is_err());
|
|
}
|
|
}
|