Add web game (plan2.md) on the independent runtime, merge-blocking gates
Builds the browser game around the existing Rust runtime: a window into the universe, not a second simulation. Pure std, no external crates. New crates: - protocol: versioned, hashable client/server messages + hand-rolled JSON value and total parser (malformed packet -> Err, never panic). - game_runtime: authoritative match state. Resolves turns through the INDEPENDENT interpreter (runtime_under_test::native_resolve), not the reference engine; filters visibility/knowledge; records and regenerates replays. A match is a pure function of (seed, roster, ordered inputs). - web_assets/web_client: embedded browser client (arena, rune editor, knowledge panels, replay viewer) + static HTTP delivery. - server: std::net HTTP + WebSocket (hand-rolled SHA-1/base64/RFC-6455 framing), turn timer, disconnect handling, panic-proof dispatch, poison-tolerant lock. - web_tests: dependency-free WebSocket test client + Phase H gates. Trust hardening per review: - game_runtime no longer delegates to reference_runtime::execute; it runs the independent interpreter that the runtime-equivalence gate proves correct. - Protocol/socket/replay/visibility/resilience gates are merge-blocking (added to the merge_group-required job in merge-gates.yml): 1k matches/0 drift, 10k fuzz/0 panics, 100 headless socket E2E, 0 hidden-state leaks. - Rendered-browser E2E is marked EXTERNAL-BLOCKED: Playwright runs advisory-only (continue-on-error, artifacts) until CI infrastructure with a browser exists; it is treated as unsatisfied, not green. The headless 100-match gate is labeled protocol-level coverage, not rendered-browser coverage. - README documents the hand-rolled crypto/parser audit risk explicitly. Fixes an integer-overflow panic in observed-volatility inference (i64 sum / abs near i64::MIN) that could poison the server mutex. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
//! Phase H gate: replay determinism. The web CI minimum is "1,000 simulated
|
||||
//! matches" with "0 replay hash mismatches". A match is a pure function of its
|
||||
//! seed, roster, and ordered inputs, so re-running the recorded inputs must
|
||||
//! reproduce the final hash exactly.
|
||||
|
||||
use game_runtime::{replay, run_scripted, solo_roster, duel_roster};
|
||||
use protocol::Action;
|
||||
use world_model::Rng;
|
||||
|
||||
/// Build a varied but deterministic script for a given seed.
|
||||
fn script(seed: u64) -> Vec<Vec<(u32, Action)>> {
|
||||
let mut rng = Rng::derive(seed, "script");
|
||||
let mut turns = Vec::new();
|
||||
for _ in 0..6 {
|
||||
let mut subs = Vec::new();
|
||||
for pid in 1..=2u32 {
|
||||
let a = match rng.below(5) {
|
||||
0 => Action::Move { dx: rng.range_i64(-1, 1) as i32, dy: rng.range_i64(-1, 1) as i32 },
|
||||
1 => Action::Cast,
|
||||
2 => Action::Attack { target: if pid == 1 { 2 } else { 1 } },
|
||||
3 => Action::Inspect { target: if pid == 1 { 2 } else { 1 } },
|
||||
_ => Action::Wait,
|
||||
};
|
||||
subs.push((pid, a));
|
||||
}
|
||||
turns.push(subs);
|
||||
}
|
||||
turns
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_thousand_matches_replay_with_zero_drift() {
|
||||
let mut mismatches = 0u32;
|
||||
for seed in 0..1000u64 {
|
||||
let roster = if seed % 2 == 0 {
|
||||
solo_roster("p")
|
||||
} else {
|
||||
duel_roster("a", "b")
|
||||
};
|
||||
let scripts = script(seed);
|
||||
let (m, log) = run_scripted(seed, &roster, &scripts);
|
||||
let again = replay(seed, &roster, &log.turns);
|
||||
if m.replay.final_hash != again.final_hash {
|
||||
mismatches += 1;
|
||||
}
|
||||
// Per-turn hashes must also agree.
|
||||
for (a, b) in m.replay.turns.iter().zip(again.turns.iter()) {
|
||||
if a.turn_hash != b.turn_hash {
|
||||
mismatches += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(mismatches, 0, "replay hash mismatches across 1000 matches");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn re_executing_same_seed_twice_is_identical() {
|
||||
for seed in [1u64, 7, 99, 12345, 0xdead_beef] {
|
||||
let roster = solo_roster("p");
|
||||
let (a, _) = run_scripted(seed, &roster, &script(seed));
|
||||
let (b, _) = run_scripted(seed, &roster, &script(seed));
|
||||
assert_eq!(a.replay.final_hash, b.replay.final_hash, "seed {seed}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user