From d4d25b903ed1a423fe84cd69a9d715f74227867d Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 8 Aug 2026 15:11:09 +0200 Subject: [PATCH 1/2] Declare CB-WP-0041 (extensive-form foundation) and CB-WP-0042 (H2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0041 answers what is true of the engine as a game-theoretic object before anything is built on it. CB-RES-0009 found the EFG is the interchange format between describing a game and analysing it, and that we already have most of one — the journal is the history, Outcome the payoff, and project(Viewer::Player(seat)) the information partition. Three gaps remain, and perfect recall is first because CFR and exploitability both assume it and nobody has checked ours. The workplan deliberately builds no port: creating a capability port is a tier-L trigger and this is M, so T04 decides whether to build one and declares it separately. T01's control includes that the answer may be NO, which would make Track B's adoption unsound as it stands. 0042 implements H2, which is ground-game's direct answer to our H1 reading. Unclaimed Problems now tick only the seats in scope — global, personal (the owner), or bond (the owner's Bond network over Bond edges only, degree 0 falling back to personal) — assigned by hidden priority, so 2p never has the bond card in play. It explicitly does not stack with H1. H2 is bigger than H1 was: it needs variant-scoped edition data (H2 overrides Problems.csv with a stress_scope column, and ours is an include_str! constant), per-Problem ownership which is new state reaching the hash and every recording, and Bond-network reachability. The controls name the likely defects in advance: traversing Rivalry edges, applying stacking once, forgetting the degree-0 fallback, and ownership silently becoming a permission to SOLVE. Chaos window 4 opens: d8 = 5 and d8 = 4, no overrides. Window 3's verdict is still owed. Co-Authored-By: Claude Opus 5 --- ...B-WP-0041-the-extensive-form-foundation.md | 138 +++++++++++++++ .../CB-WP-0042-h2-scoped-problem-stress.md | 161 ++++++++++++++++++ 2 files changed, 299 insertions(+) create mode 100644 workplans/CB-WP-0041-the-extensive-form-foundation.md create mode 100644 workplans/CB-WP-0042-h2-scoped-problem-stress.md diff --git a/workplans/CB-WP-0041-the-extensive-form-foundation.md b/workplans/CB-WP-0041-the-extensive-form-foundation.md new file mode 100644 index 0000000..13fd553 --- /dev/null +++ b/workplans/CB-WP-0041-the-extensive-form-foundation.md @@ -0,0 +1,138 @@ +--- +id: CB-WP-0041 +kind: product +title: "The extensive-form foundation" +status: active +--- + +# Purpose + +``` +structural tier M (states what the kernel claims about itself as a + game-theoretic object, and prepares — but does not + build — a capability port) +chaos d8 = 5 → no override +declared tier M +``` + +**Declaration 1 of chaos window 4.** Window 3 closed at 12 declarations +with one override that changed nothing, and **its verdict is still owed** +([`ChaosRollHistory.md`](../specs/ChaosRollHistory.md)). + +## Why + +[CB-RES-0009](../research/CB-RES-0009-extensive-form-is-the-lingua-franca.md) +found that the **extensive-form game is the interchange format** between +describing a game and analysing it — Ludii's universality result grounds +its language in EFGs, and OpenSpiel's CFR, best-response and +exploitability consume them. + +**And that we already have most of one**, under other names: + +| EFG component | ours | +|---|---| +| histories | the journal — `Applied { actor, command, events }` in order | +| actions | `bot::legal_commands(state, seat)` | +| **information sets** | **`state.project(Viewer::Player(seat))`** | +| payoffs | `Outcome` / `score()` | +| terminal | `outcome.is_some()` | + +**This workplan does not build the port.** It answers what is true of the +engine today, because a port built on an unchecked assumption is worse +than no port — and one of the three gaps invalidates every equilibrium +concept if it turns out badly. + +## Task: does the engine satisfy perfect recall? + +```task +id: CB-WP-0041-T01 +status: todo +priority: high +``` + +**Perfect recall** — every player remembers their own past actions and +observations — is the assumption **CFR and exploitability both rest on**. +Nobody has checked whether ours holds. + +Formally, for a seat `i` and any two histories `h`, `h'` in the same +information set of `i`: the *sequence of `i`'s own actions and +information sets* along `h` and `h'` must be identical. + +**Controls:** +- **derived from the journal and the projection**, not asserted — the + check must be able to say NO; +- **a positive control**: a deliberately forgetful projection must fail + it, or the check proves nothing; +- **stated per seat count**, since the partition depends on how many + seats there are to hide from; +- **the answer may be that we do NOT have perfect recall**, and that is + reported as plainly as the other outcome. It would be a real finding + and would make Track B's adoption unsound as it stands. + +## Task: say precisely what our chance is + +```task +id: CB-WP-0041-T02 +status: todo +priority: high +``` + +`setup` shuffles, deals and draws Lead from a seeded RNG, and the +mid-game reshuffle derives from seed and round. **So a clay-borg game is +one chance realisation, not a game with chance nodes**, and the panels +approximate the distribution by sampling seeds. + +**Controls:** +- **state it, do not fix it.** Monte Carlo over seeds is legitimate and + is what we do; the defect would be calling it an EFG; +- **name every point where chance enters**, from the code, not from + memory; +- **say what an explicit chance player would cost** — that is the input to + T04's decision, and guessing it is how a port gets built on a hope. + +## Task: state the simultaneity encoding, and check it + +```task +id: CB-WP-0041-T03 +status: todo +priority: medium +``` + +Commit/reveal **is** the textbook EFG encoding of simultaneous moves: +sequence them, and hide the earlier move in an information set. + +**Control:** it is not enough to say so. **The projection must actually +hide another seat's selection before Reveal**, and a test must fail if it +stops doing that. That property is load-bearing for every claim in §1 of +the research note and is currently only implied by +`SelectionView::Hidden`. + +## Task: decide whether to build the port at all + +```task +id: CB-WP-0041-T04 +status: todo +priority: medium +``` + +`decisions/ADR-*.md`, written **after** T01–T03 and not before. + +**It must be able to conclude "no".** Options include exporting an EFG, +adopting OpenSpiel's API directly for analysis only, or deciding the gaps +are too expensive and Track B borrows vocabulary rather than machinery. + +**Controls:** +- **the decision cites T01's answer**, because if perfect recall fails, + most of the option space closes; +- **it states what it would cost to be wrong**; +- **a port is declared separately, at its own tier.** Creating a capability + port is a tier-L trigger and this workplan is M — it may not smuggle one + in. + +## Not in this workplan + +- **No EFG export, no OpenSpiel integration, no equilibrium computation.** +- **No answer to "is exploitability meaningful for a co-operative game + with a shared threshold"** — that is the other open question from + CB-RES-0009 §6, it is a question about game theory rather than about our + engine, and it wants its own pass. diff --git a/workplans/CB-WP-0042-h2-scoped-problem-stress.md b/workplans/CB-WP-0042-h2-scoped-problem-stress.md new file mode 100644 index 0000000..1563f0c --- /dev/null +++ b/workplans/CB-WP-0042-h2-scoped-problem-stress.md @@ -0,0 +1,161 @@ +--- +id: CB-WP-0042 +kind: product +title: "H2 — scoped problem stress" +status: ready +--- + +# Purpose + +``` +structural tier M (touches a canonical interface: edition loading + becomes variant-parameterised, and the kernel gains + per-Problem state) +chaos d8 = 4 → no override +declared tier M +``` + +**Declaration 2 of chaos window 4.** + +## Why, and it is a direct answer to ours + +We reported that H1-A behaves as a **solve-rate tax**: a flat +1 to every +seat while any Problem is unclaimed, so the pressure scales with the +number of Problems while the intended effect does not. + +`ground-game` responded with **H2 — scoped problem stress**. Unclaimed +Problems now tick **only the seats in their scope**: + +| scope | who takes the +1 | +|---|---| +| `global` | all seats | +| `personal` | the Problem's **owner** | +| `bond` | the owner's **Bond network** — owner plus every seat reachable over Bond edges only, never Rivalry; degree 0 falls back to personal | + +Assigned by hidden priority: **0 (Surface) global, 1 personal, 2 personal, +3 bond, 4 personal** — which is a seat-band dial without a difficulty +card, since 2p never has the bond card in play. + +**It does not stack with H1.** `replaces_experiments: [h1-problem-stress]`, +base is `ground-darvo-r0`, and there is **no ATTACK self-soothe**. + +## Why this is bigger than H1 was + +H1 was two arithmetic deltas on existing state. H2 needs three things we +do not have: + +1. **Variant-scoped edition data.** H2 overrides `Problems.csv` with a + new `stress_scope` column. `edition.rs` `include_str!`s r0's copy at + compile time — **the data is currently a constant, not a parameter.** +2. **Per-Problem ownership**, assigned at setup. That is **new state**, + so it reaches the state hash and every recording. +3. **Bond-network reachability** — a graph traversal over Bond edges + only, with a degree-0 fallback. + +## Task: variant-scoped edition data + +```task +id: CB-WP-0042-T01 +status: todo +priority: high +``` + +**Controls:** +- **the baseline's data is bit-for-bit what it was**, and its state hashes + do not move — the control that protected CB-WP-0038 and the one that + invalidates every prior measurement if it fails; +- the H2 package is **vendored with digests** like every other borrowed + file, and `edition-check` covers it — sibling discovery walks the disk + now, so an undigested file fails (CB-REV-0003 #8); +- **`stress_scope`'s assignment is read from the file, not hardcoded.** + The delta states the priority→scope mapping; **it is the edition's to + state and ours to read**, and F25 exists because we hardcoded numbers + the edition already carried. + +## Task: ownership at setup + +```task +id: CB-WP-0042-T02 +status: todo +priority: high +``` + +`H2-OWN`: ascending hidden priority among in-play non-global Problems, +starting at Lead, stepping clockwise. Global Problems have no owner. + +**Controls:** +- **deterministic**, and asserted so — the delta says "deterministic for + sims" and a seeded-but-unstated order would be untestable; +- **the baseline carries no owners and its hash does not move.** New + state that is `None` under baseline must serialise as it did before, or + every recorded scenario breaks; +- **exactly one owner per eligible Problem**, checked at every seat count, + because the assignment walks two sequences at once and off-by-one is + the obvious failure. + +## Task: scoped pressure + +```task +id: CB-WP-0042-T03 +status: todo +priority: high +``` + +`H2-A`, at Round End, before the clamp and the DARVO arm check — the same +ordering H1-A needed, and the same trap. + +**Controls:** +- **each scope fails on its own**, by mutation: global hitting only the + owner, personal hitting everyone, bond stopping at the owner; +- **`stacking: true` is tested** — two open bond Problems must tick the + network **twice**, and a careless implementation applies it once; +- **Rivalry edges are not traversed.** The delta says Bond only, and a + traversal that follows any relation is the single most likely defect; +- **degree 0 falls back to personal**, which is the branch a test forgets; +- ordering asserted as in CB-WP-0038: pressure before the arm check. + +## Task: SOLVE is not restricted by scope + +```task +id: CB-WP-0042-T04 +status: todo +priority: medium +``` + +`H2-SOLVE`: any seat with a matching suit may claim; the owner need not be +the solver, and altruistic clearing is **intended**. + +**Control:** our engine has no owner concept today, so this is already +true — which makes it a **regression test, not a feature**. T02 adds +ownership, and the risk is that ownership silently becomes a permission. +The test must fail if it does. + +## Task: measure, and report what fails + +```task +id: CB-WP-0042-T05 +status: todo +priority: high +``` + +Same instrument as H1, same panel, both variants in one run. + +**Controls:** +- **greedy and `reactive` both**, because H1's whole verdict turned on + which policy was asked (CB-EV-0031); +- **report against ground-game's own criteria** for H2, from their design + note — **read it, do not reuse H1's §3.2 from memory**; +- **the bond-scope hypothesis is the interesting one and must be measured + directly**: their claim is that a shared tick makes a Bond network + *jointly motivated* to solve that card. Whether a policy that does not + model other seats can express that at all is an open question, and the + honest answer may be "our panel cannot test this hypothesis"; +- **`make panels` runs it**, or the figures come from an ungated binary + again (CB-REV-0002 #7). + +## Not in this workplan + +- **No stacking with H1.** The package forbids it explicitly. +- **No felt-play.** H2's central claim is about *motivation* — a bonded + pair caring about each other's card — and 200-game aggregates measure + dynamics, not that ([`Taxonomy.md`](../specs/Taxonomy.md) §4). From 4b40a537a601559ccc966304c0c4c0a3ff691287 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 8 Aug 2026 15:18:36 +0200 Subject: [PATCH 2/2] CB-WP-0041 T01: perfect recall fails on the projection, holds on the history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The answer is "it depends what you call an information set", and the distinction is the result. 44,938 decision points, random play, 2/3/4/6 seats. Reading A — information set = the seat's current projection, which is what project(Viewer::Player(seat)) returns and what the page renders: 22 violations. Reading B — information set = the seat's observation history, every view seen and action taken in order: 0. The Reading A witness is concrete. Two histories reach a byte-identical view — round 3, Select step, same hand, same claimed Problem — where the seat had played SOLVE then GROUND-OU(protect) in one and SUPPORT then SOLVE in the other. The view does not tell the seat what it did, because our state is a snapshot rather than a history: selections clear each round and effects coincide, so a player cannot reconstruct their own past from the present. In a real game the player's memory supplies it; in the state, nothing does. That is precisely OpenSpiel's ObservationString vs InformationStateString split, arrived at here by measurement rather than read off. project() is an observation, not an information state. So Track B is not closed, it is constrained, and usefully: an extensive-form game built from this engine must key information sets on observation histories, never on project(). Both directions are asserted — Reading B empty AND Reading A non-empty — because if the sample stops finding Reading A violations the conclusion is unsupported and must be re-derived rather than quietly kept. And the check samples, so it can falsify perfect recall and cannot establish it: Reading B's zero means no counterexample was drawn, which is printed as such. Wired into make panels, so it is re-derived by the gate rather than by hand. Co-Authored-By: Claude Opus 5 --- Makefile | 1 + games/ground/examples/perfect-recall.rs | 243 ++++++++++++++++++ gates.toml | 2 +- ...B-WP-0041-the-extensive-form-foundation.md | 39 ++- 4 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 games/ground/examples/perfect-recall.rs diff --git a/Makefile b/Makefile index 8eb0f83..914d2a2 100644 --- a/Makefile +++ b/Makefile @@ -237,6 +237,7 @@ difficulty: panels: @cargo run --release -q -p games-ground --example attack-value @cargo run --release -q -p games-ground --example regulation + @cargo run --release -q -p games-ground --example perfect-recall # CB-WP-0022 T05: the design-finding register, reported over # specs/GroundRules.md. Shows the QUEUE by default; the log of closed diff --git a/games/ground/examples/perfect-recall.rs b/games/ground/examples/perfect-recall.rs new file mode 100644 index 0000000..dd48798 --- /dev/null +++ b/games/ground/examples/perfect-recall.rs @@ -0,0 +1,243 @@ +//! **Does this engine satisfy perfect recall?** (CB-WP-0041 T01) +//! +//! Perfect recall — every player remembers their own past actions and +//! observations — is the assumption **CFR and exploitability both rest +//! on** ([`CB-RES-0009`]). Nobody had checked whether ours holds. +//! +//! Formally: for a seat `i`, any two histories in the same information +//! set of `i` must agree on the sequence of `i`'s own past actions. +//! +//! **What this can and cannot say.** It samples histories and looks for a +//! pair that lands in the same information set with different own-action +//! prefixes. So: +//! +//! > **It can prove perfect recall FAILS. It cannot prove it holds.** +//! +//! A clean run means "no violation found in this sample", which is a +//! weaker statement than "the property holds" and is reported as such. +//! Saying otherwise would be an ACCOUNT failure over an INSTRUMENT that +//! cannot support it ([`Taxonomy.md`] §2.2). +//! +//! [`CB-RES-0009`]: ../../../research/CB-RES-0009-extensive-form-is-the-lingua-franca.md +//! [`Taxonomy.md`]: ../../../specs/Taxonomy.md + +use std::cell::RefCell; +use std::collections::BTreeMap; +use std::rc::Rc; + +use cb_game_runtime::{Project, ScenarioGame, Setup, Viewer}; +use cb_kernel::PlayerId; +use games_ground::bot::{play, Choice, Policy, RandomPolicy}; +use games_ground::{GroundCommand, GroundState}; + +/// One decision point: what the seat could see, and what it had done. +struct Observed { + /// **Reading A** — the information set taken to be the seat's + /// CURRENT projection, which is what `project(Viewer::Player(seat))` + /// returns and what the page renders. + observation: String, + /// **Reading B** — the information set taken to be the seat's whole + /// OBSERVATION HISTORY: every view it has seen and every action it + /// has taken, in order. + /// + /// OpenSpiel draws exactly this distinction — + /// `ObservationString` versus `InformationStateString` — and the + /// second exists because the first does not give perfect recall. + info_state: String, + /// The seat's own past actions, in order — the thing perfect recall + /// says must be determined by the information set. + own_actions: Vec, +} + +/// Wraps a policy and records what each seat saw before it moved. +struct Recorder { + inner: P, + log: Rc>>, + own: Rc>>>, + hist: Rc>>>, +} + +impl Policy for Recorder

{ + fn name(&self) -> &'static str { + "recorder" + } + fn choose( + &mut self, + state: &GroundState, + seat: PlayerId, + legal: &[GroundCommand], + may_pass: bool, + ) -> Choice { + // The information set is what this seat can see — nothing more. + let view = state.project(Viewer::Player(seat)); + let observation = serde_json::to_string(&view).expect("view serialises"); + let own_actions = self.own.borrow().get(&seat).cloned().unwrap_or_default(); + // The observation history: what this seat has seen and done, in + // order, up to and including now. + let mut hist = self.hist.borrow_mut(); + let h = hist.entry(seat).or_default(); + h.push(observation.clone()); + let info_state = h.join("\u{1e}"); + drop(hist); + self.log.borrow_mut().push(( + seat, + Observed { + observation, + info_state, + own_actions, + }, + )); + + let choice = self.inner.choose(state, seat, legal, may_pass); + let taken = match &choice { + Choice::Command(i) => format!("{:?}", legal[*i]), + Choice::Pass => "pass".to_string(), + }; + self.own + .borrow_mut() + .entry(seat) + .or_default() + .push(taken.clone()); + // The action is part of what the seat remembers. + self.hist.borrow_mut().entry(seat).or_default().push(taken); + choice + } +} + +fn main() { + // Random play, because the question is about the SHAPE of the + // information partition and a greedy policy visits a narrow slice of + // it. A policy that always makes the same choice cannot produce two + // histories in one information set with different prefixes. + let seeds = 0..400u64; + let mut points = 0usize; + let mut observation_violations: Vec = Vec::new(); + let mut info_state_violations: Vec = Vec::new(); + let mut witness: Option<(String, Vec, Vec)> = None; + + for players in [2u8, 3, 4, 6] { + // Each reading maps its key to the own-action prefix first + // seen with it. A second, different prefix is a violation. + let mut by_observation: BTreeMap> = BTreeMap::new(); + let mut by_info_state: BTreeMap> = BTreeMap::new(); + + for seed in seeds.clone() { + let Ok(st) = GroundState::setup( + &Setup { + players, + preset: format!("standard-{players}p"), + patch: Default::default(), + }, + seed, + ) else { + continue; + }; + let log = Rc::new(RefCell::new(Vec::new())); + let own = Rc::new(RefCell::new(BTreeMap::new())); + let hist = Rc::new(RefCell::new(BTreeMap::new())); + let mut ps: Vec> = (0..players) + .map(|i| { + Box::new(Recorder { + inner: RandomPolicy::new(seed ^ u64::from(i) ^ 0x9E37), + log: log.clone(), + own: own.clone(), + hist: hist.clone(), + }) as Box + }) + .collect(); + let Ok(_) = play(st, &mut ps) else { continue }; + + for (seat, obs) in log.borrow().iter() { + points += 1; + // The key includes WHO is looking: two seats with + // identical views are different information sets. + let a = format!("{seat:?}|{}", obs.observation); + match by_observation.get(&a) { + None => { + by_observation.insert(a, obs.own_actions.clone()); + } + Some(prior) if *prior == obs.own_actions => {} + Some(prior) => { + if observation_violations.is_empty() { + witness = Some(( + obs.observation.clone(), + prior.clone(), + obs.own_actions.clone(), + )); + } + observation_violations.push(format!("{players}p seed {seed} {seat:?}")); + } + } + + let b = format!("{seat:?}|{}", obs.info_state); + match by_info_state.get(&b) { + None => { + by_info_state.insert(b, obs.own_actions.clone()); + } + Some(prior) if *prior == obs.own_actions => {} + Some(prior) => { + info_state_violations.push(format!( + "{players}p seed {seed} {seat:?}: {prior:?} vs {:?}", + obs.own_actions + )); + } + } + } + } + } + + println!("perfect recall — CB-WP-0041 T01\n"); + println!(" decision points sampled {points}\n"); + println!(" Reading A — information set = the seat's CURRENT projection"); + println!( + " violations {}", + observation_violations.len() + ); + println!(" Reading B — information set = the seat's OBSERVATION HISTORY"); + println!( + " violations {}\n", + info_state_violations.len() + ); + + if let Some((view, a, b)) = &witness { + println!(" Reading A witness — one view, two own-action prefixes:"); + println!(" A: {a:?}"); + println!(" B: {b:?}"); + println!(" shared view: {}\n", &view[..view.len().min(220)]); + } + + // **Reading B is the load-bearing claim.** If observation histories + // do not give perfect recall, no EFG can be built from this engine + // and Track B closes. + assert!( + info_state_violations.is_empty(), + "perfect recall FAILS even under observation histories: {} violation(s). \ + No extensive-form game can be built from this engine, and every \ + equilibrium concept is unsound here.\n{}", + info_state_violations.len(), + info_state_violations.first().cloned().unwrap_or_default() + ); + + // **Reading A failing is the FINDING, not an error.** It is why + // OpenSpiel separates ObservationString from InformationStateString. + assert!( + !observation_violations.is_empty(), + "Reading A found no violation. Either the sample is too small or the \ + projection carries more history than it appears to — either way the \ + conclusion below is unsupported and must be re-derived." + ); + + println!(" RESULT"); + println!(" The current projection is an OBSERVATION, not an information"); + println!( + " state: {} sampled pairs share a view while disagreeing about", + observation_violations.len() + ); + println!(" what the seat itself had done. Perfect recall FAILS on that"); + println!(" reading, and an EFG keyed on `project()` would be unsound."); + println!(); + println!(" Keyed on the observation HISTORY, no violation was found."); + println!(" **That is not a proof** — this check can falsify perfect"); + println!(" recall and cannot establish it. A clean run means no"); + println!(" counterexample was drawn from this sample."); +} diff --git a/gates.toml b/gates.toml index 93116d7..d3501fd 100644 --- a/gates.toml +++ b/gates.toml @@ -178,7 +178,7 @@ id = "CB-REV-0002/7" name = "variant panels" target = "panels" cadence = "all" -checks = "both H1 measurement harnesses run; a short cell fails, and so does a cell whose games did not reach an outcome" +checks = "the measurement harnesses run; a short cell fails, a cell whose games did not reach an outcome fails, and perfect recall is re-derived on both readings" notes = """ Registered because the second adversarial review asked what the harness would report if the work silently stopped, and the answer was "green, and diff --git a/workplans/CB-WP-0041-the-extensive-form-foundation.md b/workplans/CB-WP-0041-the-extensive-form-foundation.md index 13fd553..3fbc691 100644 --- a/workplans/CB-WP-0041-the-extensive-form-foundation.md +++ b/workplans/CB-WP-0041-the-extensive-form-foundation.md @@ -46,7 +46,7 @@ concept if it turns out badly. ```task id: CB-WP-0041-T01 -status: todo +status: done priority: high ``` @@ -69,6 +69,43 @@ information sets* along `h` and `h'` must be identical. reported as plainly as the other outcome. It would be a real finding and would make Track B's adoption unsound as it stands. +**Done 2026-08-08. The answer is "it depends what you call an information +set", and the distinction is the result.** 44,938 decision points, random +play, 2/3/4/6 seats. + +| reading | information set is… | violations | +|---|---|---| +| **A** | the seat's **current projection** — what `project(Viewer::Player(seat))` returns and the page renders | **22** | +| **B** | the seat's **observation history** — every view seen and action taken, in order | **0** | + +**Reading A fails, and the witness is concrete**: two histories reach a +byte-identical view — round 3, Select, same hand, same claimed Problem — +where the seat had played `SOLVE, GROUND—OU(protect)` in one and +`SUPPORT, SOLVE` in the other. **The view does not tell the seat what it +did.** + +**The mechanism is that our state is a snapshot, not a history.** +Selections clear each round and effects coincide, so a player cannot +reconstruct their own past from the present. In a real game the player's +memory supplies it; in the state, nothing does. + +**This is precisely OpenSpiel's `ObservationString` vs +`InformationStateString` split**, arrived at here by measurement rather +than by reading it off. `project()` is an *observation*. + +**So Track B is not closed — it is constrained**, and usefully: + +> **An extensive-form game built from this engine must key information +> sets on observation histories, never on `project()`.** + +**Both directions are asserted.** Reading B empty, *and* Reading A +non-empty — because if the sample stops finding Reading A violations the +conclusion is unsupported and must be re-derived, not quietly kept. + +**What this cannot say.** It samples; it can falsify perfect recall and +cannot establish it. Reading B's zero means *no counterexample was +drawn*, which is weaker than "the property holds" and is printed as such. + ## Task: say precisely what our chance is ```task