T07: Cargo workspace scaffold — cb-kernel/cb-events/cb-game-runtime/games-ground/cb-sim, HashMap deny-lint, scenario format + runner stub, Criterion skeleton, Makefile, CI
Some checks failed
ci / check (push) Has been cancelled

This commit is contained in:
tegwick 2026-07-31 01:57:13 +02:00
parent 396990539a
commit 467e2c561d
23 changed files with 1625 additions and 0 deletions

21
games/ground/Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[package]
name = "games-ground"
edition.workspace = true
version.workspace = true
license-file.workspace = true
[dependencies]
cb-kernel.workspace = true
cb-events.workspace = true
cb-game-runtime.workspace = true
serde.workspace = true
[dev-dependencies]
criterion.workspace = true
[[bench]]
name = "synthetic"
harness = false
[lints]
workspace = true

View file

@ -0,0 +1,49 @@
//! Criterion skeleton for AM-6/AM-7 (GameKernel §4), wired to the
//! CB-RES-0001 baseline shape (3-player commit/reveal synthetic workload).
//! T08 replaces the placeholder body with the real aggregate loop; the
//! bench IDs and workload sizes are fixed here so results stay comparable
//! to benchmarks/baselines/ recordings.
use cb_events::state_hash;
use cb_game_runtime::CommitWindow;
use cb_kernel::PlayerId;
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
/// Placeholder workload: open/submit/reveal one 3-player commit window and
/// hash a small value. Exists so the bench harness, IDs, and baseline
/// wiring compile and run before the aggregate exists.
fn commit_reveal_round() -> usize {
let mut w: CommitWindow<u8> = CommitWindow::open([PlayerId(0), PlayerId(1), PlayerId(2)]);
for p in 0..3u8 {
w.submit(PlayerId(p), p).unwrap();
}
let revealed = w.reveal().unwrap();
state_hash(&revealed.len()).len()
}
fn bench_synthetic(c: &mut Criterion) {
let mut group = c.benchmark_group("synthetic-ground-3p");
// AM-6 headline workload sizes; AM-7 compares 5k vs 100k throughput.
for &rounds in &[5_000usize, 100_000] {
group.bench_function(format!("commit-reveal-{rounds}"), |b| {
b.iter_batched(
|| rounds,
|n| {
let mut acc = 0usize;
for _ in 0..n / 1000 {
// Scaffold runs 1/1000th scale until T08 wires the
// real aggregate; the group/ID layout is what T07
// delivers.
acc += commit_reveal_round();
}
acc
},
BatchSize::SmallInput,
)
});
}
group.finish();
}
criterion_group!(benches, bench_synthetic);
criterion_main!(benches);

119
games/ground/src/lib.rs Normal file
View file

@ -0,0 +1,119 @@
//! games-ground — the GROUND rules aggregate (specs/GroundRules.md,
//! GameKernel K15K16). T07 scaffolds the state shell; validate/fold per
//! GR-rule land in T08 with rule IDs cross-referenced in doc comments.
use cb_kernel::PlayerId;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
/// GR-O02: per-player state.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlayerState {
/// GR-F01: clamped 05.
pub stress: u8,
/// GR-F03.
pub freedom_ready: bool,
/// GR-D01/D02: OFF or the pending/active stage.
pub darvo: DarvoStage,
pub hand: Vec<SolutionCard>,
pub protection: u8,
/// Blame tokens in front of this player (GR-T02), keyed by owner.
pub blame_from: Vec<PlayerId>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DarvoStage {
Off,
Deny,
Attack,
Reverse,
}
/// GR-O04: one Problem card's live state.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProblemState {
pub suit: Suit,
pub value: u8,
pub face_up: bool,
pub denied: bool,
pub claimed_by: Option<PlayerId>,
/// GR-A11: protected from Deny this round by GROUND—OU.
pub protected_this_round: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Suit {
Clarify,
Repair,
Boundary,
Change,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct SolutionCard {
pub suit: Suit,
}
/// GR-O05: at most one relation per pair; endpoints ordered low→high.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Relation {
Bond,
Rivalry,
}
/// GR-O01..O05: the authoritative GROUND aggregate. Fields use ordered
/// collections only (GameKernel K6).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GroundState {
pub round: u8,
pub lead: PlayerId,
pub players: BTreeMap<PlayerId, PlayerState>,
/// Keyed by (low, high) player pair.
pub relations: BTreeMap<(PlayerId, PlayerId), Relation>,
pub problems: BTreeMap<u32, ProblemState>,
pub solution_deck: Vec<SolutionCard>,
pub solution_discard: Vec<SolutionCard>,
/// Focus placements: sequence owner → target (GR-T03).
pub focus: BTreeMap<PlayerId, PlayerId>,
}
#[cfg(test)]
mod tests {
use super::*;
use cb_events::state_hash_hex;
fn tiny_state() -> GroundState {
GroundState {
round: 1,
lead: PlayerId(0),
players: BTreeMap::from([(
PlayerId(0),
PlayerState {
stress: 2,
freedom_ready: true,
darvo: DarvoStage::Off,
hand: vec![SolutionCard { suit: Suit::Repair }],
protection: 0,
blame_from: vec![],
},
)]),
relations: BTreeMap::new(),
problems: BTreeMap::new(),
solution_deck: vec![],
solution_discard: vec![],
focus: BTreeMap::new(),
}
}
/// K7 on the real aggregate: hash stable across clones, sensitive to
/// semantic change.
#[test]
fn ground_state_hashes_canonically() {
let a = tiny_state();
let b = a.clone();
assert_eq!(state_hash_hex(&a), state_hash_hex(&b));
let mut c = a.clone();
c.players.get_mut(&PlayerId(0)).unwrap().stress = 5;
assert_ne!(state_hash_hex(&a), state_hash_hex(&c));
}
}