irregular6612 commited on
Commit
5ba7629
·
1 Parent(s): 642444a

feat(web): single-file color-grid frontend (start / play / review)

Browse files
Files changed (1) hide show
  1. proteus/web/static/index.html +196 -1
proteus/web/static/index.html CHANGED
@@ -1 +1,196 @@
1
- <!doctype html><title>proteus</title><body>placeholder</body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>PROTEUS Arena — Play</title>
7
+ <style>
8
+ :root { --bg:#11131a; --fg:#e8e8ee; --muted:#8a90a2; --line:#2a2e3a; }
9
+ * { box-sizing: border-box; }
10
+ body { margin:0; background:var(--bg); color:var(--fg);
11
+ font:14px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
12
+ display:flex; min-height:100vh; align-items:center; justify-content:center; }
13
+ .card { background:#171a23; border:1px solid var(--line); border-radius:12px;
14
+ padding:24px; width:min(720px, 94vw); }
15
+ h1 { font-size:18px; margin:0 0 16px; }
16
+ label { display:block; margin:10px 0 4px; color:var(--muted); }
17
+ select, input { background:#0e1017; color:var(--fg); border:1px solid var(--line);
18
+ border-radius:6px; padding:6px 8px; font:inherit; }
19
+ .row { display:flex; gap:16px; flex-wrap:wrap; align-items:flex-end; }
20
+ button { background:#2a3140; color:var(--fg); border:1px solid var(--line);
21
+ border-radius:8px; padding:8px 14px; font:inherit; cursor:pointer; }
22
+ button:hover { background:#343c4e; }
23
+ button.primary { background:#3b5bdb; border-color:#3b5bdb; }
24
+ #grid { display:grid; gap:2px; margin:18px auto; width:max-content;
25
+ background:var(--line); padding:2px; border-radius:6px; }
26
+ .cell { width:30px; height:30px; border-radius:3px; }
27
+ .status { color:var(--muted); display:flex; justify-content:space-between; }
28
+ .pad { display:grid; grid-template-columns:repeat(3,56px); gap:8px;
29
+ justify-content:center; margin-top:12px; }
30
+ .pad button { height:48px; }
31
+ .pad .blank { visibility:hidden; }
32
+ .hidden { display:none; }
33
+ .outcome { font-size:22px; text-align:center; margin:8px 0 16px; }
34
+ .win { color:#4FCC30; } .lose { color:#F93C31; }
35
+ table { width:100%; border-collapse:collapse; margin-top:10px; }
36
+ th, td { border-bottom:1px solid var(--line); padding:4px 6px; text-align:right; }
37
+ th:first-child, td:first-child { text-align:left; }
38
+ .diag { color:#FFDC00; }
39
+ </style>
40
+ </head>
41
+ <body>
42
+ <div class="card">
43
+ <h1>PROTEUS Arena — predator&nbsp;evade</h1>
44
+
45
+ <!-- START SCREEN -->
46
+ <div id="start">
47
+ <div class="row">
48
+ <div><label>scenario</label><select id="scenario"></select></div>
49
+ <div><label>difficulty</label><select id="difficulty"></select></div>
50
+ <div><label>seed</label><input id="seed" type="number" value="42" style="width:90px"></div>
51
+ <div><label>turns</label><input id="turns" type="number" value="10" style="width:70px"></div>
52
+ <div><label><input id="probe" type="checkbox"> probe</label></div>
53
+ <div><button class="primary" id="startBtn">New game</button></div>
54
+ </div>
55
+ <p style="color:var(--muted)">Move <b>A</b> (you) to survive the predator <b>B</b>. Arrow keys or WASD; walls block.</p>
56
+ </div>
57
+
58
+ <!-- PLAY SCREEN -->
59
+ <div id="play" class="hidden">
60
+ <div class="status"><span id="meta"></span><span id="turnInfo"></span></div>
61
+ <div id="grid"></div>
62
+ <div class="pad">
63
+ <span class="blank"></span><button data-a="up">↑</button><span class="blank"></span>
64
+ <button data-a="left">←</button><button data-a="stay">•</button><button data-a="right">→</button>
65
+ <span class="blank"></span><button data-a="down">↓</button><span class="blank"></span>
66
+ </div>
67
+ </div>
68
+
69
+ <!-- END SCREEN -->
70
+ <div id="end" class="hidden">
71
+ <div id="outcome" class="outcome"></div>
72
+ <div id="review"></div>
73
+ <p style="text-align:center;margin-top:16px"><button class="primary" id="againBtn">Play again</button></p>
74
+ <p id="savedPath" style="color:var(--muted);text-align:center"></p>
75
+ </div>
76
+ </div>
77
+
78
+ <script>
79
+ let COLORS = {}; // int -> hex (#RRGGBBAA)
80
+ let SID = null; // session id
81
+ let PLAYING = false; // accept input?
82
+
83
+ const $ = (id) => document.getElementById(id);
84
+
85
+ async function jget(url) { return (await fetch(url)).json(); }
86
+ async function jpost(url, body) {
87
+ const r = await fetch(url, {method:"POST", headers:{"Content-Type":"application/json"},
88
+ body: JSON.stringify(body || {})});
89
+ return {status:r.status, data: await r.json()};
90
+ }
91
+
92
+ function hex6(h) { return h.length >= 7 ? h.slice(0,7) : h; } // drop alpha for CSS
93
+
94
+ function drawGrid(grid) {
95
+ const g = $("grid");
96
+ const h = grid.length, w = grid[0].length;
97
+ g.style.gridTemplateColumns = `repeat(${w}, 30px)`;
98
+ g.innerHTML = "";
99
+ for (let y=0; y<h; y++) for (let x=0; x<w; x++) {
100
+ const c = document.createElement("div");
101
+ c.className = "cell";
102
+ c.style.background = hex6(COLORS[grid[y][x]] || "#000000");
103
+ g.appendChild(c);
104
+ }
105
+ }
106
+
107
+ async function animateCut(frames) {
108
+ for (const f of frames) { drawGrid(f); await new Promise(r => setTimeout(r, 280)); }
109
+ }
110
+
111
+ function showState(st) {
112
+ $("turnInfo").textContent = `Turn ${st.turn_idx} / ${st.play_turns}`;
113
+ drawGrid(st.grid);
114
+ if (st.outcome) { PLAYING = false; showEnd(st); }
115
+ }
116
+
117
+ function showEnd(st) {
118
+ $("play").classList.add("hidden");
119
+ $("end").classList.remove("hidden");
120
+ const o = $("outcome");
121
+ o.textContent = st.outcome === "survived" ? "SURVIVED" : "ELIMINATED";
122
+ o.className = "outcome " + (st.outcome === "survived" ? "win" : "lose");
123
+ const rv = st.review;
124
+ let rows = rv.turns.map(t =>
125
+ `<tr class="${t.is_diagnostic ? 'diag':''}"><td>${t.turn_idx}</td><td>${t.action}</td>`
126
+ + `<td>${t.motive_action}</td><td>${t.habit_action}</td><td>${t.reward.toFixed(1)}</td>`
127
+ + `<td>${t.was_congruent ? '✓':'✗'}</td></tr>`).join("");
128
+ let metrics = Object.entries(rv.metrics).map(([k,v]) =>
129
+ `<tr><td>${k}</td><td>${v.toFixed(2)}</td></tr>`).join("");
130
+ $("review").innerHTML =
131
+ `<table><tr><th>turn</th><th>you</th><th>optimal</th><th>habit</th><th>reward</th><th>=opt?</th></tr>${rows}</table>`
132
+ + `<table style="margin-top:14px"><tr><th>metric</th><th>value</th></tr>${metrics}</table>`;
133
+ }
134
+
135
+ async function act(action) {
136
+ if (!PLAYING) return;
137
+ PLAYING = false;
138
+ const {status, data} = await jpost(`/session/${SID}/act`, {action});
139
+ if (status !== 200) { PLAYING = true; return; } // invalid action: ignore, keep playing
140
+ const st = data.state;
141
+ if (st.outcome) {
142
+ const fin = await jpost(`/session/${SID}/finish`, {});
143
+ if (fin.status === 200) $("savedPath").textContent = "trace saved: " + fin.data.trace_path;
144
+ showEnd(st);
145
+ } else {
146
+ PLAYING = true;
147
+ showState(st);
148
+ }
149
+ }
150
+
151
+ const KEYMAP = {ArrowUp:"up", ArrowDown:"down", ArrowLeft:"left", ArrowRight:"right",
152
+ w:"up", s:"down", a:"left", d:"right", " ":"stay"};
153
+ window.addEventListener("keydown", (e) => {
154
+ const a = KEYMAP[e.key];
155
+ if (a) { e.preventDefault(); act(a); }
156
+ });
157
+ document.querySelectorAll(".pad button").forEach(b =>
158
+ b.addEventListener("click", () => act(b.dataset.a)));
159
+
160
+ async function startGame() {
161
+ const body = {
162
+ scenario: $("scenario").value,
163
+ difficulty: $("difficulty").value,
164
+ seed: Number($("seed").value),
165
+ play_turns: Number($("turns").value),
166
+ probe: $("probe").checked,
167
+ };
168
+ const {status, data} = await jpost("/session", body);
169
+ if (status !== 200) { alert(data.error || "failed to start"); return; }
170
+ SID = data.session_id;
171
+ $("start").classList.add("hidden");
172
+ $("end").classList.add("hidden");
173
+ $("play").classList.remove("hidden");
174
+ $("meta").textContent = `${body.scenario} · ${body.difficulty} · seed ${body.seed}`;
175
+ const st = data.state;
176
+ if (st.cut_frames) await animateCut(st.cut_frames);
177
+ PLAYING = true;
178
+ showState(st);
179
+ }
180
+
181
+ async function init() {
182
+ const cfg = await jget("/config");
183
+ COLORS = cfg.color_map;
184
+ $("scenario").innerHTML = cfg.scenarios.map(s => `<option>${s}</option>`).join("");
185
+ $("difficulty").innerHTML = cfg.difficulties.map(d => `<option>${d}</option>`).join("");
186
+ $("seed").value = cfg.default_seed;
187
+ $("startBtn").addEventListener("click", startGame);
188
+ $("againBtn").addEventListener("click", () => {
189
+ $("end").classList.add("hidden");
190
+ $("start").classList.remove("hidden");
191
+ });
192
+ }
193
+ init();
194
+ </script>
195
+ </body>
196
+ </html>