Two rules, two different answers, which is the point of a task phrased "implement, or amend and say why". K18 is implemented. "Criterion benches driving the same scenario format at scale" was false — the bench hardcoded its commands and never touched ScenarioFile, while MetricsAndScenarios §3 pointed at a benchmarks/ directory containing only baselines/. benchmarks/synthetic-3p.yaml now holds the workload and both the bench and bench_shape read it: the workload is data, not code. A second defect surfaced while fixing the first. After the bench switched to the file, bench_shape still hardcoded the same sequence, so the workload existed twice — deleting end_round from the YAML broke bench-test while bench_shape kept passing. Duplicated-fact drift in executable form. Both now read the same include_str! and deleting a command breaks both. Explicitly not claimed: this does not unblock AM-3. AM-3's baseline is a declarative game object — moves, turn order, rules. synthetic-3p.yaml is a command list; the rules live in games/ground. Marking it as AM-3's subject would compare a script to a game definition, which is the category error AM-3 is blocked on. The file says so in its own header, where the next person will be tempted. K14 is amended. CommitWindow had zero non-test users and GROUND enforces the same contract inline. Wiring GROUND through it was rejected: it would change the serialized shape of `selections`, which four scenario files assert by dot-path and every state hash depends on, for the sole benefit of making a sentence literally true. The deciding argument is INTENT's, not convenience: abstractions are extracted from working games rather than invented in isolation, and no concept becomes canonical until it survives a second concrete use. CommitWindow was invented before any game needed it and has survived none. Imposing it on GROUND would manufacture the first use rather than discover it. So K14 states what is actually guaranteed, CommitWindow is marked provisional in the source, and it carries a delete-by date of 2026-12-31. Kernel spec->code link 16/18 -> 18/18, stated with the caveat the gate prints every run: that is about names, not assertions. Two self-tests broke and both broke correctly. rule-coverage's gate test hardcoded "unlinked rules exist today" and failed when the last one was linked; it now computes that and asserts the gate fails iff rules are unlinked. facts' text check rejected k_unlinked once it became legitimately empty; empty now renders as "(none)" and the check distinguishes absent from empty. M-D1-MUT: 8 of 14, unchanged — K14 and K18 are kernel rules, not acceptance rows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
131 lines
4.4 KiB
Rust
131 lines
4.4 KiB
Rust
//! cb-game-runtime — round/phase machinery, commit windows, projections,
|
|
//! and the scenario runner (GameKernel §2.5, §3). Game-agnostic.
|
|
|
|
#[cfg(feature = "scenarios")]
|
|
pub mod scenario;
|
|
|
|
/// K10 replay bundles. Dev-only: a shipped game runtime writes no bundles.
|
|
#[cfg(feature = "scenarios")]
|
|
pub mod replay;
|
|
|
|
#[cfg(feature = "scenarios")]
|
|
pub use scenario::{parse_actor, run, CommandStep, RunOutcome, ScenarioFile, ScenarioGame, Setup};
|
|
|
|
use cb_kernel::PlayerId;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::BTreeMap;
|
|
|
|
/// A simultaneous commit window (GameKernel K12): the runtime opens it
|
|
/// naming who must submit; submissions are commitment events hidden from
|
|
/// projections until reveal.
|
|
///
|
|
/// **PROVISIONAL — zero non-test users as of 2026-08-01 (K14, GameKernel
|
|
/// §2.5a).** GROUND implements the same contract inline in its own
|
|
/// aggregate. This type is the *extracted* form, kept because it
|
|
/// documents the seam stage 3 and stage 4 will need — but INTENT says a
|
|
/// concept becomes canonical only *"after surviving a second concrete
|
|
/// use"*, and this has survived none.
|
|
///
|
|
/// **Delete it if no second game uses it by 2026-12-31.** A primitive
|
|
/// with one hypothetical user and a test that exercises only itself is
|
|
/// the AM-11 shape, and this project has paid for that shape twice.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CommitWindow<C> {
|
|
/// Players who must submit, and their commitment once received.
|
|
pending: BTreeMap<PlayerId, Option<C>>,
|
|
}
|
|
|
|
impl<C> CommitWindow<C> {
|
|
pub fn open(players: impl IntoIterator<Item = PlayerId>) -> Self {
|
|
Self {
|
|
pending: players.into_iter().map(|p| (p, None)).collect(),
|
|
}
|
|
}
|
|
|
|
/// Record a commitment. Errors on players outside the window or on a
|
|
/// duplicate submission (GameKernel K12).
|
|
pub fn submit(&mut self, player: PlayerId, commitment: C) -> Result<(), CommitError> {
|
|
match self.pending.get_mut(&player) {
|
|
None => Err(CommitError::NotInWindow(player)),
|
|
Some(slot @ None) => {
|
|
*slot = Some(commitment);
|
|
Ok(())
|
|
}
|
|
Some(Some(_)) => Err(CommitError::AlreadyCommitted(player)),
|
|
}
|
|
}
|
|
|
|
pub fn is_complete(&self) -> bool {
|
|
self.pending.values().all(Option::is_some)
|
|
}
|
|
|
|
/// Consume the window at reveal, yielding commitments in PlayerId
|
|
/// order (deterministic iteration, GameKernel K6).
|
|
pub fn reveal(self) -> Result<Vec<(PlayerId, C)>, CommitError> {
|
|
if !self.is_complete() {
|
|
return Err(CommitError::Incomplete);
|
|
}
|
|
Ok(self
|
|
.pending
|
|
.into_iter()
|
|
.map(|(p, c)| (p, c.expect("checked complete")))
|
|
.collect())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub enum CommitError {
|
|
NotInWindow(PlayerId),
|
|
AlreadyCommitted(PlayerId),
|
|
Incomplete,
|
|
}
|
|
|
|
impl core::fmt::Display for CommitError {
|
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
match self {
|
|
CommitError::NotInWindow(p) => write!(f, "player {p} is not in this window"),
|
|
CommitError::AlreadyCommitted(p) => write!(f, "player {p} already committed"),
|
|
CommitError::Incomplete => write!(f, "window is not complete"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Per-player projection (GameKernel K13): a total function from state to
|
|
/// what one seat may see. Implemented by game packages; the runtime only
|
|
/// fixes the shape so projections can never feed back into validation.
|
|
pub trait Project {
|
|
type View: Serialize;
|
|
fn project(&self, viewer: Viewer) -> Self::View;
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Viewer {
|
|
Player(PlayerId),
|
|
Spectator,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn commit_window_gates_and_reveals_in_order() {
|
|
let mut w: CommitWindow<u8> = CommitWindow::open([PlayerId(0), PlayerId(1)]);
|
|
assert_eq!(
|
|
w.submit(PlayerId(2), 9),
|
|
Err(CommitError::NotInWindow(PlayerId(2)))
|
|
);
|
|
w.submit(PlayerId(1), 7).unwrap();
|
|
assert_eq!(
|
|
w.submit(PlayerId(1), 8),
|
|
Err(CommitError::AlreadyCommitted(PlayerId(1)))
|
|
);
|
|
assert!(!w.is_complete());
|
|
w.submit(PlayerId(0), 5).unwrap();
|
|
assert!(w.is_complete());
|
|
assert_eq!(
|
|
w.reveal().unwrap(),
|
|
vec![(PlayerId(0), 5), (PlayerId(1), 7)]
|
|
);
|
|
}
|
|
}
|