CB-WP-0021 T01/T02/T05: the engine plays its own data — AM-7 blocks
ADR-0011 decided it: vendor the CSV with a checked digest, read it with a ~50-line reader, and let the hashes move. The declaration's constraint was measured against the WRONG BUDGET. It said a CSV crate costs 21,613 against AM-4a's 3,798 of headroom, '5.7x over, settled by measurement'. But setup and problem_priorities are cfg(scenarios) and are not in the shipped runtime at all, so AM-4a never sees them. Against AM-4b, csv costs 17,651 against 19,742 -- it FITS, with 2,091 to spare. It is refused anyway, on proportion: 89% of the budget's remaining capacity to read 20 rows. The revisit condition is stated (nested quoting, embedded newlines, multiple dialects). GR-S01 now deals Surface + hidden 1..=k as ruled, with edition values and suits. Measured: 6/9/12 available against thresholds 5/7/9 -- the game is winnable at every seat count, which is what the maintainer could not do. gd0001 is INVERTED, not deleted, and now also asserts the 6/9/12 so a deal that is reachable for the wrong reason still fails. Blast radius was scenario expectations, exactly as the ADR predicted: no scenario pinned a hash and no bundle is committed. Six scenarios and two unit tests updated, each with a note. gr-e01-threshold-unreachable-2p is RENAMED to -reachable- and rewritten as the non-provisional import check ground-game asked for by name. gr-e03's setup was restructured, not just renumbered: with values 2,2,2 its personal-edge test would have tied three ways and asserted nothing. BLOCKING: AM-7 fails at median 0.845 against its 0.9 floor. Isolated across three runs -- 3 problems + stand-in 0.97, 3 problems + edition 0.909, 4 problems + edition 0.845. State is BOUNDED (proven: identical after 5k and 100k events), so this is not the unbounded-growth defect AM-7 exists to catch; it is a bigger working set streaming a long log. Whether AM-7's floor is still right for a larger aggregate is a spec question and lowering it requires an ADR, so it is not being tuned here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f2281fa86c
commit
2da19a49b7
16 changed files with 593 additions and 167 deletions
|
|
@ -7,6 +7,8 @@
|
|||
/// aggregate. That its tests do need `scenarios` is a real seam — setup
|
||||
/// presets currently live behind that feature (see `bot.rs`).
|
||||
pub mod bot;
|
||||
#[cfg(feature = "scenarios")]
|
||||
pub mod edition;
|
||||
|
||||
/// K13's per-player projection (CB-WP-0008 T02) — the trait's first
|
||||
/// implementor. Needs the runtime's `Project`, which the game already
|
||||
|
|
@ -1788,26 +1790,12 @@ impl GroundState {
|
|||
}
|
||||
}
|
||||
|
||||
/// GR-S01: hidden-Problem priorities admitted per player count.
|
||||
#[cfg(feature = "scenarios")]
|
||||
fn problem_priorities(players: u8) -> Result<u8, String> {
|
||||
match players {
|
||||
2 => Ok(2),
|
||||
3..=4 => Ok(3),
|
||||
5..=6 => Ok(4),
|
||||
other => Err(format!("GR-S01: unsupported player count {other}")),
|
||||
}
|
||||
}
|
||||
// GR-S01's deal now lives in `edition::hidden_depth` (ADR-0011): the
|
||||
// ruled shape is Surface + hidden 1..=k, and k belongs beside the data
|
||||
// it indexes into.
|
||||
|
||||
/// GR-S04: the 24 core Solution cards, 6 per suit, in canonical order
|
||||
/// before the seeded shuffle.
|
||||
#[cfg(feature = "scenarios")]
|
||||
fn core_solution_deck() -> Vec<SolutionCard> {
|
||||
[Suit::Clarify, Suit::Repair, Suit::Boundary, Suit::Change]
|
||||
.into_iter()
|
||||
.flat_map(|suit| std::iter::repeat_n(SolutionCard { suit }, 6))
|
||||
.collect()
|
||||
}
|
||||
// GR-S04's deck now lives in `edition::solution_deck` (ADR-0011),
|
||||
// beside the Problem data it is dealt against.
|
||||
|
||||
#[cfg(feature = "scenarios")]
|
||||
impl ScenarioGame for GroundState {
|
||||
|
|
@ -1824,11 +1812,14 @@ impl ScenarioGame for GroundState {
|
|||
setup.preset
|
||||
));
|
||||
}
|
||||
let priorities = problem_priorities(seats)?;
|
||||
// GR-S01 as ruled 2026-08-04: Surface always, plus hidden
|
||||
// priorities 1..=k. The values and suits come from the edition
|
||||
// (ADR-0011); the engine used to invent both.
|
||||
let dealt = crate::edition::deal("SCN_01", seats)?;
|
||||
let mut rng = ChaChaRng::from_seed(Seed(seed));
|
||||
|
||||
// GR-S04: shuffle first, then deal, so the deal is seed-derived.
|
||||
let mut deck = core_solution_deck();
|
||||
let mut deck = crate::edition::solution_deck();
|
||||
rng.shuffle(&mut deck);
|
||||
|
||||
// GR-S02: Stress 2, Freedom READY, DARVO OFF, two Solution cards.
|
||||
|
|
@ -1849,17 +1840,18 @@ impl ScenarioGame for GroundState {
|
|||
);
|
||||
}
|
||||
|
||||
// GR-S01: priority 1 is the Surface Problem, face up; the rest
|
||||
// start face down.
|
||||
let suits = [Suit::Clarify, Suit::Repair, Suit::Boundary, Suit::Change];
|
||||
let problems = (1..=u32::from(priorities))
|
||||
.map(|priority| {
|
||||
// Surface is dealt face up; hidden Problems face down (GR-S01).
|
||||
// Keyed by 1-based position so scenario dot-paths stay stable.
|
||||
let problems: BTreeMap<u32, ProblemState> = dealt
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, p)| {
|
||||
(
|
||||
priority,
|
||||
(i + 1) as u32,
|
||||
ProblemState {
|
||||
suit: suits[(priority as usize - 1) % suits.len()],
|
||||
value: priority as u8,
|
||||
face_up: priority == 1,
|
||||
suit: p.suit,
|
||||
value: p.value,
|
||||
face_up: p.surface,
|
||||
denied: false,
|
||||
claimed_by: None,
|
||||
protected_this_round: false,
|
||||
|
|
@ -2041,10 +2033,26 @@ mod tests {
|
|||
assert_eq!(player.hand.len(), 2);
|
||||
}
|
||||
assert_eq!(state.solution_deck.len(), 24 - 6);
|
||||
// GR-S01: 3 players → priorities 1–3, priority 1 face up.
|
||||
assert_eq!(state.problems.len(), 3);
|
||||
assert!(state.problems[&1].face_up);
|
||||
assert!(!state.problems[&2].face_up);
|
||||
// GR-S01 as ruled 2026-08-04: Surface always, plus hidden
|
||||
// priorities 1..=k. At 3 players k = 3, so FOUR Problems are
|
||||
// dealt — Surface face up, the three hidden ones face down. The
|
||||
// engine used to deal Surface + (k-1), which is what made the
|
||||
// group unable to reach GR-E01's threshold.
|
||||
assert_eq!(
|
||||
state.problems.len(),
|
||||
4,
|
||||
"GR-S01 deals Surface + hidden 1..=3"
|
||||
);
|
||||
assert!(state.problems[&1].face_up, "the Surface Problem is face up");
|
||||
for slot in 2..=4 {
|
||||
assert!(
|
||||
!state.problems[&slot].face_up,
|
||||
"hidden Problem {slot} is face up"
|
||||
);
|
||||
}
|
||||
// The edition's values, not the stand-in's `value = priority`.
|
||||
let dealt: Vec<u8> = (1..=4).map(|n| state.problems[&n].value).collect();
|
||||
assert_eq!(dealt, vec![2, 2, 2, 3], "edition point_value not loaded");
|
||||
}
|
||||
|
||||
/// GR-S03/S04: the same seed reproduces setup exactly; a different
|
||||
|
|
@ -2476,25 +2484,28 @@ mod replay_probe {
|
|||
}
|
||||
}
|
||||
|
||||
/// **GD-0001: group success is arithmetically unreachable below 5
|
||||
/// seats**, and this is the reproduction rather than an argument.
|
||||
/// **GD-0001, INVERTED 2026-08-04.** Group success is reachable at
|
||||
/// every seat count.
|
||||
///
|
||||
/// The maintainer played several 3-player games on 2026-08-03 and
|
||||
/// could not win any of them. This says why: claim *every* Problem
|
||||
/// the deal puts in play, concede nothing, and the total still falls
|
||||
/// short of GR-E01's threshold at 2, 3 and 4 seats.
|
||||
/// This test used to assert the opposite, and it was right to: the
|
||||
/// maintainer played several 3-player games on 2026-08-03 and could
|
||||
/// not win any of them, because GR-S01 dealt 2/3/4 Problems worth
|
||||
/// 3/6/10 against thresholds of 5/7/9.
|
||||
///
|
||||
/// It reads both numbers out of the engine — `problem_priorities`
|
||||
/// (GR-S01's deal) and `threshold` (GR-E01) — so it cannot drift from
|
||||
/// the rules it is testing, and it holds for **either dataset**: the
|
||||
/// stand-in gives 3/6/10 and `Problems.csv` gives 4/6/9, against the
|
||||
/// same 5/7/9.
|
||||
/// ground-game ruled the deal on 2026-08-04 — **Surface always, plus
|
||||
/// hidden priorities 1..=k** — which with this edition's values gives
|
||||
/// **6 / 9 / 12**. The ruling said to invert this test rather than
|
||||
/// retire it, and that is why it is still here: a reader learns the
|
||||
/// game *became* winnable, not that a test quietly vanished.
|
||||
///
|
||||
/// **This test asserts the defect.** It is expected to keep passing
|
||||
/// until ground-game rules, and to be inverted when it does — either
|
||||
/// the deal count rises or the thresholds fall.
|
||||
/// It reads both numbers out of the engine — the deal and
|
||||
/// `threshold` — so it cannot drift from the rules it tests.
|
||||
///
|
||||
/// **The 2p case is the one to watch.** 2+2+2 against a threshold of
|
||||
/// 5 means a full clear: any two Problems sum to 4. Reachable is not
|
||||
/// forgiving, and ground-game kept that deliberately.
|
||||
#[test]
|
||||
fn gd0001_group_success_is_unreachable_below_five_seats() {
|
||||
fn gd0001_group_success_is_reachable_at_every_seat_count() {
|
||||
let mut verdicts = Vec::new();
|
||||
for seats in 2..=6u8 {
|
||||
let state = fresh_n(seats);
|
||||
|
|
@ -2516,18 +2527,24 @@ mod replay_probe {
|
|||
.filter(|(_, _, _, _, ok)| !ok)
|
||||
.map(|(s, _, _, _, _)| *s)
|
||||
.collect();
|
||||
// Positive control: a run where everything is reachable would
|
||||
// report a clean sheet and prove nothing.
|
||||
assert!(
|
||||
!verdicts.is_empty() && verdicts.iter().any(|(_, _, _, _, ok)| *ok),
|
||||
"no seat count was reachable; the harness measured nothing useful"
|
||||
unreachable.is_empty(),
|
||||
"group success is unreachable at {unreachable:?} seats — the \
|
||||
ruled deal (Surface + hidden 1..=k) is not what the engine \
|
||||
deals, or the edition values changed"
|
||||
);
|
||||
// The ruled numbers, asserted rather than implied: 6/9/12 against
|
||||
// 5/7/9. A deal that was reachable for the wrong reason — more
|
||||
// Problems, or richer ones — would pass the check above.
|
||||
let available: Vec<u32> = verdicts.iter().map(|(_, _, b, _, _)| *b).collect();
|
||||
assert_eq!(
|
||||
unreachable,
|
||||
vec![2, 3, 4],
|
||||
"GD-0001 has changed: ground-game may have ruled. Re-read the \
|
||||
finding before editing this test."
|
||||
available,
|
||||
vec![6, 9, 9, 12, 12],
|
||||
"available points are not the 6/9/12 ground-game ruled against"
|
||||
);
|
||||
// Positive control: a harness that measured nothing would report
|
||||
// an empty `unreachable` and pass.
|
||||
assert_eq!(verdicts.len(), 5, "the sweep did not cover 2..=6 seats");
|
||||
}
|
||||
|
||||
/// AM-7 scaling floor from GameKernel §5: fold throughput at 100k
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue