nima1's picture
Publish verified checkpoint and losslessly compressed study evidence
4be6a52 verified
Raw History Blame Contribute Delete
28.8 kB
"use strict";
// 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();