diff --git a/Makefile b/Makefile index 914d2a2..8eb0f83 100644 --- a/Makefile +++ b/Makefile @@ -237,7 +237,6 @@ 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 deleted file mode 100644 index dd48798..0000000 --- a/games/ground/examples/perfect-recall.rs +++ /dev/null @@ -1,243 +0,0 @@ -//! **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 d3501fd..93116d7 100644 --- a/gates.toml +++ b/gates.toml @@ -178,7 +178,7 @@ id = "CB-REV-0002/7" name = "variant panels" target = "panels" cadence = "all" -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" +checks = "both H1 measurement harnesses run; a short cell fails, and so does a cell whose games did not reach an outcome" 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 deleted file mode 100644 index 3fbc691..0000000 --- a/workplans/CB-WP-0041-the-extensive-form-foundation.md +++ /dev/null @@ -1,175 +0,0 @@ ---- -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: done -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. - -**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 -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 deleted file mode 100644 index 1563f0c..0000000 --- a/workplans/CB-WP-0042-h2-scoped-problem-stress.md +++ /dev/null @@ -1,161 +0,0 @@ ---- -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).