(function () { const $game = (selector, root = document) => root.querySelector(selector); const GAME_META = { twenty_four: { title: "24 点", kicker: "ARITHMETIC PUZZLE", action: "开始组式", }, sudoku: { title: "数独", kicker: "LOGIC GRID", action: "开始推理", }, }; function difficultySelect() { const select = document.createElement("select"); select.className = "game-difficulty"; [ ["easy", "入门"], ["standard", "标准"], ["hard", "进阶"], ].forEach(([value, label]) => { const option = document.createElement("option"); option.value = value; option.textContent = label; if (value === "standard") option.selected = true; select.append(option); }); return select; } function gameCard(game) { const meta = GAME_META[game.kind]; const article = document.createElement("article"); article.className = `game-card game-${game.kind}`; const top = document.createElement("div"); top.className = "game-card-top"; const icon = document.createElement("span"); icon.textContent = game.kind === "sudoku" ? "9×9" : "24"; const time = document.createElement("small"); time.textContent = `约 ${game.estimated_minutes} 分钟`; top.append(icon, time); const kicker = document.createElement("b"); kicker.textContent = meta.kicker; const title = document.createElement("h3"); title.textContent = game.title; const summary = document.createElement("p"); summary.textContent = game.summary; const controls = document.createElement("div"); controls.className = "game-card-controls"; const difficulty = difficultySelect(); const start = document.createElement("button"); start.className = "primary-button"; start.textContent = meta.action; start.addEventListener("click", () => startGame(game.kind, difficulty.value)); controls.append(difficulty, start); article.append(top, kicker, title, summary, controls); return article; } async function loadGames() { const root = $game("#math-game-list"); try { const games = await api("contests/games/"); root.replaceChildren(...games.map(gameCard)); } catch (error) { root.textContent = error.message; } } async function startGame(kind, difficulty) { if (!requireAuth()) return; try { const attempt = await api(`contests/games/${kind}/start/`, { method: "POST", body: { difficulty }, }); renderGame(attempt); $game("#experience-dialog").showModal(); } catch (error) { showToast(error.message); } } function gameHeading(attempt) { const fragment = document.createDocumentFragment(); const label = document.createElement("span"); label.className = "kicker"; label.textContent = `${GAME_META[attempt.kind].kicker} · ${attempt.difficulty.toUpperCase()}`; const title = document.createElement("h2"); title.textContent = GAME_META[attempt.kind].title; fragment.append(label, title); return fragment; } function resultPanel(attempt) { const panel = document.createElement("div"); panel.className = "game-result-panel"; const score = document.createElement("strong"); score.textContent = `${attempt.score} 分`; const details = document.createElement("p"); details.textContent = `用时 ${(attempt.duration_ms / 1000).toFixed(1)} 秒 · 提示 ${attempt.hints_used} 次`; panel.append(score, details); return panel; } function renderGame(attempt) { if (attempt.kind === "sudoku") renderSudoku(attempt); else renderTwentyFour(attempt); } function renderTwentyFour(attempt) { const root = $game("#experience-content"); root.replaceChildren(); root.append(gameHeading(attempt)); if (attempt.status === "completed") { root.append(resultPanel(attempt)); return; } const instruction = document.createElement("p"); instruction.className = "scene"; instruction.textContent = "四个数字必须各使用一次,只允许 +、−、×、÷ 和括号。"; const numbers = document.createElement("div"); numbers.className = "twenty-four-numbers"; attempt.puzzle.numbers.forEach((number) => { const tile = document.createElement("button"); tile.type = "button"; tile.textContent = number; tile.addEventListener("click", () => { input.value += String(number); input.focus(); }); numbers.append(tile); }); const form = document.createElement("form"); form.className = "twenty-four-form"; const input = document.createElement("input"); input.className = "formula-input"; input.placeholder = "例如:6 / (1 - 3 / 4)"; input.autocomplete = "off"; input.inputMode = "text"; input.spellcheck = false; const errorMessage = document.createElement("p"); errorMessage.className = "form-error game-form-error"; errorMessage.setAttribute("role", "alert"); input.addEventListener("input", () => { errorMessage.textContent = ""; }); const keypad = document.createElement("div"); keypad.className = "game-keypad"; ["+", "-", "*", "/", "(", ")"].forEach((operator) => { const button = document.createElement("button"); button.type = "button"; button.textContent = operator; button.addEventListener("click", () => { input.value += operator; input.focus(); }); keypad.append(button); }); const submit = document.createElement("button"); submit.className = "primary-button"; submit.type = "submit"; submit.textContent = "验证并计分"; form.append(input, keypad, errorMessage, submit); form.addEventListener("submit", async (event) => { event.preventDefault(); submit.disabled = true; errorMessage.textContent = ""; try { const result = await api( `contests/games/attempts/${attempt.attempt_id}/submit/`, { method: "POST", headers: { "Idempotency-Key": createIdempotencyKey() }, body: { expression: input.value }, }, ); renderTwentyFour(result); showToast("得到 24,成绩已记录"); } catch (error) { errorMessage.textContent = error.message; showToast(error.message); submit.disabled = false; } }); root.append(instruction, numbers, form); window.setTimeout(() => input.focus(), 50); } function sudokuCell(given, value, row, column) { const input = document.createElement("input"); input.inputMode = "numeric"; input.pattern = "[1-9]"; input.maxLength = 1; input.dataset.row = row; input.dataset.column = column; input.value = value || ""; input.className = given ? "given" : ""; input.readOnly = given; input.setAttribute("aria-label", `第 ${row + 1} 行第 ${column + 1} 列`); input.addEventListener("input", () => { input.value = input.value.replace(/[^1-9]/g, "").slice(0, 1); }); return input; } function renderSudoku(attempt) { const root = $game("#experience-content"); root.replaceChildren(); root.append(gameHeading(attempt)); if (attempt.status === "completed") { root.append(resultPanel(attempt)); return; } const instruction = document.createElement("p"); instruction.className = "scene"; instruction.textContent = "每一行、每一列和每个 3×3 九宫格都要包含 1 到 9。"; const board = document.createElement("div"); board.className = "sudoku-board"; const hints = new Map( (attempt.puzzle.hints || []).map((item) => [`${item.row}-${item.column}`, item.value]) ); attempt.puzzle.grid.forEach((rowValues, row) => { rowValues.forEach((givenValue, column) => { const hintValue = hints.get(`${row}-${column}`) || 0; const input = sudokuCell(Boolean(givenValue), givenValue || hintValue, row, column); if (hintValue) { input.readOnly = true; input.classList.add("hint"); } board.append(input); }); }); const actions = document.createElement("div"); actions.className = "sudoku-actions"; const hint = document.createElement("button"); hint.type = "button"; hint.className = "ghost-button"; hint.textContent = `提示(已用 ${attempt.hints_used}/3)`; hint.disabled = attempt.hints_used >= 3; const submit = document.createElement("button"); submit.type = "button"; submit.className = "primary-button"; submit.textContent = "检查并完成"; hint.addEventListener("click", async () => { hint.disabled = true; try { const result = await api( `contests/games/attempts/${attempt.attempt_id}/hint/`, { method: "POST", body: {} }, ); const input = $game( `[data-row="${result.row}"][data-column="${result.column}"]`, board, ); input.value = result.value; input.readOnly = true; input.classList.add("hint"); attempt.hints_used = result.hints_used; hint.textContent = `提示(已用 ${result.hints_used}/3)`; hint.disabled = result.hints_used >= 3; } catch (error) { showToast(error.message); hint.disabled = false; } }); submit.addEventListener("click", async () => { submit.disabled = true; const grid = Array.from({ length: 9 }, () => Array(9).fill(0)); $gameAll("input", board).forEach((input) => { grid[Number(input.dataset.row)][Number(input.dataset.column)] = Number(input.value || 0); }); try { const result = await api( `contests/games/attempts/${attempt.attempt_id}/submit/`, { method: "POST", headers: { "Idempotency-Key": createIdempotencyKey() }, body: { grid }, }, ); renderSudoku(result); showToast("数独完成,成绩已记录"); } catch (error) { showToast(error.message); submit.disabled = false; } }); actions.append(hint, submit); root.append(instruction, board, actions); } function $gameAll(selector, root = document) { return [...root.querySelectorAll(selector)]; } window.HuluGames = { load: loadGames }; })();