CB-WP-0036 done: the pace flag and the first ornament declarations
Some checks failed
ci / check (push) Failing after 4s
Some checks failed
ci / check (push) Failing after 4s
--pace speed|interactive, defaulting to Speed. Nothing reads it yet, and that is the point: it is the seam clay-animate attaches to, and a seam is cheap now where a retrofit would not be. A misspelt pace is refused rather than defaulting, because quietly falling back to Speed would look exactly like the renderer being broken. I3 is asserted rather than intended: the same scripted game at both paces must produce a byte-identical serialised recording and the same end state hash. Mutation-proven — leak the pace into the seed and it fails with "the recording differs by pace, so a renderer has become mechanism". specs/OrnamentRegister.md carries four declarations. This reverses the reasoning written in T03 earlier, which said the first declarations would come from F18's unvendored files: instances already existed. Hand order is what prompted the category, and "who deals" was the maintainer's own example. O3 is the interesting one — seat ORDER is mechanism because GR-R08 rotates Lead, while where a seat is drawn is not. I5 is executable: check_ornament_falsifier fails any row still declared that names no falsifier, mutation-proven red on O1. Presence, never adequacy, and the finding text says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b231c1b5e2
commit
bd9e168af5
7 changed files with 315 additions and 9 deletions
|
|
@ -1215,6 +1215,7 @@ mod tests {
|
|||
serve: Some(0),
|
||||
trial: None,
|
||||
mode: games_ground::ScoringMode::SharedGround,
|
||||
pace: crate::table::Pace::Speed,
|
||||
},
|
||||
std::io::Cursor::new(Vec::new()),
|
||||
out,
|
||||
|
|
@ -1404,6 +1405,7 @@ mod tests {
|
|||
serve: Some(0),
|
||||
trial: None,
|
||||
mode: games_ground::ScoringMode::SharedGround,
|
||||
pace: crate::table::Pace::Speed,
|
||||
},
|
||||
std::io::Cursor::new(Vec::new()),
|
||||
out,
|
||||
|
|
|
|||
|
|
@ -764,6 +764,7 @@ mod tests {
|
|||
serve: None,
|
||||
trial: None,
|
||||
mode: games_ground::ScoringMode::SharedGround,
|
||||
pace: crate::table::Pace::Speed,
|
||||
};
|
||||
let mut sink: Vec<u8> = Vec::new();
|
||||
let summary =
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ play:
|
|||
--replay DIR write a .cbreplay bundle of the finished game to DIR
|
||||
--record FILE write the finished game as a scenario YAML
|
||||
--trial FILE write a trial log: what the player said, bound to where
|
||||
--pace P speed (default) or interactive: how much ornamentation
|
||||
is performed. Never changes the game -- the recording is
|
||||
byte-identical either way (specs/Ornamentation.md)
|
||||
--mode M scoring mode: shared (GR-E02), common (GR-E03),
|
||||
coalitions (GR-E04). Default shared.
|
||||
--serve PORT play human seats in a browser on 127.0.0.1:PORT instead
|
||||
|
|
@ -127,6 +130,18 @@ fn parse_args(argv: &[String]) -> Result<Mode, String> {
|
|||
};
|
||||
i += 2;
|
||||
}
|
||||
// CB-WP-0036 T02. A SEPARATE axis from --mode: that one is
|
||||
// ScoringMode, a rule of the game; this is how much of what
|
||||
// the rules cannot see gets performed (Ornamentation §4).
|
||||
//
|
||||
// **The flag exists before anything reads it, deliberately.**
|
||||
// It is the seam `clay-animate` attaches to, and a seam is
|
||||
// cheap now where a retrofit would not be.
|
||||
"--pace" => {
|
||||
play_flags.push(flag.into());
|
||||
config.pace = value(i, argv, flag)?.parse()?;
|
||||
i += 2;
|
||||
}
|
||||
"--trial" => {
|
||||
play_flags.push(flag.into());
|
||||
config.trial = Some(value(i, argv, flag)?.into());
|
||||
|
|
@ -290,6 +305,7 @@ mod tests {
|
|||
serve: None,
|
||||
trial: None,
|
||||
mode: games_ground::ScoringMode::SharedGround,
|
||||
pace: table::Pace::Speed,
|
||||
};
|
||||
let script = "0\n".repeat(400);
|
||||
let mut out: Vec<u8> = Vec::new();
|
||||
|
|
@ -312,6 +328,78 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// **Ornamentation §5, I3** — the invariant the clay-borg /
|
||||
/// clay-animate split rests on.
|
||||
///
|
||||
/// > The same seed and the same decisions produce a **byte-identical
|
||||
/// > recording** at any pace.
|
||||
///
|
||||
/// Asserted rather than intended. If this ever fails, something that
|
||||
/// was called ornamentation has become mechanism, and the boundary in
|
||||
/// `specs/Ornamentation.md` has stopped being real — which §7 names
|
||||
/// as wrong at the root rather than patchable at the edges.
|
||||
#[test]
|
||||
fn pace_cannot_change_the_game() {
|
||||
let at = |pace| {
|
||||
let config = Config {
|
||||
seed: 42,
|
||||
players: 3,
|
||||
human_seats: vec![0],
|
||||
bot: "greedy".into(),
|
||||
replay_dir: None,
|
||||
record: None,
|
||||
serve: None,
|
||||
trial: None,
|
||||
mode: games_ground::ScoringMode::SharedGround,
|
||||
pace,
|
||||
};
|
||||
let script = "0\n".repeat(400);
|
||||
let mut out: Vec<u8> = Vec::new();
|
||||
let s = table::play(&config, script.as_bytes(), &mut out).expect("game");
|
||||
(
|
||||
serde_yaml::to_string(&s.scenario).expect("yaml"),
|
||||
s.end_state_hash,
|
||||
)
|
||||
};
|
||||
let (speed_yaml, speed_hash) = at(table::Pace::Speed);
|
||||
let (inter_yaml, inter_hash) = at(table::Pace::Interactive);
|
||||
|
||||
// The recording, byte for byte.
|
||||
assert_eq!(
|
||||
speed_yaml, inter_yaml,
|
||||
"the recording differs by pace, so a renderer has become mechanism"
|
||||
);
|
||||
// And the state hash, which is what §1.1 uses to tell the two
|
||||
// categories apart in the first place.
|
||||
assert_eq!(speed_hash, inter_hash, "pace moved the state hash");
|
||||
}
|
||||
|
||||
/// `--pace` parses, defaults to speed, and refuses what it cannot do.
|
||||
///
|
||||
/// **Speed is the default** because `sim`, `trials`, the benchmarks
|
||||
/// and every bot game run at it (Ornamentation §4).
|
||||
#[test]
|
||||
fn pace_parses_and_defaults_to_speed() {
|
||||
assert_eq!(Config::default().pace, table::Pace::Speed);
|
||||
assert_eq!(
|
||||
play_args(&["--pace", "interactive"]).expect("parse").pace,
|
||||
table::Pace::Interactive
|
||||
);
|
||||
assert_eq!(
|
||||
play_args(&["--pace", "speed"]).expect("parse").pace,
|
||||
table::Pace::Speed
|
||||
);
|
||||
// A misspelling must not silently mean the default: pace is the
|
||||
// seam clay-animate attaches to, and a typo that quietly selects
|
||||
// Speed would look like the renderer being broken.
|
||||
let e = match play_args(&["--pace", "cinematic"]) {
|
||||
Err(e) => e,
|
||||
Ok(_) => panic!("a misspelt pace was accepted"),
|
||||
};
|
||||
assert!(e.contains("cinematic"), "{e}");
|
||||
assert!(e.contains("speed") && e.contains("interactive"), "{e}");
|
||||
}
|
||||
|
||||
/// K13 at the boundary that matters: what the human is *shown* must
|
||||
/// not contain another seat's face-down selection.
|
||||
///
|
||||
|
|
@ -331,6 +419,7 @@ mod tests {
|
|||
serve: None,
|
||||
trial: None,
|
||||
mode: games_ground::ScoringMode::SharedGround,
|
||||
pace: table::Pace::Speed,
|
||||
};
|
||||
let mut out: Vec<u8> = Vec::new();
|
||||
table::play(&config, "0\n".repeat(200).as_bytes(), &mut out).expect("game");
|
||||
|
|
@ -395,6 +484,7 @@ mod tests {
|
|||
serve: None,
|
||||
trial: None,
|
||||
mode: games_ground::ScoringMode::SharedGround,
|
||||
pace: table::Pace::Speed,
|
||||
};
|
||||
let mut out: Vec<u8> = Vec::new();
|
||||
let summary = table::play(&config, "".as_bytes(), &mut out).expect("bot game");
|
||||
|
|
@ -457,6 +547,7 @@ mod tests {
|
|||
serve: None,
|
||||
trial: None,
|
||||
mode: games_ground::ScoringMode::SharedGround,
|
||||
pace: table::Pace::Speed,
|
||||
};
|
||||
let mut out: Vec<u8> = Vec::new();
|
||||
let summary = table::play(&config, "".as_bytes(), &mut out).expect("game");
|
||||
|
|
|
|||
|
|
@ -47,6 +47,43 @@ pub struct Config {
|
|||
/// patch, so two of the three shipped modes were unreachable from the
|
||||
/// only way anyone actually plays.
|
||||
pub mode: games_ground::ScoringMode,
|
||||
/// How much ornamentation is performed (CB-WP-0036,
|
||||
/// [`specs/Ornamentation.md`]).
|
||||
///
|
||||
/// **A separate axis from `mode`, deliberately.** `mode` is
|
||||
/// `ScoringMode` — a rule of the game. `pace` is how much of what the
|
||||
/// rules cannot see gets shown. Calling both of them "mode" on one
|
||||
/// driver is a collision waiting to be mis-read.
|
||||
pub pace: Pace,
|
||||
}
|
||||
|
||||
/// Speed or Interactive (Ornamentation §4).
|
||||
///
|
||||
/// **Speed is the default and always will be.** `sim`, `trials`, the
|
||||
/// benchmarks and every bot game run at it, and ornamentation must cost
|
||||
/// them *nothing* — not "little".
|
||||
///
|
||||
/// **Pace can never select different mechanism** (Ornamentation §5, I3):
|
||||
/// the same seed and the same decisions produce a byte-identical
|
||||
/// recording at either pace. That is the invariant the whole
|
||||
/// clay-borg / clay-animate split rests on, so it is asserted, not
|
||||
/// assumed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum Pace {
|
||||
#[default]
|
||||
Speed,
|
||||
Interactive,
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Pace {
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, String> {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"speed" | "fast" => Ok(Pace::Speed),
|
||||
"interactive" | "inter" => Ok(Pace::Interactive),
|
||||
other => Err(format!("unknown --pace {other:?} (speed, interactive)")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
|
|
@ -61,6 +98,7 @@ impl Default for Config {
|
|||
serve: None,
|
||||
trial: None,
|
||||
mode: games_ground::ScoringMode::SharedGround,
|
||||
pace: Pace::Speed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue