clay-borg/crates/cb-render-html/src/testfix.rs
tegwick 48a7bce04d
Some checks failed
ci / check (push) Has been cancelled
CB-WP-0048 T02/T03/T04: Configuration replaces Variant in the state
The three-armed enum could not express a configuration carrying two
modules. It does now: --variant h1 expands to problem_stress.flat_any_open
AND attack_relief.self_soothe_ge4, and scoped_plus_attack_soothe plays.

Legacy names alias forever via serde(alias="variant") plus a
scalar-or-map deserialiser. serde(default) alone would have been a silent
migration bug -- every H1/H2 recording would have come back as baseline.
All 26 scenarios pass unchanged; none pins a state hash.

Three things shipped broken first, all caught by gates rather than by
reading:

1. `profile` was in the state hash. with_config(select("ground-darvo-r0"))
   sets profile=Some("baseline") where setup alone leaves None, so two
   states at THE SAME POINT IN ASPECT SPACE hashed differently and a
   replay bundle stopped reproducing its own initial state. Now
   serde(skip): a hash covers what determines play. This was the open
   judgement from T01 and it did not survive contact with the replay path.

2. A YAML parse inside the event loop. rules() -> resolve() -> catalog()
   re-parsed catalog.yaml per rule check; AM-6 fell to 9,345 events/s
   against a 100,000 target. OnceLock, and aspect validation moved to
   where a configuration is BUILT.

   Then I nearly optimised a phantom: 470k still looked like a 3x
   regression against the "~1.7M on bnt-lap001" reference in the gate's
   own message. Making rules() free measured 491k -- this machine's
   ceiling. Before optimising against a reference, measure the ceiling
   with the suspect code removed.

3. The refusal did not fire on the path a player takes.
   `--module problem_deal.pressure_deck` played a full baseline game and
   reported success, because with_config is a builder and fell back to
   the printed rules -- the silent no-op ADR-0022 exists to refuse. Every
   unit test of resolve() passed. The helper was tested and the driver
   was not, which is CB-WP-0033's finding verbatim. The new test asserts
   refusal BY NAME and that no game was played.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 00:54:34 +02:00

162 lines
5.8 KiB
Rust

//! A populated `GroundView`, built for the coverage gate.
//!
//! Mid-DARVO, mid-GROUND, scored — **not** a fresh deal. Most of the
//! fields this gate exists to cover are empty at deal time, and a coverage
//! test run against a state where the fields are absent is the same lie in
//! a different costume (CB-WP-0011 T01).
//!
//! Built directly rather than projected from a `GroundState`. The
//! projection's own hiding rules are already asserted by
//! `games_ground::view` and by `cb-play`'s
//! `the_inspector_never_widens_the_projection`; re-asserting them here
//! would be a duplicated fact that drifts. What *this* crate must not do
//! is invent a hand it was never given, and that is what the renderer test
//! checks — with `hand: None` supplied deliberately.
use std::collections::BTreeMap;
use cb_kernel::PlayerId;
use games_ground::view::{GroundView, OutcomeView, PlayerView, ProblemView, SelectionView};
use games_ground::{
Action, Coalition, DarvoStage, DarvoTarget, GroundChoice, GroundMode, Pair, Relation,
RoundStep, ScoringMode, Selection, SolutionCard, Suit, SupportResponse,
};
pub fn seats() -> (PlayerId, PlayerId, PlayerId) {
(PlayerId(0), PlayerId(1), PlayerId(2))
}
/// The fixture, seen by `viewer` (`None` for a spectator).
pub fn view(viewer: Option<PlayerId>) -> GroundView {
let (p1, p2, p3) = seats();
let card = |suit| SolutionCard { suit };
let mut players = BTreeMap::new();
for (id, stress, protection, darvo, ready, lifted, blame) in [
(p1, 5u8, 2u8, DarvoStage::Reverse, true, true, vec![p3]),
(p2, 1, 0, DarvoStage::Off, false, false, vec![]),
(p3, 0, 0, DarvoStage::Deny, true, false, vec![]),
] {
let own = viewer == Some(id);
players.insert(
id,
PlayerView {
stress,
freedom_ready: ready,
freedom_gate_lifted: lifted,
darvo,
protection,
blame_from: blame,
hand: own.then(|| vec![card(Suit::Clarify), card(Suit::Boundary)]),
hand_size: 2,
},
);
}
let mut problems = BTreeMap::new();
problems.insert(1, ProblemView::FaceDown);
// CB-WP-0043: a third Problem so all three H2 scopes are drawn —
// global (1), personal (6) and bond (7). Two would leave one
// placement rule unexercised and the coverage probe unsatisfiable.
problems.insert(6, ProblemView::FaceDown);
problems.insert(
7,
ProblemView::FaceUp {
suit: Suit::Change,
value: 6,
denied: true,
claimed_by: Some(p2),
protected_this_round: true,
},
);
GroundView {
viewer,
round: 3,
lead: p2,
step: RoundStep::Select,
mode: ScoringMode::BondedCoalitions,
scenario: "SCN_03".into(),
players,
relations: BTreeMap::from([
(Pair::new(p1, p2), Relation::Rivalry),
(Pair::new(p2, p3), Relation::Bond),
]),
problems,
config: games_ground::config::Configuration::default(),
focus: BTreeMap::from([(p1, p3)]),
selections: BTreeMap::from([
(
p1,
SelectionView::Shown(Selection {
action: Action::Attack,
target: Some(p2),
problem: Some(7),
}),
),
(p2, SelectionView::Hidden),
// CB-WP-0034: P3 offered P2 their Support, revealed. The
// fixture now carries the case the report was about, so the
// coverage probe for `support_responses` can name a SEAT
// rather than a response with nobody attached to it.
(
p3,
SelectionView::Shown(Selection {
action: Action::Support,
target: Some(p2),
problem: None,
}),
),
]),
// CB-WP-0043: the fixture carries H2 markers, so the scoped
// layout is exercised — a fixture with none would leave the new
// placement untested and the row layout would always be taken.
problem_markers: BTreeMap::from([
(
1,
games_ground::view::ProblemMarker {
owner: None,
scope: Some(games_ground::edition::StressScope::Global),
},
),
(
6,
games_ground::view::ProblemMarker {
owner: Some(p1),
scope: Some(games_ground::edition::StressScope::Personal),
},
),
(
7,
games_ground::view::ProblemMarker {
owner: Some(p2),
scope: Some(games_ground::edition::StressScope::Bond),
},
),
]),
ground_modes: BTreeMap::from([(p3, GroundMode::Gr)]),
ground_choices: BTreeMap::from([(p3, GroundChoice::ProtectProblem { problem: 7 })]),
support_responses: BTreeMap::from([(p2, SupportResponse::AcceptBond)]),
darvo_targets: BTreeMap::from([(
p3,
DarvoTarget {
problem: Some(1),
player: Some(p2),
},
)]),
solution_deck_len: 17,
solution_discard: vec![card(Suit::Repair), card(Suit::Change)],
outcome: Some(OutcomeView {
total: 9,
threshold: 12,
group_success: false,
personal: BTreeMap::from([(p1, 3), (p2, -1), (p3, 0)]),
coalitions: vec![Coalition {
members: vec![p2, p3],
score: 4,
}],
mastery: Some(1),
winners: vec![p1],
}),
}
}