Download code/src/stackcraft/web/app.js from nima1/stackcraft-clef-flash-lora: direct link, hf CLI and curl.
- Browser
- Download file 28.8 kB
-
https://huggingface.co/nima1/stackcraft-clef-flash-lora/resolve/main/code/src/stackcraft/web/app.js
- Command line
-
hf download hf://nima1/stackcraft-clef-flash-lora/code/src/stackcraft/web/app.js
-
curl -L -o app.js https://huggingface.co/nima1/stackcraft-clef-flash-lora/resolve/main/code/src/stackcraft/web/app.js
28.8 kB
| ; | |
| // Python supplies every legal landing cell. This client never simulates a move. | |
| const $ = (id) => document.getElementById(id); | |
| const colors = { I: 1, O: 2, T: 3, S: 4, Z: 5, J: 6, L: 7 }; | |
| // Icons only: these coordinates are never used to calculate placements. | |
| const icons = { | |
| I: [[0, 0], [1, 0], [2, 0], [3, 0]], O: [[1, 0], [2, 0], [1, 1], [2, 1]], | |
| T: [[1, 0], [0, 1], [1, 1], [2, 1]], S: [[1, 0], [2, 0], [0, 1], [1, 1]], | |
| Z: [[0, 0], [1, 0], [1, 1], [2, 1]], J: [[0, 0], [0, 1], [1, 1], [2, 1]], | |
| L: [[2, 0], [0, 1], [1, 1], [2, 1]], | |
| }; | |
| let live = null; | |
| let selected = null; | |
| let busy = false; | |
| let stateSynced = false; | |
| let frames = null; | |
| let frameIndex = 0; | |
| let replayArtifact = null; | |
| let replayTimer = null; | |
| let activeSeed = 42; | |
| let raceVisible = false; | |
| let raceLoading = false; | |
| let racePlayers = null; | |
| let raceIndex = 0; | |
| let raceMax = 0; | |
| let raceTimer = null; | |
| let raceManifest = null; | |
| let raceHumanActive = false; | |
| const humanRaceCard = $("race-human-card"); | |
| const cells = Array.from({ length: 200 }, () => { | |
| const cell = document.createElement("div"); | |
| cell.className = "cell"; | |
| $("board").append(cell); | |
| return cell; | |
| }); | |
| async function api(path, options = {}) { | |
| const response = await fetch(path, { | |
| ...options, | |
| headers: { "Content-Type": "application/json", ...options.headers }, | |
| }); | |
| const result = await response.json(); | |
| if (!response.ok) { | |
| const detail = typeof result.detail === "string" ? result.detail : "The request could not be completed. Check the input and try again."; | |
| throw new Error(detail); | |
| } | |
| return result; | |
| } | |
| function reportError(error) { | |
| $("error").textContent = error.message || "Something went wrong. Please try again."; | |
| $("error").hidden = false; | |
| } | |
| async function operation(task) { | |
| if (busy) return; | |
| busy = true; | |
| $("error").hidden = true; | |
| renderControls(); | |
| try { await task(); } catch (error) { reportError(error); } | |
| finally { busy = false; renderControls(); } | |
| } | |
| function currentState() { return frames ? frames[frameIndex] : live; } | |
| function legal() { return live?.legal_actions || []; } | |
| function humanCapReached() { return Boolean(raceHumanActive && live && !live.terminal && live.pieces >= raceManifest.max_pieces); } | |
| function mountHumanBoard() { | |
| const alongside = raceVisible && raceHumanActive; | |
| if (alongside) $("race-human-slot").append($("human-board-column")); | |
| else $("human-board-home").after($("human-board-column")); | |
| humanRaceCard.hidden = !alongside; | |
| $("race-view").classList.toggle("with-human", alongside); | |
| } | |
| function renderHumanComparison() { | |
| $("comparison-human-note").hidden = !raceHumanActive || raceVisible; | |
| $("seed").disabled = raceHumanActive || busy; | |
| $("leave-comparison-race").hidden = !raceHumanActive; | |
| $("race-match-human").hidden = !raceHumanActive; | |
| $("race-human-timeline-note").hidden = !raceHumanActive; | |
| if (raceManifest) $("join-race").textContent = `${raceHumanActive ? "Restart" : "Start"} live board · seed ${raceManifest.seed}`; | |
| if (!raceHumanActive || !live) return; | |
| const cap = raceManifest.max_pieces; | |
| $("comparison-human-label").textContent = `Comparison · seed ${activeSeed} · cap ${cap}`; | |
| for (const key of ["lines", "score", "pieces"]) $("race-human-" + key).textContent = live[key].toLocaleString(); | |
| $("race-human-preview").textContent = humanCapReached() ? `Episode cap reached · ${cap} pieces` : live.terminal ? "Topped out" : `${live.current} now · ${live.next_piece} next · cap ${cap}`; | |
| $("race-human-timeline-note").textContent = `Live human: ${live.pieces}/${cap} pieces. Recorded timeline: ${raceIndex}.${raceIndex !== Math.min(live.pieces, raceMax) ? " Watching another recorded position; your board is unchanged." : " Recordings match your move count (finished runs stay frozen)."}`; | |
| } | |
| function syncRaceToHuman() { | |
| if (!raceHumanActive || !live) return; | |
| stopRace(); | |
| seekRace(live.pieces); | |
| } | |
| function leaveComparison(switchToPlay = true) { | |
| raceHumanActive = false; | |
| if (switchToPlay) setView(false); | |
| mountHumanBoard(); | |
| render(); | |
| } | |
| function chooseNearest(rotation = 0, x = 3) { | |
| const sameRotation = legal().filter((action) => action.rotation === rotation); | |
| const choices = sameRotation.length ? sameRotation : legal(); | |
| selected = choices.reduce((best, action) => !best || Math.abs(action.x - x) < Math.abs(best.x - x) ? action : best, null); | |
| } | |
| function renderPreview(id, piece) { | |
| const preview = $(id); | |
| preview.replaceChildren(); | |
| preview.setAttribute("aria-label", `${id === "current-preview" ? "Current" : "Next"} piece: ${piece || "none"}`); | |
| const grid = document.createElement("div"); | |
| grid.className = "mini-grid"; | |
| grid.style.setProperty("--piece-color", `var(--piece-${colors[piece] || 1})`); | |
| for (let y = 0; y < 2; y++) { | |
| for (let x = 0; x < 4; x++) { | |
| const cell = document.createElement("span"); | |
| cell.className = `mini-cell${icons[piece]?.some(([px, py]) => px === x && py === y) ? " filled" : ""}`; | |
| grid.append(cell); | |
| } | |
| } | |
| preview.append(grid); | |
| } | |
| function renderControls() { | |
| const playable = !busy && stateSynced && live && !live.terminal && !humanCapReached() && !frames && selected && (!raceVisible || raceHumanActive); | |
| for (const id of ["move-left", "move-right", "rotate", "drop"]) $(id).disabled = !playable; | |
| for (const id of ["restart", "play-again", "load-replay", "exit-replay"]) $(id).disabled = busy; | |
| $("download-replay").disabled = busy || (!live && !replayArtifact); | |
| $("replay-prev").disabled = busy || !frames || frameIndex === 0; | |
| $("replay-next").disabled = busy || !frames || frameIndex >= frames.length - 1; | |
| $("replay-play").disabled = busy || !frames || frames.length <= 1; | |
| $("replay-position").disabled = busy; | |
| $("play-controls").hidden = Boolean(frames); | |
| $("replay-controls").hidden = !frames; | |
| for (const id of ["play-tab", "race-tab", "leave-comparison-play", "leave-comparison-race"]) $(id).disabled = busy; | |
| $("join-race").disabled = busy || !raceManifest; | |
| renderHumanComparison(); | |
| } | |
| function render() { | |
| const state = currentState(); | |
| if (!state) return renderControls(); | |
| const capped = humanCapReached() && !frames; | |
| const ghost = !frames && !state.terminal && !capped ? new Set(selected?.cells.map(([x, y]) => y * 10 + x)) : new Set(); | |
| for (let i = 0; i < cells.length; i++) { | |
| const value = state.board[Math.floor(i / 10)][i % 10]; | |
| cells[i].className = `cell${value ? " filled" : ghost.has(i) ? " ghost" : ""}`; | |
| cells[i].style.setProperty("--piece-color", `var(--piece-${value || colors[state.current] || 1})`); | |
| } | |
| $("score").textContent = state.score.toLocaleString(); | |
| $("lines").textContent = state.lines.toLocaleString(); | |
| $("pieces").textContent = state.pieces.toLocaleString(); | |
| $("current-name").textContent = state.current; | |
| $("move-counter").textContent = `MOVE ${String(state.pieces + (state.terminal || capped ? 0 : 1)).padStart(3, "0")}`; | |
| $("board-mode").textContent = frames ? "SAVED REPLAY" : raceHumanActive ? "LIVE HUMAN" : "YOUR BOARD"; | |
| $("player-status").textContent = frames ? "Watching a recorded game" : !stateSynced ? "Connection lost · restart to continue" : capped ? "Comparison cap reached" : state.terminal ? "Run complete" : "Ready for your next move"; | |
| $("terminal-overlay").hidden = (!state.terminal && !capped) || Boolean(frames); | |
| $("terminal-eyebrow").textContent = capped ? "EPISODE CAP" : "RUN COMPLETE"; | |
| $("terminal-title").textContent = capped ? "Cap reached." : "One more try?"; | |
| $("terminal-description").textContent = capped ? `${raceManifest.max_pieces} pieces placed. This is the comparison limit, not a top-out.` : "No legal placements remain."; | |
| $("placement-label").textContent = frames ? `Recorded move ${frameIndex}` : capped ? `Comparison cap: ${raceManifest.max_pieces}` : state.terminal ? "No legal placements" : selected ? `COL ${selected.x + 1} / ROT ${selected.rotation + 1}` : "Choose your landing spot"; | |
| $("board").setAttribute("aria-label", `${frames ? "Replay" : "Game"} board. ${state.pieces} pieces placed, ${state.lines} lines, score ${state.score}.${capped ? " Comparison episode cap reached." : state.terminal ? " Game over." : selected && !frames ? ` ${state.current} piece at column ${selected.x + 1}, rotation ${selected.rotation + 1}. Use left and right to move, up to rotate, space to drop.` : ""}`); | |
| renderPreview("current-preview", state.current); | |
| renderPreview("next-preview", state.next_piece); | |
| if (frames) { | |
| $("replay-position").max = frames.length - 1; | |
| $("replay-position").value = frameIndex; | |
| $("replay-progress").textContent = `${frameIndex} / ${frames.length - 1}`; | |
| } | |
| renderControls(); | |
| } | |
| function stopReplay() { | |
| if (replayTimer !== null) clearInterval(replayTimer); | |
| replayTimer = null; | |
| $("replay-play").textContent = "Play"; | |
| $("replay-play").setAttribute("aria-label", "Play replay"); | |
| } | |
| async function restart() { | |
| const text = raceHumanActive ? String(raceManifest.seed) : $("seed").value.trim(); | |
| const seed = Number(text); | |
| if (!text || !Number.isSafeInteger(seed) || seed < 0 || seed > 2147483647) { | |
| reportError(new Error("Choose a whole-number seed between 0 and 2,147,483,647.")); | |
| return; | |
| } | |
| await operation(async () => { | |
| const snapshot = await api("/api/games", { method: "POST", body: JSON.stringify({ seed }) }); | |
| stopReplay(); | |
| frames = null; | |
| replayArtifact = null; | |
| live = snapshot; | |
| stateSynced = true; | |
| activeSeed = seed; | |
| $("seed").value = seed; | |
| chooseNearest(); | |
| syncRaceToHuman(); | |
| render(); | |
| $("announcement").textContent = `New game, seed ${seed}. ${live.current} piece ready.`; | |
| }); | |
| } | |
| function move(direction) { | |
| if (busy || !stateSynced || frames || !selected || live.terminal || humanCapReached()) return; | |
| const row = legal().filter((action) => action.rotation === selected.rotation).sort((a, b) => a.x - b.x); | |
| const index = row.findIndex((action) => action.id === selected.id); | |
| selected = row[Math.max(0, Math.min(row.length - 1, index + direction))]; | |
| render(); | |
| } | |
| function rotate() { | |
| if (busy || !stateSynced || frames || !selected || live.terminal || humanCapReached()) return; | |
| const rotations = [...new Set(legal().map((action) => action.rotation))].sort((a, b) => a - b); | |
| const nextRotation = rotations[(rotations.indexOf(selected.rotation) + 1) % rotations.length]; | |
| chooseNearest(nextRotation, selected.x); | |
| render(); | |
| } | |
| async function drop() { | |
| if (!live || !stateSynced || live.terminal || humanCapReached() || !selected || frames) return; | |
| await operation(async () => { | |
| const oldLines = live.lines; | |
| const sessionPath = `/api/games/${encodeURIComponent(live.id)}`; | |
| try { | |
| live = await api(`${sessionPath}/moves`, { | |
| method: "POST", | |
| body: JSON.stringify({ action_id: selected.id, expected_pieces: live.pieces }), | |
| }); | |
| stateSynced = true; | |
| } catch (error) { | |
| // A failed response can follow an accepted move. Never retry from stale state. | |
| try { | |
| live = await api(sessionPath); | |
| stateSynced = true; | |
| chooseNearest(); | |
| syncRaceToHuman(); | |
| render(); | |
| } catch { | |
| stateSynced = false; | |
| selected = null; | |
| render(); | |
| throw new Error("Could not confirm the game state. Play is disabled to avoid repeating a move. Reconnect and start a new game."); | |
| } | |
| throw error; | |
| } | |
| chooseNearest(); | |
| syncRaceToHuman(); | |
| render(); | |
| const clear = live.lines - oldLines; | |
| $("announcement").textContent = humanCapReached() ? `Comparison cap reached after ${live.pieces} pieces. Score ${live.score}.` : live.terminal ? `Game over. ${live.lines} lines cleared. Score ${live.score}.` : `${clear ? `${clear} ${clear === 1 ? "line" : "lines"} cleared! ` : ""}${live.current} piece ready. Score ${live.score}.`; | |
| }); | |
| } | |
| // Pointer users can immediately continue with arrow/space shortcuts. Keyboard | |
| // activation retains button focus so Tab/Enter navigation stays conventional. | |
| function placementControl(event, action) { | |
| action(); | |
| if (event.detail > 0) $("board").focus({ preventScroll: true }); | |
| } | |
| $("move-left").addEventListener("click", (event) => placementControl(event, () => move(-1))); | |
| $("move-right").addEventListener("click", (event) => placementControl(event, () => move(1))); | |
| $("rotate").addEventListener("click", (event) => placementControl(event, rotate)); | |
| $("drop").addEventListener("click", (event) => placementControl(event, drop)); | |
| $("restart").addEventListener("click", restart); | |
| $("play-again").addEventListener("click", restart); | |
| $("seed").addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); restart(); } }); | |
| document.addEventListener("keydown", (event) => { | |
| if (event.target.matches("input, textarea, select, button, a, summary") || event.ctrlKey || event.metaKey || event.altKey) return; | |
| if ((raceVisible && !raceHumanActive) || frames || busy || !stateSynced || !live || live.terminal || humanCapReached()) return; | |
| const handler = { ArrowLeft: () => move(-1), ArrowRight: () => move(1), ArrowUp: rotate, " ": drop, Enter: drop }[event.key]; | |
| if (handler) { event.preventDefault(); if (!event.repeat || event.key.startsWith("Arrow")) handler(); } | |
| }); | |
| $("board").addEventListener("click", (event) => { | |
| if (frames || busy || !stateSynced || !selected || live.terminal || humanCapReached()) return; | |
| const rect = $("board").getBoundingClientRect(); | |
| const x = Math.floor((event.clientX - rect.left) / (rect.width / 10)); | |
| chooseNearest(selected.rotation, x); | |
| render(); | |
| $("board").focus({ preventScroll: true }); | |
| }); | |
| $("download-replay").addEventListener("click", () => operation(async () => { | |
| const artifact = frames ? replayArtifact : await api(`/api/games/${encodeURIComponent(live.id)}/replay`); | |
| const url = URL.createObjectURL(new Blob([JSON.stringify(artifact, null, 2) + "\n"], { type: "application/json" })); | |
| const link = document.createElement("a"); | |
| link.href = url; | |
| link.download = `stackcraft-seed-${artifact.seed ?? activeSeed}-replay.json`; | |
| document.body.append(link); | |
| link.click(); | |
| link.remove(); | |
| setTimeout(() => URL.revokeObjectURL(url), 1000); | |
| })); | |
| $("load-replay").addEventListener("click", () => $("replay-file").click()); | |
| $("replay-file").addEventListener("change", (event) => { | |
| const file = event.target.files[0]; | |
| event.target.value = ""; | |
| if (!file) return; | |
| operation(async () => { | |
| if (file.size > 5 * 1024 * 1024) throw new Error("Choose a replay smaller than 5 MB."); | |
| let artifact; | |
| try { artifact = JSON.parse(await file.text()); } catch { throw new Error("This file is not valid JSON. Choose a Stackcraft replay."); } | |
| const result = await api("/api/replays", { method: "POST", body: JSON.stringify(artifact) }); | |
| if (!Array.isArray(result.frames) || !result.frames.length) throw new Error("The replay contains no frames."); | |
| stopReplay(); | |
| if (raceHumanActive) leaveComparison(false); | |
| frames = result.frames; | |
| replayArtifact = artifact; | |
| frameIndex = 0; | |
| render(); | |
| $("announcement").textContent = `Replay loaded. ${frames.length - 1} recorded moves.`; | |
| }); | |
| }); | |
| function seek(index) { | |
| if (!frames) return; | |
| frameIndex = Math.max(0, Math.min(frames.length - 1, index)); | |
| render(); | |
| } | |
| $("replay-position").addEventListener("input", (event) => { stopReplay(); seek(Number(event.target.value)); }); | |
| $("replay-prev").addEventListener("click", () => { stopReplay(); seek(frameIndex - 1); }); | |
| $("replay-next").addEventListener("click", () => { stopReplay(); seek(frameIndex + 1); }); | |
| $("replay-play").addEventListener("click", () => { | |
| if (replayTimer !== null) return stopReplay(); | |
| if (!frames || frames.length <= 1) return; | |
| if (frameIndex === frames.length - 1) seek(0); | |
| $("replay-play").textContent = "Pause"; | |
| $("replay-play").setAttribute("aria-label", "Pause replay"); | |
| replayTimer = setInterval(() => { seek(frameIndex + 1); if (frameIndex >= frames.length - 1) stopReplay(); }, 450); | |
| }); | |
| $("exit-replay").addEventListener("click", () => { | |
| stopReplay(); | |
| frames = null; | |
| replayArtifact = null; | |
| render(); | |
| if (!live) restart(); | |
| }); | |
| // Race state is separate from the human session and its imported replay. | |
| function stopRace() { | |
| if (raceTimer !== null) clearInterval(raceTimer); | |
| raceTimer = null; | |
| $("race-play").textContent = "Play race ▶"; | |
| $("race-play").setAttribute("aria-label", "Play recorded race"); | |
| } | |
| function raceCard(player, index) { | |
| const card = document.createElement("article"); | |
| card.className = "race-card"; | |
| const heading = document.createElement("div"); | |
| heading.className = "race-card-heading"; | |
| const name = document.createElement("h3"); | |
| name.id = `race-player-${index}`; | |
| name.textContent = player.name; | |
| const status = document.createElement("span"); | |
| status.className = "race-player-status"; | |
| heading.append(name, status); | |
| const wrap = document.createElement("div"); | |
| wrap.className = "board-wrap"; | |
| const board = document.createElement("div"); | |
| board.className = "board race-board"; | |
| board.setAttribute("role", "img"); | |
| const boardCells = Array.from({ length: 200 }, () => { | |
| const cell = document.createElement("div"); | |
| cell.className = "cell"; | |
| board.append(cell); | |
| return cell; | |
| }); | |
| wrap.append(board); | |
| const metrics = document.createElement("dl"); | |
| metrics.className = "race-metrics"; | |
| const values = {}; | |
| for (const [key, label] of [["lines", "LINES"], ["score", "SCORE"], ["pieces", "PLACED"]]) { | |
| const group = document.createElement("div"); | |
| const term = document.createElement("dt"); | |
| term.textContent = label; | |
| const value = document.createElement("dd"); | |
| values[key] = value; | |
| group.append(term, value); | |
| metrics.append(group); | |
| } | |
| const details = document.createElement("details"); | |
| details.className = "race-piece-detail"; | |
| const decisionSummary = document.createElement("summary"); | |
| const alternatives = document.createElement("div"); | |
| alternatives.className = "race-alternatives"; | |
| details.append(decisionSummary, alternatives); | |
| const revision = document.createElement("p"); | |
| revision.className = "race-revision"; | |
| revision.textContent = player.revision; | |
| revision.title = `Player revision: ${player.revision}`; | |
| card.setAttribute("aria-labelledby", name.id); | |
| card.append(heading, wrap, metrics, details, revision); | |
| $("race-boards").append(card); | |
| return { ...player, board, boardCells, status, values, details, decisionSummary, alternatives }; | |
| } | |
| function renderRace() { | |
| if (!racePlayers) return; | |
| for (const player of racePlayers) { | |
| const index = Math.min(raceIndex, player.frames.length - 1); | |
| const state = player.frames[index]; | |
| for (let i = 0; i < player.boardCells.length; i++) { | |
| const value = state.board[Math.floor(i / 10)][i % 10]; | |
| player.boardCells[i].className = `cell${value ? " filled" : ""}`; | |
| player.boardCells[i].style.setProperty("--piece-color", `var(--piece-${value || 1})`); | |
| } | |
| for (const key of ["lines", "score", "pieces"]) player.values[key].textContent = state[key].toLocaleString(); | |
| const ended = index === player.frames.length - 1; | |
| const failed = ended && player.outcome?.status === "error"; | |
| player.status.textContent = failed ? "PLAYER ERROR" : state.terminal ? "TOPPED OUT" : ended ? player.outcome?.cap_hit ? "CAP REACHED" : "RECORDING ENDED" : "RECORDED"; | |
| player.status.classList.toggle("ended", ended); | |
| player.status.classList.toggle("failed", failed); | |
| player.alternatives.replaceChildren(); | |
| const decision = player.decisions?.[index]; | |
| const distribution = decision?.probabilities; | |
| if (failed) { | |
| player.decisionSummary.textContent = "Inference error · details"; | |
| const message = document.createElement("p"); | |
| message.textContent = decision?.error?.message || player.errors?.[0]?.message || "The recorded player failed at this position."; | |
| player.alternatives.append(message); | |
| } else if (decision?.action_id) { | |
| const probability = distribution?.[decision.action_id]; | |
| player.decisionSummary.textContent = `Next ${decision.action_id}${typeof probability === "number" ? ` · ${(probability * 100).toFixed(1)}%` : " · chosen"}`; | |
| const context = document.createElement("p"); | |
| context.textContent = `Recorded choice for ${state.current}; preview ${state.next_piece}.`; | |
| player.alternatives.append(context); | |
| if (distribution) { | |
| const heading = document.createElement("p"); | |
| heading.textContent = "Other recorded options"; | |
| player.alternatives.append(heading); | |
| const options = Object.entries(distribution).filter(([id]) => id !== decision.action_id).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 3); | |
| for (const [id, value] of options) { | |
| const line = document.createElement("div"); | |
| line.textContent = `${id}: ${(value * 100).toFixed(1)}%`; | |
| player.alternatives.append(line); | |
| } | |
| } else { | |
| const noProbabilities = document.createElement("p"); | |
| noProbabilities.textContent = "This player does not report probabilities."; | |
| player.alternatives.append(noProbabilities); | |
| } | |
| if (typeof decision.decision_seconds === "number") { | |
| const latency = document.createElement("p"); | |
| latency.textContent = `Measured decision: ${(decision.decision_seconds * 1000).toFixed(1)} ms. Playback is independent.`; | |
| player.alternatives.append(latency); | |
| } | |
| } else { | |
| player.decisionSummary.textContent = state.terminal ? "No legal placements remain" : ended ? "Recording ended" : `Current ${state.current} · Next ${state.next_piece}`; | |
| const note = document.createElement("p"); | |
| note.textContent = ended ? "No next recorded decision." : "This older replay contains moves only, without per-decision probabilities."; | |
| player.alternatives.append(note); | |
| } | |
| player.board.setAttribute("aria-label", `${player.name} recorded board. ${state.pieces} pieces placed, ${state.lines} lines, score ${state.score}.${state.terminal ? " Topped out." : ""}`); | |
| } | |
| $("race-position").value = raceIndex; | |
| $("race-position").max = raceMax; | |
| $("race-progress").textContent = `${raceIndex} / ${raceMax}`; | |
| $("race-prev").disabled = raceIndex === 0; | |
| $("race-next").disabled = raceIndex === raceMax; | |
| $("race-play").disabled = raceMax === 0; | |
| $("race-restart").disabled = raceMax === 0; | |
| renderHumanComparison(); | |
| } | |
| async function loadRace() { | |
| if (racePlayers || raceLoading) return; | |
| raceLoading = true; | |
| $("race-loading").hidden = false; | |
| $("race-loading").textContent = "Loading recorded player games…"; | |
| try { | |
| const manifest = await api("/static/baseline-demo.json"); | |
| if (manifest.schema_version !== 1 || !Number.isSafeInteger(manifest.seed) || !Number.isSafeInteger(manifest.max_pieces) || manifest.max_pieces < 1 || !Array.isArray(manifest.players) || manifest.players.length < 2 || manifest.players.length > 4) { | |
| throw new Error("The recorded race manifest has an unsupported format."); | |
| } | |
| for (const player of manifest.players) { | |
| if (typeof player.name !== "string" || typeof player.revision !== "string" || player.replay?.seed !== manifest.seed || !Array.isArray(player.replay?.actions) || player.replay.actions.length > manifest.max_pieces) { | |
| throw new Error("Race players must use the same seed and the declared episode cap."); | |
| } | |
| } | |
| const loaded = await Promise.all(manifest.players.map(async (player) => { | |
| const result = await api("/api/replays", { method: "POST", body: JSON.stringify(player.replay) }); | |
| if (!Array.isArray(result.frames) || result.frames.length !== player.replay.actions.length + 1) throw new Error("A race replay has an invalid frame count."); | |
| return { ...player, frames: result.frames }; | |
| })); | |
| $("race-boards").replaceChildren(humanRaceCard); | |
| raceManifest = manifest; | |
| racePlayers = loaded.map(raceCard); | |
| raceMax = Math.max(...racePlayers.map((player) => player.frames.length - 1)); | |
| $("race-label").textContent = typeof manifest.label === "string" ? manifest.label : "Recorded player comparison"; | |
| $("race-context").textContent = `Seed ${manifest.seed} · ${manifest.max_pieces}-piece episode cap · Same seven-bag sequence`; | |
| $("race-inference-label").textContent = "No live model inference"; | |
| $("race-caption").textContent = typeof manifest.disclaimer === "string" ? manifest.disclaimer : "Recorded development games, not a held-out benchmark. Playback speed does not represent decision latency."; | |
| $("race-report-link-wrap").hidden = true; | |
| $("race-report-link").removeAttribute("href"); | |
| if (typeof manifest.report_url === "string") { | |
| try { | |
| const reportUrl = new URL(manifest.report_url); | |
| if (reportUrl.protocol === "https:" && reportUrl.hostname && !reportUrl.username && !reportUrl.password) { | |
| $("race-report-link").href = reportUrl.href; | |
| $("race-report-link-wrap").hidden = false; | |
| } | |
| } catch { /* An invalid optional link must not prevent replay. */ } | |
| } | |
| $("race-source-list").replaceChildren(); | |
| const sources = Array.isArray(manifest.sources) ? manifest.sources : []; | |
| $("race-provenance").hidden = sources.length === 0; | |
| for (const source of sources) { | |
| const line = document.createElement("li"); | |
| line.textContent = `${source.file || "Source artifact"} · SHA-256 ${source.sha256 || "not supplied"}`; | |
| $("race-source-list").append(line); | |
| } | |
| $("race-loading").hidden = true; | |
| $("race-transport").hidden = false; | |
| $("race-join-controls").hidden = false; | |
| renderRace(); | |
| renderControls(); | |
| } catch (error) { | |
| $("race-loading").textContent = `Race unavailable: ${error.message} Switch to Play and back to retry.`; | |
| $("race-context").textContent = "No comparison results are available yet."; | |
| } finally { raceLoading = false; } | |
| } | |
| function setView(watchRace) { | |
| if (busy) return; | |
| raceVisible = watchRace; | |
| stopReplay(); | |
| stopRace(); | |
| $("play-view").hidden = watchRace; | |
| $("race-view").hidden = !watchRace; | |
| for (const [id, active] of [["play-tab", !watchRace], ["race-tab", watchRace]]) { | |
| $(id).classList.toggle("active", active); | |
| $(id).setAttribute("aria-selected", String(active)); | |
| $(id).tabIndex = active ? 0 : -1; | |
| } | |
| mountHumanBoard(); | |
| if (watchRace) { | |
| loadRace(); | |
| syncRaceToHuman(); | |
| } | |
| render(); | |
| } | |
| $("join-race").addEventListener("click", () => operation(async () => { | |
| if (!raceManifest) return; | |
| const snapshot = await api("/api/games", { method: "POST", body: JSON.stringify({ seed: raceManifest.seed }) }); | |
| stopReplay(); | |
| stopRace(); | |
| frames = null; | |
| replayArtifact = null; | |
| live = snapshot; | |
| stateSynced = true; | |
| activeSeed = raceManifest.seed; | |
| $("seed").value = activeSeed; | |
| raceHumanActive = true; | |
| chooseNearest(); | |
| mountHumanBoard(); | |
| syncRaceToHuman(); | |
| render(); | |
| $("announcement").textContent = `Live comparison started on seed ${activeSeed}, with a ${raceManifest.max_pieces}-piece cap. The other boards are recorded.`; | |
| $("board").focus({ preventScroll: true }); | |
| })); | |
| $("leave-comparison-play").addEventListener("click", () => leaveComparison()); | |
| $("leave-comparison-race").addEventListener("click", () => leaveComparison()); | |
| $("race-match-human").addEventListener("click", syncRaceToHuman); | |
| $("play-tab").addEventListener("click", () => setView(false)); | |
| $("race-tab").addEventListener("click", () => setView(true)); | |
| for (const id of ["play-tab", "race-tab"]) { | |
| $(id).addEventListener("keydown", (event) => { | |
| if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return; | |
| event.preventDefault(); | |
| const watchRace = event.key === "End" || (event.key !== "Home" && !raceVisible); | |
| setView(watchRace); | |
| $(watchRace ? "race-tab" : "play-tab").focus(); | |
| }); | |
| } | |
| function seekRace(index) { | |
| if (!racePlayers) return; | |
| raceIndex = Math.max(0, Math.min(raceMax, index)); | |
| renderRace(); | |
| } | |
| $("race-position").addEventListener("input", (event) => { stopRace(); seekRace(Number(event.target.value)); }); | |
| $("race-restart").addEventListener("click", () => { stopRace(); seekRace(0); }); | |
| $("race-prev").addEventListener("click", () => { stopRace(); seekRace(raceIndex - 1); }); | |
| $("race-next").addEventListener("click", () => { stopRace(); seekRace(raceIndex + 1); }); | |
| $("race-play").addEventListener("click", () => { | |
| if (raceTimer !== null) return stopRace(); | |
| if (!racePlayers || raceMax === 0) return; | |
| if (raceIndex === raceMax) seekRace(0); | |
| $("race-play").textContent = "Pause race Ⅱ"; | |
| $("race-play").setAttribute("aria-label", "Pause recorded race"); | |
| raceTimer = setInterval(() => { | |
| seekRace(raceIndex + 1); | |
| if (raceIndex === raceMax) stopRace(); | |
| }, 160); | |
| }); | |
| restart(); | |