T04: adversarial review round + survey corrections + ADR-0002 (reimplement, assimilate patterns)

This commit is contained in:
tegwick 2026-07-31 01:25:02 +02:00
parent a7e31d4210
commit 53c1b18ec1
7 changed files with 414 additions and 94 deletions

View file

@ -1,15 +1,15 @@
# CB-RES-0001: game-state kernel
capability: game.kernel.authoritative-state
status: draft # becomes approved only after adversarial review (T04)
tier: L (structural L, chaos roll pending at T04 declaration — see history trail)
status: approved # adversarial review 2026-07-31: challenge + response in history/
tier: L (structural L, chaos d10=9 → no override)
runnable-baseline: invoked — harness in research/CB-RES-0001-harness/boardgame-io/
review-trail: history/260731-game-kernel-{research,challenge,response}.md
Survey of the best existing implementations of a **turn/phase game-state
kernel**: deterministic authoritative state, command → validation → events,
simultaneous commit/reveal, hidden information, replay. Conducted
2026-07-31; research trail in
[history/260731-game-kernel-research.md](../history/260731-game-kernel-research.md).
2026-07-31; revised same day after adversarial review.
---
@ -24,34 +24,42 @@ The most direct comparator: a declarative turn-based game engine.
`phases`, `turn.stages`.
- **Mutation:** moves are reducer functions run through Redux + Immer;
mutate a draft, framework produces immutable next state and appends to an
action **log** (basis for time travel).
action **log** (basis for time travel) and, unless `disableUndo` is set,
an **undo stack** holding a full state snapshot per move.
- **Determinism/replay:** seeded RNG via `random` plugin; log + seed give
replay and time travel. Measured: same seed → identical state hash across
runs; different seed diverges. ✅
- **Hidden information:** `playerView` projection (e.g. `STRIP_SECRET`) —
server strips secret state per player.
server strips secret state per player. *Not exercised by our workload*
(provenance note: commit/reveal shape measured; hidden-info cost not).
- **Simultaneous actions:** `activePlayers` stages give simultaneous move
windows; no built-in cryptographic commit/reveal — commitment is plain
state the server can see (fine for server-authoritative, nothing for
peer settings).
state the server can see.
- **Maturity:** 12.4k GitHub stars, but **inactive** — last npm release
0.50.2 ≈ 4 years ago (Snyk: "maintenance: Inactive").
- **Measured performance** (our harness, synthetic 3-player GROUND-shaped
commit/reveal workload, Node v24, this machine `bnt-lap001`):
0.50.2 ≈ 4 years ago (checkable on npm; Snyk lists maintenance Inactive).
- **Measured performance** (harness, synthetic 3-player GROUND-shaped
commit/reveal workload, Node v24, `bnt-lap001`; run-to-run variance on
this machine is material — ±25% observed on identical configs):
| applied moves | moves/s | elapsed | RSS |
| applied moves | default (moves/s) | `disableUndo` (moves/s) | RSS after run (isolated) |
|---:|---:|---:|---:|
| 5,000 | 1,930 | 2.6 s | 224 MB |
| 10,000 | 1,605 | 6.2 s | 229 MB |
| 20,000 | 870 | 23.0 s | 271 MB |
| 100,000 | did not finish in 300 s | — | — |
| 5,000 | 1,4391,930 | 1,118 | ~100 MB |
| 10,000 | 1,605 | 1,076 | 132 MB |
| 20,000 | 870 | 942 | 153 MB |
| 40,000 | — | 733 | 232 MB |
| 100,000 | DNF @300 s | DNF @240 s | — |
**Per-move cost grows with history length** (log accumulation + state
pipeline): throughput halves as move count doubles — superlinear total
cost. This is architectural (unbounded redux log per client), not a
tuning artifact.
**Two degradation mechanisms, separately attributed:** (a) the undo
stack — an O(n) per-move array spread with full state snapshots — is the
dominant superlinear term at small N and is disable-able via the
documented `disableUndo` flag; (b) with undo disabled, throughput still
falls 34% from 5k→40k and 100k still does not finish, consistent with
unbounded client log/deltalog accumulation, which has no client-side off
switch in 0.50.2. Even the best configuration degrades with history.
(`updatePlayerID` in the timed loop measured separately: ~2 µs/call,
<0.5% contamination.)
- **Weight:** 120 transitive npm packages, 37 MB `node_modules`, core
package 3.9 MB.
package 3.9 MB (independently re-verified in review).
### 2. Tabletop Simulator scripting model (Lua) — cited
@ -65,50 +73,75 @@ semantics*, not a rules kernel.
physics is primary (sandbox mode in Clay-Borg terms).
- **Determinism/replay:** none. Hidden info via hand zones (engine feature,
not a projection model).
- Valuable as the pattern source for object-attached behavior and hand
zones; architecturally the anti-model for an authoritative kernel.
- Pattern source for object-attached behavior and hand zones;
architecturally the anti-model for an authoritative kernel.
### 3. Rune SDK (JS) — cited
Modern (active, 20242026) deterministic multiplayer engine for casual web
games.
Modern (active) deterministic multiplayer engine for casual web games.
- **Data model/mutation:** pure `logic.js` — game state + action functions,
statically checked for nondeterminism (mutation escape, `Math.random`
patched deterministic).
- **Sync:** predict-rollback: all clients + server simulate the same
deterministic logic; server authoritative, clients predict. Strongest
determinism *discipline* of the candidates — enforced by tooling, not
convention.
- **Limits:** platform-bound (Rune's hosted app ecosystem), not an
embeddable open kernel; no phase/stage framework, hidden-information
projection, or event-sourced replay surface comparable to boardgame.io.
deterministic logic. Strongest determinism *discipline* of the
candidates — enforced by tooling, not convention.
- **Limits:** platform-bound (Rune's hosted ecosystem), not an embeddable
open kernel; no phase framework, hidden-information projection, or
event-sourced replay surface.
### 4. Event-sourcing kernels (Rust `cqrs-es` pattern / EventStoreDB) — cited
### 4. OpenSpiel (C++/Python, DeepMind) — cited *(added after review)*
The general-purpose form of our mutation pipeline (command → validate →
events → fold).
Research games kernel explicitly built for **simultaneous-move and
imperfect-information games**.
- Aggregates validate commands and emit events; state is a fold over the
append-only log; snapshots bound replay cost. Replay/audit are native.
- **Performance (cited/estimated):** in-process Rust event application is
memory-bandwidth-bound — order 10⁵10⁶ small events/s per core is the
commonly reported range for fold-style aggregates; dedicated stores
(EventStoreDB) sustain tens of thousands of appends/s over the network.
No game semantics: phases, visibility, simultaneity all DIY.
- Deterministic state; serializable; replay via action histories;
information-state abstractions for imperfect information; large game
library. Active. As an in-process C++ kernel it is also a credible D3
comparator (unmeasured here — open follow-up).
- **Limits:** research-oriented — no client visibility *projection* layer,
no networking/session model, no snapshot format, monolithic C++/Python
build rather than an embeddable capability boundary. The semantics
overlap with our kernel is real; the production layer is absent.
### 5. bevy_ecs 0.x (Rust) — cited
### 5. Ludii / GGP-GDL lineage (JVM) — cited *(added after review)*
Archetypal ECS; the world/spatial layer in our architecture, surveyed as a
kernel candidate for completeness.
General game systems whose core value is **ease of rule specification**
(the D1 dimension): games written as ludemes (Ludii, 1,000+ games) or GDL
rules, executed by a general engine.
- Cache-friendly iteration: millions of entity-component accesses per frame
(cited from Bevy's own benches; ns-scale per component access).
- No authoritative command/event pipeline, no replay, no hidden-info
projection; determinism requires care (system ordering, hash maps).
Confirms the ADR-anticipated split: ECS for world representation,
**typed aggregates for the semantic kernel** — not a competitor on this
capability.
- Proof that rule description languages can cover enormous game spaces
compactly; the D1 literature our kernel spec should be checked against.
- **Limits:** research/archival focus; no production multiplayer stack,
no hidden-info projection for clients, JVM-bound; performance oriented
to AI playouts, not authoritative session serving.
### 6. Event-sourcing kernels (Rust `cqrs-es` pattern / EventStoreDB) — cited
Aggregates validate commands and emit events; state is a fold over the
append-only log; snapshots bound replay cost. Replay/audit native.
**Performance: estimated** — order 10⁵10⁶ small events/s per core for
in-process fold-style application; dedicated stores sustain tens of
thousands of appends/s over the network. *No reproducible citation held;
treated as directional only.* No game semantics.
### 7. bevy_ecs (Rust) — cited
Archetypal ECS; the world/spatial layer in our architecture. Millions of
entity-component accesses per frame (Bevy's published benches). No
authoritative command/event pipeline, replay, or hidden-info projection —
confirms the split: ECS for world representation, typed aggregates for the
semantic kernel.
### Secondary references (not fully surveyed)
- **Board Game Arena Studio** — dominant commercial turn-based rules
framework (PHP): server-authoritative state machine, hidden info, full
replay. Closed platform; stronger commercial reference than TTS.
- **Colyseus** — active JS authoritative-state multiplayer server; room
state sync without game-rules semantics (no phases/legality/replay
framework). The "active JS" counterweight to inactive boardgame.io.
- **Vassal** — module player for humans; no programmable rules kernel.
---
@ -116,39 +149,53 @@ kernel candidate for completeness.
| Dimension | Baseline holder | Metric | Value | Provenance |
|---|---|---|---|---|
| D1 ease of specification | boardgame.io | LOC to express the synthetic 3p commit/reveal game (declarative object) | ~45 LOC | measured (harness bench.js game def) |
| D1 | — (no candidate) | rule-to-scenario traceability (M-D1-COV) | 0 % — none of the candidates link rules to tests | measured/observed |
| D1 ease of specification | boardgame.io | LOC to express the synthetic 3p commit/reveal game (declarative object) | ~36 LOC | measured (bench.js); gameable — secondary indicator |
| D1 (qualitative bar) | Ludii/GDL | rule-description-language coverage of large game spaces | qualitative | cited |
| D2 implementation weight | boardgame.io | transitive deps / install size | 120 pkgs / 37 MB | measured |
| D3 throughput | boardgame.io | applied moves/s, 3p workload @5k moves | 1,930 moves/s | measured, bnt-lap001 |
| D3 scaling | boardgame.io | throughput @20k vs @5k moves | 0.45× (superlinear cost) | measured, bnt-lap001 |
| D3 memory | boardgame.io | RSS @5k moves | 224 MB | measured, bnt-lap001 |
| D3 ceiling (adjacent layer) | in-proc event-sourcing (Rust) | events applied/s per core | ~10⁵10⁶ | cited/estimated — directional, caps our verdict at parity unless we measure a Rust comparator |
| D3 throughput | boardgame.io (`disableUndo`, best config) | applied moves/s, 3p workload @5k moves | ~1,100 (default config 1,4001,900; ±25% machine variance) | measured, bnt-lap001 |
| D3 scaling | boardgame.io (`disableUndo`, best config) | throughput @40k vs @5k moves | 0.66× (default config: 0.45× @20k; both DNF @100k) | measured, bnt-lap001 |
| D3 memory | boardgame.io | RSS after run, isolated process | ~100 MB @5k → 232 MB @40k | indicative (raw RSS incl. Node baseline) |
| D4 optionality | Rune | determinism enforced by tooling | static nondeterminism checks | cited |
| D4 | boardgame.io | replaceability of subsystems | plugin API, but JS-ecosystem-locked; no null/reference impl pattern | observed |
**Headline benchmark-to-beat for the Clay-Borg kernel (proposed for the
ADR):** ≥ 100,000 applied events/s sustained with **flat scaling** (throughput
@100k events within 10% of @5k), deterministic replay bit-identical, on the
same machine and workload shape as the boardgame.io harness.
Observation (not a baseline row): no surveyed candidate links numbered
rules to test scenarios (M-D1-COV-style traceability); any nonzero
coverage exceeds the field, so this is a direction, not a bar.
**Headline target for the Clay-Borg kernel (stipulated engineering target,
not competitor-derived):** ≥ 100,000 applied events/s sustained with **flat
scaling** (throughput @100k events within 10% of @5k), deterministic replay
bit-identical, same machine and workload shape as the harness. The
event-sourcing ceiling row is an estimate; until a Rust comparator is
measured locally (open follow-up), evidence rows leaning on it cap at
parity per MetricsAndScenarios §3.
## Verdict
- **Per dimension:** D1 — boardgame.io's declarative game object is the bar
to match; nobody has rule-to-scenario traceability (open surpass lane).
D2 — boardgame.io's 120-dep footprint is beatable by an order of
magnitude in Rust. D3 — boardgame.io is slow *and* degrades; the honest
comparison class is in-proc event sourcing (10⁵10⁶/s), and our D3
advantage over boardgame.io is partly language choice — the meaningful
target is **flat scaling + the 100k/s floor**, not the ×50 headline.
D4 — Rune's tooling-enforced determinism is the discipline to assimilate;
no candidate offers a null/reference/optimized port pattern.
- **What none of them do:** combine deterministic replayable authoritative
state, first-class simultaneous commit/reveal with hidden-information
projection, flat per-event cost with snapshots, and an embeddable
language-portable boundary. That combination is the surpass opportunity.
- **Risks in these baselines:** the boardgame.io harness measures the
headless *client* pipeline (includes subscription/log overhead — canonical
usage, but a bare server-side master could differ); the event-sourcing
numbers are cited, not locally measured; TTS and Rune numbers are
qualitative. The D3 event-sourcing row is directional and caps related
evidence verdicts at parity per MetricsAndScenarios §3.
- **D1:** boardgame.io's declarative game object is the bar to match for
compactness; Ludii/GDL is the literature bar for rule-language
expressiveness. Rule-to-scenario traceability is an open lane no one
occupies.
- **D2:** boardgame.io's 120-dep/37 MB footprint is beatable by an order
of magnitude.
- **D3:** boardgame.io degrades with history even in its best measured
configuration; our advantage over it is partly language choice, so the
meaningful commitment is the stipulated flat-scaling + 100k/s target,
not the multiplier over boardgame.io.
- **D4:** Rune's tooling-enforced determinism is the discipline to
assimilate; no candidate offers a null/reference/optimized port pattern
or a WIT-style embeddable boundary.
- **The surpass opportunity, stated precisely:** OpenSpiel covers
simultaneous-move + imperfect-information *semantics* with deterministic
replay; boardgame.io covers declarative rules + client projection +
networking; Rune covers enforced determinism; event sourcing covers
flat-cost replay with snapshots. **No candidate combines** the semantic
coverage with a production projection/networking layer, snapshot-bounded
flat-cost replay, and an embeddable, language-portable capability
boundary. That combination — not raw speed over an inactive JS library —
is what the Clay-Borg kernel should be built to demonstrate.
- **Risks that remain:** event-sourcing D3 row unsourced (estimate;
parity-cap active); OpenSpiel unmeasured locally; harness measures the
headless client pipeline (canonical usage — a bare server Master could
differ); ±25% run-to-run variance on this machine; hidden-info cost
unmeasured.

View file

@ -10,6 +10,8 @@ const { Stage } = require('boardgame.io/core');
const crypto = require('crypto');
const NUM_MOVES = parseInt(process.argv[2] || '100000', 10);
const DISABLE_UNDO = process.argv.includes('--disable-undo');
const SINGLE_RUN = process.argv.includes('--single-run'); // isolated memory measurement
const NUM_PLAYERS = 3;
const SyntheticGround = {
@ -51,7 +53,7 @@ const SyntheticGround = {
function makeClient(seed) {
return Client({
game: { ...SyntheticGround, seed },
game: { ...SyntheticGround, seed, disableUndo: DISABLE_UNDO },
numPlayers: NUM_PLAYERS,
playerID: '0',
});
@ -89,13 +91,14 @@ function run(seed, numMoves, collectHash) {
}
// --- throughput ---
const warm = run(42, Math.min(NUM_MOVES, 5000), false); // warmup/JIT
if (!SINGLE_RUN) run(42, Math.min(NUM_MOVES, 5000), false); // warmup/JIT
const main = run(42, NUM_MOVES, true);
const memAfterMain = process.memoryUsage();
// --- determinism: same seed twice, different seed once ---
const detA = run(7, 4000, true);
const detB = run(7, 4000, true);
const detC = run(8, 4000, true);
const detA = SINGLE_RUN ? {hash: 'skipped'} : run(7, 4000, true);
const detB = SINGLE_RUN ? {hash: 'skipped'} : run(7, 4000, true);
const detC = SINGLE_RUN ? {hash: 'x'} : run(8, 4000, true);
// --- memory ---
const mem = process.memoryUsage();
@ -112,5 +115,8 @@ const result = {
determinism_diff_seed_differs: detA.hash !== detC.hash,
rss_mb: Math.round(mem.rss / 1048576),
heap_mb: Math.round(mem.heapUsed / 1048576),
rss_after_main_mb: Math.round(memAfterMain.rss / 1048576),
disable_undo: DISABLE_UNDO,
single_run: SINGLE_RUN,
};
console.log(JSON.stringify(result, null, 2));

View file

@ -4,13 +4,68 @@
"machine": "bnt-lap001 (WSL2)",
"recorded": "2026-07-31",
"workload": "synthetic-ground 3p commit/reveal (bench.js)",
"runs": [
{"applied_moves": 5000, "moves_per_sec": 1930, "elapsed_ms": 2590, "rss_mb": 224},
{"applied_moves": 10000, "moves_per_sec": 1605, "elapsed_ms": 6232, "rss_mb": 229},
{"applied_moves": 20000, "moves_per_sec": 870, "elapsed_ms": 22978, "rss_mb": 271},
{"applied_moves": 100000, "note": "did not finish within 300s; killed"}
],
"determinism_same_seed": true,
"determinism_diff_seed_differs": true,
"dependency_stats": {"transitive_packages": 120, "node_modules_size": "37M", "core_package_size": "3.9M"}
}
"dependency_stats": {
"transitive_packages": 120,
"node_modules_size": "37M",
"core_package_size": "3.9M"
},
"runs_default_config": [
{
"applied_moves": 5000,
"moves_per_sec": 1930,
"elapsed_ms": 2590,
"rss_mb": 224
},
{
"applied_moves": 10000,
"moves_per_sec": 1605,
"elapsed_ms": 6232,
"rss_mb": 229
},
{
"applied_moves": 20000,
"moves_per_sec": 870,
"elapsed_ms": 22978,
"rss_mb": 271
},
{
"applied_moves": 100000,
"note": "did not finish within 300s; killed"
}
],
"runs_disable_undo": [
{
"applied_moves": 5000,
"moves_per_sec": 1118,
"rss_after_main_mb": 100
},
{
"applied_moves": 10000,
"moves_per_sec": 1076,
"rss_after_main_mb": 132
},
{
"applied_moves": 20000,
"moves_per_sec": 942,
"rss_after_main_mb": 153
},
{
"applied_moves": 40000,
"moves_per_sec": 733,
"elapsed_ms": 54595,
"rss_after_main_mb": 232
},
{
"applied_moves": 100000,
"note": "did not finish within 240s (single-run mode); killed"
}
],
"controls": {
"with_undo_5k_recheck_moves_per_sec": 1439,
"run_to_run_variance_note": "\u00b125% observed on identical config (1930 vs 1439 @5k with undo)",
"updatePlayerID_calls_per_sec": 466508
},
"revision": "2026-07-31 post-adversarial-review: added disableUndo runs, isolated memory, controls"
}