Compare commits
No commits in common. "d30938b2593c23cd3e3071658abf7e87b2334025" and "de25c4cae096bf3282ca4c34e332c3d573604e8c" have entirely different histories.
d30938b259
...
de25c4cae0
21 changed files with 111 additions and 1357 deletions
11
Makefile
11
Makefile
|
|
@ -35,11 +35,6 @@ PLAYERS ?= 3
|
||||||
# a session that could not say which variant it was is what produced
|
# a session that could not say which variant it was is what produced
|
||||||
# the report "I did a session and noted no changes".
|
# the report "I did a session and noted no changes".
|
||||||
VARIANT ?= ground-darvo-r0
|
VARIANT ?= ground-darvo-r0
|
||||||
# CB-WP-0047: the other three boards and the other two modes were
|
|
||||||
# unreachable from the way anyone actually plays. Defaults are what was
|
|
||||||
# always played, so `make ground` is unchanged.
|
|
||||||
SCENARIO ?= 1
|
|
||||||
MODE ?= shared
|
|
||||||
PORT ?= 0
|
PORT ?= 0
|
||||||
|
|
||||||
# Every cargo recipe runs at the repo root; the shell does not persist cd.
|
# Every cargo recipe runs at the repo root; the shell does not persist cd.
|
||||||
|
|
@ -77,14 +72,13 @@ help:
|
||||||
## type in the notes panel is bound to the position you typed it at.
|
## type in the notes panel is bound to the position you typed it at.
|
||||||
## `make ground PLAYERS=2 SLUG=darvo-confusion`
|
## `make ground PLAYERS=2 SLUG=darvo-confusion`
|
||||||
## Read the notes back afterwards with `make trials`.
|
## Read the notes back afterwards with `make trials`.
|
||||||
## play a session (VARIANT=ground-darvo-r0|h1|h2, SCENARIO=1|2|3|4, MODE=shared|common|coalitions)
|
## play a session (VARIANT=ground-darvo-r0|h1|h2)
|
||||||
ground:
|
ground:
|
||||||
@echo " rules: $(VARIANT) scenario: $(SCENARIO) scoring: $(MODE)"
|
@echo " rules: $(VARIANT)"
|
||||||
@mkdir -p $(REPO)/trials
|
@mkdir -p $(REPO)/trials
|
||||||
@echo " trial: trials/$(TRIAL_NAME).md (notes) + .yaml (the game)"
|
@echo " trial: trials/$(TRIAL_NAME).md (notes) + .yaml (the game)"
|
||||||
$(IN_REPO) $(CARGO) run -q -p cb-play -- \
|
$(IN_REPO) $(CARGO) run -q -p cb-play -- \
|
||||||
--players $(PLAYERS) --serve $(PORT) --variant $(VARIANT) \
|
--players $(PLAYERS) --serve $(PORT) --variant $(VARIANT) \
|
||||||
--scenario $(SCENARIO) --mode $(MODE) \
|
|
||||||
--record trials/$(TRIAL_NAME).yaml \
|
--record trials/$(TRIAL_NAME).yaml \
|
||||||
--trial trials/$(TRIAL_NAME).md $(ARGS)
|
--trial trials/$(TRIAL_NAME).md $(ARGS)
|
||||||
|
|
||||||
|
|
@ -251,7 +245,6 @@ panels:
|
||||||
@cargo run --release -q -p games-ground --example regulation
|
@cargo run --release -q -p games-ground --example regulation
|
||||||
@cargo run --release -q -p games-ground --example perfect-recall
|
@cargo run --release -q -p games-ground --example perfect-recall
|
||||||
@cargo run --release -q -p games-ground --example h2-panel
|
@cargo run --release -q -p games-ground --example h2-panel
|
||||||
@cargo run --release -q -p games-ground --example scenario-panel
|
|
||||||
|
|
||||||
# CB-WP-0022 T05: the design-finding register, reported over
|
# CB-WP-0022 T05: the design-finding register, reported over
|
||||||
# specs/GroundRules.md. Shows the QUEUE by default; the log of closed
|
# specs/GroundRules.md. Shows the QUEUE by default; the log of closed
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@
|
||||||
| workplan | CB-WP-0043 | done | — | workplans/CB-WP-0043-a-problem-sits-where-its-stress-lands.md |
|
| workplan | CB-WP-0043 | done | — | workplans/CB-WP-0043-a-problem-sits-where-its-stress-lands.md |
|
||||||
| workplan | CB-WP-0044 | done | — | workplans/CB-WP-0044-the-page-says-which-rules-it-plays.md |
|
| workplan | CB-WP-0044 | done | — | workplans/CB-WP-0044-the-page-says-which-rules-it-plays.md |
|
||||||
| workplan | CB-WP-0045 | done | — | workplans/CB-WP-0045-the-table-names-what-it-shows.md |
|
| workplan | CB-WP-0045 | done | — | workplans/CB-WP-0045-the-table-names-what-it-shows.md |
|
||||||
| workplan | CB-WP-0046 | done | — | workplans/CB-WP-0046-the-rule-the-placement-encodes.md |
|
|
||||||
| task | CB-WP-0001-T01 | done | — | workplans/CB-WP-0001-inner-loop.md |
|
| task | CB-WP-0001-T01 | done | — | workplans/CB-WP-0001-inner-loop.md |
|
||||||
| task | CB-WP-0001-T02 | done | — | workplans/CB-WP-0001-inner-loop.md |
|
| task | CB-WP-0001-T02 | done | — | workplans/CB-WP-0001-inner-loop.md |
|
||||||
| task | CB-WP-0001-T03 | done | — | workplans/CB-WP-0001-inner-loop.md |
|
| task | CB-WP-0001-T03 | done | — | workplans/CB-WP-0001-inner-loop.md |
|
||||||
|
|
@ -246,4 +245,3 @@
|
||||||
| task | CB-WP-0043-T01 | done | — | workplans/CB-WP-0043-a-problem-sits-where-its-stress-lands.md |
|
| task | CB-WP-0043-T01 | done | — | workplans/CB-WP-0043-a-problem-sits-where-its-stress-lands.md |
|
||||||
| task | CB-WP-0044-T01 | done | — | workplans/CB-WP-0044-the-page-says-which-rules-it-plays.md |
|
| task | CB-WP-0044-T01 | done | — | workplans/CB-WP-0044-the-page-says-which-rules-it-plays.md |
|
||||||
| task | CB-WP-0045-T01 | done | — | workplans/CB-WP-0045-the-table-names-what-it-shows.md |
|
| task | CB-WP-0045-T01 | done | — | workplans/CB-WP-0045-the-table-names-what-it-shows.md |
|
||||||
| task | CB-WP-0046-T01 | done | — | workplans/CB-WP-0046-the-rule-the-placement-encodes.md |
|
|
||||||
|
|
|
||||||
|
|
@ -1069,22 +1069,13 @@ pub fn document_with_log(
|
||||||
s,
|
s,
|
||||||
"<h1>GROUND \u{2014} round {round}, step {step:?}</h1>\
|
"<h1>GROUND \u{2014} round {round}, step {step:?}</h1>\
|
||||||
<div><span class=\"k\">lead</span> {lead} \
|
<div><span class=\"k\">lead</span> {lead} \
|
||||||
<span class=\"k\">scoring</span> {mode} \
|
<span class=\"k\">scoring</span> {mode:?} \
|
||||||
<span class=\"k\">rules</span> {variant} \
|
<span class=\"k\">rules</span> {variant} \
|
||||||
<span class=\"k\">scenario</span> {scenario} \
|
|
||||||
<span class=\"k\">viewing as</span> {who}</div>",
|
<span class=\"k\">viewing as</span> {who}</div>",
|
||||||
round = view.round,
|
round = view.round,
|
||||||
step = view.step,
|
step = view.step,
|
||||||
lead = seat_name(view.lead),
|
lead = seat_name(view.lead),
|
||||||
// CB-WP-0047: this was `{mode:?}` — `CommonProblem` where the
|
mode = view.mode,
|
||||||
// Mode card is titled "COMMON PROBLEM, PERSONAL EDGE". The exact
|
|
||||||
// defect CB-WP-0034 fixed on the move buttons, still standing in
|
|
||||||
// the header, on the one line that says what winning means.
|
|
||||||
mode = esc(&mode_title(view.mode)),
|
|
||||||
// CB-WP-0047: and WHICH of the four boards this is. Four
|
|
||||||
// scenarios became selectable in this pass; before it, a page
|
|
||||||
// that never named one was at least never wrong.
|
|
||||||
scenario = esc(&scenario_title(&view.scenario)),
|
|
||||||
// CB-WP-0044: the page must say which rules it is playing. It did
|
// CB-WP-0044: the page must say which rules it is playing. It did
|
||||||
// not, so a player who ran the default and was told the layout had
|
// not, so a player who ran the default and was told the layout had
|
||||||
// changed reported "no changes" — correctly, because the baseline
|
// changed reported "no changes" — correctly, because the baseline
|
||||||
|
|
@ -1315,46 +1306,6 @@ fn body(s: &mut String, view: &GroundView) {
|
||||||
s.push_str("<div class=\"card\">no problems in play</div>");
|
s.push_str("<div class=\"card\">no problems in play</div>");
|
||||||
}
|
}
|
||||||
s.push_str(&table_svg(view));
|
s.push_str(&table_svg(view));
|
||||||
// CB-WP-0047: WHAT THIS GAME IS ABOUT, and WHAT WINNING MEANS.
|
|
||||||
//
|
|
||||||
// The premise was vendored and unread while one scenario was
|
|
||||||
// hardcoded — with a single board there was nothing to tell apart.
|
|
||||||
// Selecting among four makes an unnamed board a real defect.
|
|
||||||
//
|
|
||||||
// The Mode card is the sharper one: it is the only statement of the
|
|
||||||
// win condition, three of them differ completely, and the page had
|
|
||||||
// never shown any of them. A player in BONDED COALITIONS was not
|
|
||||||
// told that Bonds decide the sides at the end.
|
|
||||||
if let Some(premise) = scenario_premise(&view.scenario) {
|
|
||||||
let _ = write!(
|
|
||||||
s,
|
|
||||||
"<details class=\"card\"><summary>{} \u{2014} what happened\
|
|
||||||
</summary>{}</details>",
|
|
||||||
esc(&scenario_title(&view.scenario)),
|
|
||||||
esc(&premise)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some((rules, tiebreak)) = mode_rules_text(view.mode) {
|
|
||||||
let _ = write!(
|
|
||||||
s,
|
|
||||||
"<details class=\"card\"><summary>{} \u{2014} how this game is won\
|
|
||||||
</summary>{}",
|
|
||||||
esc(&mode_title(view.mode)),
|
|
||||||
esc(&rules)
|
|
||||||
);
|
|
||||||
// The tiebreak is a rule the player can PLAY FOR — "lower Stress,
|
|
||||||
// then more Bonds" changes what a losing seat should do in round
|
|
||||||
// five — and it lives in its own column, so it renders as its own
|
|
||||||
// sentence rather than being silently dropped.
|
|
||||||
if !tiebreak.trim().is_empty() && !tiebreak.eq_ignore_ascii_case("Not applicable.") {
|
|
||||||
let _ = write!(
|
|
||||||
s,
|
|
||||||
"<br><span class=\"k\">tiebreak</span> {}",
|
|
||||||
esc(&tiebreak)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
s.push_str("</details>");
|
|
||||||
}
|
|
||||||
// CB-WP-0046: the placement is legible, the RULE it encodes was not.
|
// CB-WP-0046: the placement is legible, the RULE it encodes was not.
|
||||||
//
|
//
|
||||||
// Shown only when a non-global scope is actually on the table — under
|
// Shown only when a non-global scope is actually on the table — under
|
||||||
|
|
@ -1572,59 +1523,6 @@ fn stress_scope_rule_text() -> Option<String> {
|
||||||
games_ground::edition::stress_scope_rule()
|
games_ground::edition::stress_scope_rule()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Mode card's printed title, not the Rust variant's name.
|
|
||||||
///
|
|
||||||
/// Falls back to the id, which is at least a thing the edition says —
|
|
||||||
/// never to `Debug`, which is a thing only the compiler says.
|
|
||||||
fn mode_title(mode: games_ground::ScoringMode) -> String {
|
|
||||||
use games_ground::ScoringMode as M;
|
|
||||||
let want = match mode {
|
|
||||||
M::SharedGround => "MODE_COOP",
|
|
||||||
M::CommonProblem => "MODE_SEMI",
|
|
||||||
M::BondedCoalitions => "MODE_COALITION",
|
|
||||||
};
|
|
||||||
games_ground::edition::modes()
|
|
||||||
.ok()
|
|
||||||
.and_then(|ms| {
|
|
||||||
ms.into_iter()
|
|
||||||
.find(|(c, _)| c.id == want)
|
|
||||||
.map(|(c, _)| c.title)
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| want.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What the Mode card says about how this game is won (CB-WP-0047).
|
|
||||||
fn mode_rules_text(mode: games_ground::ScoringMode) -> Option<(String, String)> {
|
|
||||||
use games_ground::ScoringMode as M;
|
|
||||||
let want = match mode {
|
|
||||||
M::SharedGround => "MODE_COOP",
|
|
||||||
M::CommonProblem => "MODE_SEMI",
|
|
||||||
M::BondedCoalitions => "MODE_COALITION",
|
|
||||||
};
|
|
||||||
games_ground::edition::modes().ok().and_then(|ms| {
|
|
||||||
ms.into_iter()
|
|
||||||
.find(|(c, _)| c.id == want)
|
|
||||||
.map(|(c, tie)| (c.rules_text, tie))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The Scenario card's printed title.
|
|
||||||
fn scenario_title(id: &str) -> String {
|
|
||||||
games_ground::edition::scenarios()
|
|
||||||
.ok()
|
|
||||||
.and_then(|ss| ss.into_iter().find(|s| s.id == id).map(|s| s.title))
|
|
||||||
.unwrap_or_else(|| id.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The Scenario card's premise — what the table is arguing about.
|
|
||||||
fn scenario_premise(id: &str) -> Option<String> {
|
|
||||||
games_ground::edition::scenarios()
|
|
||||||
.ok()?
|
|
||||||
.into_iter()
|
|
||||||
.find(|s| s.id == id)
|
|
||||||
.map(|s| s.premise)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A GROUND sub-choice, in words, naming whoever it touches.
|
/// A GROUND sub-choice, in words, naming whoever it touches.
|
||||||
fn ground_choice_label(c: &games_ground::GroundChoice) -> String {
|
fn ground_choice_label(c: &games_ground::GroundChoice) -> String {
|
||||||
use games_ground::GroundChoice as G;
|
use games_ground::GroundChoice as G;
|
||||||
|
|
|
||||||
|
|
@ -135,12 +135,7 @@ mod coverage {
|
||||||
("round", "round 3"),
|
("round", "round 3"),
|
||||||
("lead", "lead P2"),
|
("lead", "lead P2"),
|
||||||
("step", "step Select"),
|
("step", "step Select"),
|
||||||
// Was "scoring BondedCoalitions" — the Rust variant's name.
|
("mode", "scoring BondedCoalitions"),
|
||||||
// **The probe was certifying the defect**, and went red the
|
|
||||||
// moment the defect was fixed. Third time this exact shape has
|
|
||||||
// been found (CB-WP-0024, CB-WP-0034): a probe that names a
|
|
||||||
// rendering holds that rendering in place, so probes name FACTS.
|
|
||||||
("mode", "BONDED COALITIONS"),
|
|
||||||
("viewer", "viewing as P1"),
|
("viewer", "viewing as P1"),
|
||||||
("solution_deck_len", "draw pile: 17 remaining"),
|
("solution_deck_len", "draw pile: 17 remaining"),
|
||||||
("solution_discard.*.suit", "discard Repair Change"),
|
("solution_discard.*.suit", "discard Repair Change"),
|
||||||
|
|
@ -193,11 +188,6 @@ mod coverage {
|
||||||
// OMITTED, and the page would be illegible to `text_of` and to a
|
// OMITTED, and the page would be illegible to `text_of` and to a
|
||||||
// screen reader alike.
|
// screen reader alike.
|
||||||
("variant", "ground-darvo-r0"),
|
("variant", "ground-darvo-r0"),
|
||||||
// The fixture plays SCN_03, so the token is that card's TITLE.
|
|
||||||
// Not "SCN_03": a probe that matches an id would go green
|
|
||||||
// against a page showing the id, which is the machine's name for
|
|
||||||
// the board and not the player's (CB-WP-0034's finding).
|
|
||||||
("scenario", "Decision Without Consent"),
|
|
||||||
("problem_markers.*.owner", "P1\u{2019}s alone"),
|
("problem_markers.*.owner", "P1\u{2019}s alone"),
|
||||||
("problem_markers.*.scope", "P2\u{2019}s Bond network"),
|
("problem_markers.*.scope", "P2\u{2019}s Bond network"),
|
||||||
("outcome.total", "total 9"),
|
("outcome.total", "total 9"),
|
||||||
|
|
@ -707,79 +697,6 @@ mod gamelog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// **The page names the Mode by its printed title** (CB-WP-0047).
|
|
||||||
///
|
|
||||||
/// The header read `scoring CommonProblem` — the Rust variant's
|
|
||||||
/// name, where the Mode card is titled *"COMMON PROBLEM, PERSONAL
|
|
||||||
/// EDGE"*. CB-WP-0034 deleted this exact defect from the move
|
|
||||||
/// buttons and it was still standing on the one line that says what
|
|
||||||
/// winning means.
|
|
||||||
#[test]
|
|
||||||
fn every_mode_is_named_and_explained_in_the_editions_words() {
|
|
||||||
use games_ground::ScoringMode as M;
|
|
||||||
for mode in [M::SharedGround, M::CommonProblem, M::BondedCoalitions] {
|
|
||||||
let mut v = crate::testfix::view(Some(PlayerId(0)));
|
|
||||||
v.mode = mode;
|
|
||||||
let text = crate::text_of(&crate::doc::document(
|
|
||||||
&v,
|
|
||||||
&[],
|
|
||||||
"/c",
|
|
||||||
Some(PlayerId(0)),
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
assert!(
|
|
||||||
!text.contains(&format!("{mode:?}")),
|
|
||||||
"{mode:?}: the page shows the Rust variant's name"
|
|
||||||
);
|
|
||||||
let (card, _) = games_ground::edition::modes()
|
|
||||||
.expect("Modes.csv")
|
|
||||||
.into_iter()
|
|
||||||
.find(|(c, _)| {
|
|
||||||
c.id == match mode {
|
|
||||||
M::SharedGround => "MODE_COOP",
|
|
||||||
M::CommonProblem => "MODE_SEMI",
|
|
||||||
M::BondedCoalitions => "MODE_COALITION",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.expect("the mode card");
|
|
||||||
assert!(
|
|
||||||
text.contains(&card.title),
|
|
||||||
"{mode:?}: the Mode card's title {:?} is not on the page",
|
|
||||||
card.title
|
|
||||||
);
|
|
||||||
// **And what it MEANS.** Three modes define winning three
|
|
||||||
// different ways and the page had never stated any of them.
|
|
||||||
let head: String = card.rules_text.chars().take(50).collect();
|
|
||||||
assert!(
|
|
||||||
text.contains(&head),
|
|
||||||
"{mode:?}: nothing on the page says how this game is won"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// **The page names which of the four boards this is** (CB-WP-0047).
|
|
||||||
#[test]
|
|
||||||
fn every_scenario_is_named_with_its_premise() {
|
|
||||||
for s in games_ground::edition::scenarios().expect("Scenarios.csv") {
|
|
||||||
let mut v = crate::testfix::view(Some(PlayerId(0)));
|
|
||||||
v.scenario = s.id.clone();
|
|
||||||
let text = crate::text_of(&crate::doc::document(
|
|
||||||
&v,
|
|
||||||
&[],
|
|
||||||
"/c",
|
|
||||||
Some(PlayerId(0)),
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
assert!(text.contains(&s.title), "{}: the board is unnamed", s.id);
|
|
||||||
let head: String = s.premise.chars().take(50).collect();
|
|
||||||
assert!(
|
|
||||||
text.contains(&head),
|
|
||||||
"{}: the page does not say what the table is arguing about",
|
|
||||||
s.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// **A scope says what it does** (CB-WP-0046).
|
/// **A scope says what it does** (CB-WP-0046).
|
||||||
///
|
///
|
||||||
/// CB-WP-0045 shipped the placement and recorded the gap with the
|
/// CB-WP-0045 shipped the placement and recorded the gap with the
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,6 @@ pub fn view(viewer: Option<PlayerId>) -> GroundView {
|
||||||
lead: p2,
|
lead: p2,
|
||||||
step: RoundStep::Select,
|
step: RoundStep::Select,
|
||||||
mode: ScoringMode::BondedCoalitions,
|
mode: ScoringMode::BondedCoalitions,
|
||||||
scenario: "SCN_03".into(),
|
|
||||||
players,
|
players,
|
||||||
relations: BTreeMap::from([
|
relations: BTreeMap::from([
|
||||||
(Pair::new(p1, p2), Relation::Rivalry),
|
(Pair::new(p1, p2), Relation::Rivalry),
|
||||||
|
|
|
||||||
|
|
@ -1,252 +1,98 @@
|
||||||
# GROUND edition catalog — schema 2: composable modules
|
# Selectable GROUND edition / rules packages.
|
||||||
# Docs: CATALOG.md
|
# Schema docs: CATALOG.md
|
||||||
# clay-borg: select baseline + 0..N modules (≤1 per axis), or a named profile.
|
# clay-borg: pin by variant_id → path (+ optional git_pin).
|
||||||
|
|
||||||
schema_version: 2
|
schema_version: 1
|
||||||
updated: "2026-08-08"
|
updated: "2026-08-08"
|
||||||
default_baseline: ground-darvo-r0
|
default_variant: ground-darvo-r0
|
||||||
default_profile: baseline
|
# H2 added; H1 remains selectable as reject-as-baseline control.
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
packages:
|
||||||
# Axes — orthogonal dimensions. At most one non-default module per axis.
|
- variant_id: ground-darvo-r0
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
axes:
|
|
||||||
- id: problem_stress
|
|
||||||
title: Problem → Stress routing
|
|
||||||
default_module: problem_stress.none
|
|
||||||
summary: >
|
|
||||||
Whether and how unclaimed Problems raise Stress at Round End.
|
|
||||||
|
|
||||||
- id: attack_relief
|
|
||||||
title: ATTACK self-soothing
|
|
||||||
default_module: attack_relief.none
|
|
||||||
summary: >
|
|
||||||
Whether resolving ATTACK can reduce the attacker's Stress.
|
|
||||||
|
|
||||||
- id: end_condition
|
|
||||||
title: How the game ends
|
|
||||||
default_module: end_condition.fixed_rounds_5
|
|
||||||
summary: >
|
|
||||||
Fixed round clock vs clear-board / collapse / hybrid ends.
|
|
||||||
|
|
||||||
- id: problem_deal
|
|
||||||
title: How Problems enter play
|
|
||||||
default_module: problem_deal.fixed_setup
|
|
||||||
summary: >
|
|
||||||
Fixed setup deal only vs mid-game influx (pressure deck, etc.).
|
|
||||||
|
|
||||||
# Future axes (not yet registered): setup_difficulty, sequence_pacing,
|
|
||||||
# ground_as_sequence, status_stress, competence_track.
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Baseline content package (CSV edition data)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
baselines:
|
|
||||||
- baseline_id: ground-darvo-r0
|
|
||||||
path: editions/ground-darvo-r0
|
path: editions/ground-darvo-r0
|
||||||
|
kind: baseline
|
||||||
|
base: null
|
||||||
selectable: true
|
selectable: true
|
||||||
status: baseline
|
status: baseline
|
||||||
dataset_id: GROUND-DARVO-CORE-0.1
|
dataset_id: GROUND-DARVO-CORE-0.1
|
||||||
title: "GROUND DARVO Edition — core r0"
|
title: "GROUND DARVO Edition — core r0"
|
||||||
summary: >
|
summary: >
|
||||||
Print/playtest content. Modes, deal 6/9/12, thresholds 5/7/9.
|
Playtest baseline. Surface + hidden 1..k deal; available 6/9/12;
|
||||||
Default modules on all axes = r0 printed behaviour.
|
thresholds 5/7/9. Stress ledger has no problem pressure; ATTACK
|
||||||
utility_estimate: >
|
does not self-soothe.
|
||||||
Ship-default content. Stress has no problem pressure until a
|
hypothesis_ref: null
|
||||||
problem_stress module is selected.
|
workplan_ref: null
|
||||||
decision: none
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Modules — independent variations (one directory each)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
modules:
|
|
||||||
# --- problem_stress ---
|
|
||||||
- module_id: problem_stress.none
|
|
||||||
axis: problem_stress
|
|
||||||
path: editions/modules/problem_stress/none
|
|
||||||
is_default: true
|
|
||||||
selectable: true
|
|
||||||
status: baseline-default
|
|
||||||
rules_delta: null
|
rules_delta: null
|
||||||
summary: Unclaimed Problems do not raise Stress (r0).
|
changed_files: []
|
||||||
|
utility_estimate: >
|
||||||
|
Ship-default. RPT-0003 + CB-EV-0031: under non-attacking competent
|
||||||
|
policies, peak Stress held never exceeds start 2 — ATTACK is the
|
||||||
|
sole inbound pressure. Keep pinned until a successor is accepted.
|
||||||
decision: none
|
decision: none
|
||||||
|
git_pin: null
|
||||||
- module_id: problem_stress.flat_any_open
|
|
||||||
axis: problem_stress
|
|
||||||
path: editions/modules/problem_stress/flat_any_open
|
|
||||||
is_default: false
|
|
||||||
selectable: true
|
|
||||||
status: measured
|
|
||||||
rules_delta: editions/modules/problem_stress/flat_any_open/rules_delta.yaml
|
|
||||||
legacy_experiment_ids: [h1-problem-stress]
|
|
||||||
measurement_ref: reports/260808-clay-borg-h1-measured.md
|
|
||||||
summary: >
|
|
||||||
+1 Stress to every seat if any Problem unclaimed (former H1-A).
|
|
||||||
utility_estimate: >
|
|
||||||
Reject as sole pressure: greedy 3–4p wins → 0. Keep for A/B control.
|
|
||||||
decision: reject-as-baseline
|
|
||||||
clay_borg_notes: Former H1-A; implement without H1-B unless attack_relief also selected.
|
|
||||||
|
|
||||||
- module_id: problem_stress.scoped
|
|
||||||
axis: problem_stress
|
|
||||||
path: editions/modules/problem_stress/scoped
|
|
||||||
is_default: false
|
|
||||||
selectable: true
|
|
||||||
status: measured
|
|
||||||
rules_delta: editions/modules/problem_stress/scoped/rules_delta.yaml
|
|
||||||
data_overlays: [Problems.csv, Rules_Text.csv]
|
|
||||||
legacy_experiment_ids: [h2-scoped-problem-stress]
|
|
||||||
measurement_ref: reports/260808-clay-borg-h2-measured.md
|
|
||||||
hypothesis_ref: history/260808-h2-scoped-problem-stress.md
|
|
||||||
summary: >
|
|
||||||
+1 Stress per unclaimed Problem to stress_scope (personal/bond/global).
|
|
||||||
utility_estimate: >
|
|
||||||
Scoping works (RPT-0005). Keep; candidate promote with other axes later.
|
|
||||||
decision: keep-as-experiment
|
|
||||||
clay_borg_notes: >
|
clay_borg_notes: >
|
||||||
Prefer path editions/modules/problem_stress/scoped. Use with_variant()/owners.
|
Vendor CSVs as today (Problems, Actions, Solutions, Modes, Tokens).
|
||||||
legacy_experiment_id h2-scoped-problem-stress remains an alias profile.
|
Kernel implements printed r0 rules.
|
||||||
|
|
||||||
# --- attack_relief ---
|
- variant_id: h1-problem-stress
|
||||||
- module_id: attack_relief.none
|
|
||||||
axis: attack_relief
|
|
||||||
path: editions/modules/attack_relief/none
|
|
||||||
is_default: true
|
|
||||||
selectable: true
|
|
||||||
status: baseline-default
|
|
||||||
rules_delta: null
|
|
||||||
summary: ATTACK does not self-soothe (r0).
|
|
||||||
decision: none
|
|
||||||
|
|
||||||
- module_id: attack_relief.self_soothe_ge4
|
|
||||||
axis: attack_relief
|
|
||||||
path: editions/modules/attack_relief/self_soothe_ge4
|
|
||||||
is_default: false
|
|
||||||
selectable: true
|
|
||||||
status: measured-as-combo
|
|
||||||
rules_delta: editions/modules/attack_relief/self_soothe_ge4/rules_delta.yaml
|
|
||||||
legacy_experiment_ids: [h1-problem-stress]
|
|
||||||
measurement_ref: reports/260808-clay-borg-h1-measured.md
|
|
||||||
summary: Uncancelled ATTACK at Stress ≥4 → attacker −1 Stress (former H1-B).
|
|
||||||
utility_estimate: >
|
|
||||||
Alone unmeasured. With flat problem stress, dead for competent play.
|
|
||||||
Re-measure with problem_stress.scoped via profile scoped_plus_attack_soothe.
|
|
||||||
decision: none
|
|
||||||
clay_borg_notes: Independent of problem_stress; compose explicitly.
|
|
||||||
|
|
||||||
# --- end_condition ---
|
|
||||||
- module_id: end_condition.fixed_rounds_5
|
|
||||||
axis: end_condition
|
|
||||||
path: editions/modules/end_condition/fixed_rounds_5
|
|
||||||
is_default: true
|
|
||||||
selectable: true
|
|
||||||
status: baseline-default
|
|
||||||
rules_delta: null
|
|
||||||
summary: Always 5 rounds then threshold scoring (r0).
|
|
||||||
decision: none
|
|
||||||
|
|
||||||
- module_id: end_condition.hybrid_clear_collapse
|
|
||||||
axis: end_condition
|
|
||||||
path: editions/modules/end_condition/hybrid_clear_collapse
|
|
||||||
is_default: false
|
|
||||||
selectable: true
|
|
||||||
status: proposed
|
|
||||||
rules_delta: editions/modules/end_condition/hybrid_clear_collapse/rules_delta.yaml
|
|
||||||
hypothesis_ref: history/260808-deal-end-sequences-design.md
|
|
||||||
summary: >
|
|
||||||
End on board clear, group collapse, or round ceiling (draft rules).
|
|
||||||
utility_estimate: Unimplemented — draft only.
|
|
||||||
decision: none
|
|
||||||
clay_borg_notes: Kernel pending; do not claim measured until implemented.
|
|
||||||
|
|
||||||
# --- problem_deal ---
|
|
||||||
- module_id: problem_deal.fixed_setup
|
|
||||||
axis: problem_deal
|
|
||||||
path: editions/modules/problem_deal/fixed_setup
|
|
||||||
is_default: true
|
|
||||||
selectable: true
|
|
||||||
status: baseline-default
|
|
||||||
rules_delta: null
|
|
||||||
summary: Surface + hidden 1..k at setup only (r0).
|
|
||||||
decision: none
|
|
||||||
|
|
||||||
- module_id: problem_deal.pressure_deck
|
|
||||||
axis: problem_deal
|
|
||||||
path: editions/modules/problem_deal/pressure_deck
|
|
||||||
is_default: false
|
|
||||||
selectable: true
|
|
||||||
status: proposed
|
|
||||||
rules_delta: editions/modules/problem_deal/pressure_deck/rules_delta.yaml
|
|
||||||
hypothesis_ref: history/260808-deal-end-sequences-design.md
|
|
||||||
summary: >
|
|
||||||
Small start set + mid-game draws from Pressure deck (draft).
|
|
||||||
utility_estimate: Unimplemented — draft only.
|
|
||||||
decision: none
|
|
||||||
clay_borg_notes: >
|
|
||||||
Kernel pending. Recommended compose with problem_stress.scoped.
|
|
||||||
v0 draft uses 0-point drawn cards.
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Profiles — named compositions (convenience; not a second rules source)
|
|
||||||
# Selection = baseline + modules list. Defaults fill missing axes.
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
profiles:
|
|
||||||
- profile_id: baseline
|
|
||||||
title: Pure r0
|
|
||||||
modules: []
|
|
||||||
summary: All axis defaults — printed ground-darvo-r0 behaviour.
|
|
||||||
|
|
||||||
- profile_id: h1
|
|
||||||
title: Legacy H1 (flat problem stress + attack soothe)
|
|
||||||
modules:
|
|
||||||
- problem_stress.flat_any_open
|
|
||||||
- attack_relief.self_soothe_ge4
|
|
||||||
legacy_experiment_id: h1-problem-stress
|
|
||||||
summary: Equivalent to old monolithic experiment h1-problem-stress.
|
|
||||||
decision: reject-as-baseline
|
|
||||||
|
|
||||||
- profile_id: h2
|
|
||||||
title: Legacy H2 (scoped problem stress only)
|
|
||||||
modules:
|
|
||||||
- problem_stress.scoped
|
|
||||||
legacy_experiment_id: h2-scoped-problem-stress
|
|
||||||
summary: Equivalent to old monolithic experiment h2-scoped-problem-stress.
|
|
||||||
decision: keep-as-experiment
|
|
||||||
|
|
||||||
- profile_id: scoped_plus_attack_soothe
|
|
||||||
title: Scoped stress + ATTACK self-soothe
|
|
||||||
modules:
|
|
||||||
- problem_stress.scoped
|
|
||||||
- attack_relief.self_soothe_ge4
|
|
||||||
summary: First intentional multi-axis combo after modular catalog.
|
|
||||||
status: unmeasured
|
|
||||||
|
|
||||||
- profile_id: scoped_plus_hybrid_end
|
|
||||||
title: Scoped stress + hybrid end (when end module ships)
|
|
||||||
modules:
|
|
||||||
- problem_stress.scoped
|
|
||||||
- end_condition.hybrid_clear_collapse
|
|
||||||
summary: Requires end_condition.hybrid_clear_collapse kernel support.
|
|
||||||
status: proposed
|
|
||||||
|
|
||||||
- profile_id: scoped_plus_pressure_deck
|
|
||||||
title: Scoped stress + pressure deck (when deal module ships)
|
|
||||||
modules:
|
|
||||||
- problem_stress.scoped
|
|
||||||
- problem_deal.pressure_deck
|
|
||||||
summary: Requires problem_deal.pressure_deck kernel support.
|
|
||||||
status: proposed
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Legacy experiment paths (still on disk; prefer modules + profiles)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
legacy_experiments:
|
|
||||||
- experiment_id: h1-problem-stress
|
|
||||||
path: editions/experiments/h1-problem-stress
|
path: editions/experiments/h1-problem-stress
|
||||||
equivalent_profile: h1
|
kind: experiment
|
||||||
note: Prefer profile h1 or modules problem_stress.flat_any_open + attack_relief.self_soothe_ge4
|
base: ground-darvo-r0
|
||||||
|
selectable: true
|
||||||
|
status: measured
|
||||||
|
dataset_id: GROUND-DARVO-EXP-H1-0.1
|
||||||
|
title: "H1 — problem pressure + high-stress ATTACK self-soothe"
|
||||||
|
summary: >
|
||||||
|
Two deltas only: (A) +1 Stress each player at Round End if any
|
||||||
|
Problem remains unclaimed; (B) uncancelled ACT_ATTACK by a seat
|
||||||
|
at Stress ≥4 gives that seat −1 Stress. Thresholds/deal unchanged.
|
||||||
|
hypothesis_ref: history/260807-attack-darvo-stress-design.md
|
||||||
|
workplan_ref: workplans/GROUND-WP-0006-h1-problem-stress-experiment.md
|
||||||
|
measurement_ref: reports/260808-clay-borg-h1-measured.md
|
||||||
|
rules_delta: editions/experiments/h1-problem-stress/rules_delta.yaml
|
||||||
|
changed_files:
|
||||||
|
- Actions.csv
|
||||||
|
- Rules_Text.csv
|
||||||
|
- metadata.json
|
||||||
|
utility_estimate: >
|
||||||
|
Measured 2026-08-08 (CB-EV-0030/0031; instrument caveat applies).
|
||||||
|
Direction right, magnitude wrong: H1-A is a solve-rate tax that
|
||||||
|
competent seats absorb with GROUND (Stress ~3); H1-B never fires
|
||||||
|
for them. DARVO arms for unregulated seats; group wins collapse
|
||||||
|
to 0 at 3p+ under greedy SHARED GROUND. Do not promote as-is.
|
||||||
|
decision: reject-as-baseline
|
||||||
|
# Package stays selectable for regression compare; not a ship pin.
|
||||||
|
git_pin: null
|
||||||
|
clay_borg_notes: >
|
||||||
|
Kernel implements H1-A/H1-B. Keep for A/B against successors (H2…).
|
||||||
|
|
||||||
- experiment_id: h2-scoped-problem-stress
|
- variant_id: h2-scoped-problem-stress
|
||||||
path: editions/experiments/h2-scoped-problem-stress
|
path: editions/experiments/h2-scoped-problem-stress
|
||||||
equivalent_profile: h2
|
kind: experiment
|
||||||
note: Prefer profile h2 or module problem_stress.scoped
|
base: ground-darvo-r0
|
||||||
|
selectable: true
|
||||||
|
status: experimental
|
||||||
|
dataset_id: GROUND-DARVO-EXP-H2-0.1
|
||||||
|
title: "H2 — scoped problem stress (personal / bond / global)"
|
||||||
|
summary: >
|
||||||
|
Unclaimed Problems apply +1 End Stress only to stress_scope:
|
||||||
|
personal=owner, bond=owner's Bond network, global=all. Surface
|
||||||
|
global; priority 3 is bond (in play at 3p+). Anyone may SOLVE any
|
||||||
|
card. Not stacked on H1; no ATTACK self-soothe.
|
||||||
|
hypothesis_ref: history/260808-h2-scoped-problem-stress.md
|
||||||
|
workplan_ref: workplans/GROUND-WP-0007-h2-scoped-problem-stress.md
|
||||||
|
measurement_ref: null
|
||||||
|
rules_delta: editions/experiments/h2-scoped-problem-stress/rules_delta.yaml
|
||||||
|
changed_files:
|
||||||
|
- Problems.csv
|
||||||
|
- Rules_Text.csv
|
||||||
|
- metadata.json
|
||||||
|
utility_estimate: >
|
||||||
|
Unmeasured. Expected: stress variance up; group wins at 3–4p much
|
||||||
|
better than H1; bond cards create joint SOLVE incentive in networks.
|
||||||
|
decision: none
|
||||||
|
git_pin: null
|
||||||
|
clay_borg_notes: >
|
||||||
|
Implement rules_delta H2-SCOPE, H2-OWN, H2-A, H2-SOLVE on base r0.
|
||||||
|
Do not also apply H1 deltas. Problems.csv adds stress_scope column.
|
||||||
|
Report bond-card SOLVE rates and stress variance vs r0 and h1.
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **variant_id** | `h1-problem-stress` (**legacy** — prefer profile `h1` = `problem_stress.flat_any_open` + `attack_relief.self_soothe_ge4`) |
|
| **variant_id** | `h1-problem-stress` |
|
||||||
| **modules** | flat problem stress + attack self-soothe (see `editions/modules/`) |
|
|
||||||
| **base** | `ground-darvo-r0` |
|
| **base** | `ground-darvo-r0` |
|
||||||
| **status** | measured — reject as baseline (2026-08-08); keep for A/B |
|
| **status** | measured — reject as baseline (2026-08-08); keep for A/B |
|
||||||
| **measurement** | [`../../../reports/260808-clay-borg-h1-measured.md`](../../../reports/260808-clay-borg-h1-measured.md) |
|
| **measurement** | [`../../../reports/260808-clay-borg-h1-measured.md`](../../../reports/260808-clay-borg-h1-measured.md) |
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,9 @@
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **variant_id** | `h2-scoped-problem-stress` (**legacy** — prefer module `problem_stress.scoped` / profile `h2`) |
|
| **variant_id** | `h2-scoped-problem-stress` |
|
||||||
| **module** | [`../../modules/problem_stress/scoped/`](../../modules/problem_stress/scoped/) |
|
|
||||||
| **base** | `ground-darvo-r0` (not stacked on H1) |
|
| **base** | `ground-darvo-r0` (not stacked on H1) |
|
||||||
| **status** | measured — keep-as-experiment |
|
| **status** | experimental |
|
||||||
| **catalog** | [`../../catalog.yaml`](../../catalog.yaml) |
|
| **catalog** | [`../../catalog.yaml`](../../catalog.yaml) |
|
||||||
| **design note** | [`../../../history/260808-h2-scoped-problem-stress.md`](../../../history/260808-h2-scoped-problem-stress.md) |
|
| **design note** | [`../../../history/260808-h2-scoped-problem-stress.md`](../../../history/260808-h2-scoped-problem-stress.md) |
|
||||||
| **workplan** | [`../../../workplans/GROUND-WP-0007-h2-scoped-problem-stress.md`](../../../workplans/GROUND-WP-0007-h2-scoped-problem-stress.md) |
|
| **workplan** | [`../../../workplans/GROUND-WP-0007-h2-scoped-problem-stress.md`](../../../workplans/GROUND-WP-0007-h2-scoped-problem-stress.md) |
|
||||||
|
|
|
||||||
|
|
@ -58,16 +58,16 @@ the adversarial review ([CB-REV-0001](../../reviews/CB-REV-0001-h1.md))
|
||||||
reported it unverified and it was a real gap.
|
reported it unverified and it was a real gap.
|
||||||
|
|
||||||
```
|
```
|
||||||
sha256 cd0c0db9eef5e9f94df6c1b26837c5e800d9eb8c79ee113a537767aa88934c76 ../catalog.yaml
|
sha256 c469f984c3147861815e8fbd67cf1254de73368fad1654f4177f740c0c280499 ../catalog.yaml
|
||||||
sha256 4c81bae21d2ecb70c7424fa17445246a9b551b10e258d448634816c564e28f09 ../experiments/h2-scoped-problem-stress/Problems.csv
|
sha256 4c81bae21d2ecb70c7424fa17445246a9b551b10e258d448634816c564e28f09 ../experiments/h2-scoped-problem-stress/Problems.csv
|
||||||
sha256 8ed8deb7ad142de1bda70dc11add339f742e8c12ac1f02d5f7c7ec8f17f77b1d ../experiments/h2-scoped-problem-stress/Rules_Text.csv
|
sha256 8ed8deb7ad142de1bda70dc11add339f742e8c12ac1f02d5f7c7ec8f17f77b1d ../experiments/h2-scoped-problem-stress/Rules_Text.csv
|
||||||
sha256 b0ffea41820ef35960d8c75ff0a734370a29a738e49fda74851fe5c2daafe53e ../experiments/h2-scoped-problem-stress/VARIANT.md
|
sha256 b2db91edc6798efca1ccf0b048bb0d7cb324e808e01d636ebca813527bcd0db2 ../experiments/h2-scoped-problem-stress/VARIANT.md
|
||||||
sha256 abf994f585fdfa9b2822614a7961e98141cded6a91c446916ff7fd48642de0a7 ../experiments/h2-scoped-problem-stress/metadata.json
|
sha256 abf994f585fdfa9b2822614a7961e98141cded6a91c446916ff7fd48642de0a7 ../experiments/h2-scoped-problem-stress/metadata.json
|
||||||
sha256 8dc569b2ae62f88f7f64e282bc6bba3dbcc3ffcad276591baafbdd24cb6c16b7 ../experiments/h2-scoped-problem-stress/rules_delta.yaml
|
sha256 8dc569b2ae62f88f7f64e282bc6bba3dbcc3ffcad276591baafbdd24cb6c16b7 ../experiments/h2-scoped-problem-stress/rules_delta.yaml
|
||||||
sha256 f58e81f84ea2b0d16e39932261eb3f3d9890345cdf37ad6f0b3abc00636840be ../experiments/h1-problem-stress/rules_delta.yaml
|
sha256 f58e81f84ea2b0d16e39932261eb3f3d9890345cdf37ad6f0b3abc00636840be ../experiments/h1-problem-stress/rules_delta.yaml
|
||||||
sha256 7b1cc0149122b855e827bc930576ed165bf7dd8d62707e845a9e514ce3521f8e ../experiments/h1-problem-stress/Actions.csv
|
sha256 7b1cc0149122b855e827bc930576ed165bf7dd8d62707e845a9e514ce3521f8e ../experiments/h1-problem-stress/Actions.csv
|
||||||
sha256 62785f5e7e245c60171624d15de2f40187a44fec54f93c7d9705cf52584b1078 ../experiments/h1-problem-stress/Rules_Text.csv
|
sha256 62785f5e7e245c60171624d15de2f40187a44fec54f93c7d9705cf52584b1078 ../experiments/h1-problem-stress/Rules_Text.csv
|
||||||
sha256 49897a68056643a8cfccff32c9e4a9811018b4e4689b90a85124a487f2318369 ../experiments/h1-problem-stress/VARIANT.md
|
sha256 b2714210c142f1d6d5bed8f9a795019e51829c627f9599ac0ebfa04ef60a37a5 ../experiments/h1-problem-stress/VARIANT.md
|
||||||
sha256 443199db94601cc889557e5e86823f374dbfdf875962fc84f95c01865605101c ../experiments/h1-problem-stress/metadata.json
|
sha256 443199db94601cc889557e5e86823f374dbfdf875962fc84f95c01865605101c ../experiments/h1-problem-stress/metadata.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
//! Every board, in every mode, at every seat band (CB-WP-0047).
|
|
||||||
//!
|
|
||||||
//! The engine dealt `SCN_01` and only `SCN_01` for the whole life of this
|
|
||||||
//! repo: `deal` took a scenario id, `setup` passed a literal, and **15 of
|
|
||||||
//! the 20 Problem cards had never been dealt by anything**. The three
|
|
||||||
//! scoring modes were reachable from the driver but no measurement had
|
|
||||||
//! ever compared them.
|
|
||||||
//!
|
|
||||||
//! This plays all of it. It is a **characterisation panel**, not a
|
|
||||||
//! hypothesis test: nothing here predicts an outcome, and the numbers
|
|
||||||
//! exist so that a later change to a deck or a mode is visible as a
|
|
||||||
//! change rather than discovered by a player.
|
|
||||||
//!
|
|
||||||
//! **SCN_01 and SCN_02 are the same board.** Identical suits and values
|
|
||||||
//! at every priority; they differ only in prose. They are both played
|
|
||||||
//! anyway — a panel that dropped one would hide the day they diverge —
|
|
||||||
//! and the column is expected to match, which is itself a control.
|
|
||||||
|
|
||||||
use cb_game_runtime::{ScenarioGame, Setup};
|
|
||||||
use games_ground::bot::{play, GreedyPolicy, Policy};
|
|
||||||
use games_ground::{GroundState, ScoringMode};
|
|
||||||
|
|
||||||
/// Games per cell. Named once so the banner and the assertion cannot
|
|
||||||
/// disagree — a previous panel printed "200 games per cell" over
|
|
||||||
/// 196-game columns (CB-REV-0002 #1).
|
|
||||||
const GAMES: u32 = 100;
|
|
||||||
|
|
||||||
struct Cell {
|
|
||||||
games: u32,
|
|
||||||
/// Games that reached an outcome over the full five rounds. A game
|
|
||||||
/// that RAN is not a game that was PLAYED (CB-REV-0003 #2).
|
|
||||||
played: u32,
|
|
||||||
group_success: u32,
|
|
||||||
/// Seats named a winner. Under SHARED GROUND that is everyone or
|
|
||||||
/// nobody; under the other two it is the point of the mode.
|
|
||||||
winners: u32,
|
|
||||||
/// Claimed value summed over games, so the threshold has something
|
|
||||||
/// to be compared against.
|
|
||||||
total: u32,
|
|
||||||
threshold: u32,
|
|
||||||
setup_fails: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sweep(scenario: &str, mode: ScoringMode, players: u8) -> Cell {
|
|
||||||
let mut c = Cell {
|
|
||||||
games: 0,
|
|
||||||
played: 0,
|
|
||||||
group_success: 0,
|
|
||||||
winners: 0,
|
|
||||||
total: 0,
|
|
||||||
threshold: 0,
|
|
||||||
setup_fails: 0,
|
|
||||||
};
|
|
||||||
let preset = if scenario == "SCN_01" {
|
|
||||||
format!("standard-{players}p")
|
|
||||||
} else {
|
|
||||||
format!("scn-{}-{players}p", scenario.rsplit('_').next().unwrap())
|
|
||||||
};
|
|
||||||
for seed in 0..GAMES as u64 {
|
|
||||||
let Ok(mut st) = GroundState::setup(
|
|
||||||
&Setup {
|
|
||||||
players,
|
|
||||||
preset: preset.clone(),
|
|
||||||
patch: Default::default(),
|
|
||||||
},
|
|
||||||
seed,
|
|
||||||
) else {
|
|
||||||
// Counted, not skipped (CB-REV-0001 #11).
|
|
||||||
c.setup_fails += 1;
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
st.mode = mode;
|
|
||||||
// The board actually dealt, so the panel cannot report a
|
|
||||||
// scenario it did not play.
|
|
||||||
assert_eq!(st.scenario, scenario, "{preset} dealt {}", st.scenario);
|
|
||||||
let mut ps: Vec<Box<dyn Policy>> = (0..players)
|
|
||||||
.map(|_| Box::new(GreedyPolicy) as Box<dyn Policy>)
|
|
||||||
.collect();
|
|
||||||
let g = match play(st, &mut ps) {
|
|
||||||
Ok(g) => g,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!(" !! {scenario} {mode:?} {players}p seed {seed}: {e:?}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
c.games += 1;
|
|
||||||
if g.state.outcome.is_some() && g.rounds == 5 {
|
|
||||||
c.played += 1;
|
|
||||||
}
|
|
||||||
if let Some(o) = &g.state.outcome {
|
|
||||||
if o.group_success {
|
|
||||||
c.group_success += 1;
|
|
||||||
}
|
|
||||||
c.winners += o.winners.len() as u32;
|
|
||||||
c.total += o.total;
|
|
||||||
c.threshold = o.threshold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert_eq!(
|
|
||||||
c.games, GAMES,
|
|
||||||
"{scenario} {mode:?} {players}p: only {} of {GAMES} games ran \
|
|
||||||
({} setups refused) — every number in the cell is over a sample \
|
|
||||||
nobody chose",
|
|
||||||
c.games, c.setup_fails
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
c.played, GAMES,
|
|
||||||
"{scenario} {mode:?} {players}p: {} of {} games reached an outcome \
|
|
||||||
over five rounds",
|
|
||||||
c.played, c.games
|
|
||||||
);
|
|
||||||
c
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
println!("CB-WP-0047 — the four Scenarios, the three Modes\n");
|
|
||||||
println!("Greedy throughout; {GAMES} games per cell; seeds 0..{GAMES}.");
|
|
||||||
println!("`won` is group success; `win/g` is winning SEATS per game,");
|
|
||||||
println!("which is what separates the modes — SHARED GROUND names all");
|
|
||||||
println!("or none, the other two name a subset.\n");
|
|
||||||
|
|
||||||
let scenarios = games_ground::edition::scenarios().expect("Scenarios.csv");
|
|
||||||
// Every mode the kernel has. Adding a fourth without extending the
|
|
||||||
// panel is the omission this list exists to make loud.
|
|
||||||
let modes = [
|
|
||||||
("SHARED GROUND ", ScoringMode::SharedGround),
|
|
||||||
("COMMON PROBLEM ", ScoringMode::CommonProblem),
|
|
||||||
("BONDED COALITIONS ", ScoringMode::BondedCoalitions),
|
|
||||||
];
|
|
||||||
|
|
||||||
for players in [2u8, 4, 6] {
|
|
||||||
println!("{players} players");
|
|
||||||
println!(
|
|
||||||
" {:<32} {:<18} {:>5} {:>7} {:>6} {:>9}",
|
|
||||||
"scenario", "mode", "won", "win/g", "pts", "threshold"
|
|
||||||
);
|
|
||||||
for s in &scenarios {
|
|
||||||
for (label, mode) in &modes {
|
|
||||||
let c = sweep(&s.id, *mode, players);
|
|
||||||
println!(
|
|
||||||
" {:<32} {label} {:>5} {:>7.2} {:>6.1} {:>9}",
|
|
||||||
format!("{} {}", s.id, s.title),
|
|
||||||
c.group_success,
|
|
||||||
f64::from(c.winners) / f64::from(c.games),
|
|
||||||
f64::from(c.total) / f64::from(c.games),
|
|
||||||
c.threshold,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("SCN_01 and SCN_02 are the same board — identical suits and");
|
|
||||||
println!("values at every priority. Their rows are expected to MATCH,");
|
|
||||||
println!("and a divergence means one of the two decks was edited.");
|
|
||||||
}
|
|
||||||
|
|
@ -474,11 +474,6 @@ pub fn darvo_stages() -> Result<Vec<DarvoStageText>, String> {
|
||||||
pub struct ScenarioText {
|
pub struct ScenarioText {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
/// What the game is ABOUT, in the edition's words (CB-WP-0047).
|
|
||||||
///
|
|
||||||
/// Vendored and unread until the four scenarios became selectable:
|
|
||||||
/// with one scenario hardcoded there was nothing to tell apart.
|
|
||||||
pub premise: String,
|
|
||||||
pub surface_problem_id: String,
|
pub surface_problem_id: String,
|
||||||
pub hidden_problem_ids: Vec<String>,
|
pub hidden_problem_ids: Vec<String>,
|
||||||
/// GR-E01 by seat band, as the EDITION states it. The engine has its
|
/// GR-E01 by seat band, as the EDITION states it. The engine has its
|
||||||
|
|
@ -499,7 +494,6 @@ pub fn scenarios() -> Result<Vec<ScenarioText>, String> {
|
||||||
out.push(ScenarioText {
|
out.push(ScenarioText {
|
||||||
id: t.get(row, "scenario_id")?.to_string(),
|
id: t.get(row, "scenario_id")?.to_string(),
|
||||||
title: t.get(row, "title")?.to_string(),
|
title: t.get(row, "title")?.to_string(),
|
||||||
premise: t.get(row, "premise")?.to_string(),
|
|
||||||
surface_problem_id: t.get(row, "surface_problem_id")?.to_string(),
|
surface_problem_id: t.get(row, "surface_problem_id")?.to_string(),
|
||||||
hidden_problem_ids: t
|
hidden_problem_ids: t
|
||||||
.get(row, "hidden_problem_ids")?
|
.get(row, "hidden_problem_ids")?
|
||||||
|
|
@ -1013,91 +1007,6 @@ mod card_text_tests {
|
||||||
///
|
///
|
||||||
/// Same shape as F24's solution deck. `inert`, role `default`: green
|
/// Same shape as F24's solution deck. `inert`, role `default`: green
|
||||||
/// because they agree, red the moment either side moves.
|
/// because they agree, red the moment either side moves.
|
||||||
/// **All four scenarios deal, and they are not the same board**
|
|
||||||
/// (CB-WP-0047).
|
|
||||||
///
|
|
||||||
/// `setup` passed the literal `"SCN_01"`, so 15 of the 20 Problem
|
|
||||||
/// cards had never been dealt by anything. This deals every scenario
|
|
||||||
/// at every seat band and holds the deal to the Scenario card.
|
|
||||||
#[test]
|
|
||||||
fn every_scenario_deals_the_board_its_card_states() {
|
|
||||||
for s in scenarios().expect("Scenarios.csv") {
|
|
||||||
for (players, k) in [(2u8, 2usize), (4, 3), (6, 4)] {
|
|
||||||
let dealt =
|
|
||||||
deal(&s.id, players).unwrap_or_else(|e| panic!("{} at {players}p: {e}", s.id));
|
|
||||||
assert_eq!(
|
|
||||||
dealt.len(),
|
|
||||||
k + 1,
|
|
||||||
"{} at {players}p: Surface + {k} hidden is {} cards",
|
|
||||||
s.id,
|
|
||||||
k + 1
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
dealt.iter().filter(|p| p.surface).count(),
|
|
||||||
1,
|
|
||||||
"{}: exactly one Surface Problem is dealt face up",
|
|
||||||
s.id
|
|
||||||
);
|
|
||||||
// The card states the available total; the deal must be
|
|
||||||
// able to reach the threshold or the board is unwinnable.
|
|
||||||
let available: u32 = dealt.iter().map(|p| u32::from(p.value)).sum();
|
|
||||||
let threshold = match players {
|
|
||||||
0..=2 => s.thresholds.0,
|
|
||||||
3..=4 => s.thresholds.1,
|
|
||||||
_ => s.thresholds.2,
|
|
||||||
};
|
|
||||||
assert!(
|
|
||||||
available >= threshold,
|
|
||||||
"{} at {players}p: {available} points available against a \
|
|
||||||
threshold of {threshold} — the group cannot win in principle",
|
|
||||||
s.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// **Two of the four scenarios are the same board** (CB-WP-0047).
|
|
||||||
///
|
|
||||||
/// SCN_01 and SCN_02 have identical suit-and-value profiles at every
|
|
||||||
/// priority, so they play identically and differ only in prose. Not
|
|
||||||
/// a defect — a designed reskin is a legitimate choice — but it is a
|
|
||||||
/// fact about what "four scenarios" buys, and measuring them as four
|
|
||||||
/// independent boards would be measuring two of them twice.
|
|
||||||
///
|
|
||||||
/// This is a **characterisation** test: it pins what is true today so
|
|
||||||
/// that a change to either deck is a decision rather than a drift.
|
|
||||||
#[test]
|
|
||||||
fn which_scenarios_are_mechanically_distinct() {
|
|
||||||
let profile = |id: &str| -> Vec<(u8, String, u8)> {
|
|
||||||
let mut v: Vec<_> = problems_of(id)
|
|
||||||
.expect(id)
|
|
||||||
.into_iter()
|
|
||||||
.map(|p| (p.priority, format!("{:?}", p.suit), p.value))
|
|
||||||
.collect();
|
|
||||||
v.sort();
|
|
||||||
v
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
profile("SCN_01"),
|
|
||||||
profile("SCN_02"),
|
|
||||||
"SCN_01 and SCN_02 diverged — they were identical boards; \
|
|
||||||
if this is intended, the panel now measures four real boards"
|
|
||||||
);
|
|
||||||
for pair in [
|
|
||||||
("SCN_01", "SCN_03"),
|
|
||||||
("SCN_01", "SCN_04"),
|
|
||||||
("SCN_03", "SCN_04"),
|
|
||||||
] {
|
|
||||||
assert_ne!(
|
|
||||||
profile(pair.0),
|
|
||||||
profile(pair.1),
|
|
||||||
"{} and {} became the same board",
|
|
||||||
pair.0,
|
|
||||||
pair.1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_engine_agrees_with_the_editions_own_numbers() {
|
fn the_engine_agrees_with_the_editions_own_numbers() {
|
||||||
for s in scenarios().expect("Scenarios.csv") {
|
for s in scenarios().expect("Scenarios.csv") {
|
||||||
|
|
|
||||||
|
|
@ -200,21 +200,6 @@ pub struct GroundState {
|
||||||
/// variants existed still loads, as baseline — which is what it was.
|
/// variants existed still loads, as baseline — which is what it was.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub variant: Variant,
|
pub variant: Variant,
|
||||||
/// Which of the edition's four Scenarios is on the table
|
|
||||||
/// (CB-WP-0047).
|
|
||||||
///
|
|
||||||
/// **The engine dealt `SCN_01` and only `SCN_01`** for the whole life
|
|
||||||
/// of this repo — the other three decks were vendored, gated, and
|
|
||||||
/// never played. Fifteen of the twenty Problem cards had never
|
|
||||||
/// reached a table.
|
|
||||||
///
|
|
||||||
/// In the state for the same reason `variant` is: the threshold and
|
|
||||||
/// the whole board follow from it, so a recording that did not carry
|
|
||||||
/// it could not be replayed. `#[serde(default)]` returns `SCN_01`,
|
|
||||||
/// which is what every recording written before this field existed
|
|
||||||
/// actually played.
|
|
||||||
#[serde(default = "default_scenario")]
|
|
||||||
pub scenario: String,
|
|
||||||
/// GR-R09: set once the game has ended and scoring has run.
|
/// GR-R09: set once the game has ended and scoring has run.
|
||||||
pub outcome: Option<Outcome>,
|
pub outcome: Option<Outcome>,
|
||||||
/// GR-S04/U4: retained so a deck reshuffle stays a pure function of
|
/// GR-S04/U4: retained so a deck reshuffle stays a pure function of
|
||||||
|
|
@ -222,53 +207,6 @@ pub struct GroundState {
|
||||||
pub seed: u64,
|
pub seed: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GR-E01 by seat band, as the engine used to hardcode it.
|
|
||||||
///
|
|
||||||
/// **Kept as the fallback, and only as the fallback.** All four
|
|
||||||
/// scenarios print 5/7/9, so this and the edition agree today — which is
|
|
||||||
/// exactly why a test comparing the two numbers proves nothing. See
|
|
||||||
/// `threshold_from`.
|
|
||||||
fn seat_band_threshold(seats: usize) -> u32 {
|
|
||||||
match seats {
|
|
||||||
0..=2 => 5,
|
|
||||||
3..=4 => 7,
|
|
||||||
_ => 9,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The threshold a Scenario card states, or the seat band if the edition
|
|
||||||
/// does not have that card (CB-WP-0047).
|
|
||||||
///
|
|
||||||
/// **Split out so it can be tested against a card that disagrees.** The
|
|
||||||
/// first version read the edition inline, and mutating it back to the
|
|
||||||
/// hardcoded bands left every test green — because all four scenarios
|
|
||||||
/// print 5/7/9 and the two paths are observationally identical on every
|
|
||||||
/// input the edition can supply. A control that cannot separate the
|
|
||||||
/// thing it is about from its fallback is not a control; taking a list
|
|
||||||
/// as an argument lets one be written.
|
|
||||||
#[cfg(feature = "scenarios")]
|
|
||||||
fn threshold_from(list: &[crate::edition::ScenarioText], scenario: &str, seats: usize) -> u32 {
|
|
||||||
match list.iter().find(|s| s.id == scenario) {
|
|
||||||
Some(s) => {
|
|
||||||
let (two, three_four, five_six) = s.thresholds;
|
|
||||||
match seats {
|
|
||||||
0..=2 => two,
|
|
||||||
3..=4 => three_four,
|
|
||||||
_ => five_six,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => seat_band_threshold(seats),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The scenario every recording written before CB-WP-0047 played.
|
|
||||||
///
|
|
||||||
/// **Not "the first scenario" — the one that was actually dealt.** The
|
|
||||||
/// distinction matters if the edition ever reorders `Scenarios.csv`.
|
|
||||||
fn default_scenario() -> String {
|
|
||||||
"SCN_01".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// GR-E02..E04: the three scoring modes.
|
/// GR-E02..E04: the three scoring modes.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum ScoringMode {
|
pub enum ScoringMode {
|
||||||
|
|
@ -1793,31 +1731,13 @@ impl GroundState {
|
||||||
events
|
events
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GR-E01: the Scenario threshold, **off the Scenario card**.
|
/// GR-E01: the Scenario threshold by player count (dataset 0.1).
|
||||||
///
|
|
||||||
/// This was `match seats { 0..=2 => 5, 3..=4 => 7, _ => 9 }` — the
|
|
||||||
/// engine's own copy of a number four cards already print, which is
|
|
||||||
/// exactly what F25 was raised about. It was right only because all
|
|
||||||
/// four scenarios happen to agree, and a fifth scenario with a
|
|
||||||
/// different threshold would have been scored against the old one
|
|
||||||
/// with nothing to say so.
|
|
||||||
///
|
|
||||||
/// The bands survive as the fallback for a state whose scenario the
|
|
||||||
/// edition cannot supply. **That path is unreachable through
|
|
||||||
/// `setup`**, which refuses an unknown scenario before dealing, and
|
|
||||||
/// a test holds it unreachable — a fallback nothing can reach is the
|
|
||||||
/// only kind that cannot silently answer for the real one.
|
|
||||||
#[cfg(feature = "scenarios")]
|
|
||||||
fn threshold(&self) -> u32 {
|
fn threshold(&self) -> u32 {
|
||||||
let list = crate::edition::scenarios().unwrap_or_default();
|
match self.players.len() {
|
||||||
threshold_from(&list, &self.scenario, self.players.len())
|
0..=2 => 5,
|
||||||
}
|
3..=4 => 7,
|
||||||
|
_ => 9,
|
||||||
/// Without the `scenarios` feature there is no edition to read, so
|
}
|
||||||
/// the seat bands are the whole answer rather than a fallback.
|
|
||||||
#[cfg(not(feature = "scenarios"))]
|
|
||||||
fn threshold(&self) -> u32 {
|
|
||||||
seat_band_threshold(self.players.len())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GR-E01..E04: final scoring for the configured mode.
|
/// GR-E01..E04: final scoring for the configured mode.
|
||||||
|
|
@ -2135,61 +2055,6 @@ impl GroundState {
|
||||||
// GR-S04's deck now lives in `edition::solution_deck` (ADR-0011),
|
// GR-S04's deck now lives in `edition::solution_deck` (ADR-0011),
|
||||||
// beside the Problem data it is dealt against.
|
// beside the Problem data it is dealt against.
|
||||||
|
|
||||||
/// Which Scenario a `Setup` preset names, and for how many seats
|
|
||||||
/// (CB-WP-0047).
|
|
||||||
///
|
|
||||||
/// | preset | scenario |
|
|
||||||
/// |---|---|
|
|
||||||
/// | `standard-4p` | `SCN_01` |
|
|
||||||
/// | `scn-03-4p` | `SCN_03` |
|
|
||||||
///
|
|
||||||
/// **`standard-Np` keeps meaning exactly what it meant**, which is not a
|
|
||||||
/// convenience: twenty-six recorded scenarios name it, and a grammar
|
|
||||||
/// that redefined it would have moved every one of their boards while
|
|
||||||
/// their hashes still claimed to pin them (ADR-0019's discipline, and
|
|
||||||
/// the reason `serde(default)` on the field returns `SCN_01`).
|
|
||||||
///
|
|
||||||
/// The seat count is checked here rather than after the deal, because
|
|
||||||
/// `deal` refuses an unknown scenario and a preset naming the wrong seat
|
|
||||||
/// count would otherwise be reported as a scenario problem.
|
|
||||||
#[cfg(feature = "scenarios")]
|
|
||||||
fn parse_preset(preset: &str, seats: u8) -> Result<String, String> {
|
|
||||||
let suffix = format!("-{seats}p");
|
|
||||||
let Some(head) = preset.strip_suffix(&suffix) else {
|
|
||||||
return Err(format!(
|
|
||||||
"preset {preset:?} does not match {seats} players \
|
|
||||||
(expected {:?} or e.g. {:?})",
|
|
||||||
format!("standard{suffix}"),
|
|
||||||
format!("scn-02{suffix}"),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
let id = match head {
|
|
||||||
"standard" => default_scenario(),
|
|
||||||
other => {
|
|
||||||
let n = other.strip_prefix("scn-").ok_or_else(|| {
|
|
||||||
format!("preset {preset:?}: expected \"standard\" or \"scn-0N\", got {other:?}")
|
|
||||||
})?;
|
|
||||||
format!("SCN_{n}")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// **Checked against the edition, not against a pattern.** `SCN_09`
|
|
||||||
// matches the shape and is not a scenario; dealing it would fail
|
|
||||||
// later with a message about Problems rather than about the preset.
|
|
||||||
let known = crate::edition::scenarios()?;
|
|
||||||
if !known.iter().any(|s| s.id == id) {
|
|
||||||
return Err(format!(
|
|
||||||
"preset {preset:?} names {id}, which the edition does not have \
|
|
||||||
(it has {})",
|
|
||||||
known
|
|
||||||
.iter()
|
|
||||||
.map(|s| s.id.as_str())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "scenarios")]
|
#[cfg(feature = "scenarios")]
|
||||||
impl ScenarioGame for GroundState {
|
impl ScenarioGame for GroundState {
|
||||||
/// GR-S01..S04. The `standard-Np` presets differ only in seat count;
|
/// GR-S01..S04. The `standard-Np` presets differ only in seat count;
|
||||||
|
|
@ -2198,13 +2063,17 @@ impl ScenarioGame for GroundState {
|
||||||
/// modelled.
|
/// modelled.
|
||||||
fn setup(setup: &Setup, seed: u64) -> Result<Self, String> {
|
fn setup(setup: &Setup, seed: u64) -> Result<Self, String> {
|
||||||
let seats = setup.players;
|
let seats = setup.players;
|
||||||
let scenario = parse_preset(&setup.preset, seats)?;
|
let expected = format!("standard-{seats}p");
|
||||||
|
if setup.preset != expected {
|
||||||
|
return Err(format!(
|
||||||
|
"preset {:?} does not match {seats} players (expected {expected:?})",
|
||||||
|
setup.preset
|
||||||
|
));
|
||||||
|
}
|
||||||
// GR-S01 as ruled 2026-08-04: Surface always, plus hidden
|
// GR-S01 as ruled 2026-08-04: Surface always, plus hidden
|
||||||
// priorities 1..=k. The values and suits come from the edition
|
// priorities 1..=k. The values and suits come from the edition
|
||||||
// (ADR-0011); the engine used to invent both. **And now the
|
// (ADR-0011); the engine used to invent both.
|
||||||
// scenario does too** — this argument was the literal `"SCN_01"`
|
let dealt = crate::edition::deal("SCN_01", seats)?;
|
||||||
// for the whole life of the repo (CB-WP-0047).
|
|
||||||
let dealt = crate::edition::deal(&scenario, seats)?;
|
|
||||||
let mut rng = ChaChaRng::from_seed(Seed(seed));
|
let mut rng = ChaChaRng::from_seed(Seed(seed));
|
||||||
|
|
||||||
// GR-S04: shuffle first, then deal, so the deal is seed-derived.
|
// GR-S04: shuffle first, then deal, so the deal is seed-derived.
|
||||||
|
|
@ -2270,7 +2139,6 @@ impl ScenarioGame for GroundState {
|
||||||
support_responses: BTreeMap::new(),
|
support_responses: BTreeMap::new(),
|
||||||
darvo_targets: BTreeMap::new(),
|
darvo_targets: BTreeMap::new(),
|
||||||
mode: ScoringMode::SharedGround,
|
mode: ScoringMode::SharedGround,
|
||||||
scenario,
|
|
||||||
// Baseline. The driver overwrites this after setup and
|
// Baseline. The driver overwrites this after setup and
|
||||||
// before the hash is taken, which is the route `mode` uses
|
// before the hash is taken, which is the route `mode` uses
|
||||||
// (`table.rs`) — so a recorded session replays under the
|
// (`table.rs`) — so a recorded session replays under the
|
||||||
|
|
@ -3475,7 +3343,6 @@ mod tests {
|
||||||
darvo_targets: BTreeMap::new(),
|
darvo_targets: BTreeMap::new(),
|
||||||
mode: ScoringMode::SharedGround,
|
mode: ScoringMode::SharedGround,
|
||||||
variant: Variant::Baseline,
|
variant: Variant::Baseline,
|
||||||
scenario: default_scenario(),
|
|
||||||
outcome: None,
|
outcome: None,
|
||||||
seed: 0,
|
seed: 0,
|
||||||
}
|
}
|
||||||
|
|
@ -3513,204 +3380,6 @@ mod tests {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// **The mastery rating counts cards; the Mode card counts points**
|
|
||||||
/// (F28, CB-WP-0047).
|
|
||||||
///
|
|
||||||
/// MODE_COOP reads: *"All claimed Problem cards form one shared
|
|
||||||
/// score. … For a mastery rating, subtract 1 for each Blame token
|
|
||||||
/// still in play and 1 for each Denied Problem."*
|
|
||||||
///
|
|
||||||
/// The shared score is the claimed **value** — `total`, which is what
|
|
||||||
/// the threshold is compared against two lines earlier. `mastery`
|
|
||||||
/// subtracts the same penalties from the claimed **count**. Both
|
|
||||||
/// readings fit the sentence; they disagree on every game where a
|
|
||||||
/// 3-point Problem is claimed, which is most of them.
|
|
||||||
///
|
|
||||||
/// **A characterisation test, not a correction.** Scoring is
|
|
||||||
/// `ground-game`'s to rule on (ADR-0015). This pins what the engine
|
|
||||||
/// does today and shows the size of the disagreement, so the ruling
|
|
||||||
/// has a number in front of it.
|
|
||||||
#[test]
|
|
||||||
fn the_mastery_rating_and_the_shared_score_count_different_things() {
|
|
||||||
let mut s = setup_3p(11);
|
|
||||||
s.mode = ScoringMode::SharedGround;
|
|
||||||
// Two claimed Problems worth 2 and 3 — five points, two cards.
|
|
||||||
let seats: Vec<PlayerId> = s.players.keys().copied().collect();
|
|
||||||
s.problems.insert(
|
|
||||||
1,
|
|
||||||
ProblemState {
|
|
||||||
suit: Suit::Repair,
|
|
||||||
value: 2,
|
|
||||||
face_up: true,
|
|
||||||
denied: false,
|
|
||||||
claimed_by: Some(seats[0]),
|
|
||||||
protected_this_round: false,
|
|
||||||
owner: None,
|
|
||||||
scope: None,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
s.problems.insert(
|
|
||||||
2,
|
|
||||||
ProblemState {
|
|
||||||
suit: Suit::Change,
|
|
||||||
value: 3,
|
|
||||||
face_up: true,
|
|
||||||
denied: false,
|
|
||||||
claimed_by: Some(seats[1]),
|
|
||||||
protected_this_round: false,
|
|
||||||
owner: None,
|
|
||||||
scope: None,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let o = s.score();
|
|
||||||
assert_eq!(o.total, 5, "the shared score is claimed VALUE");
|
|
||||||
assert_eq!(
|
|
||||||
o.mastery,
|
|
||||||
Some(2),
|
|
||||||
"mastery is the claimed COUNT, with no penalties in play"
|
|
||||||
);
|
|
||||||
// The disagreement, stated as a number rather than as a worry.
|
|
||||||
assert_ne!(
|
|
||||||
i32::try_from(o.total).unwrap(),
|
|
||||||
o.mastery.unwrap(),
|
|
||||||
"the two readings agree on this board, so F28 has no bite here \
|
|
||||||
and the example needs replacing"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// **Every scenario is reachable through `setup`** (CB-WP-0047).
|
|
||||||
///
|
|
||||||
/// `setup` passed the literal `"SCN_01"`. `deal` had taken a
|
|
||||||
/// scenario id since the day it was written and no caller ever
|
|
||||||
/// passed a different one — the parameter was the whole seam and it
|
|
||||||
/// sat unused, which is why three decks went unplayed without any
|
|
||||||
/// gate noticing.
|
|
||||||
#[test]
|
|
||||||
fn every_scenario_can_be_set_up_and_carries_its_own_board() {
|
|
||||||
use cb_game_runtime::{ScenarioGame, Setup};
|
|
||||||
let mut boards = std::collections::BTreeSet::new();
|
|
||||||
for s in crate::edition::scenarios().expect("Scenarios.csv") {
|
|
||||||
let preset = if s.id == "SCN_01" {
|
|
||||||
"standard-4p".to_string()
|
|
||||||
} else {
|
|
||||||
format!("scn-{}-4p", s.id.rsplit('_').next().unwrap())
|
|
||||||
};
|
|
||||||
let state = GroundState::setup(
|
|
||||||
&Setup {
|
|
||||||
players: 4,
|
|
||||||
preset: preset.clone(),
|
|
||||||
patch: Default::default(),
|
|
||||||
},
|
|
||||||
7,
|
|
||||||
)
|
|
||||||
.unwrap_or_else(|e| panic!("{preset}: {e}"));
|
|
||||||
assert_eq!(state.scenario, s.id, "{preset} dealt the wrong scenario");
|
|
||||||
boards.insert(
|
|
||||||
state
|
|
||||||
.problems
|
|
||||||
.values()
|
|
||||||
.map(|p| (format!("{:?}", p.suit), p.value))
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// SCN_01 and SCN_02 are the same board by design, so four
|
|
||||||
// scenarios yield THREE distinct boards. Asserting 4 here would
|
|
||||||
// be asserting a fact about the edition that is not true.
|
|
||||||
assert_eq!(
|
|
||||||
boards.len(),
|
|
||||||
3,
|
|
||||||
"the four scenarios yield {} distinct boards; SCN_01 and SCN_02 \
|
|
||||||
were identical and the other two differ",
|
|
||||||
boards.len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The preset grammar, including what it must keep meaning.
|
|
||||||
#[test]
|
|
||||||
fn the_preset_names_a_scenario_and_a_seat_count() {
|
|
||||||
assert_eq!(parse_preset("standard-4p", 4).unwrap(), "SCN_01");
|
|
||||||
assert_eq!(parse_preset("scn-03-6p", 6).unwrap(), "SCN_03");
|
|
||||||
// Twenty-six recordings name `standard-Np`; if this ever stopped
|
|
||||||
// meaning SCN_01 every one of them would replay a different board
|
|
||||||
// while its hash still claimed to pin the old one.
|
|
||||||
assert_eq!(parse_preset("standard-2p", 2).unwrap(), "SCN_01");
|
|
||||||
// A preset that looks like an id but names no card is refused
|
|
||||||
// HERE, so the error is about the preset and not about Problems.
|
|
||||||
assert!(parse_preset("scn-09-4p", 4).is_err());
|
|
||||||
assert!(parse_preset("standard-4p", 3).is_err());
|
|
||||||
assert!(parse_preset("nonsense-4p", 4).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// **The threshold comes off the Scenario card** (CB-WP-0047, F25).
|
|
||||||
#[test]
|
|
||||||
fn the_threshold_is_the_editions_and_the_fallback_is_unreachable() {
|
|
||||||
use cb_game_runtime::{ScenarioGame, Setup};
|
|
||||||
for s in crate::edition::scenarios().expect("Scenarios.csv") {
|
|
||||||
for (players, want) in [
|
|
||||||
(2u8, s.thresholds.0),
|
|
||||||
(4, s.thresholds.1),
|
|
||||||
(6, s.thresholds.2),
|
|
||||||
] {
|
|
||||||
let preset = if s.id == "SCN_01" {
|
|
||||||
format!("standard-{players}p")
|
|
||||||
} else {
|
|
||||||
format!("scn-{}-{players}p", s.id.rsplit('_').next().unwrap())
|
|
||||||
};
|
|
||||||
let state = GroundState::setup(
|
|
||||||
&Setup {
|
|
||||||
players,
|
|
||||||
preset,
|
|
||||||
patch: Default::default(),
|
|
||||||
},
|
|
||||||
3,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
state.threshold(),
|
|
||||||
want,
|
|
||||||
"{} at {players}p: the engine scored against its own number",
|
|
||||||
s.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// **The real control.** Every shipped scenario prints 5/7/9, so
|
|
||||||
// the loop above passes whether the number came off the card or
|
|
||||||
// off the old hardcoded bands — verified by mutation: reverting
|
|
||||||
// to the bands left it green. This asks a card that DISAGREES.
|
|
||||||
let odd = crate::edition::ScenarioText {
|
|
||||||
id: "SCN_XX".into(),
|
|
||||||
title: "a card that disagrees".into(),
|
|
||||||
premise: String::new(),
|
|
||||||
surface_problem_id: String::new(),
|
|
||||||
hidden_problem_ids: vec![],
|
|
||||||
thresholds: (4, 6, 8),
|
|
||||||
starting_stress: String::new(),
|
|
||||||
round_track: String::new(),
|
|
||||||
};
|
|
||||||
for (seats, want) in [(2usize, 4), (4, 6), (6, 8)] {
|
|
||||||
assert_eq!(
|
|
||||||
threshold_from(std::slice::from_ref(&odd), "SCN_XX", seats),
|
|
||||||
want,
|
|
||||||
"the threshold did not come off the Scenario card"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// And the fallback is what answers for a card the edition lacks.
|
|
||||||
assert_eq!(threshold_from(&[], "SCN_XX", 4), 7);
|
|
||||||
|
|
||||||
// That fallback is unreachable through `setup`, which refuses an
|
|
||||||
// unknown scenario before dealing. A fallback nothing can reach
|
|
||||||
// is the only kind that cannot silently answer for the real one.
|
|
||||||
assert!(GroundState::setup(
|
|
||||||
&Setup {
|
|
||||||
players: 4,
|
|
||||||
preset: "scn-09-4p".into(),
|
|
||||||
patch: Default::default(),
|
|
||||||
},
|
|
||||||
3
|
|
||||||
)
|
|
||||||
.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// GR-S02/S04: every seat starts at Stress 2 with two dealt cards,
|
/// GR-S02/S04: every seat starts at Stress 2 with two dealt cards,
|
||||||
/// and the deck loses exactly what was dealt.
|
/// and the deck loses exactly what was dealt.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -48,14 +48,6 @@ pub struct GroundView {
|
||||||
/// change as "no changes" after playing the default.
|
/// change as "no changes" after playing the default.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub variant: crate::Variant,
|
pub variant: crate::Variant,
|
||||||
/// Which Scenario is on the table (CB-WP-0047).
|
|
||||||
///
|
|
||||||
/// The player was told the premise by nothing: the same four boards
|
|
||||||
/// would have arrived unannounced. Same reasoning as `variant` —
|
|
||||||
/// CB-WP-0044 shipped a rules change the page could not name and the
|
|
||||||
/// maintainer reported it as "no changes".
|
|
||||||
#[serde(default = "crate::default_scenario")]
|
|
||||||
pub scenario: String,
|
|
||||||
pub players: BTreeMap<PlayerId, PlayerView>,
|
pub players: BTreeMap<PlayerId, PlayerView>,
|
||||||
pub relations: BTreeMap<Pair, Relation>,
|
pub relations: BTreeMap<Pair, Relation>,
|
||||||
pub problems: BTreeMap<u32, ProblemView>,
|
pub problems: BTreeMap<u32, ProblemView>,
|
||||||
|
|
@ -186,7 +178,6 @@ impl Project for GroundState {
|
||||||
step: self.step,
|
step: self.step,
|
||||||
mode: self.mode,
|
mode: self.mode,
|
||||||
variant: self.variant,
|
variant: self.variant,
|
||||||
scenario: self.scenario.clone(),
|
|
||||||
players: self
|
players: self
|
||||||
.players
|
.players
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
||||||
|
|
@ -54,36 +54,9 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by
|
||||||
| F23 | inconsistent | applied | decisions/ADR-0017-chaos-window-2-verdict.md | counterexample | 2026-08-07 | clay-borg |
|
| F23 | inconsistent | applied | decisions/ADR-0017-chaos-window-2-verdict.md | counterexample | 2026-08-07 | clay-borg |
|
||||||
| F22 | underdetermined | withdrawn | games_ground::edition::supply_tests::play_never_exceeds_the_components_the_box_holds | counterexample | 2026-08-07 | clay-borg |
|
| F22 | underdetermined | withdrawn | games_ground::edition::supply_tests::play_never_exceeds_the_components_the_box_holds | counterexample | 2026-08-07 | clay-borg |
|
||||||
| F26 | inert | raised | `crates/cb-render-html/src/lib.rs::a_scoped_table_says_what_a_scope_does` | counterexample | 2026-08-08 | ground-game |
|
| F26 | inert | raised | `crates/cb-render-html/src/lib.rs::a_scoped_table_says_what_a_scope_does` | counterexample | 2026-08-08 | ground-game |
|
||||||
| F27 | unplayed | raised | `games/ground/examples/scenario-panel.rs` | counterexample | 2026-08-08 | clay-borg |
|
|
||||||
| F28 | underdetermined | raised | `games_ground::tests::the_mastery_rating_and_the_shared_score_count_different_things` | counterexample | 2026-08-08 | ground-game |
|
|
||||||
|
|
||||||
<!-- design-register:end -->
|
<!-- design-register:end -->
|
||||||
|
|
||||||
- **F27 — the two competitive modes are scoring lenses over cooperative
|
|
||||||
play.** `scenario-panel` finds group success *exactly* equal across
|
|
||||||
SHARED GROUND, COMMON PROBLEM and BONDED COALITIONS in all 36 cells.
|
|
||||||
Correct arithmetic: `GreedyPolicy` maximises the group outcome and
|
|
||||||
never reads `state.mode`, so the same games are played and only the
|
|
||||||
winner set is carved differently. **Whether a mode changes how GROUND
|
|
||||||
is played is therefore untested**, and cannot be tested by any panel
|
|
||||||
built from the current policies — it needs one that plays for personal
|
|
||||||
score. `unplayed` rather than `inert`: the modes score correctly, they
|
|
||||||
have simply never faced a seat that wanted to win alone.
|
|
||||||
|
|
||||||
- **F28 — SHARED GROUND's mastery rating counts cards where the mode
|
|
||||||
card counts points.** *"All claimed Problem cards form one shared score.
|
|
||||||
… For a mastery rating, subtract 1 for each Blame token still in play
|
|
||||||
and 1 for each Denied Problem."* The shared score is claimed **value**
|
|
||||||
(`total`); `mastery` subtracts the same penalties from the claimed
|
|
||||||
**count**. Both readings fit the sentence and they differ on every game
|
|
||||||
where a 3-point Problem is claimed. Ours to report, theirs to rule.
|
|
||||||
|
|
||||||
**Sensitivity:** vary only the point value of the claimed Problems and
|
|
||||||
the gap moves with `sum(value) - count`. The two readings agree exactly
|
|
||||||
when every claimed Problem is worth 1 — no Problem in the edition is —
|
|
||||||
or when nothing is claimed, which is a loss. Everything else is held
|
|
||||||
fixed: same board, same penalties, same threshold.
|
|
||||||
|
|
||||||
- **F26 — a package that adds a FILE is invisible, where a package that
|
- **F26 — a package that adds a FILE is invisible, where a package that
|
||||||
adds a column is not.** `h2-scoped-problem-stress` ships
|
adds a column is not.** `h2-scoped-problem-stress` ships
|
||||||
`Rules_Text.csv` — twenty-two passages of player-facing rules, including
|
`Rules_Text.csv` — twenty-two passages of player-facing rules, including
|
||||||
|
|
|
||||||
|
|
@ -1288,7 +1288,6 @@ mod tests {
|
||||||
serve: Some(0),
|
serve: Some(0),
|
||||||
trial: None,
|
trial: None,
|
||||||
mode: games_ground::ScoringMode::SharedGround,
|
mode: games_ground::ScoringMode::SharedGround,
|
||||||
scenario: "SCN_01".into(),
|
|
||||||
pace: crate::table::Pace::Speed,
|
pace: crate::table::Pace::Speed,
|
||||||
variant: games_ground::Variant::Baseline,
|
variant: games_ground::Variant::Baseline,
|
||||||
},
|
},
|
||||||
|
|
@ -1480,7 +1479,6 @@ mod tests {
|
||||||
serve: Some(0),
|
serve: Some(0),
|
||||||
trial: None,
|
trial: None,
|
||||||
mode: games_ground::ScoringMode::SharedGround,
|
mode: games_ground::ScoringMode::SharedGround,
|
||||||
scenario: "SCN_01".into(),
|
|
||||||
pace: crate::table::Pace::Speed,
|
pace: crate::table::Pace::Speed,
|
||||||
variant: games_ground::Variant::Baseline,
|
variant: games_ground::Variant::Baseline,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -140,18 +140,12 @@ fn render_player(id: PlayerId, p: &PlayerView, is_viewer: bool) -> String {
|
||||||
pub fn render(view: &GroundView) -> String {
|
pub fn render(view: &GroundView) -> String {
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
"\nround {} step {:?} lead {} mode {:?} rules {} scenario {} \
|
"\nround {} step {:?} lead {} mode {:?} rules {} deck {} discard [{}]\n",
|
||||||
deck {} discard [{}]\n",
|
|
||||||
view.round,
|
view.round,
|
||||||
view.step,
|
view.step,
|
||||||
seat_name(view.lead),
|
seat_name(view.lead),
|
||||||
view.mode,
|
view.mode,
|
||||||
view.variant.id(),
|
view.variant.id(),
|
||||||
// CB-WP-0047: WHICH of the four boards. The id, not the title,
|
|
||||||
// because this is the machine-facing view — `cb-play inspect` is
|
|
||||||
// read by a maintainer diffing states, where the HTML page is
|
|
||||||
// read by a player and shows the printed title.
|
|
||||||
view.scenario,
|
|
||||||
view.solution_deck_len,
|
view.solution_deck_len,
|
||||||
cards(&view.solution_discard),
|
cards(&view.solution_discard),
|
||||||
));
|
));
|
||||||
|
|
@ -503,7 +497,6 @@ mod tests {
|
||||||
// CB-WP-0044: the inspector must say which rules it is replaying,
|
// CB-WP-0044: the inspector must say which rules it is replaying,
|
||||||
// for the same reason the page must.
|
// for the same reason the page must.
|
||||||
("variant", "rules ground-darvo-r0"),
|
("variant", "rules ground-darvo-r0"),
|
||||||
("scenario", "scenario SCN_"),
|
|
||||||
("viewer", "(you)"),
|
("viewer", "(you)"),
|
||||||
("solution_deck_len", "deck 11"),
|
("solution_deck_len", "deck 11"),
|
||||||
("solution_discard.*.suit", "discard [Repair, Change]"),
|
("solution_discard.*.suit", "discard [Repair, Change]"),
|
||||||
|
|
@ -621,7 +614,6 @@ mod tests {
|
||||||
lead: p2,
|
lead: p2,
|
||||||
step: RoundStep::Resolve,
|
step: RoundStep::Resolve,
|
||||||
mode: ScoringMode::BondedCoalitions,
|
mode: ScoringMode::BondedCoalitions,
|
||||||
scenario: "SCN_01".into(),
|
|
||||||
players,
|
players,
|
||||||
relations: BTreeMap::from([
|
relations: BTreeMap::from([
|
||||||
(Pair::new(p1, p2), Relation::Bond),
|
(Pair::new(p1, p2), Relation::Bond),
|
||||||
|
|
@ -778,7 +770,6 @@ mod tests {
|
||||||
serve: None,
|
serve: None,
|
||||||
trial: None,
|
trial: None,
|
||||||
mode: games_ground::ScoringMode::SharedGround,
|
mode: games_ground::ScoringMode::SharedGround,
|
||||||
scenario: "SCN_01".into(),
|
|
||||||
pace: crate::table::Pace::Speed,
|
pace: crate::table::Pace::Speed,
|
||||||
variant: games_ground::Variant::Baseline,
|
variant: games_ground::Variant::Baseline,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -62,61 +62,6 @@ enum Mode {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A scenario as the player names it, as the edition names it.
|
|
||||||
///
|
|
||||||
/// Accepts `2`, `02`, `scn-02`, `SCN_02` and the title's first word, so
|
|
||||||
/// that `--scenario confidence` works — the ids are the edition's
|
|
||||||
/// vocabulary and *"Broken Confidence"* is the player's.
|
|
||||||
///
|
|
||||||
/// **Validated against the edition, never against a pattern.** `SCN_09`
|
|
||||||
/// looks exactly like an id; the error names what the edition has.
|
|
||||||
fn normalise_scenario(v: &str) -> Result<String, String> {
|
|
||||||
let known = games_ground::edition::scenarios()?;
|
|
||||||
let want = v.trim().to_ascii_lowercase();
|
|
||||||
let digits: String = want.chars().filter(|c| c.is_ascii_digit()).collect();
|
|
||||||
let by_id = |s: &games_ground::edition::ScenarioText| {
|
|
||||||
s.id.to_ascii_lowercase() == want
|
|
||||||
|| (!digits.is_empty()
|
|
||||||
&& s.id
|
|
||||||
.rsplit('_')
|
|
||||||
.next()
|
|
||||||
.is_some_and(|n| n.parse::<u32>().ok() == digits.parse::<u32>().ok()))
|
|
||||||
};
|
|
||||||
if let Some(s) = known.iter().find(|s| by_id(s)) {
|
|
||||||
return Ok(s.id.clone());
|
|
||||||
}
|
|
||||||
// By title, so the four cards can be named by what is printed on them.
|
|
||||||
if let Some(s) = known
|
|
||||||
.iter()
|
|
||||||
.find(|s| s.title.to_ascii_lowercase().contains(&want) && !want.is_empty())
|
|
||||||
{
|
|
||||||
return Ok(s.id.clone());
|
|
||||||
}
|
|
||||||
Err(format!(
|
|
||||||
"unknown --scenario {v:?} (the edition has {})",
|
|
||||||
known
|
|
||||||
.iter()
|
|
||||||
.map(|s| format!("{} {:?}", s.id, s.title))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `scn-02-4p`, or `standard-4p` for the scenario every recording plays.
|
|
||||||
///
|
|
||||||
/// **`SCN_01` keeps the old preset string.** Twenty-six recorded
|
|
||||||
/// scenarios name `standard-Np`; emitting `scn-01-Np` for the same board
|
|
||||||
/// would have made every one of them unreplayable to no purpose.
|
|
||||||
pub fn scenario_preset(scenario: &str, players: u8) -> String {
|
|
||||||
if scenario == "SCN_01" {
|
|
||||||
return format!("standard-{players}p");
|
|
||||||
}
|
|
||||||
format!(
|
|
||||||
"scn-{}-{players}p",
|
|
||||||
scenario.rsplit('_').next().unwrap_or(scenario)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_args(argv: &[String]) -> Result<Mode, String> {
|
fn parse_args(argv: &[String]) -> Result<Mode, String> {
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
let mut source: Option<std::path::PathBuf> = None;
|
let mut source: Option<std::path::PathBuf> = None;
|
||||||
|
|
@ -196,16 +141,6 @@ fn parse_args(argv: &[String]) -> Result<Mode, String> {
|
||||||
// cheap now where a retrofit would not be.
|
// cheap now where a retrofit would not be.
|
||||||
// CB-WP-0038: ground-game names these in
|
// CB-WP-0038: ground-game names these in
|
||||||
// `editions/catalog.yaml`; `--variant` takes that id.
|
// `editions/catalog.yaml`; `--variant` takes that id.
|
||||||
// CB-WP-0047: WHICH SCENARIO. `deal` has taken a scenario id
|
|
||||||
// since it was written; the call site passed the literal
|
|
||||||
// "SCN_01", so three of four decks were unreachable from the
|
|
||||||
// driver — the same shape as `--mode` before F14.
|
|
||||||
"--scenario" => {
|
|
||||||
play_flags.push(flag.into());
|
|
||||||
let v = value(i, argv, flag)?;
|
|
||||||
config.scenario = normalise_scenario(&v)?;
|
|
||||||
i += 2;
|
|
||||||
}
|
|
||||||
"--variant" => {
|
"--variant" => {
|
||||||
play_flags.push(flag.into());
|
play_flags.push(flag.into());
|
||||||
config.variant = value(i, argv, flag)?.parse()?;
|
config.variant = value(i, argv, flag)?.parse()?;
|
||||||
|
|
@ -379,7 +314,6 @@ mod tests {
|
||||||
serve: None,
|
serve: None,
|
||||||
trial: None,
|
trial: None,
|
||||||
mode: games_ground::ScoringMode::SharedGround,
|
mode: games_ground::ScoringMode::SharedGround,
|
||||||
scenario: "SCN_01".into(),
|
|
||||||
pace: table::Pace::Speed,
|
pace: table::Pace::Speed,
|
||||||
variant: games_ground::Variant::Baseline,
|
variant: games_ground::Variant::Baseline,
|
||||||
};
|
};
|
||||||
|
|
@ -427,7 +361,6 @@ mod tests {
|
||||||
serve: None,
|
serve: None,
|
||||||
trial: None,
|
trial: None,
|
||||||
mode: games_ground::ScoringMode::SharedGround,
|
mode: games_ground::ScoringMode::SharedGround,
|
||||||
scenario: "SCN_01".into(),
|
|
||||||
pace,
|
pace,
|
||||||
variant: games_ground::Variant::Baseline,
|
variant: games_ground::Variant::Baseline,
|
||||||
};
|
};
|
||||||
|
|
@ -497,7 +430,6 @@ mod tests {
|
||||||
serve: None,
|
serve: None,
|
||||||
trial: None,
|
trial: None,
|
||||||
mode: games_ground::ScoringMode::SharedGround,
|
mode: games_ground::ScoringMode::SharedGround,
|
||||||
scenario: "SCN_01".into(),
|
|
||||||
pace: table::Pace::Speed,
|
pace: table::Pace::Speed,
|
||||||
variant: games_ground::Variant::Baseline,
|
variant: games_ground::Variant::Baseline,
|
||||||
};
|
};
|
||||||
|
|
@ -564,7 +496,6 @@ mod tests {
|
||||||
serve: None,
|
serve: None,
|
||||||
trial: None,
|
trial: None,
|
||||||
mode: games_ground::ScoringMode::SharedGround,
|
mode: games_ground::ScoringMode::SharedGround,
|
||||||
scenario: "SCN_01".into(),
|
|
||||||
pace: table::Pace::Speed,
|
pace: table::Pace::Speed,
|
||||||
variant: games_ground::Variant::Baseline,
|
variant: games_ground::Variant::Baseline,
|
||||||
};
|
};
|
||||||
|
|
@ -629,7 +560,6 @@ mod tests {
|
||||||
serve: None,
|
serve: None,
|
||||||
trial: None,
|
trial: None,
|
||||||
mode: games_ground::ScoringMode::SharedGround,
|
mode: games_ground::ScoringMode::SharedGround,
|
||||||
scenario: "SCN_01".into(),
|
|
||||||
pace: table::Pace::Speed,
|
pace: table::Pace::Speed,
|
||||||
variant: games_ground::Variant::Baseline,
|
variant: games_ground::Variant::Baseline,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -47,14 +47,6 @@ pub struct Config {
|
||||||
/// patch, so two of the three shipped modes were unreachable from the
|
/// patch, so two of the three shipped modes were unreachable from the
|
||||||
/// only way anyone actually plays.
|
/// only way anyone actually plays.
|
||||||
pub mode: games_ground::ScoringMode,
|
pub mode: games_ground::ScoringMode,
|
||||||
/// Which of the edition's four Scenarios to deal (CB-WP-0047).
|
|
||||||
///
|
|
||||||
/// **The same shape as `mode` and the same history.** `setup` dealt
|
|
||||||
/// the literal `"SCN_01"`, so three of the four vendored decks — 15
|
|
||||||
/// of the 20 Problem cards — had never reached a table through the
|
|
||||||
/// only way anyone actually plays. Stored as the preset's scenario
|
|
||||||
/// id so the kernel does the validating.
|
|
||||||
pub scenario: String,
|
|
||||||
/// How much ornamentation is performed (CB-WP-0036,
|
/// How much ornamentation is performed (CB-WP-0036,
|
||||||
/// [`specs/Ornamentation.md`]).
|
/// [`specs/Ornamentation.md`]).
|
||||||
///
|
///
|
||||||
|
|
@ -111,7 +103,6 @@ impl Default for Config {
|
||||||
serve: None,
|
serve: None,
|
||||||
trial: None,
|
trial: None,
|
||||||
mode: games_ground::ScoringMode::SharedGround,
|
mode: games_ground::ScoringMode::SharedGround,
|
||||||
scenario: "SCN_01".into(),
|
|
||||||
pace: Pace::Speed,
|
pace: Pace::Speed,
|
||||||
variant: games_ground::Variant::Baseline,
|
variant: games_ground::Variant::Baseline,
|
||||||
}
|
}
|
||||||
|
|
@ -408,7 +399,7 @@ fn run_game<'a, R: BufRead + 'a, W: Write + 'a>(
|
||||||
) -> Result<(Summary, crate::hotseat::EndChoice), String> {
|
) -> Result<(Summary, crate::hotseat::EndChoice), String> {
|
||||||
let setup = Setup {
|
let setup = Setup {
|
||||||
players: config.players,
|
players: config.players,
|
||||||
preset: crate::scenario_preset(&config.scenario, config.players),
|
preset: format!("standard-{}p", config.players),
|
||||||
patch: Default::default(),
|
patch: Default::default(),
|
||||||
};
|
};
|
||||||
let mut initial = <GroundState as cb_game_runtime::ScenarioGame>::setup(&setup, config.seed)?;
|
let mut initial = <GroundState as cb_game_runtime::ScenarioGame>::setup(&setup, config.seed)?;
|
||||||
|
|
|
||||||
|
|
@ -53,16 +53,7 @@ def reproduced(row, root=ROOT):
|
||||||
"""GameDesign §1.1: the artifact must resolve. A named test is admitted
|
"""GameDesign §1.1: the artifact must resolve. A named test is admitted
|
||||||
by its `crate::module::name` shape; anything else must be a real path
|
by its `crate::module::name` shape; anything else must be a real path
|
||||||
on disk, and that is checked by stat, not by prefix."""
|
on disk, and that is checked by stat, not by prefix."""
|
||||||
# **Backticks are markdown, not part of the path** (CB-WP-0047).
|
p = row["repro"]
|
||||||
#
|
|
||||||
# The register writes code spans, and every NAMED TEST survived that
|
|
||||||
# because `::` short-circuits before the stat. A backticked FILE path
|
|
||||||
# did not: os.path.exists("`games/.../x.rs`") is False, so a
|
|
||||||
# reproduction sitting on disk was reported as absent and its finding
|
|
||||||
# counted as debt against a target of zero. Same shape as ADR-0018:
|
|
||||||
# the computation was right and the string was not what it looked
|
|
||||||
# like. Found by F27, whose panel existed and did not count.
|
|
||||||
p = row["repro"].strip().strip("`").strip()
|
|
||||||
if p in ("", "—", "-"):
|
if p in ("", "—", "-"):
|
||||||
return False
|
return False
|
||||||
if "::" in p: # a named test
|
if "::" in p: # a named test
|
||||||
|
|
@ -178,17 +169,6 @@ def self_test():
|
||||||
check("a named test counts", reproduced({"repro": "games_ground::view::tests::a_spectator_sees_no_hands"}))
|
check("a named test counts", reproduced({"repro": "games_ground::view::tests::a_spectator_sees_no_hands"}))
|
||||||
check("an em-dash does not count", not reproduced({"repro": "—"}))
|
check("an em-dash does not count", not reproduced({"repro": "—"}))
|
||||||
|
|
||||||
# CB-WP-0047: the register writes CODE SPANS, so this is fed markdown.
|
|
||||||
check("a BACKTICKED path that exists counts",
|
|
||||||
reproduced({"repro": "`scenarios/ground/gr-p05-solve-legality.yaml`"}),
|
|
||||||
"a reproduction on disk was reported as absent, and the finding "
|
|
||||||
"counted as debt against a target of zero")
|
|
||||||
check("a backticked path that does NOT exist still does not count",
|
|
||||||
not reproduced({"repro": "`scenarios/ground/nope.yaml`"}),
|
|
||||||
"stripping the span must not turn the check off")
|
|
||||||
check("a backticked em-dash does not count",
|
|
||||||
not reproduced({"repro": "`—`"}))
|
|
||||||
|
|
||||||
# The distinction the backfill discovered: a green DEFAULT is expected,
|
# The distinction the backfill discovered: a green DEFAULT is expected,
|
||||||
# a green COUNTEREXAMPLE is the alarm. Without this the report cried
|
# a green COUNTEREXAMPLE is the alarm. Without this the report cried
|
||||||
# wolf over U2, whose scenario is green precisely because the
|
# wolf over U2, whose scenario is green precisely because the
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ id: CB-WP-0046
|
||||||
kind: product
|
kind: product
|
||||||
title: "The rule the placement encodes"
|
title: "The rule the placement encodes"
|
||||||
status: done
|
status: done
|
||||||
state_hub_workstream_id: "169242ac-024d-4001-aae2-d9bbf710fe2a"
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Purpose
|
# Purpose
|
||||||
|
|
@ -81,7 +80,6 @@ also why an unstamped log reports `unrecorded` and not `ground-darvo-r0`.
|
||||||
id: CB-WP-0046-T01
|
id: CB-WP-0046-T01
|
||||||
status: done
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "64b9e9bb-bcaa-45e5-975f-3527e4c95c05"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Controls, all mutation-proven:**
|
**Controls, all mutation-proven:**
|
||||||
|
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
---
|
|
||||||
id: CB-WP-0047
|
|
||||||
kind: product
|
|
||||||
title: "All four boards and all three modes"
|
|
||||||
status: done
|
|
||||||
---
|
|
||||||
|
|
||||||
# Purpose
|
|
||||||
|
|
||||||
```
|
|
||||||
structural tier M (changes the state and therefore the recording, and
|
|
||||||
moves a reported number -- the threshold -- from the
|
|
||||||
engine to the edition)
|
|
||||||
declared tier M
|
|
||||||
```
|
|
||||||
|
|
||||||
> *"lets have a look at the cards and the rules to those cards again and
|
|
||||||
> implement all the scenarios and modes"*
|
|
||||||
|
|
||||||
## What was actually missing
|
|
||||||
|
|
||||||
The two halves turned out to be very different sizes.
|
|
||||||
|
|
||||||
**The modes were already implemented.** `ScoringMode`'s three arms all
|
|
||||||
score, `--mode` reaches them, F14 fixed the driver. What was missing was
|
|
||||||
that **nothing had ever compared them**, and the page never named them —
|
|
||||||
see below.
|
|
||||||
|
|
||||||
**The scenarios were not implemented at all.** `edition::deal` has taken a
|
|
||||||
`scenario_id` since the day it was written, and the only caller passed the
|
|
||||||
literal `"SCN_01"`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
let dealt = crate::edition::deal("SCN_01", seats)?;
|
|
||||||
```
|
|
||||||
|
|
||||||
**The seam was the whole mechanism and it sat unused.** 15 of the 20
|
|
||||||
Problem cards had never been dealt by anything — not a gate, not a panel,
|
|
||||||
not a player. Nothing was red, because nothing asked.
|
|
||||||
|
|
||||||
## Task: deal every board, name every mode
|
|
||||||
|
|
||||||
```task
|
|
||||||
id: CB-WP-0047-T01
|
|
||||||
status: done
|
|
||||||
priority: high
|
|
||||||
```
|
|
||||||
|
|
||||||
### The scenario is state
|
|
||||||
|
|
||||||
`GroundState.scenario`, `#[serde(default = "default_scenario")]` → `SCN_01`.
|
|
||||||
In the state for the same reason `variant` is (CB-WP-0038): the board and
|
|
||||||
the threshold follow from it, so a recording that did not carry it could
|
|
||||||
not be replayed.
|
|
||||||
|
|
||||||
**`standard-Np` keeps meaning `SCN_01`.** Twenty-six recorded scenarios
|
|
||||||
name that preset; a grammar that redefined it would have moved every one
|
|
||||||
of their boards while their hashes still claimed to pin them. `scn-03-4p`
|
|
||||||
names the rest. `--scenario` accepts `3`, `scn-03`, `SCN_03` or
|
|
||||||
`confidence` — ids are the edition's vocabulary, *"Broken Confidence"* is
|
|
||||||
the player's — and is validated against the edition, never a pattern, so
|
|
||||||
`SCN_09` is refused by name.
|
|
||||||
|
|
||||||
### The threshold now comes off the Scenario card
|
|
||||||
|
|
||||||
`threshold()` was `match seats { 0..=2 => 5, 3..=4 => 7, _ => 9 }` — the
|
|
||||||
engine's own copy of a number four cards already print, which is what F25
|
|
||||||
was raised about.
|
|
||||||
|
|
||||||
**The first version of this control was worthless and mutation said so.**
|
|
||||||
Reverting to the hardcoded bands left every test green, because all four
|
|
||||||
scenarios print 5/7/9 and the two paths are observationally identical on
|
|
||||||
every input the edition can supply. A control that cannot separate the
|
|
||||||
thing it is about from its fallback is not a control.
|
|
||||||
|
|
||||||
Fixed by splitting `threshold_from(list, scenario, seats)`, which can be
|
|
||||||
handed a card that **disagrees** (4/6/8). That test goes red under the
|
|
||||||
mutation; the loop over real scenarios never could.
|
|
||||||
|
|
||||||
### The page names the mode, the board, and what winning means
|
|
||||||
|
|
||||||
The header read `scoring CommonProblem` — the Rust variant's name, where
|
|
||||||
the Mode card is titled *"COMMON PROBLEM, PERSONAL EDGE"*. **CB-WP-0034
|
|
||||||
deleted this exact defect from the move buttons** and it was still
|
|
||||||
standing on the one line that says what winning means.
|
|
||||||
|
|
||||||
**And the coverage probe was holding it in place.** `("mode", "scoring
|
|
||||||
BondedCoalitions")` — a probe matching `Debug` output, which went red the
|
|
||||||
moment the defect was fixed. That is the **third** time this shape has
|
|
||||||
been found (CB-WP-0024, CB-WP-0034, here), and it is now specific enough
|
|
||||||
to state as a rule: *a probe that names a rendering pins that rendering;
|
|
||||||
probes name facts.*
|
|
||||||
|
|
||||||
The page now carries the Scenario's premise (*what happened*) and the Mode
|
|
||||||
card's rules text (*how this game is won*) — including the **tiebreak**,
|
|
||||||
which is a rule a player can play for: "lower Stress, then more Bonds"
|
|
||||||
changes what a losing seat should do in round five, and it lived in a
|
|
||||||
column nothing read.
|
|
||||||
|
|
||||||
### Mutations
|
|
||||||
|
|
||||||
| mutation | what went red |
|
|
||||||
|---|---|
|
|
||||||
| `setup` deals `"SCN_01"` regardless of preset | *"the four scenarios yield 1 distinct board"* |
|
|
||||||
| `threshold_from` ignores the card | *"the threshold did not come off the Scenario card"* |
|
|
||||||
| header back to `{mode:?}` | *"the page shows the Rust variant's name"* |
|
|
||||||
|
|
||||||
**Done 2026-08-08.** `make all` green. Verified live on
|
|
||||||
`make ground SCENARIO=4 MODE=coalitions`.
|
|
||||||
|
|
||||||
## What the panel found
|
|
||||||
|
|
||||||
`make panels` gained `scenario-panel`: 4 scenarios × 3 modes × 3 seat
|
|
||||||
bands, greedy throughout, 100 games per cell.
|
|
||||||
|
|
||||||
**1. Two of the four scenarios are the same board.** SCN_01 and SCN_02
|
|
||||||
have identical suits and values at every priority; every cell matches
|
|
||||||
exactly. Not a defect — a designed reskin is a legitimate choice — but
|
|
||||||
"four scenarios" buys **three** boards, and a panel that measured them as
|
|
||||||
four independent boards would be measuring one of them twice. Pinned by a
|
|
||||||
characterisation test so a future divergence is a decision, not a drift.
|
|
||||||
|
|
||||||
**2. SCN_04 is the hard board at 2 players** — 52% group success against
|
|
||||||
67% and 73%. It is the only deck that needs **two Repair** solutions in
|
|
||||||
the 2p deal, and the 2p Solution draw cannot always supply them. The seat
|
|
||||||
band the thresholds treat as uniform is not uniform across boards.
|
|
||||||
|
|
||||||
**3. The three modes produce identical play.** Group success is *exactly*
|
|
||||||
equal across all three modes in every cell. That is correct arithmetic and
|
|
||||||
a real finding: `GreedyPolicy` maximises the group outcome and never
|
|
||||||
consults `state.mode`, so **the two competitive modes are scoring lenses
|
|
||||||
over cooperative play**. `win/g` differs only by how the winner set is
|
|
||||||
carved from the same games. Whether COMMON PROBLEM and BONDED COALITIONS
|
|
||||||
change how the game is *played* is untested and cannot be tested by this
|
|
||||||
panel — it needs a policy that plays for personal score. Filed as F27.
|
|
||||||
|
|
||||||
**4. Every board is a formality at 6 players** — 100% group success in all
|
|
||||||
twelve cells. Consistent with F17's shape and with H2's motivation; noted,
|
|
||||||
not acted on.
|
|
||||||
|
|
||||||
## Not done here
|
|
||||||
|
|
||||||
- **No policy plays for personal score**, so F27 stands open and the two
|
|
||||||
competitive modes remain unexercised as *incentives*. This is the
|
|
||||||
largest remaining gap and it is the natural next pass.
|
|
||||||
- **SHARED GROUND's mastery rating counts CLAIMED CARDS, not points.**
|
|
||||||
The Mode card says *"All claimed Problem cards form one shared score …
|
|
||||||
subtract 1 for each Blame token still in play and 1 for each Denied
|
|
||||||
Problem"* — and the shared score is the claimed **value**, while
|
|
||||||
`mastery` subtracts from the claimed **count**. Underdetermined rather
|
|
||||||
than plainly wrong; raised for `ground-game` rather than changed here,
|
|
||||||
because scoring is theirs to rule on.
|
|
||||||
- **The trial log stamps the variant (CB-WP-0046) but not the scenario or
|
|
||||||
the mode.** A note from SCN_04 coalitions is indistinguishable from a
|
|
||||||
baseline SHARED GROUND note, which is the same defect one pass later in
|
|
||||||
two new axes. The marker already carries attributes, so this is small.
|
|
||||||
- **`design.py` reported an existing reproduction as absent.** It stats
|
|
||||||
`row["repro"]` directly, and the register writes **code spans** — so a
|
|
||||||
backticked file path was `os.path.exists("`games/…/x.rs`")` = False.
|
|
||||||
Named tests survived because `::` short-circuits before the stat, which
|
|
||||||
is why it had never shown. F27's panel existed and counted as debt
|
|
||||||
against a target of zero. ADR-0018's shape again: right computation,
|
|
||||||
wrong string. Fixed with three checks, including that stripping the
|
|
||||||
span must not turn the check off.
|
|
||||||
|
|
||||||
- **`catalog.yaml` and both `VARIANT.md`s were re-vendored in this pass**: `ground-game` moved H2
|
|
||||||
from `experimental` to `measured`, `decision: keep-as-experiment`,
|
|
||||||
citing our CB-EV-0032. That is their ruling on the previous pass, not
|
|
||||||
work done here.
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue