Files
Hulumath-Web/backend/static/js/games.js
T

305 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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 };
})();