irregular6612 Claude Opus 4.8 (1M context) commited on
Commit
efab165
·
1 Parent(s): f885bcf

docs(web): interactive color-grid web play — brainstormed design (SSOT)

Browse files

stdlib-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.