Spaces:
Runtime error
Runtime error
Commit ·
efab165
1
Parent(s): f885bcf
docs(web): interactive color-grid web play — brainstormed design (SSOT)
Browse filesstdlib-only http.server + single index.html (zero new deps); stepwise
InteractiveSession pinned to SessionRunner by a golden equivalence test;
fairness invariant (live=grid only, review=full disclosure); always-saved
trace compatible with proteus compare.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs/superpowers/specs/2026-06-02-proteus-web-interactive-play-design.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PROTEUS — Web Interactive Play (color-grid) — Design
|
| 2 |
+
|
| 3 |
+
> Immutable design (SSOT). Brainstormed 2026-06-02. Implements the spec §12
|
| 4 |
+
> deferred "web UI for human play". Companion to the CP5 human-play / viz design
|
| 5 |
+
> (`2026-06-01-proteus-cp5-viz-humanplay-design.md`).
|
| 6 |
+
|
| 7 |
+
## 1. Goal & scope
|
| 8 |
+
|
| 9 |
+
Let a person play the currently-implemented PROTEUS arena (`predator_evade`,
|
| 10 |
+
survival motive) **interactively in a browser** with a **color-grid**
|
| 11 |
+
visualization (not the CLI ASCII view), and **always persist a trace** that is
|
| 12 |
+
schema-identical to LLM/CLI traces so the play feeds `proteus compare` as a
|
| 13 |
+
human baseline.
|
| 14 |
+
|
| 15 |
+
**Decisions locked during brainstorming:**
|
| 16 |
+
|
| 17 |
+
- **Purpose = C** — play-centric UX, but every game **always** saves a trace to
|
| 18 |
+
`runs/*.jsonl` compatible with `proteus compare`.
|
| 19 |
+
- **Config = A** — the browser **start screen** picks scenario / difficulty /
|
| 20 |
+
seed / play-turns / probe; the server stays up across many games.
|
| 21 |
+
- **Stack = A** — **standard library only** (`http.server`) + a **single static
|
| 22 |
+
`index.html`** (vanilla JS, no build step, no CDN). **Zero new dependencies**;
|
| 23 |
+
the `.venv` offline/SDK-free discipline is preserved.
|
| 24 |
+
- **Session driving = Approach 2** — a stepwise `InteractiveSession` (no
|
| 25 |
+
threads), pinned to `SessionRunner` by a golden equivalence test.
|
| 26 |
+
|
| 27 |
+
**Out of scope (YAGNI):** multi-user / auth / persistence DB, the deprecated
|
| 28 |
+
`interface/backend` leaderboard (`arena.*`) stack, live truecolor for LLM runs,
|
| 29 |
+
a second scenario (still single-scenario per the current slice), LLM-as-judge.
|
| 30 |
+
|
| 31 |
+
## 2. Fairness invariant (carried from CP5)
|
| 32 |
+
|
| 33 |
+
The LLM never sees `reward` / `optimal_action` / `habit_action` live. Therefore:
|
| 34 |
+
|
| 35 |
+
- **Live view = grid + available actions only** (fair — the human reads what the
|
| 36 |
+
model reads, just in color instead of ASCII).
|
| 37 |
+
- **Post-game review = full disclosure** — per-turn action vs optimal/habit,
|
| 38 |
+
reward trajectory, and the 8 metrics, shown only once `outcome` is set.
|
| 39 |
+
|
| 40 |
+
Breaking this (showing reward/optimal live) would invalidate the human baseline,
|
| 41 |
+
so it is a design-level constraint, not a UI nicety.
|
| 42 |
+
|
| 43 |
+
## 3. Components
|
| 44 |
+
|
| 45 |
+
```
|
| 46 |
+
proteus/runtime/interactive.py # InteractiveSession: state()/step()/finish(); drives engine directly, no threads
|
| 47 |
+
proteus/runtime/session.py # refactor: extract shared build + per-turn body + finalize helpers (pure extraction)
|
| 48 |
+
proteus/web/__init__.py
|
| 49 |
+
proteus/web/server.py # stdlib http.server routing + in-memory session registry + JSON I/O
|
| 50 |
+
proteus/web/static/index.html # single file: color grid + direction buttons/keyboard + start screen + end review
|
| 51 |
+
proteus/web/__main__.py # `python -m proteus.web` launcher (host/port flags)
|
| 52 |
+
tests/runtime/test_interactive_equivalence.py # golden: same actions -> SessionTrace == SessionRunner(HumanAgent)
|
| 53 |
+
tests/web/test_server.py # route handlers called directly (no socket): happy + error paths, serialization
|
| 54 |
+
tests/web/test_import_safety.py # importing proteus.web pulls no matplotlib / network / provider SDK
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
New runtime dependencies: **none**. `.venv` unchanged.
|
| 58 |
+
|
| 59 |
+
### 3.1 Shared extraction (improve-the-code-we-touch)
|
| 60 |
+
|
| 61 |
+
To avoid divergence between `SessionRunner` and `InteractiveSession`, extract
|
| 62 |
+
the currently-inline logic into shared, pure helpers reused by both:
|
| 63 |
+
|
| 64 |
+
- `build_session(scenario_name, seed, difficulty, play_turns) -> (scenario, game, cut_frames)`
|
| 65 |
+
— the body of `SessionRunner._build_and_replay_cut` (build + Cut pre-roll +
|
| 66 |
+
the "Cut must not terminate" guard).
|
| 67 |
+
- `play_one_turn(scenario, game, agent_action, *, use_probe, ...) -> TurnTrace`
|
| 68 |
+
— the per-turn body (optimal/habit/positions → apply → `step_reward` →
|
| 69 |
+
TurnTrace). `SessionRunner` gets the action from `agent.act()`;
|
| 70 |
+
`InteractiveSession` gets it from the HTTP request. The shared helper takes the
|
| 71 |
+
**already-decided action** plus the pre-move answer-key/position computation so
|
| 72 |
+
both callers produce identical TurnTraces.
|
| 73 |
+
- `finalize(scenario_name, seed, difficulty, play_turns, turns, game, scenario,
|
| 74 |
+
motive_category, model) -> SessionTrace` — the body after the loop
|
| 75 |
+
(`optimal_rollout` + `compute_metrics` + `SessionTrace` construction).
|
| 76 |
+
|
| 77 |
+
This is a **pure extraction**: `SessionRunner.run()` becomes
|
| 78 |
+
`build → loop(play_one_turn) → finalize`, with **no behavior change**, pinned by
|
| 79 |
+
the existing 127 tests. `InteractiveSession` calls the same three helpers.
|
| 80 |
+
|
| 81 |
+
## 4. InteractiveSession (`proteus/runtime/interactive.py`)
|
| 82 |
+
|
| 83 |
+
Holds `scenario`, `game`, `cut_frames`, `play_turns`, `use_probe`,
|
| 84 |
+
`turns: list[TurnTrace]`, `seed`, `difficulty`, `scenario_name`. No threads.
|
| 85 |
+
|
| 86 |
+
- `__init__(scenario_name, *, difficulty, seed, play_turns, use_probe=False,
|
| 87 |
+
motive_category="survival")` → calls `build_session(...)`.
|
| 88 |
+
- `state() -> dict` — the JSON `state` object (see §6). Includes `cut_frames`
|
| 89 |
+
(int grids) only while `phase == "cut_intro"` / first fetch; `review` only when
|
| 90 |
+
`done`.
|
| 91 |
+
- `step(action) -> dict` — validates `action ∈ _ACTIONS`; if the game is already
|
| 92 |
+
terminal raises `SessionFinishedError`; computes pre-move answer keys
|
| 93 |
+
(optimal/habit) + positions, calls `play_one_turn(...)`, appends the TurnTrace,
|
| 94 |
+
re-checks terminal; returns the new `state()`.
|
| 95 |
+
- `finish() -> SessionTrace` — requires terminal **or** budget exhausted; calls
|
| 96 |
+
`finalize(...)`; idempotent (returns the cached trace if already finalized).
|
| 97 |
+
- Helpers: `_grid_to_list(np_grid) -> list[list[int]]` (int cells),
|
| 98 |
+
`_is_done() -> bool` (`game.eliminated or game.survived or len==play_turns`).
|
| 99 |
+
|
| 100 |
+
`model` on the emitted trace = `"human"` (HumanAgent parity; no `_provider`).
|
| 101 |
+
|
| 102 |
+
## 5. HTTP API (stdlib `http.server`, JSON)
|
| 103 |
+
|
| 104 |
+
| Method / path | Request body | Response | Notes |
|
| 105 |
+
|---|---|---|---|
|
| 106 |
+
| `GET /` | — | `index.html` | single page (text/html) |
|
| 107 |
+
| `GET /config` | — | `{scenarios, difficulties, color_map, default_seed}` | fills start screen; `color_map` sent once (int→hex) |
|
| 108 |
+
| `POST /session` | `{scenario, difficulty, seed, play_turns, probe}` | `{session_id, state}` | builds InteractiveSession; first `state` carries `cut_frames` |
|
| 109 |
+
| `GET /session/{id}` | — | `{state}` | poll / recover current state |
|
| 110 |
+
| `POST /session/{id}/act` | `{action}` | `{state}` | one turn; invalid action → 400 |
|
| 111 |
+
| `POST /session/{id}/finish` | — | `{trace_path, metrics}` | requires done; appends trace + returns metrics |
|
| 112 |
+
|
| 113 |
+
Routing is a small `BaseHTTPRequestHandler` with a path-prefix dispatch table;
|
| 114 |
+
JSON read via `Content-Length`, written with `application/json`. A module-level
|
| 115 |
+
`dict[str, InteractiveSession]` registry (in-memory, local single-user). Static
|
| 116 |
+
`index.html` read from `proteus/web/static/` (no directory traversal — only the
|
| 117 |
+
one fixed file is served).
|
| 118 |
+
|
| 119 |
+
## 6. State / data flow
|
| 120 |
+
|
| 121 |
+
`state` object (live view = **fair**, no reward/optimal):
|
| 122 |
+
|
| 123 |
+
```json
|
| 124 |
+
{ "phase": "cut_intro|play|done",
|
| 125 |
+
"turn_idx": 3, "play_turns": 10,
|
| 126 |
+
"grid": [[0,0,5,...], ...],
|
| 127 |
+
"cut_frames": [[[...]], ...],
|
| 128 |
+
"legend": {"5": "#", "6": "A", "8": "B"},
|
| 129 |
+
"actions": ["up","down","left","right","stay"],
|
| 130 |
+
"outcome": null,
|
| 131 |
+
"review": null }
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
- `grid` = `game.current_grid()` as a 2D `int` list; the client colors each cell
|
| 135 |
+
via `/config.color_map` (int→hex from `proteus.arc_grid.rendering.COLOR_MAP`).
|
| 136 |
+
- `cut_frames` = the Cut pre-roll as int grids (present on the first state only);
|
| 137 |
+
the client animates it once (~0.3 s/frame) before live play.
|
| 138 |
+
- `legend` = scenario int→symbol (auxiliary text only; color is primary).
|
| 139 |
+
- `review` (only when `done`): `{turns:[{turn_idx, action, motive_action,
|
| 140 |
+
habit_action, reward, is_diagnostic, was_congruent}], metrics:{...8...},
|
| 141 |
+
outcome, trace_path}`.
|
| 142 |
+
|
| 143 |
+
Flow: `POST /session` → build + Cut → first `state` (with `cut_frames`). Browser
|
| 144 |
+
animates Cut, switches to live. Each `POST /act` → `step(action)` → new `state`.
|
| 145 |
+
On terminal/budget, `state.outcome` is set; the client calls `POST /finish` →
|
| 146 |
+
`finalize` → `append_trace("runs/web_<scenario>_<difficulty>.jsonl")` → returns
|
| 147 |
+
metrics + `trace_path`; client shows the end/review screen.
|
| 148 |
+
|
| 149 |
+
## 7. Frontend (`proteus/web/static/index.html`, vanilla JS)
|
| 150 |
+
|
| 151 |
+
- **Start screen**: scenario `<select>`, difficulty radios
|
| 152 |
+
(easy/medium/hard/expert), seed input (default 42), play-turns input, probe
|
| 153 |
+
checkbox → "New game".
|
| 154 |
+
- **Play screen**:
|
| 155 |
+
- **color grid** — CSS-grid of `<div>` cells (or `<canvas>`); each cell filled
|
| 156 |
+
by `color_map[gridValue]`; cell size auto-fits the band (8×8 … 12×12). Symbols
|
| 157 |
+
distinguished by color; `legend` shown as small helper text.
|
| 158 |
+
- **direction input** — on-screen buttons (↑ ↓ ← → · stay) **and** keyboard
|
| 159 |
+
(WASD + arrow keys). Invalid keys ignored.
|
| 160 |
+
- status bar: `Turn N / M`, scenario · difficulty · seed.
|
| 161 |
+
- Cut pre-roll played once at game start.
|
| 162 |
+
- **End screen**: `SURVIVED` / `ELIMINATED` + `review` panel (per-turn action vs
|
| 163 |
+
optimal/habit, reward, 8 metrics, saved `trace_path`) + "Play again" → start
|
| 164 |
+
screen.
|
| 165 |
+
|
| 166 |
+
No build step, no CDN (offline). Inline CSS/JS only.
|
| 167 |
+
|
| 168 |
+
## 8. Error handling
|
| 169 |
+
|
| 170 |
+
Structured JSON errors from the stdlib handler:
|
| 171 |
+
|
| 172 |
+
- unknown `session_id` → **404** `{error}`
|
| 173 |
+
- invalid `action` → **400**
|
| 174 |
+
- `act` on a finished session → **409**
|
| 175 |
+
- unknown scenario / difficulty → **400**
|
| 176 |
+
- `finish` before done → **409**
|
| 177 |
+
- any unexpected handler exception → **500** `{error}` (no stack to client)
|
| 178 |
+
|
| 179 |
+
## 9. Testing
|
| 180 |
+
|
| 181 |
+
- `tests/runtime/test_interactive_equivalence.py` — **golden**: a scripted action
|
| 182 |
+
sequence driven through `InteractiveSession.step` vs the same actions through
|
| 183 |
+
`SessionRunner` with a scripted `HumanAgent`; assert the two `SessionTrace`
|
| 184 |
+
objects are field-identical **except `model`** (both `"human"` here anyway) and
|
| 185 |
+
byte-identical `cut_frames`, per-turn fields, and the 8 metrics.
|
| 186 |
+
- `tests/web/test_server.py` — construct the handler and call its route methods
|
| 187 |
+
directly (no real socket): `/config`, `/session` (returns id + state with int
|
| 188 |
+
grid + cut_frames), `/act` happy + invalid-action 400 + finished 409, unknown
|
| 189 |
+
session 404, `/finish` appends a trace + returns metrics. Assert `state.grid` is
|
| 190 |
+
`list[list[int]]` and contains no reward/optimal keys (fairness).
|
| 191 |
+
- `tests/web/test_import_safety.py` — importing `proteus.web` / `proteus.web.server`
|
| 192 |
+
imports no matplotlib, opens no socket, imports no provider SDK.
|
| 193 |
+
- existing **127 tests** stay green — the SessionRunner extraction is a pure
|
| 194 |
+
refactor pinned by them; assert the suite count is `127 + new`.
|
| 195 |
+
|
| 196 |
+
## 10. CLI / launch
|
| 197 |
+
|
| 198 |
+
`python -m proteus.web [--host 127.0.0.1] [--port 8000]` (defaults to localhost
|
| 199 |
+
so it is not exposed off-box). Prints the URL. Optionally wire a `proteus serve`
|
| 200 |
+
subcommand mirroring the other CLI verbs (nice-to-have; the `__main__` launcher
|
| 201 |
+
is the contract).
|
| 202 |
+
|
| 203 |
+
## 11. Acceptance
|
| 204 |
+
|
| 205 |
+
- Server starts with **zero new deps** in `.venv`; `GET /` serves the page;
|
| 206 |
+
a full game (start → several `/act` → terminal → `/finish`) writes a
|
| 207 |
+
`runs/web_*.jsonl` line that `proteus replay` and `proteus compare` both accept.
|
| 208 |
+
- Live `state` carries **no** reward/optimal/habit (fairness); the end `review`
|
| 209 |
+
carries them.
|
| 210 |
+
- Golden equivalence test green; full suite `127 + new` passed; offline import
|
| 211 |
+
invariant guarded.
|
| 212 |
+
|
| 213 |
+
## 12. Deferred / open
|
| 214 |
+
|
| 215 |
+
- `proteus serve` subcommand vs `python -m proteus.web` only — pick during plan.
|
| 216 |
+
- Optional Cut-frame animation speed control / skip button (nice-to-have).
|
| 217 |
+
- Multi-session concurrency is supported by the registry but untested beyond one
|
| 218 |
+
local user (YAGNI for now).
|
| 219 |
+
- Reusing this server for live-LLM spectating (watch a model play in color) is a
|
| 220 |
+
natural follow-on but out of scope here.
|