Knowurknot commited on
Commit
69ef828
·
verified ·
1 Parent(s): 9bc2de6

Upgrade bioCAPT chess Arena

Browse files
Files changed (2) hide show
  1. README.md +8 -2
  2. index.html +248 -177
README.md CHANGED
@@ -8,9 +8,9 @@ pinned: false
8
  app_file: index.html
9
  ---
10
 
11
- # CAPT bioCAPT Chess
12
 
13
- A static Hugging Face Space for playing chess against a bioCAPT-backed deterministic move signal.
14
 
15
  The browser enforces legal chess moves with `chess.js`. The CAPT/bioCAPT sealed WASM backend runs on Cloudflare Workers and exposes `POST /chess/evaluate`.
16
 
@@ -22,6 +22,12 @@ https://capt-biocapt-wasm-edge.knowurknottty.workers.dev
22
 
23
  ## Claim Boundary
24
 
 
 
 
 
 
 
25
  This Space is a public interaction surface, not a claim of chess mastery. It does not store HLE answers, training data, private prompts, secrets, or raw memory state.
26
 
27
  If no Cloudflare Worker URL is configured, the page remains playable for local move inspection but will not fake a bioCAPT move.
 
8
  app_file: index.html
9
  ---
10
 
11
+ # CAPT bioCAPT Chess Arena
12
 
13
+ A static Hugging Face Space for playing chess in a proof-visible arena against a bioCAPT-backed deterministic move signal.
14
 
15
  The browser enforces legal chess moves with `chess.js`. The CAPT/bioCAPT sealed WASM backend runs on Cloudflare Workers and exposes `POST /chess/evaluate`.
16
 
 
22
 
23
  ## Claim Boundary
24
 
25
+ Arena modes:
26
+
27
+ - Human vs bioCAPT
28
+ - bioCAPT vs bioCAPT
29
+ - Position Lab
30
+
31
  This Space is a public interaction surface, not a claim of chess mastery. It does not store HLE answers, training data, private prompts, secrets, or raw memory state.
32
 
33
  If no Cloudflare Worker URL is configured, the page remains playable for local move inspection but will not fake a bioCAPT move.
index.html CHANGED
@@ -3,31 +3,25 @@
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>CAPT bioCAPT Chess</title>
7
  <style>
8
  :root {
9
  color-scheme: light;
10
- --ink: #162018;
11
- --muted: #5f6f66;
12
- --line: #d4ded8;
13
  --panel: #f7faf8;
14
- --light: #eef3e9;
15
- --dark: #6f8b77;
16
- --accent: #125f4c;
17
- --warn: #9a4a17;
 
 
18
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
19
  }
20
  * { box-sizing: border-box; }
21
- body {
22
- margin: 0;
23
- color: var(--ink);
24
- background: #fbfdfb;
25
- }
26
- main {
27
- width: min(1180px, calc(100vw - 32px));
28
- margin: 0 auto;
29
- padding: 28px 0 36px;
30
- }
31
  header {
32
  display: grid;
33
  grid-template-columns: 1fr auto;
@@ -35,138 +29,53 @@
35
  align-items: end;
36
  border-bottom: 1px solid var(--line);
37
  padding-bottom: 16px;
38
- margin-bottom: 22px;
39
- }
40
- h1 {
41
- margin: 0;
42
- font-size: clamp(30px, 5vw, 56px);
43
- line-height: 0.95;
44
- letter-spacing: 0;
45
- }
46
- .subtitle {
47
- margin: 10px 0 0;
48
- max-width: 780px;
49
- color: var(--muted);
50
- font-size: 16px;
51
- line-height: 1.45;
52
- }
53
- .status {
54
- display: grid;
55
- gap: 8px;
56
- justify-items: end;
57
- font-size: 13px;
58
- color: var(--muted);
59
- }
60
- .pill {
61
- border: 1px solid var(--line);
62
- border-radius: 999px;
63
- padding: 7px 10px;
64
- background: white;
65
- color: var(--ink);
66
- }
67
- .layout {
68
- display: grid;
69
- grid-template-columns: minmax(320px, 560px) 1fr;
70
- gap: 24px;
71
- align-items: start;
72
- }
73
- .board {
74
- width: min(100%, 560px);
75
- aspect-ratio: 1;
76
- display: grid;
77
- grid-template-columns: repeat(8, 1fr);
78
- border: 2px solid #29362d;
79
- }
80
- .square {
81
- border: 0;
82
- padding: 0;
83
- display: grid;
84
- place-items: center;
85
- font-size: clamp(26px, 7vw, 54px);
86
- line-height: 1;
87
- cursor: pointer;
88
- color: #162018;
89
- min-width: 0;
90
- }
91
  .square.light { background: var(--light); }
92
  .square.dark { background: var(--dark); }
93
  .square.selected { outline: 4px solid #f0c84b; outline-offset: -4px; }
94
- .square.target { box-shadow: inset 0 0 0 5px rgba(18, 95, 76, 0.55); }
95
- .panel {
96
- border: 1px solid var(--line);
97
- border-radius: 8px;
98
- background: white;
99
- padding: 16px;
100
- }
101
  .panel + .panel { margin-top: 14px; }
102
- h2 {
103
- margin: 0 0 10px;
104
- font-size: 17px;
105
- letter-spacing: 0;
106
- }
107
- .controls {
108
- display: flex;
109
- flex-wrap: wrap;
110
- gap: 8px;
111
- }
112
- button, input {
113
- font: inherit;
114
- }
115
- button {
116
  border: 1px solid var(--line);
117
  background: var(--panel);
118
  color: var(--ink);
119
  border-radius: 7px;
120
  min-height: 38px;
121
  padding: 0 12px;
122
- cursor: pointer;
123
- }
124
- button.primary {
125
- background: var(--accent);
126
- color: white;
127
- border-color: var(--accent);
128
- }
129
- button:disabled {
130
- cursor: not-allowed;
131
- opacity: 0.55;
132
- }
133
- label {
134
- display: grid;
135
- gap: 6px;
136
- color: var(--muted);
137
- font-size: 13px;
138
- }
139
- input {
140
- width: 100%;
141
- border: 1px solid var(--line);
142
- border-radius: 7px;
143
- padding: 10px;
144
- color: var(--ink);
145
- background: white;
146
- }
147
- .log {
148
- margin: 0;
149
- padding: 0;
150
- list-style: none;
151
- display: grid;
152
- gap: 7px;
153
- max-height: 220px;
154
- overflow: auto;
155
- font-size: 13px;
156
- }
157
- .log li {
158
- border-bottom: 1px solid #edf1ee;
159
- padding-bottom: 7px;
160
- }
161
- .small {
162
- color: var(--muted);
163
- font-size: 13px;
164
- line-height: 1.45;
165
  }
 
 
 
 
 
 
 
 
 
 
166
  .warn { color: var(--warn); }
167
- @media (max-width: 860px) {
 
 
168
  header, .layout { grid-template-columns: 1fr; }
169
  .status { justify-items: start; }
 
170
  }
171
  </style>
172
  </head>
@@ -174,52 +83,89 @@
174
  <main>
175
  <header>
176
  <div>
177
- <h1>CAPT bioCAPT Chess</h1>
178
- <p class="subtitle">Play legal chess in the browser while a Cloudflare Worker calls sealed bioCAPT WASM for deterministic move ranking. The UI separates rule legality from WASM signal so the proof boundary stays visible.</p>
179
  </div>
180
  <div class="status">
181
  <span class="pill" id="turnPill">White to move</span>
182
- <span class="pill" id="backendPill">Backend not configured</span>
183
  </div>
184
  </header>
 
185
  <section class="layout">
186
  <div>
187
  <div class="board" id="board" aria-label="Chess board"></div>
 
 
 
 
 
 
 
 
 
188
  </div>
 
189
  <div>
190
  <section class="panel">
191
- <h2>Game</h2>
192
  <div class="controls">
193
  <button class="primary" id="bioMove">bioCAPT move</button>
 
 
194
  <button id="undo">Undo</button>
195
  <button id="reset">Reset</button>
196
  <button id="flip">Flip</button>
197
  </div>
198
  <p class="small" id="gameStatus">Select a white piece to move.</p>
199
  </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  <section class="panel">
201
  <h2>Cloudflare Worker</h2>
202
  <label>
203
  Backend URL
204
- <input id="backendUrl" placeholder="https://capt-biocapt-wasm.<account>.workers.dev" />
205
  </label>
206
  <div class="controls" style="margin-top: 10px;">
207
  <button id="saveBackend">Save</button>
208
  <button id="checkBackend">Check</button>
209
  </div>
210
- <p class="small" id="backendStatus">No backend is configured. The app will not simulate bioCAPT moves.</p>
211
  </section>
 
212
  <section class="panel">
213
- <h2>bioCAPT Trace</h2>
214
- <ul class="log" id="trace"></ul>
 
 
 
 
 
 
 
 
215
  </section>
 
216
  <section class="panel">
217
- <h2>Boundary</h2>
218
- <p class="small">Legal move generation is handled by chess.js in the browser. bioCAPT supplies a deterministic ranking signal through the Cloudflare WASM API. This does not claim chess mastery, medical validity, AGI, or production memory integration.</p>
219
  </section>
220
  </div>
221
  </section>
222
  </main>
 
223
  <script type="module">
224
  import { Chess } from "https://cdn.jsdelivr.net/npm/chess.js@1.0.0/+esm";
225
 
@@ -229,49 +175,76 @@
229
  };
230
  const DEFAULT_BACKEND_URL = "https://capt-biocapt-wasm-edge.knowurknottty.workers.dev";
231
  const game = new Chess();
 
 
 
 
 
 
 
 
232
  let selected = null;
233
  let flipped = false;
 
 
 
 
234
  let backendUrl = localStorage.getItem("capt_biocapt_worker_url") || DEFAULT_BACKEND_URL;
235
 
236
  const board = document.getElementById("board");
237
  const trace = document.getElementById("trace");
 
238
  const gameStatus = document.getElementById("gameStatus");
239
  const backendStatus = document.getElementById("backendStatus");
240
  const backendInput = document.getElementById("backendUrl");
241
  backendInput.value = backendUrl;
242
 
243
- document.getElementById("reset").addEventListener("click", () => {
244
- game.reset();
245
- selected = null;
246
- trace.replaceChildren();
247
- render();
248
  });
 
249
  document.getElementById("undo").addEventListener("click", () => {
250
  game.undo();
251
- game.undo();
252
  selected = null;
253
  render();
254
  });
255
- document.getElementById("flip").addEventListener("click", () => {
256
- flipped = !flipped;
257
- render();
258
- });
259
  document.getElementById("saveBackend").addEventListener("click", () => {
260
  backendUrl = normalizeBackend(backendInput.value);
 
261
  localStorage.setItem("capt_biocapt_worker_url", backendUrl);
262
  renderBackendStatus("Backend URL saved.", false);
 
263
  });
264
  document.getElementById("checkBackend").addEventListener("click", checkBackend);
265
- document.getElementById("bioMove").addEventListener("click", requestBioCaptMove);
 
 
 
 
266
 
267
  render();
 
 
 
 
 
 
 
 
 
 
 
 
 
268
 
269
  function render() {
270
  board.replaceChildren();
 
271
  const files = flipped ? ["h","g","f","e","d","c","b","a"] : ["a","b","c","d","e","f","g","h"];
272
  const ranks = flipped ? [1,2,3,4,5,6,7,8] : [8,7,6,5,4,3,2,1];
273
  const targets = selected ? new Set(game.moves({ square: selected, verbose: true }).map((move) => move.to)) : new Set();
274
-
275
  for (const rank of ranks) {
276
  for (const file of files) {
277
  const square = `${file}${rank}`;
@@ -286,15 +259,16 @@
286
  board.appendChild(button);
287
  }
288
  }
289
-
290
  document.getElementById("turnPill").textContent = `${game.turn() === "w" ? "White" : "Black"} to move`;
291
- document.getElementById("backendPill").textContent = backendUrl ? "Backend configured" : "Backend not configured";
292
- document.getElementById("bioMove").disabled = !backendUrl || game.isGameOver();
 
293
  gameStatus.textContent = statusText();
294
- if (!backendUrl) renderBackendStatus("No backend is configured. The app will not simulate bioCAPT moves.", true);
295
  }
296
 
297
  function clickSquare(square) {
 
298
  const piece = game.get(square);
299
  if (!selected) {
300
  if (piece && piece.color === game.turn()) selected = square;
@@ -303,64 +277,161 @@
303
  }
304
  const move = game.move({ from: selected, to: square, promotion: "q" });
305
  if (move) {
306
- addTrace(`Human: ${move.san}`);
307
  selected = null;
308
  render();
309
- if (!game.isGameOver() && backendUrl) window.setTimeout(requestBioCaptMove, 250);
310
  return;
311
  }
312
  selected = piece && piece.color === game.turn() ? square : null;
313
  render();
314
  }
315
 
316
- async function requestBioCaptMove() {
317
  if (!backendUrl) {
318
  renderBackendStatus("Backend is required for bioCAPT moves. No fallback move was made.", true);
319
- return;
320
  }
321
- const legalMoves = game.moves({ verbose: true }).map((move) => move.from + move.to + (move.promotion || ""));
 
322
  try {
323
  const response = await fetch(`${backendUrl}/chess/evaluate`, {
324
  method: "POST",
325
  headers: { "content-type": "application/json" },
326
- body: JSON.stringify({ fen: game.fen(), legal_moves: legalMoves })
327
  });
328
  const data = await response.json();
329
  if (!response.ok) throw new Error(data.error || "backend request failed");
330
  const chosen = data.selected_move;
331
  const move = chosen ? game.move({ from: chosen.slice(0, 2), to: chosen.slice(2, 4), promotion: chosen.slice(4) || "q" }) : null;
332
  if (!move) throw new Error("backend returned no legal move");
333
- addTrace(`bioCAPT: ${move.san} | signal ${data.biocapt_signal} | ${data.mode}`);
 
 
 
 
 
 
 
 
 
 
 
 
334
  render();
 
335
  } catch (error) {
336
  renderBackendStatus(`bioCAPT request failed: ${error.message}`, true);
337
  addTrace(`bioCAPT blocked: ${error.message}`);
 
338
  }
339
  }
340
 
341
- async function checkBackend() {
342
- backendUrl = normalizeBackend(backendInput.value);
343
- if (!backendUrl) {
344
- renderBackendStatus("Enter a Cloudflare Worker URL first.", true);
345
- return;
 
 
 
346
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  try {
348
  const response = await fetch(`${backendUrl}/products`);
349
  const data = await response.json();
350
  if (!response.ok) throw new Error(data.error || "backend check failed");
351
- renderBackendStatus(`Connected. CAPT ${data.products.capt.wasm_sha256.slice(0, 12)}; bioCAPT ${data.products.biocapt.wasm_sha256.slice(0, 12)}.`, false);
352
- localStorage.setItem("capt_biocapt_worker_url", backendUrl);
 
 
353
  render();
354
  } catch (error) {
 
355
  renderBackendStatus(`Backend check failed: ${error.message}`, true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
  }
357
  }
358
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  function statusText() {
360
  if (game.isCheckmate()) return "Checkmate.";
361
  if (game.isDraw()) return "Draw.";
362
  if (game.isCheck()) return "Check.";
363
- return selected ? `Selected ${selected}.` : "Select a piece to move.";
364
  }
365
 
366
  function addTrace(text) {
 
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>CAPT bioCAPT Chess Arena</title>
7
  <style>
8
  :root {
9
  color-scheme: light;
10
+ --ink: #142018;
11
+ --muted: #607168;
12
+ --line: #d5dfda;
13
  --panel: #f7faf8;
14
+ --light: #eef4ea;
15
+ --dark: #718f7b;
16
+ --accent: #12644f;
17
+ --accent-2: #234c86;
18
+ --warn: #984812;
19
+ --bad: #8f2636;
20
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
21
  }
22
  * { box-sizing: border-box; }
23
+ body { margin: 0; color: var(--ink); background: #fbfdfb; }
24
+ main { width: min(1240px, calc(100vw - 32px)); margin: 0 auto; padding: 28px 0 36px; }
 
 
 
 
 
 
 
 
25
  header {
26
  display: grid;
27
  grid-template-columns: 1fr auto;
 
29
  align-items: end;
30
  border-bottom: 1px solid var(--line);
31
  padding-bottom: 16px;
32
+ margin-bottom: 20px;
33
+ }
34
+ h1 { margin: 0; font-size: clamp(30px, 5vw, 58px); line-height: 0.95; letter-spacing: 0; }
35
+ h2 { margin: 0 0 10px; font-size: 17px; letter-spacing: 0; }
36
+ .subtitle { margin: 10px 0 0; max-width: 820px; color: var(--muted); font-size: 16px; line-height: 1.45; }
37
+ .status { display: grid; gap: 8px; justify-items: end; font-size: 13px; color: var(--muted); }
38
+ .pill { border: 1px solid var(--line); border-radius: 999px; padding: 7px 10px; background: white; color: var(--ink); }
39
+ .layout { display: grid; grid-template-columns: minmax(320px, 560px) 1fr; gap: 24px; align-items: start; }
40
+ .board { width: min(100%, 560px); aspect-ratio: 1; display: grid; grid-template-columns: repeat(8, 1fr); border: 2px solid #29362d; }
41
+ .square { border: 0; padding: 0; display: grid; place-items: center; font-size: clamp(26px, 7vw, 54px); line-height: 1; cursor: pointer; color: #142018; min-width: 0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  .square.light { background: var(--light); }
43
  .square.dark { background: var(--dark); }
44
  .square.selected { outline: 4px solid #f0c84b; outline-offset: -4px; }
45
+ .square.target { box-shadow: inset 0 0 0 5px rgba(18, 100, 79, 0.55); }
46
+ .panel { border: 1px solid var(--line); border-radius: 8px; background: white; padding: 16px; }
 
 
 
 
 
47
  .panel + .panel { margin-top: 14px; }
48
+ .grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
49
+ .metric { border: 1px solid #e8eee9; border-radius: 7px; padding: 10px; background: var(--panel); }
50
+ .metric strong { display: block; font-size: 12px; color: var(--muted); font-weight: 600; margin-bottom: 4px; }
51
+ .metric span { font-size: 14px; overflow-wrap: anywhere; }
52
+ .controls, .tabs { display: flex; flex-wrap: wrap; gap: 8px; }
53
+ button, input, select, textarea { font: inherit; }
54
+ button, select {
 
 
 
 
 
 
 
55
  border: 1px solid var(--line);
56
  background: var(--panel);
57
  color: var(--ink);
58
  border-radius: 7px;
59
  min-height: 38px;
60
  padding: 0 12px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  }
62
+ button { cursor: pointer; }
63
+ button.primary, button.active { background: var(--accent); color: white; border-color: var(--accent); }
64
+ button.secondary { background: var(--accent-2); color: white; border-color: var(--accent-2); }
65
+ button:disabled { cursor: not-allowed; opacity: 0.55; }
66
+ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; }
67
+ input, textarea { width: 100%; border: 1px solid var(--line); border-radius: 7px; padding: 10px; color: var(--ink); background: white; }
68
+ textarea { min-height: 74px; resize: vertical; }
69
+ .log { margin: 0; padding: 0; list-style: none; display: grid; gap: 7px; max-height: 250px; overflow: auto; font-size: 13px; }
70
+ .log li { border-bottom: 1px solid #edf1ee; padding-bottom: 7px; overflow-wrap: anywhere; }
71
+ .small { color: var(--muted); font-size: 13px; line-height: 1.45; }
72
  .warn { color: var(--warn); }
73
+ .bad { color: var(--bad); }
74
+ .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
75
+ @media (max-width: 900px) {
76
  header, .layout { grid-template-columns: 1fr; }
77
  .status { justify-items: start; }
78
+ .grid { grid-template-columns: 1fr; }
79
  }
80
  </style>
81
  </head>
 
83
  <main>
84
  <header>
85
  <div>
86
+ <h1>CAPT bioCAPT Chess Arena</h1>
87
+ <p class="subtitle">A proof-visible chess surface: chess.js enforces legal moves, Cloudflare calls sealed bioCAPT WASM, and every bioCAPT move exposes hashes, rejected claims, and a downloadable game manifest.</p>
88
  </div>
89
  <div class="status">
90
  <span class="pill" id="turnPill">White to move</span>
91
+ <span class="pill" id="backendPill">Backend checking</span>
92
  </div>
93
  </header>
94
+
95
  <section class="layout">
96
  <div>
97
  <div class="board" id="board" aria-label="Chess board"></div>
98
+ <section class="panel" style="margin-top: 14px;">
99
+ <h2>Arena Mode</h2>
100
+ <div class="tabs">
101
+ <button class="active" data-mode="human">Human vs bioCAPT</button>
102
+ <button data-mode="selfplay">bioCAPT vs bioCAPT</button>
103
+ <button data-mode="position">Position Lab</button>
104
+ </div>
105
+ <p class="small" id="modeHelp">You play White. bioCAPT ranks legal Black moves through the Worker.</p>
106
+ </section>
107
  </div>
108
+
109
  <div>
110
  <section class="panel">
111
+ <h2>Controls</h2>
112
  <div class="controls">
113
  <button class="primary" id="bioMove">bioCAPT move</button>
114
+ <button class="secondary" id="selfPlay">Run bioCAPT vs bioCAPT</button>
115
+ <button id="downloadManifest">Download manifest</button>
116
  <button id="undo">Undo</button>
117
  <button id="reset">Reset</button>
118
  <button id="flip">Flip</button>
119
  </div>
120
  <p class="small" id="gameStatus">Select a white piece to move.</p>
121
  </section>
122
+
123
+ <section class="panel">
124
+ <h2>Position Lab</h2>
125
+ <label>
126
+ FEN
127
+ <textarea id="fenInput"></textarea>
128
+ </label>
129
+ <div class="controls" style="margin-top: 10px;">
130
+ <button id="loadFen">Load FEN</button>
131
+ <button id="copyFen">Copy FEN</button>
132
+ </div>
133
+ </section>
134
+
135
  <section class="panel">
136
  <h2>Cloudflare Worker</h2>
137
  <label>
138
  Backend URL
139
+ <input id="backendUrl" />
140
  </label>
141
  <div class="controls" style="margin-top: 10px;">
142
  <button id="saveBackend">Save</button>
143
  <button id="checkBackend">Check</button>
144
  </div>
145
+ <p class="small" id="backendStatus">Checking backend.</p>
146
  </section>
147
+
148
  <section class="panel">
149
+ <h2>Proof Trace</h2>
150
+ <div class="grid">
151
+ <div class="metric"><strong>CAPT WASM</strong><span class="mono" id="captHash">unknown</span></div>
152
+ <div class="metric"><strong>bioCAPT WASM</strong><span class="mono" id="bioHash">unknown</span></div>
153
+ <div class="metric"><strong>Selected Move</strong><span id="selectedMove">none</span></div>
154
+ <div class="metric"><strong>Trace Hash</strong><span class="mono" id="traceHash">none</span></div>
155
+ <div class="metric"><strong>Legal Moves</strong><span id="legalMoveCount">0</span></div>
156
+ <div class="metric"><strong>bioCAPT Signal</strong><span id="bioSignal">none</span></div>
157
+ </div>
158
+ <p class="small bad" id="rejectedClaims">Unsupported claims remain rejected.</p>
159
  </section>
160
+
161
  <section class="panel">
162
+ <h2>Move Trace</h2>
163
+ <ul class="log" id="trace"></ul>
164
  </section>
165
  </div>
166
  </section>
167
  </main>
168
+
169
  <script type="module">
170
  import { Chess } from "https://cdn.jsdelivr.net/npm/chess.js@1.0.0/+esm";
171
 
 
175
  };
176
  const DEFAULT_BACKEND_URL = "https://capt-biocapt-wasm-edge.knowurknottty.workers.dev";
177
  const game = new Chess();
178
+ const manifest = {
179
+ manifest_version: "capt_chess_arena_v1",
180
+ started_at: new Date().toISOString(),
181
+ backend_url: DEFAULT_BACKEND_URL,
182
+ claim_boundary: "Deterministic bioCAPT WASM signal ranking over legal chess moves; not a chess-strength proof.",
183
+ moves: [],
184
+ proof_traces: []
185
+ };
186
  let selected = null;
187
  let flipped = false;
188
+ let mode = "human";
189
+ let selfPlaying = false;
190
+ let lastProof = null;
191
+ let products = null;
192
  let backendUrl = localStorage.getItem("capt_biocapt_worker_url") || DEFAULT_BACKEND_URL;
193
 
194
  const board = document.getElementById("board");
195
  const trace = document.getElementById("trace");
196
+ const fenInput = document.getElementById("fenInput");
197
  const gameStatus = document.getElementById("gameStatus");
198
  const backendStatus = document.getElementById("backendStatus");
199
  const backendInput = document.getElementById("backendUrl");
200
  backendInput.value = backendUrl;
201
 
202
+ document.querySelectorAll("[data-mode]").forEach((button) => {
203
+ button.addEventListener("click", () => setMode(button.dataset.mode));
 
 
 
204
  });
205
+ document.getElementById("reset").addEventListener("click", resetGame);
206
  document.getElementById("undo").addEventListener("click", () => {
207
  game.undo();
208
+ if (mode === "human") game.undo();
209
  selected = null;
210
  render();
211
  });
212
+ document.getElementById("flip").addEventListener("click", () => { flipped = !flipped; render(); });
 
 
 
213
  document.getElementById("saveBackend").addEventListener("click", () => {
214
  backendUrl = normalizeBackend(backendInput.value);
215
+ manifest.backend_url = backendUrl;
216
  localStorage.setItem("capt_biocapt_worker_url", backendUrl);
217
  renderBackendStatus("Backend URL saved.", false);
218
+ render();
219
  });
220
  document.getElementById("checkBackend").addEventListener("click", checkBackend);
221
+ document.getElementById("bioMove").addEventListener("click", () => requestBioCaptMove("manual"));
222
+ document.getElementById("selfPlay").addEventListener("click", runSelfPlay);
223
+ document.getElementById("downloadManifest").addEventListener("click", downloadManifest);
224
+ document.getElementById("loadFen").addEventListener("click", loadFen);
225
+ document.getElementById("copyFen").addEventListener("click", () => navigator.clipboard?.writeText(game.fen()));
226
 
227
  render();
228
+ checkBackend();
229
+
230
+ function setMode(nextMode) {
231
+ mode = nextMode;
232
+ document.querySelectorAll("[data-mode]").forEach((button) => button.classList.toggle("active", button.dataset.mode === mode));
233
+ const help = {
234
+ human: "You play White. bioCAPT ranks legal Black moves through the Worker.",
235
+ selfplay: "bioCAPT requests are made for both sides until the game ends or 80 plies are reached.",
236
+ position: "Paste any legal FEN and ask bioCAPT to rank that position's legal moves."
237
+ };
238
+ document.getElementById("modeHelp").textContent = help[mode];
239
+ render();
240
+ }
241
 
242
  function render() {
243
  board.replaceChildren();
244
+ fenInput.value = game.fen();
245
  const files = flipped ? ["h","g","f","e","d","c","b","a"] : ["a","b","c","d","e","f","g","h"];
246
  const ranks = flipped ? [1,2,3,4,5,6,7,8] : [8,7,6,5,4,3,2,1];
247
  const targets = selected ? new Set(game.moves({ square: selected, verbose: true }).map((move) => move.to)) : new Set();
 
248
  for (const rank of ranks) {
249
  for (const file of files) {
250
  const square = `${file}${rank}`;
 
259
  board.appendChild(button);
260
  }
261
  }
 
262
  document.getElementById("turnPill").textContent = `${game.turn() === "w" ? "White" : "Black"} to move`;
263
+ document.getElementById("backendPill").textContent = products ? "Backend verified" : (backendUrl ? "Backend configured" : "Backend missing");
264
+ document.getElementById("bioMove").disabled = !backendUrl || game.isGameOver() || selfPlaying;
265
+ document.getElementById("selfPlay").disabled = !backendUrl || game.isGameOver() || selfPlaying;
266
  gameStatus.textContent = statusText();
267
+ renderProof(lastProof);
268
  }
269
 
270
  function clickSquare(square) {
271
+ if (mode !== "human" || game.turn() !== "w" || selfPlaying) return;
272
  const piece = game.get(square);
273
  if (!selected) {
274
  if (piece && piece.color === game.turn()) selected = square;
 
277
  }
278
  const move = game.move({ from: selected, to: square, promotion: "q" });
279
  if (move) {
280
+ recordHumanMove(move);
281
  selected = null;
282
  render();
283
+ if (!game.isGameOver() && backendUrl) window.setTimeout(() => requestBioCaptMove("human_vs_biocapt"), 250);
284
  return;
285
  }
286
  selected = piece && piece.color === game.turn() ? square : null;
287
  render();
288
  }
289
 
290
+ async function requestBioCaptMove(reason) {
291
  if (!backendUrl) {
292
  renderBackendStatus("Backend is required for bioCAPT moves. No fallback move was made.", true);
293
+ return null;
294
  }
295
+ const fenBefore = game.fen();
296
+ const legalMoveManifests = buildLegalMoveManifests();
297
  try {
298
  const response = await fetch(`${backendUrl}/chess/evaluate`, {
299
  method: "POST",
300
  headers: { "content-type": "application/json" },
301
+ body: JSON.stringify({ fen: fenBefore, legal_move_manifests: legalMoveManifests })
302
  });
303
  const data = await response.json();
304
  if (!response.ok) throw new Error(data.error || "backend request failed");
305
  const chosen = data.selected_move;
306
  const move = chosen ? game.move({ from: chosen.slice(0, 2), to: chosen.slice(2, 4), promotion: chosen.slice(4) || "q" }) : null;
307
  if (!move) throw new Error("backend returned no legal move");
308
+ lastProof = data;
309
+ manifest.proof_traces.push(data);
310
+ manifest.moves.push({
311
+ ply: manifest.moves.length + 1,
312
+ actor: "bioCAPT",
313
+ reason,
314
+ uci: chosen,
315
+ san: move.san,
316
+ fen_before: data.fen_before,
317
+ fen_after: game.fen(),
318
+ trace_hash: data.trace_hash
319
+ });
320
+ addTrace(`bioCAPT: ${move.san} | ${data.trace_hash} | signal ${data.biocapt_signal}`);
321
  render();
322
+ return data;
323
  } catch (error) {
324
  renderBackendStatus(`bioCAPT request failed: ${error.message}`, true);
325
  addTrace(`bioCAPT blocked: ${error.message}`);
326
+ return null;
327
  }
328
  }
329
 
330
+ async function runSelfPlay() {
331
+ if (selfPlaying) return;
332
+ setMode("selfplay");
333
+ selfPlaying = true;
334
+ for (let i = 0; i < 80 && !game.isGameOver(); i += 1) {
335
+ const result = await requestBioCaptMove("selfplay");
336
+ if (!result) break;
337
+ await new Promise((resolve) => setTimeout(resolve, 90));
338
  }
339
+ selfPlaying = false;
340
+ render();
341
+ }
342
+
343
+ function buildLegalMoveManifests() {
344
+ return game.moves({ verbose: true }).map((move) => {
345
+ const clone = new Chess(game.fen());
346
+ clone.move({ from: move.from, to: move.to, promotion: move.promotion || "q" });
347
+ return {
348
+ move: move.from + move.to + (move.promotion || ""),
349
+ san: move.san,
350
+ fen_after: clone.fen()
351
+ };
352
+ });
353
+ }
354
+
355
+ async function checkBackend() {
356
+ backendUrl = normalizeBackend(backendInput.value || DEFAULT_BACKEND_URL);
357
  try {
358
  const response = await fetch(`${backendUrl}/products`);
359
  const data = await response.json();
360
  if (!response.ok) throw new Error(data.error || "backend check failed");
361
+ products = data.products;
362
+ manifest.backend_url = backendUrl;
363
+ manifest.products = products;
364
+ renderBackendStatus(`Connected. CAPT ${products.capt.wasm_sha256.slice(0, 12)}; bioCAPT ${products.biocapt.wasm_sha256.slice(0, 12)}.`, false);
365
  render();
366
  } catch (error) {
367
+ products = null;
368
  renderBackendStatus(`Backend check failed: ${error.message}`, true);
369
+ render();
370
+ }
371
+ }
372
+
373
+ function recordHumanMove(move) {
374
+ manifest.moves.push({
375
+ ply: manifest.moves.length + 1,
376
+ actor: "human",
377
+ uci: move.from + move.to + (move.promotion || ""),
378
+ san: move.san,
379
+ fen_after: game.fen()
380
+ });
381
+ addTrace(`Human: ${move.san}`);
382
+ }
383
+
384
+ function loadFen() {
385
+ try {
386
+ game.load(fenInput.value.trim());
387
+ selected = null;
388
+ lastProof = null;
389
+ manifest.moves.push({ event: "load_fen", fen: game.fen(), at: new Date().toISOString() });
390
+ render();
391
+ } catch (error) {
392
+ gameStatus.textContent = `Invalid FEN: ${error.message}`;
393
  }
394
  }
395
 
396
+ function downloadManifest() {
397
+ manifest.finished_at = new Date().toISOString();
398
+ manifest.final_fen = game.fen();
399
+ manifest.result = game.isGameOver() ? statusText() : "in_progress";
400
+ const blob = new Blob([JSON.stringify(manifest, null, 2)], { type: "application/json" });
401
+ const url = URL.createObjectURL(blob);
402
+ const link = document.createElement("a");
403
+ link.href = url;
404
+ link.download = `capt-chess-arena-${Date.now()}.json`;
405
+ link.click();
406
+ URL.revokeObjectURL(url);
407
+ }
408
+
409
+ function resetGame() {
410
+ game.reset();
411
+ selected = null;
412
+ lastProof = null;
413
+ manifest.moves = [];
414
+ manifest.proof_traces = [];
415
+ manifest.started_at = new Date().toISOString();
416
+ trace.replaceChildren();
417
+ render();
418
+ }
419
+
420
+ function renderProof(proof) {
421
+ document.getElementById("captHash").textContent = products?.capt?.wasm_sha256 || "unknown";
422
+ document.getElementById("bioHash").textContent = products?.biocapt?.wasm_sha256 || "unknown";
423
+ document.getElementById("selectedMove").textContent = proof?.selected_move || "none";
424
+ document.getElementById("traceHash").textContent = proof?.trace_hash || "none";
425
+ document.getElementById("legalMoveCount").textContent = String(proof?.legal_move_count || game.moves().length);
426
+ document.getElementById("bioSignal").textContent = proof?.biocapt_signal ?? "none";
427
+ document.getElementById("rejectedClaims").textContent = proof?.unsupported_claims?.join(" | ") || "Unsupported claims remain rejected.";
428
+ }
429
+
430
  function statusText() {
431
  if (game.isCheckmate()) return "Checkmate.";
432
  if (game.isDraw()) return "Draw.";
433
  if (game.isCheck()) return "Check.";
434
+ return selected ? `Selected ${selected}.` : `${mode === "position" ? "Position Lab" : "Arena"} ready.`;
435
  }
436
 
437
  function addTrace(text) {