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:
@@ -134,3 +134,93 @@ cargo run --release -p replay_corpus --bin freeze -- 10000
|
||||
Everything is seed-derived and integer-only (SplitMix64 RNG, FNV-1a content
|
||||
hashing, wrapping/guarded arithmetic). No floating point enters a canonical
|
||||
hash, so replay is bit-exact across machines and runs. No external crates.
|
||||
|
||||
## The web game (plan2.md)
|
||||
|
||||
A browser game is built **around** the existing runtime — it is a playable
|
||||
window into the Rust universe, never a second simulation. The browser sends only
|
||||
*intent*; the server is the sole authority; every rune program executes through
|
||||
the **independent** interpreter (`runtime_under_test::native_resolve`) against
|
||||
the shared world. The game deliberately does **not** call the reference engine —
|
||||
the interpreter it uses is the one the runtime-equivalence gate proves correct
|
||||
(with a negative control proving that gate can fail). Same constraints as the
|
||||
rest of the repo: pure `std`, no external crates (the WebSocket server
|
||||
hand-rolls SHA-1, base64, and RFC 6455 framing; JSON is hand-rolled with a total
|
||||
parser).
|
||||
|
||||
> Audit note: the hand-rolled SHA-1 / base64 / RFC-6455 framing and JSON parser
|
||||
> are checked against published test vectors (RFC 6455 §1.3 accept key, SHA-1
|
||||
> "abc", base64 length cases) and a fuzz gate, but they are bespoke
|
||||
> cryptographic/parsing code and carry audit risk relative to a reviewed
|
||||
> library. They exist to honor the repo's no-external-crates rule; a future
|
||||
> hardening pass could swap in vetted implementations behind the same interface.
|
||||
|
||||
```
|
||||
Rust runtime → game_runtime (authority) → protocol (WS messages) → server → browser
|
||||
```
|
||||
|
||||
| Crate | Role |
|
||||
|-------|------|
|
||||
| `protocol` | Versioned, hashable, **total-decode** client/server messages + JSON value/parser. A malformed packet yields `Err`, never a panic. |
|
||||
| `game_runtime` | Authoritative match state. Resolves turns through the **independent interpreter** (`runtime_under_test`, not the reference engine), filters visibility/knowledge, records + regenerates replays. A match is a pure function of `(seed, roster, ordered inputs)`. |
|
||||
| `web_assets` | The embedded browser client (HTML/CSS/JS): arena, rune editor, domain/knowledge panels, replay viewer. |
|
||||
| `web_client` | Static-asset HTTP delivery (keeps raw assets separate from framing). |
|
||||
| `server` | `std::net` HTTP + WebSocket server: turn timer, action collection, disconnect handling, panic-proof dispatch. |
|
||||
| `web_tests` | A dependency-free WebSocket test client + the Phase H gates. |
|
||||
|
||||
### Running it
|
||||
|
||||
```bash
|
||||
cargo run --release -p server --bin magicka-server # serve on 127.0.0.1:8080
|
||||
# then open http://127.0.0.1:8080 in a browser
|
||||
MAGICKA_ADDR=0.0.0.0:9000 MAGICKA_TURN_MS=8000 cargo run --release -p server --bin magicka-server
|
||||
```
|
||||
|
||||
Join is immediate (1 player + a training dummy). A duel shares a match by id:
|
||||
two browsers that `JoinMatch` the same `match_id` take slots 1 and 2.
|
||||
|
||||
### Web CI gates (Phase H)
|
||||
|
||||
These gates are **merge-blocking**: they run inside the merge-required job in
|
||||
`.github/workflows/merge-gates.yml` (and as fast PR feedback in
|
||||
`web-gates.yml`). They are the Rust suite in `crates/web_tests`, run with
|
||||
`cargo test -p web_tests`:
|
||||
|
||||
| Gate | Test | Minimum | Status |
|
||||
|------|------|---------|--------|
|
||||
| Replay determinism | `determinism.rs` | 1,000 simulated matches, **0 hash mismatches** | merge-blocking |
|
||||
| Protocol fuzz | `fuzz.rs` | 10,000 fuzz cases, **0 panics** (+ a live server survives a malformed-packet burst) | merge-blocking |
|
||||
| End-to-end matches | `e2e.rs` | **100** full matches over real sockets; recorded replay reproduces every live per-turn hash | merge-blocking |
|
||||
| Hidden-state leaks | `visibility.rs` | **0 leaks** — no client-bound frame carries a hidden key; redaction counts every withheld value | merge-blocking |
|
||||
| Disconnect / timer edges | `resilience.rs` | mid-match disconnect does not corrupt the match; wrong-turn / late submits are rejected deterministically | merge-blocking |
|
||||
| Rendered-browser E2E | `e2e/specs/play.spec.js` | a real browser joins, casts, and replays a match | **external-blocked (advisory only)** |
|
||||
|
||||
Scope honesty — two distinct things, not conflated:
|
||||
|
||||
- The "100 E2E matches" merge-blocking gate drives the full
|
||||
HTTP→WebSocket→protocol→runtime path **headlessly over real sockets**. This is
|
||||
protocol-level coverage. It is **not** rendered-browser coverage and is not
|
||||
claimed as such.
|
||||
- Rendered-browser coverage is **blocked on CI infrastructure**: this CI has no
|
||||
real browser, so the Playwright suite under `crates/web_tests/e2e/` cannot be
|
||||
merge-blocking yet. It runs **advisory-only** (`continue-on-error`) in the
|
||||
`rendered-browser-e2e` job and uploads its report as an artifact. Until a CI
|
||||
runner with a browser exists, rendered-browser E2E is treated as
|
||||
**unsatisfied**, not green. Run it locally with:
|
||||
|
||||
```bash
|
||||
cd crates/web_tests/e2e && npm install && npx playwright install chromium && npm test
|
||||
```
|
||||
|
||||
### Acceptance criteria mapping (plan2.md)
|
||||
|
||||
| Criterion | Where it holds |
|
||||
|-----------|----------------|
|
||||
| A player can join a browser match | `server` join + `web_assets` client; `e2e.rs::single_match_full_playthrough` |
|
||||
| A turn timer runs | `server` timer thread; client header countdown |
|
||||
| Inspect / move / attack / cast | `Action` in `protocol`; `game_runtime::apply_action` |
|
||||
| Rune programs execute only on the server | `game_runtime` is the only caller of the interpreter (`runtime_under_test::native_resolve`); client never imports `EngineConfig` (asserted in `web_assets`) |
|
||||
| Results return as filtered observations | `VisibleWorldSnapshot`; `visibility.rs` |
|
||||
| Replay can reproduce the match | `game_runtime::replay`; `determinism.rs`, `e2e.rs` |
|
||||
| Browser cannot alter hidden truth | intent-only protocol; `visibility.rs` leak gate |
|
||||
| CI proves protocol, replay, visibility, authority | merge-blocking gates in `merge-gates.yml` (+ `web-gates.yml`); rendered-browser E2E remains external-blocked |
|
||||
|
||||
Reference in New Issue
Block a user