Pabloler21 Claude Opus 4.8 commited on
Commit
35d9271
Β·
1 Parent(s): 702bac2

docs: game-view UI polish implementation plan (transcript/voices/color/feedback)

Browse files
docs/superpowers/plans/2026-06-15-game-ui-polish.md ADDED
@@ -0,0 +1,408 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Game-View UI Polish Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans. Steps use checkbox (`- [ ]`).
4
+
5
+ **Goal:** Polish the in-game UI for playability β€” a readable fading transcript with a clear two-voice hierarchy, an amber "memory" / red "cruelty" color language, and whispered in-voice feedback for capture/recall/recover.
6
+
7
+ **Architecture:** Mostly CSS in `styles.css` Β§26 plus two small seams that ride the **existing per-turn marker pattern** (the `.tone-now` β†’ `applyTone()` mechanism): `render_entity` emits a `.cue-now[data-cue][data-seq]` marker, and a new `applyCue()` in `_HEAD_JS` (polled by the existing `wire()` loop) pulses the right drawer chip, drops a transient whisper, and tags the recall line amber. No chat engine / recall / ending / voice changes.
8
+
9
+ **Tech Stack:** Gradio 6 (HTML+CSS+`_HEAD_JS`), pure CSS, pytest for the one pure helper (`render_entity` marker).
10
+
11
+ **Reference spec:** `docs/superpowers/specs/2026-06-15-game-ui-polish-design.md`. **Decisions:** transcript=B, voices=V1, color=B, feedback=B.
12
+
13
+ **Verification:** visuals by eye (headless unreliable β€” CLAUDE.md); `import app` + `pytest -q` stay green (262). Gradio chatbot internal class names vary β€” target `.bot`/`.user` (load-bearing, already used) and verify `.message-row`/`.prose` against the running app.
14
+
15
+ ---
16
+
17
+ ### Task 1: Color var β€” the amber "memory" accent
18
+
19
+ **Files:** Modify `styles.css` (Β§1 `:root`, ~line 12)
20
+
21
+ - [ ] **Step 1: Add the memory accent var**
22
+
23
+ In `:root` (after the existing accent vars near line 20), add:
24
+
25
+ ```css
26
+ /* memory/humanity accent (recall line, recovered items, feedback pulses) β€”
27
+ distinct from the dried-blood red reserved for cruelty/bad-ending */
28
+ --mem-accent: #e0b283;
29
+ --mem-glow: rgba(200,130,70,0.55);
30
+ ```
31
+
32
+ - [ ] **Step 2: Verify import**
33
+
34
+ Run: `./.venv/Scripts/python -c "import app; print('ok')"` β†’ `ok` (CSS isn't parsed by Python; this just confirms nothing else broke).
35
+
36
+ - [ ] **Step 3: Commit**
37
+
38
+ ```bash
39
+ git add styles.css
40
+ git commit -m "style(game): add --mem-accent amber var (memory/humanity)"
41
+ ```
42
+
43
+ ---
44
+
45
+ ### Task 2: Transcript fade + two-voice hierarchy (B + V1)
46
+
47
+ **Files:** Modify `styles.css` Β§26 (the `#game-dialogue` chat rules, ~lines 1669-1691)
48
+
49
+ Make the conversation read as a fading transcript: last lines visible, older ones dimmer; the child italic & unlabeled, the player upright with a `you` typewriter tag, centered (never scattered). Recall lines turn amber.
50
+
51
+ - [ ] **Step 1: Replace the `#game-dialogue` chat block**
52
+
53
+ Replace the current rules from `#game-dialogue .chatbot {` through the `.user` rule (the block ending at the line that sets `.user ... color:#b9b2cc`) with:
54
+
55
+ ```css
56
+ #game-dialogue .chatbot {
57
+ width: 100% !important;
58
+ max-height: 160px !important;
59
+ background: transparent !important;
60
+ border: none !important;
61
+ border-image: none !important;
62
+ box-shadow: none !important;
63
+ backdrop-filter: none !important;
64
+ padding: 0 !important;
65
+ overflow: hidden; /* old lines fade out, no scrollbar */
66
+ }
67
+ /* transparent rows, centered text β€” kill Gradio's bubble chrome */
68
+ #game-dialogue .message-row,
69
+ #game-dialogue .message,
70
+ #game-dialogue .bubble {
71
+ background: transparent !important; border: none !important;
72
+ box-shadow: none !important; text-align: center !important;
73
+ padding: 2px 0 !important; margin: 0 !important;
74
+ }
75
+ /* fade-by-recency: newest crisp, older dimmer, 4th-back and beyond hidden.
76
+ (Gradio wraps each message in a .message-row β€” verify this selector against
77
+ the running app; if rows are .message instead, swap the selector.) */
78
+ #game-dialogue .message-row { opacity: 0.38; transition: opacity 0.4s ease; }
79
+ #game-dialogue .message-row:nth-last-child(2) { opacity: 0.6; }
80
+ #game-dialogue .message-row:nth-last-child(1) { opacity: 1; }
81
+ #game-dialogue .message-row:nth-last-child(n+4) { display: none; }
82
+
83
+ /* the child β€” italic, bright, unlabeled (it owns the scene) */
84
+ #game-dialogue .bot, #game-dialogue .bot .prose, #game-dialogue .bot p {
85
+ font-style: italic !important; font-size: 1.2rem !important; line-height: 1.55 !important;
86
+ color: #ece6f4 !important; text-align: center !important;
87
+ text-shadow: 0 2px 16px #000 !important;
88
+ }
89
+ /* you β€” upright, muted, with a small typewriter tag */
90
+ #game-dialogue .user, #game-dialogue .user .prose, #game-dialogue .user p {
91
+ font-style: normal !important; font-size: 1.0rem !important;
92
+ color: #aaa2be !important; text-align: center !important; background: transparent !important;
93
+ }
94
+ #game-dialogue .user .prose::before {
95
+ content: 'you ';
96
+ font-family: 'Special Elite', monospace;
97
+ font-size: 0.6rem; letter-spacing: 0.18em; text-transform: uppercase;
98
+ color: #6f6590; margin-right: 7px;
99
+ }
100
+ /* a recall line (it claims your memory) β€” JS tags the last .bot row .recall-line */
101
+ #game-dialogue .bot.recall-line, #game-dialogue .bot.recall-line .prose,
102
+ #game-dialogue .bot.recall-line p {
103
+ color: var(--mem-accent) !important;
104
+ text-shadow: 0 0 20px var(--mem-glow) !important;
105
+ }
106
+ ```
107
+
108
+ - [ ] **Step 2: Live-verify (user, by eye)**
109
+
110
+ Run `.venv/Scripts/python app.py`, enter the game, send 2-3 messages. Confirm: the child's lines are italic/centered, your lines are upright/centered with a `you` tag (NOT scattered left/right), older lines dim and only ~3 show. If lines don't fade, the row selector is wrong β€” change `.message-row` to the actual per-message wrapper class (inspect element) and re-verify.
111
+
112
+ - [ ] **Step 3: Commit**
113
+
114
+ ```bash
115
+ git add styles.css
116
+ git commit -m "style(game): fading transcript + two-voice hierarchy (B + V1)"
117
+ ```
118
+
119
+ ---
120
+
121
+ ### Task 3: Recovered items use the amber accent
122
+
123
+ **Files:** Modify `styles.css` Β§26 (add a rule near the drawer/recovered styles)
124
+
125
+ The "What it remembers" items are the child's recovered humanity β†’ amber, per the color decision.
126
+
127
+ - [ ] **Step 1: Add the recovered-item color rule**
128
+
129
+ Append in Β§26 (near the `.game-drawer` rules):
130
+
131
+ ```css
132
+ /* the child's recovered memories read in the warm memory accent */
133
+ .game-drawer .recovered-item {
134
+ color: var(--mem-accent) !important;
135
+ border-left-color: rgba(200,130,70,0.5) !important;
136
+ }
137
+ ```
138
+
139
+ - [ ] **Step 2: Commit**
140
+
141
+ ```bash
142
+ git add styles.css
143
+ git commit -m "style(game): recovered items in the amber memory accent"
144
+ ```
145
+
146
+ ---
147
+
148
+ ### Task 4: `render_entity` emits a per-turn cue marker
149
+
150
+ **Files:** Modify `render.py` (`render_entity` signature + return, ~lines 79-313); Test: `tests/test_render.py`
151
+
152
+ Add an optional `cue` that surfaces a hidden marker (mirroring `.tone-now`) so the head-JS can fire the right feedback once per turn.
153
+
154
+ - [ ] **Step 1: Write the failing test**
155
+
156
+ In `tests/test_render.py`:
157
+
158
+ ```python
159
+ def test_entity_cue_marker():
160
+ from render import render_entity
161
+ assert 'data-cue="recall"' in render_entity(60, cue="recall", seq=3)
162
+ assert 'data-seq="3"' in render_entity(60, cue="recall", seq=3)
163
+ # no cue β†’ no marker
164
+ assert 'cue-now' not in render_entity(20)
165
+ ```
166
+
167
+ - [ ] **Step 2: Run it (fails)**
168
+
169
+ Run: `./.venv/Scripts/python -m pytest tests/test_render.py::test_entity_cue_marker -v` β†’ FAIL (unexpected `cue` kwarg).
170
+
171
+ - [ ] **Step 3: Add the `cue` param**
172
+
173
+ Change the signature:
174
+
175
+ ```python
176
+ def render_entity(affinity: int, mode: str = "idle", seq: int = 0,
177
+ tone: int = 0, cue: str = "") -> str:
178
+ ```
179
+
180
+ - [ ] **Step 4: Emit the marker in the return**
181
+
182
+ In the final `return` (the block starting with the `tone-now` span), prepend a cue marker when `cue` is set. Replace the return with:
183
+
184
+ ```python
185
+ tone_token = tone_class.split("-", 1)[1] if "-" in tone_class else "neutral"
186
+ cue_mark = (f'<span class="cue-now" data-cue="{cue}" data-seq="{seq}" '
187
+ 'style="display:none"></span>') if cue else ""
188
+ return (
189
+ cue_mark
190
+ + f'<span class="tone-now" data-tone="{tone_token}" style="display:none"></span>'
191
+ f'<div class="entity-scene {tone_class}">'
192
+ f'{figure}{flash}{tint}'
193
+ '</div>'
194
+ f'{ghost}'
195
+ )
196
+ ```
197
+
198
+ - [ ] **Step 5: Run the test + full render suite**
199
+
200
+ Run: `./.venv/Scripts/python -m pytest tests/test_render.py -q` β†’ PASS.
201
+
202
+ - [ ] **Step 6: Commit**
203
+
204
+ ```bash
205
+ git add render.py tests/test_render.py
206
+ git commit -m "feat(entity): optional per-turn cue marker (capture/recall/recover)"
207
+ ```
208
+
209
+ ---
210
+
211
+ ### Task 5: `chat()` computes and passes the cue
212
+
213
+ **Files:** Modify `app.py` (the main turn body + yield, ~lines 923-1034)
214
+
215
+ Surface which beat happened this turn so render_entity can mark it. Precedence: recall > recover > capture.
216
+
217
+ - [ ] **Step 1: Capture the fragment count BEFORE it can increment**
218
+
219
+ Find `do_recall, recall_memory = should_recall(state)` (~line 923). Immediately AFTER it, add:
220
+
221
+ ```python
222
+ frag_before = state.get("fragments_told", 0)
223
+ ```
224
+
225
+ - [ ] **Step 2: Compute the cue just before the final yield**
226
+
227
+ Find `just_claimed = recall_memory if (do_recall and recall_memory) else None` (~line 1020). Immediately AFTER it, add:
228
+
229
+ ```python
230
+ recovered_now = state.get("fragments_told", 0) > frag_before
231
+ cue = ("recall" if (do_recall and recall_memory)
232
+ else "recover" if recovered_now
233
+ else "capture" if captured else "")
234
+ ```
235
+
236
+ - [ ] **Step 3: Pass the cue into the final `render_entity`**
237
+
238
+ In the final yield, change:
239
+
240
+ ```python
241
+ render_entity(state["affinity"], mode, seq=state["turn"],
242
+ tone=state.get("tone", 0)),
243
+ ```
244
+
245
+ to:
246
+
247
+ ```python
248
+ render_entity(state["affinity"], mode, seq=state["turn"],
249
+ tone=state.get("tone", 0), cue=cue),
250
+ ```
251
+
252
+ - [ ] **Step 4: Verify import + suite**
253
+
254
+ Run: `./.venv/Scripts/python -c "import app; print('ok')"` β†’ `ok`; then `./.venv/Scripts/python -m pytest -q` β†’ 263 passed (262 + the new render test).
255
+
256
+ - [ ] **Step 5: Commit**
257
+
258
+ ```bash
259
+ git add app.py
260
+ git commit -m "feat(game): chat() surfaces the per-turn cue (recall/recover/capture)"
261
+ ```
262
+
263
+ ---
264
+
265
+ ### Task 6: `applyCue()` β€” drawer pulse, whisper, recall-line tag
266
+
267
+ **Files:** Modify `app.py` (`_HEAD_JS`: add `applyCue()` + call it in `wire()`, ~lines 428-442)
268
+
269
+ Mirror `applyTone()`: read the marker, fire once per `data-seq`, pulse the matching chip, drop a transient whisper, and amber-tag the recall line.
270
+
271
+ - [ ] **Step 1: Add `applyCue()` after `applyTone()`**
272
+
273
+ Immediately after the `applyTone()` function (it ends at line ~439, the closing `}` before `function wire()`), insert:
274
+
275
+ ```javascript
276
+ // once-per-turn feedback: pulse the relevant drawer chip, drop a whisper,
277
+ // amber-tag the recall line. Rides the same marker pattern as applyTone.
278
+ var CUE_WHISPER = { capture: '\\u2014 it keeps this \\u2014',
279
+ recover: '\\u2014 it remembers \\u2014' };
280
+ function applyCue() {
281
+ var gv = document.getElementById('game-view');
282
+ var mark = document.querySelector('#game-view .cue-now[data-cue]');
283
+ if (!gv || !mark) return;
284
+ var seq = mark.getAttribute('data-seq');
285
+ if (gv.dataset.cueSeq === seq) return; // already fired this turn
286
+ gv.dataset.cueSeq = seq;
287
+ var cue = mark.getAttribute('data-cue');
288
+ var rightSide = (cue === 'recover');
289
+ var drawer = document.getElementById(rightSide ? 'drawer-right' : 'drawer-left');
290
+ if (drawer) {
291
+ drawer.classList.add('pulse');
292
+ setTimeout(function () { drawer.classList.remove('pulse'); }, 2500);
293
+ }
294
+ if (cue === 'recall') {
295
+ var bots = document.querySelectorAll('#game-dialogue .bot');
296
+ if (bots.length) bots[bots.length - 1].classList.add('recall-line');
297
+ }
298
+ var text = CUE_WHISPER[cue];
299
+ if (text && drawer) {
300
+ var w = document.createElement('div');
301
+ w.className = 'cue-whisper';
302
+ w.textContent = text;
303
+ var r = drawer.getBoundingClientRect();
304
+ w.style.left = r.left + 'px';
305
+ w.style.top = (r.bottom + 6) + 'px';
306
+ document.body.appendChild(w);
307
+ setTimeout(function () { w.classList.add('out'); }, 60);
308
+ setTimeout(function () { w.remove(); }, 2800);
309
+ }
310
+ }
311
+ ```
312
+
313
+ - [ ] **Step 2: Call it from `wire()`**
314
+
315
+ In `wire()`, the line `applyTone();` (~line 442) β€” add `applyCue();` right after it:
316
+
317
+ ```javascript
318
+ function wire() {
319
+ applyTone();
320
+ applyCue();
321
+ ```
322
+
323
+ - [ ] **Step 3: Verify import + that the JS shipped**
324
+
325
+ Run: `./.venv/Scripts/python -c "import app; print('ok')"` β†’ `ok`. (The `\\u2014` escapes are em-dashes; keep them escaped β€” this file has hit UTF-8 mangling before, see CLAUDE.md.)
326
+
327
+ - [ ] **Step 4: Commit**
328
+
329
+ ```bash
330
+ git add app.py
331
+ git commit -m "feat(game): applyCue β€” chip pulse + in-voice whisper + recall-line tag"
332
+ ```
333
+
334
+ ---
335
+
336
+ ### Task 7: CSS β€” chip pulse + whisper styling
337
+
338
+ **Files:** Modify `styles.css` Β§26
339
+
340
+ - [ ] **Step 1: Add the pulse + whisper rules**
341
+
342
+ Append in Β§26:
343
+
344
+ ```css
345
+ /* feedback: the drawer chip breathes amber when its collection changes */
346
+ .game-drawer.pulse {
347
+ border-color: rgba(200,150,80,0.6) !important;
348
+ box-shadow: 0 0 18px rgba(200,150,80,0.45) !important;
349
+ animation: chip-pulse 1.1s ease-in-out 2;
350
+ }
351
+ .game-drawer.pulse .drawer-head { color: var(--mem-accent) !important; }
352
+ @keyframes chip-pulse {
353
+ 0%, 100% { box-shadow: 0 0 10px rgba(200,150,80,0.25); }
354
+ 50% { box-shadow: 0 0 22px rgba(200,150,80,0.55); }
355
+ }
356
+
357
+ /* the in-voice whisper that breathes next to the chip, then fades */
358
+ .cue-whisper {
359
+ position: fixed; z-index: 60; pointer-events: none;
360
+ font-family: 'Special Elite', monospace;
361
+ font-size: 0.62rem; letter-spacing: 0.14em;
362
+ color: var(--mem-accent); text-shadow: 0 0 10px var(--mem-glow);
363
+ opacity: 0; transform: translateY(-3px);
364
+ transition: opacity 0.5s ease, transform 0.5s ease;
365
+ }
366
+ .cue-whisper.out { opacity: 0; transform: translateY(-8px); }
367
+ .cue-whisper { animation: whisper-in 0.5s ease forwards; }
368
+ @keyframes whisper-in { from { opacity: 0; } 40% { opacity: 0.9; } to { opacity: 0.9; } }
369
+ ```
370
+
371
+ (The `.out` class drives the fade-away after ~2.5 s; `whisper-in` brings it in.)
372
+
373
+ - [ ] **Step 2: Reduced-motion safety**
374
+
375
+ In the existing `@media (prefers-reduced-motion: reduce)` block (near the end of Β§26), add:
376
+
377
+ ```css
378
+ .game-drawer.pulse { animation: none !important; }
379
+ .cue-whisper { animation: none !important; opacity: 0.9 !important; }
380
+ ```
381
+
382
+ - [ ] **Step 3: Commit**
383
+
384
+ ```bash
385
+ git add styles.css
386
+ git commit -m "style(game): chip pulse + in-voice whisper feedback styling"
387
+ ```
388
+
389
+ ---
390
+
391
+ ### Task 8: Full suite + live verification
392
+
393
+ - [ ] **Step 1:** Run `./.venv/Scripts/python -m pytest -q` β†’ all green (263). Fix any breakage.
394
+ - [ ] **Step 2 (user, by eye + ear, `.venv-tts` + Ollama):** play a few turns and confirm:
395
+ - Conversation reads as a centered fading transcript; child italic, your lines tagged `you`, never scattered; only ~3 lines show.
396
+ - On a captured memory: the **Treasure** chip pulses amber + "β€” it keeps this β€”" breathes and fades.
397
+ - On a recall: the child's line turns amber + the Treasure item shows claimed.
398
+ - On a recovered fragment: the **What it remembers** chip pulses + "β€” it remembers β€”".
399
+ - Recovered items render amber; cruelty/bad-ending still red.
400
+ - [ ] **Step 3:** Commit any tuning.
401
+
402
+ ---
403
+
404
+ ## Self-Review
405
+ - **Spec coverage:** transcript B (Task 2) βœ…; voices V1 β€” `you` tag + child italic + recall amber (Tasks 2, 6) βœ…; color B β€” amber memory var + recall + recovered + pulses, red untouched (Tasks 1, 2, 3, 7) βœ…; feedback B β€” thinking unchanged, capture/recover whisper + chip pulse, recall glow (Tasks 4-7) βœ…; bond unchanged βœ…; no engine changes βœ….
406
+ - **Placeholder scan:** every code step shows full code; no TBDs.
407
+ - **Type/seam consistency:** `cue` kwarg added in Task 4 is passed in Task 5; the `.cue-now[data-cue][data-seq]` marker (Task 4) is read by `applyCue()` (Task 6); `--mem-accent`/`--mem-glow` (Task 1) used in Tasks 2/3/7; `.recall-line` tagged in Task 6, styled in Task 2; `.pulse` added in Task 6, styled in Task 7; `#drawer-left`/`#drawer-right` are existing ids (confirmed).
408
+ - **Fragility flagged:** the `.message-row` fade selector and `.user .prose::before` tag are Gradio-version-dependent β€” Task 2 Step 2 verifies them live; `.bot`/`.user` are the load-bearing classes already in use.