(function () { "use strict"; var BASE = document.body.dataset.baseUrl || "/"; let gameState = null; let timerInterval = null; let selectedCells = []; let feedbackTimeout = null; function titleCase(word) { return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); } // DOM refs const setupDiv = document.getElementById("game-setup"); const boardDiv = document.getElementById("game-board"); const categorySelect = document.getElementById("category-select"); const boardSizeInput = document.getElementById("board-size"); const sizeDisplay = document.getElementById("size-display"); const wordCountInput = document.getElementById("word-count"); const wordCountDisplay = document.getElementById("word-count-display"); const newGameBtn = document.getElementById("new-game-btn"); const gridTable = document.getElementById("grid-table"); const wordList = document.getElementById("word-list"); const wordInput = document.getElementById("word-input"); const guessBtn = document.getElementById("guess-btn"); const clearSelectionBtn = document.getElementById("clear-selection-btn"); const guessFeedback = document.getElementById("guess-feedback"); const timerEl = document.getElementById("timer"); const completionBanner = document.getElementById("completion-banner"); const finalTime = document.getElementById("final-time"); const playAgainBtn = document.getElementById("play-again-btn"); const inputArea = document.getElementById("input-area"); const playerNameInput = document.getElementById("player-name"); // Load categories async function loadCategories() { try { const resp = await fetch(BASE + "api/categories"); if (!resp.ok) throw new Error("Server error"); const categories = await resp.json(); categorySelect.innerHTML = ""; categories.forEach(function (cat) { const opt = document.createElement("option"); opt.value = cat.id; opt.textContent = cat.name.charAt(0).toUpperCase() + cat.name.slice(1); categorySelect.appendChild(opt); }); } catch (err) { showFeedback("Failed to load categories.", "text-danger"); } } boardSizeInput.addEventListener("input", function () { sizeDisplay.textContent = this.value; }); wordCountInput.addEventListener("input", function () { wordCountDisplay.textContent = this.value; }); // New game newGameBtn.addEventListener("click", async function () { try { const categoryId = parseInt(categorySelect.value); const boardSize = parseInt(boardSizeInput.value); const wordCount = parseInt(wordCountInput.value); const playerName = playerNameInput.value.trim(); const resp = await fetch(BASE + "api/game/new", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ category_id: categoryId, board_size: boardSize, word_count: wordCount, player_name: playerName, }), }); if (!resp.ok) { const errData = await resp.json().catch(function () { return {}; }); throw new Error(errData.error || "Failed to create game"); } gameState = await resp.json(); showBoard(); } catch (err) { showFeedback(err.message || "Failed to create game.", "text-danger"); } }); function showBoard() { setupDiv.classList.add("d-none"); boardDiv.classList.remove("d-none"); completionBanner.classList.add("d-none"); playAgainBtn.classList.add("d-none"); inputArea.classList.remove("d-none"); selectedCells = []; renderGrid(); renderWordList(); startTimer(); } function renderGrid() { gridTable.innerHTML = ""; gameState.grid.forEach(function (row, r) { const tr = document.createElement("tr"); row.forEach(function (letter, c) { const td = document.createElement("td"); td.textContent = letter; td.dataset.row = r; td.dataset.col = c; td.addEventListener("click", function () { onCellClick(r, c, td); }); tr.appendChild(td); }); gridTable.appendChild(tr); }); } function renderWordList() { wordList.innerHTML = ""; gameState.words.forEach(function (w) { const li = document.createElement("li"); li.className = "list-group-item"; li.textContent = titleCase(w.text); li.dataset.word = w.text; if (w.found) { li.classList.add("word-found"); } wordList.appendChild(li); }); } // Timer function startTimer() { if (timerInterval) clearInterval(timerInterval); const startedAt = new Date(gameState.started_at); function tick() { const elapsed = Math.floor((Date.now() - startedAt.getTime()) / 1000); const mins = Math.floor(elapsed / 60); const secs = elapsed % 60; timerEl.textContent = mins + ":" + (secs < 10 ? "0" : "") + secs; } tick(); timerInterval = setInterval(tick, 1000); } function stopTimer() { if (timerInterval) { clearInterval(timerInterval); timerInterval = null; } } // Text guess guessBtn.addEventListener("click", submitTextGuess); wordInput.addEventListener("keydown", function (e) { if (e.key === "Enter") submitTextGuess(); }); async function submitTextGuess() { const word = wordInput.value.trim(); if (!word) return; wordInput.value = ""; guessBtn.disabled = true; try { await sendGuess({ word: word }); } finally { guessBtn.disabled = false; } } // Click selection — two-click: first letter, then last letter function onCellClick(row, col, td) { if (gameState.status === "completed") return; // No start cell yet — this is the first click if (selectedCells.length === 0) { selectedCells.push({ row: row, col: col }); td.classList.add("cell-selected"); updateSelectionButtons(); return; } // Clicking the same cell — deselect if (selectedCells[0].row === row && selectedCells[0].col === col) { clearSelection(); return; } // Second click — compute line from start to end var startR = selectedCells[0].row; var startC = selectedCells[0].col; var dr = row - startR; var dc = col - startC; // Validate straight line var absDr = Math.abs(dr); var absDc = Math.abs(dc); if (absDr !== absDc && absDr !== 0 && absDc !== 0) { showFeedback("Selection must form a straight line.", "text-warning"); clearSelection(); return; } var steps = Math.max(absDr, absDc); if (steps === 0) return; // Unit direction var stepR = dr === 0 ? 0 : dr / absDr; var stepC = dc === 0 ? 0 : dc / absDc; // Build full cell list from start to end var cells = []; for (var i = 0; i <= steps; i++) { cells.push({ row: startR + stepR * i, col: startC + stepC * i }); } // Highlight all cells cells.forEach(function (c) { var cell = gridTable.querySelector( 'td[data-row="' + c.row + '"][data-col="' + c.col + '"]' ); if (cell) cell.classList.add("cell-selected"); }); // Auto-submit selectedCells = cells; guessBtn.disabled = true; sendGuess({ cells: cells }).then(function () { guessBtn.disabled = false; clearSelection(); }); } function updateSelectionButtons() { if (selectedCells.length > 0) { clearSelectionBtn.classList.remove("d-none"); } else { clearSelectionBtn.classList.add("d-none"); } } clearSelectionBtn.addEventListener("click", clearSelection); function clearSelection() { selectedCells = []; document.querySelectorAll(".cell-selected").forEach(function (td) { td.classList.remove("cell-selected"); }); updateSelectionButtons(); } // Send guess to server async function sendGuess(payload) { try { const resp = await fetch(BASE + "api/game/" + gameState.id + "/guess", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!resp.ok) throw new Error("Server error"); const result = await resp.json(); gameState.words = result.words; gameState.status = result.status; if (result.correct) { highlightFoundCells(result.cells); renderWordList(); showFeedback("Found: " + titleCase(result.word) + "!", "text-success"); } else { showFeedback("Not a match.", "text-danger"); } if (result.status === "completed") { gameState.completed_at = result.completed_at; gameState.player_name = result.player_name; onGameComplete(result.completed_at); } } catch (err) { showFeedback("Request failed. Try again.", "text-danger"); } } function highlightFoundCells(cells) { cells.forEach(function (c) { const td = gridTable.querySelector( 'td[data-row="' + c.row + '"][data-col="' + c.col + '"]' ); if (td) td.classList.add("cell-found"); }); } function showFeedback(msg, cls) { if (feedbackTimeout) clearTimeout(feedbackTimeout); guessFeedback.textContent = msg; guessFeedback.className = "mt-2 " + cls; feedbackTimeout = setTimeout(function () { guessFeedback.textContent = ""; guessFeedback.className = "mt-2"; feedbackTimeout = null; }, 2000); } function onGameComplete(completedAt) { stopTimer(); completionBanner.classList.remove("d-none"); finalTime.textContent = timerEl.textContent; playAgainBtn.classList.remove("d-none"); inputArea.classList.add("d-none"); var startedAt = new Date(gameState.started_at); var endedAt = new Date(completedAt); var seconds = Math.floor((endedAt.getTime() - startedAt.getTime()) / 1000); saveRecentGame({ game_id: gameState.id, player_name: gameState.player_name || "Anonymous", category: gameState.category, time_display: timerEl.textContent, time_seconds: seconds, board_size: gameState.board_size, word_count: gameState.words.length, completed_at: completedAt, }); } function saveRecentGame(entry) { var KEY = "ws-recent-games"; try { var games = JSON.parse(localStorage.getItem(KEY) || "[]"); games.unshift(entry); if (games.length > 20) games = games.slice(0, 20); localStorage.setItem(KEY, JSON.stringify(games)); } catch (e) { /* localStorage unavailable */ } } playAgainBtn.addEventListener("click", function () { boardDiv.classList.add("d-none"); setupDiv.classList.remove("d-none"); }); // Init loadCategories(); })();