// --- Embeddings API Configuration --- // Si estamos en localhost para dev, apuntamos al puerto 8000. Si estamos en producción o en un Space de HF, usamos el origin actual. const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; const EMBEDDINGS_API_URL = isLocalhost ? "http://localhost:8000" : window.location.origin; // Game State let CORRECT_WORD = null; let gameNumber = 0; let attempts = 0; let guesses = []; let gameWon = false; let startTime = Date.now(); let isLoading = false; let serverOnline = false; let isDailyMode = true; // Default to daily challenge let currentDailyIdentifier = null; // Tracks current day based on server time async function fetchRandomWord(exclude = '') { const url = `${EMBEDDINGS_API_URL}/random-word${exclude ? `?exclude=${encodeURIComponent(exclude)}` : ''}`; const response = await fetch(url); if (!response.ok) throw new Error('Could not fetch word'); const data = await response.json(); return data.word; } async function fetchDailyWord() { const url = `${EMBEDDINGS_API_URL}/daily-word`; const response = await fetch(url); if (!response.ok) throw new Error('Could not fetch daily word'); const data = await response.json(); return data; } // DOM Elements const input = document.getElementById('word-input'); const submitBtn = document.getElementById('btn-submit'); const guessesList = document.getElementById('guesses-list'); const victoryModal = document.getElementById('victory-modal'); const helpModal = document.getElementById('help-modal'); const statsModal = document.getElementById('stats-modal'); const btnHelp = document.getElementById('btn-help'); const btnStats = document.getElementById('btn-stats'); const btnCloseHelp = document.getElementById('btn-close-help'); const btnCloseStats = document.getElementById('btn-close-stats'); const btnCloseStatsAction = document.getElementById('btn-close-stats-action'); const btnUnderstood = document.getElementById('btn-understood'); const btnShare = document.getElementById('btn-share'); const btnNewGame = document.getElementById('btn-new-game'); const btnNewWord = document.getElementById('btn-new-word'); // Share Modal Elements const shareModal = document.getElementById('share-modal'); const btnCloseShare = document.getElementById('btn-close-share'); const btnShareWa = document.getElementById('btn-share-wa'); const btnShareTw = document.getElementById('btn-share-tw'); const btnShareFb = document.getElementById('btn-share-fb'); const btnShareCopy = document.getElementById('btn-share-copy'); const btnHint = document.getElementById('btn-hint'); const btnGiveUp = document.getElementById('btn-give-up'); // New Modal Elements const messageModal = document.getElementById('message-modal'); const confirmModal = document.getElementById('confirm-modal'); const btnCloseMessage = document.getElementById('btn-close-message'); const btnMessageOk = document.getElementById('btn-message-ok'); const btnConfirmCancel = document.getElementById('btn-confirm-cancel'); const btnConfirmOk = document.getElementById('btn-confirm-ok'); // Server status element const serverStatusEl = document.getElementById('server-status'); // Initialize document.addEventListener('DOMContentLoaded', async () => { input.disabled = true; submitBtn.disabled = true; input.focus(); const online = await checkServerHealth(); if (online) { try { const dailyData = await fetchDailyWord(); const dailyWord = dailyData.word; currentDailyIdentifier = dailyData.date; // Check if user has won today's daily word to enforce it const dailyStateJSON = localStorage.getItem(`MF_STATE_DAILY_${currentDailyIdentifier}`); let hasWonDaily = false; if (dailyStateJSON) { const ds = JSON.parse(dailyStateJSON); hasWonDaily = ds.gameWon; } // Determine which mode to load const savedMode = localStorage.getItem('MF_LAST_MODE'); // Forced to daily if it's a new day and hasn't been won if (!hasWonDaily) { isDailyMode = true; } else { isDailyMode = savedMode === null ? true : (savedMode === 'daily'); } if (isDailyMode) { CORRECT_WORD = dailyWord; gameNumber = dailyData.game_number; loadGameState(currentDailyIdentifier); } else { // If last mode was practice, try to load it, otherwise fallback to daily if (!loadGameState('practice')) { isDailyMode = true; CORRECT_WORD = dailyWord; gameNumber = dailyData.game_number; loadGameState(currentDailyIdentifier); } } updateGameModeUI(); console.log(`🎮 Game loaded. Mode: ${isDailyMode ? 'Daily' : 'Practice'}`); } catch (e) { console.error('Initial load failed:', e); startNewGame(); } } else { // Offline fallback CORRECT_WORD = 'TIEMPO'; currentDailyIdentifier = 'offline'; loadGameState('offline'); updateGameModeUI(); } input.disabled = false; submitBtn.disabled = false; input.focus(); setInterval(checkServerHealth, 30000); }); async function checkServerHealth() { try { const response = await fetch(`${EMBEDDINGS_API_URL}/health`, { signal: AbortSignal.timeout(3000) }); if (response.ok) { serverOnline = true; updateServerStatus(true); return true; } } catch { // Server not reachable } serverOnline = false; updateServerStatus(false); return false; } function updateServerStatus(online) { if (!serverStatusEl) return; serverStatusEl.classList.toggle('offline', !online); serverStatusEl.title = online ? 'Servidor online' : 'Servidor offline'; } // Help button const btnHelpFloat = document.getElementById('btn-help-float'); const openHelp = () => { console.log("Help opened"); helpModal.classList.remove('hidden'); // Focus "¡Entendido!" button setTimeout(() => document.getElementById('btn-understood').focus(), 50); }; btnHelp.addEventListener('click', openHelp); if (btnHelpFloat) btnHelpFloat.addEventListener('click', openHelp); btnCloseHelp.addEventListener('click', () => helpModal.classList.add('hidden')); btnUnderstood.addEventListener('click', () => helpModal.classList.add('hidden')); // Stats button const btnStatsFloat = document.getElementById('btn-stats-float'); const openStats = () => { if (gameWon) { victoryModal.classList.remove('hidden'); // Focus first action button in victory modal setTimeout(() => victoryModal.querySelector('button')?.focus(), 50); } else { updateStatsModal(); statsModal.classList.remove('hidden'); setTimeout(() => statsModal.querySelector('button.btn-primary')?.focus(), 50); } }; btnStats.addEventListener('click', openStats); if (btnStatsFloat) btnStatsFloat.addEventListener('click', openStats); btnCloseStats.addEventListener('click', () => statsModal.classList.add('hidden')); btnCloseStatsAction.addEventListener('click', () => statsModal.classList.add('hidden')); // Close modals on backdrop click const allModals = [victoryModal, helpModal, statsModal, messageModal, confirmModal, shareModal]; allModals.forEach(modal => { modal.addEventListener('click', (e) => { if (e.target === modal) { closeActiveModal(); } }); }); // Keyboard Navigation for Modals document.addEventListener('keydown', (e) => { const activeModal = allModals.find(m => !m.classList.contains('hidden')); if (!activeModal) return; // 1. ESC to close/cancel if (e.key === 'Escape') { e.preventDefault(); closeActiveModal(); return; } // Identify focusable buttons in the modal const buttons = Array.from(activeModal.querySelectorAll('button:not([disabled]):not(.modal-close)')); if (buttons.length === 0) return; const focusedIdx = buttons.indexOf(document.activeElement); // 2. ENTER to accept if (e.key === 'Enter') { // If nothing is focused but there's only one button or a primary one, click it if (focusedIdx === -1) { const primary = activeModal.querySelector('.btn-primary') || buttons[0]; if (primary) primary.click(); } else { // If something is focused, Enter naturally clicks it (browser default), // but we can ensure it here if needed. } return; } // 3. Arrow Keys to move focus if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault(); const nextIdx = (focusedIdx + 1) % buttons.length; buttons[nextIdx].focus(); } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); const prevIdx = (focusedIdx - 1 + buttons.length) % buttons.length; buttons[prevIdx].focus(); } }); function closeActiveModal() { const activeModal = allModals.find(m => !m.classList.contains('hidden')); if (!activeModal) return; if (activeModal === confirmModal) { if (document.getElementById('btn-confirm-cancel')) document.getElementById('btn-confirm-cancel').click(); } else if (activeModal === messageModal) { if (document.getElementById('btn-close-message')) document.getElementById('btn-close-message').click(); } else if (activeModal === helpModal) { document.getElementById('btn-close-help').click(); } else if (activeModal === statsModal) { document.getElementById('btn-close-stats').click(); } else if (activeModal === victoryModal) { // Just hide it activeModal.classList.add('hidden'); } else { activeModal.classList.add('hidden'); } } // --- Mobile Toolbar: wire mob-btns to the same handlers as desktop buttons --- const mobBtns = [ ['btn-new-game-mob', 'btn-new-game'], ['btn-hint-mob', 'btn-hint'], ['btn-give-up-mob', 'btn-give-up'], ['btn-stats-mob', 'btn-stats'], ['btn-help-mob', 'btn-help'], ]; mobBtns.forEach(([mobId, desktopId]) => { const mobEl = document.getElementById(mobId); const deskEl = document.getElementById(desktopId); if (mobEl && deskEl) mobEl.addEventListener('click', () => deskEl.click()); }); // Back to Top Button Logic const btnBackToTop = document.getElementById('btn-back-to-top'); const gameBoard = document.querySelector('.game-board'); if (gameBoard && btnBackToTop) { gameBoard.addEventListener('scroll', () => { // Show button if scrolled down more than 300px if (gameBoard.scrollTop > 300) { btnBackToTop.classList.remove('hidden'); } else { btnBackToTop.classList.add('hidden'); } }); btnBackToTop.addEventListener('click', () => { gameBoard.scrollTo({ top: 0, behavior: 'smooth' }); }); } function updateStatsModal() { document.getElementById('stats-attempts').textContent = attempts; document.getElementById('stats-words').textContent = guesses.length; const bestRank = guesses.length > 0 ? Math.min(...guesses.map(g => g.rank)) : null; document.getElementById('stats-best-rank').textContent = bestRank ? `#${bestRank}` : '—'; // Status emoji let status = '🔍'; if (bestRank && bestRank <= 10) status = '🔥'; else if (bestRank && bestRank <= 100) status = '🎯'; else if (bestRank && bestRank <= 500) status = '👀'; else if (bestRank && bestRank <= 1500) status = '🌤️'; else if (bestRank) status = '❄️'; document.getElementById('stats-status').textContent = status; // Progress bar (logarithmic scale: rank 50000 = 0%, rank 1 = 100%) let progressPercent = 0; let progressText = 'Sin intentos'; if (bestRank) { progressPercent = Math.max(2, Math.min(100, 100 - (Math.log(bestRank) / Math.log(50000)) * 100)); if (bestRank <= 10) progressText = '¡Casi la tienes!'; else if (bestRank <= 100) progressText = 'Muy cerca'; else if (bestRank <= 500) progressText = 'Calentando'; else if (bestRank <= 1500) progressText = 'Tibio'; else progressText = 'Buscando...'; } document.getElementById('stats-progress-fill').style.width = `${progressPercent}%`; document.getElementById('stats-progress-text').textContent = progressText; // Hint const hintEl = document.getElementById('stats-hint'); if (bestRank && bestRank <= 100) { hintEl.querySelector('span').textContent = '¡Vas muy bien! Prueba sinónimos de tus mejores palabras.'; } else if (bestRank && bestRank <= 1000) { hintEl.querySelector('span').textContent = 'Estás en la dirección correcta. Piensa en palabras del mismo campo semántico.'; } else { hintEl.querySelector('span').textContent = 'Escribe palabras para descubrir la palabra secreta.'; } } submitBtn.addEventListener('click', handleGuess); input.addEventListener('keypress', (e) => { if (e.key === 'Enter') handleGuess(); }); btnShare.addEventListener('click', shareResult); btnNewGame.addEventListener('click', async () => { // 1. Check if midnight passed on the server if (serverOnline) { try { const dailyData = await fetchDailyWord(); if (currentDailyIdentifier && dailyData.date !== currentDailyIdentifier) { // Midnight passed, new day! currentDailyIdentifier = dailyData.date; CORRECT_WORD = dailyData.word; gameNumber = dailyData.game_number; isDailyMode = true; // Force daily mode await showMessage("¡Nuevo Día!", "Ya es medianoche. El juego se reinició a la nueva palabra diaria obligatoria."); loadGameState(currentDailyIdentifier); updateGameModeUI(); input.value = ''; input.disabled = false; submitBtn.disabled = false; input.focus(); return; // Stop practice game start } } catch (e) { console.error("Failed to check midnight status", e); } } // Self-healing: Ensure gameWon is true if the word has been found if (guesses.some(g => g.rank === 1)) { gameWon = true; } if (isDailyMode && !gameWon) { await showMessage("Palabra Diaria", "Para jugar al Modo Infinito primero tenés que adivinar la palabra del día de hoy o rendirte."); return; } if (!isDailyMode && attempts > 0 && !gameWon) { const confirmed = await showConfirm("Nueva Palabra", "¿Seguro que querés abandonar esta partida y empezar otra?"); if (!confirmed) return; } startNewGame(); }); btnNewWord.addEventListener('click', () => { victoryModal.classList.add('hidden'); btnNewGame.click(); // Share logic to check midnight and modes }); btnHint.addEventListener('click', () => { console.log("Hint button clicked"); handleHint(); }); btnGiveUp.addEventListener('click', async () => { if (!gameWon && attempts > 0) { const confirmed = await showConfirm("Rendirse", "¿Seguro que te rendís? Se revelará la palabra secreta."); if (!confirmed) return; } handleGiveUp(); }); // Share Modal Buttons btnCloseShare.addEventListener('click', () => shareModal.classList.add('hidden')); btnShareWa.addEventListener('click', () => { const text = getShareText(); const url = `https://api.whatsapp.com/send?text=${encodeURIComponent(text)}`; window.open(url, '_blank'); }); btnShareTw.addEventListener('click', () => { const text = getShareText(); const url = `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}`; window.open(url, '_blank'); }); btnShareFb.addEventListener('click', () => { // Facebook only allows sharing a URL, not custom text directly in the sharer // We'll share the current page URL const url = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(window.location.href)}`; window.open(url, '_blank'); }); btnShareCopy.addEventListener('click', async () => { const text = getShareText(); try { await navigator.clipboard.writeText(text); const icon = btnShareCopy.querySelector('i'); const span = btnShareCopy.querySelector('span'); const originalIcon = icon.className; const originalText = span.innerText; // Should be 'Copiar' icon.className = 'fa-solid fa-check'; span.innerText = '¡Copiado!'; setTimeout(() => { icon.className = originalIcon; span.innerText = originalText; }, 2000); } catch (err) { console.error('Failed to copy: ', err); } }); async function handleGuess() { if (gameWon || isLoading) return; // Check server status if (!serverOnline) { const isBack = await checkServerHealth(); if (!isBack) { await showMessage("Servidor Desconectado", "⚠️ El servidor de embeddings no está activo.\n\nEjecuta el servidor con:\n cd server\n python app.py"); return; } } const word = input.value.trim().toUpperCase(); // 1. Basic validation if (!word) return; if (word.length < 2) { await showMessage("Palabra muy corta", "La palabra debe tener al menos 2 letras."); return; } // 2. Validate only letters (including Spanish characters) if (!/^[A-ZÁÉÍÓÚÑÜ]+$/.test(word)) { await showMessage("Caracteres inválidos", "Solo se permiten letras."); return; } // 3. Check if already guessed (before calling API) if (guesses.some(g => g.word === word)) { shakeInput(); input.value = ''; return; } // UI Loading State setLoading(true); try { const result = await getRankFromEmbeddings(word); if (!result.found) { await showMessage("Palabra no encontrada", `La palabra "${word}" no se encuentra en el diccionario.\nPrueba con otra palabra.`); return; } attempts++; const guessResult = { word: word, rank: result.rank, similarity: result.similarity, id: Date.now() }; guesses.push(guessResult); guesses.sort((a, b) => a.rank - b.rank); // Sort by rank (1 is best) renderGuesses(guessResult.id); saveGameState(); input.value = ''; input.focus(); if (result.is_correct) { triggerWin(); } } catch (error) { console.error("Error fetching rank:", error); if (error.message.includes("RATE_LIMIT")) { await showMessage("Pausa", "Te quedaste sin energía. Estás enviando muchas palabras muy rápido.\nPor favor, esperá un minuto para seguir jugando y no agotar los recursos de HuggingFace."); } else { await showMessage("Error", "Ocurrió un error al consultar el servidor de embeddings.\nVerifica que esté corriendo."); } } finally { setLoading(false); } } async function getRankFromEmbeddings(guessWord) { // 1. Direct check for correct word (Zero cost) if (guessWord === CORRECT_WORD) { return { rank: 1, similarity: 1.0, found: true, normalized_word: guessWord.toLowerCase(), is_correct: true }; } // 2. Check Local Cache (Zero cost) const cacheKey = `MF_EMB_${CORRECT_WORD}_${guessWord}`; const cachedResult = localStorage.getItem(cacheKey); if (cachedResult) { const parsed = JSON.parse(cachedResult); console.log(`%c Cache Hit: ${guessWord} -> #${parsed.rank}`, 'color: cyan'); return parsed; } // 3. Call local embeddings server const response = await fetch(`${EMBEDDINGS_API_URL}/rank`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ guess: guessWord, secret: CORRECT_WORD }) }); if (!response.ok) { const errText = await response.text(); console.error("Embeddings API Error:", response.status, errText); if (response.status === 429) { throw new Error("RATE_LIMIT"); } throw new Error(`Embeddings API Error: ${response.status}`); } const data = await response.json(); // 4. Save to Cache if (data.found) { localStorage.setItem(cacheKey, JSON.stringify(data)); } return data; } async function handleHint() { if (gameWon || isLoading || !serverOnline) return; const confirmed = await showConfirm("¿Pedir Pista?", "¿Quieres una pista? El juego continuará pero contará como un intento."); if (!confirmed) { return; } setLoading(true); try { const response = await fetch(`${EMBEDDINGS_API_URL}/hint`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ secret: CORRECT_WORD, excluded_words: guesses.map(g => g.word) }) }); if (!response.ok) { if (response.status === 429) { throw new Error("RATE_LIMIT"); } throw new Error("Hint failed"); } const result = await response.json(); attempts++; const hintResult = { word: result.normalized_word.toUpperCase(), rank: result.rank, similarity: result.similarity, id: Date.now(), isHint: true }; guesses.push(hintResult); guesses.sort((a, b) => a.rank - b.rank); renderGuesses(hintResult.id); saveGameState(); } catch (error) { console.error("Hint error:", error); if (error.message.includes("RATE_LIMIT")) { await showMessage("Pausa", "Te quedaste sin energía. Estás enviando peticiones muy rápido.\nPor favor, esperá un minuto para pedir más pistas."); } else { await showMessage("Error", "No se pudo obtener una pista."); } } finally { setLoading(false); } } async function handleGiveUp() { if (gameWon) return; const confirmed = await showConfirm("¿Rendirse?", "¿Estás seguro de que quieres rendirte? Se revelará la palabra secreta."); if (confirmed) { triggerWin(true); // true means gave up } } function setLoading(loading) { isLoading = loading; if (loading) { submitBtn.innerHTML = ''; submitBtn.disabled = true; input.disabled = true; } else { submitBtn.innerHTML = ''; submitBtn.disabled = false; input.disabled = false; input.focus(); } } function renderGuesses(newGuessId = null) { guessesList.innerHTML = ''; let displayGuesses = [...guesses]; // Mover el nuevo intento al principio de la lista visualmente if (newGuessId) { const newGuessIndex = displayGuesses.findIndex(g => g.id === newGuessId); if (newGuessIndex !== -1) { const newGuess = displayGuesses.splice(newGuessIndex, 1)[0]; displayGuesses.unshift(newGuess); } } displayGuesses.forEach(guess => { const item = document.createElement('div'); // Determine class & status based on rank let rankClass = 'rank-low'; let statusText = 'LEJOS'; if (guess.rank === 1) { rankClass = 'rank-1'; statusText = 'ENCONTRADA'; } else if (guess.rank <= 300) { rankClass = 'rank-high'; statusText = 'MUY CERCA'; } else if (guess.rank <= 1500) { rankClass = 'rank-med'; statusText = 'TIBIO'; } item.className = `guess-item ${rankClass}`; // Highlight new card if (guess.id === newGuessId) { item.classList.add('new-guess'); // Scroll al top para asegurar que se ve la caja de input y la nueva palabra setTimeout(() => { const gameBoard = document.querySelector('.game-board'); if (gameBoard) { gameBoard.scrollTo({ top: 0, behavior: 'smooth' }); } else { window.scrollTo({ top: 0, behavior: 'smooth' }); } }, 100); } // Card Layout: Info (Left) + Rank (Right) item.innerHTML = `
${guess.word}${guess.isHint ? 'PISTA' : ''} ${statusText}
${guess.rank} `; guessesList.appendChild(item); }); } function triggerWin(gaveUp = false) { gameWon = true; if (!gaveUp) { confetti({ particleCount: 150, spread: 70, origin: { y: 0.6 }, colors: ['#0df259', '#ffd700', '#ffffff'] }); } const titleEl = document.getElementById('victory-title'); const iconEl = document.getElementById('victory-icon'); if (gaveUp) { titleEl.innerText = "Fin del Juego"; iconEl.className = "fa-solid fa-flag"; document.getElementById('victory-header').classList.add('gave-up'); } else { titleEl.innerText = "¡Victoria!"; iconEl.className = "fa-solid fa-trophy"; document.getElementById('victory-header').classList.remove('gave-up'); } setTimeout(() => { document.getElementById('winning-word').innerText = CORRECT_WORD; document.getElementById('victory-attempts').innerText = attempts; // Focus primary action for keyboard users setTimeout(() => document.getElementById('btn-share')?.focus(), 100); const elapsedTime = Math.floor((Date.now() - startTime) / 1000); const mins = Math.floor(elapsedTime / 60).toString().padStart(2, '0'); const secs = (elapsedTime % 60).toString().padStart(2, '0'); document.getElementById('victory-time').innerText = `${mins}:${secs}`; victoryModal.classList.remove('hidden'); }, gaveUp ? 100 : 800); } function shakeInput() { input.classList.add('shake'); setTimeout(() => input.classList.remove('shake'), 500); } function shareResult() { shareModal.classList.remove('hidden'); } function getShareText() { let result = isDailyMode ? `Ponele #${gameNumber}\n` : `Ponele (Infinito)\n`; result += `en ${attempts} intentos\n`; // Visual progress (Face-based heat scale) // Starting with 🎯 on the third line as requested result += '🎯 '; const recentGuesses = [...guesses].sort((a, b) => b.id - a.id).slice(0, 8).reverse(); recentGuesses.forEach(g => { if (g.rank === 1) result += '🎯'; else if (g.rank <= 300) result += '😎'; else if (g.rank <= 1500) result += '😐'; else result += '🥶'; }); result += `\njugá en https://ponele.ar`; return result; } function updateGameModeUI() { const label = document.getElementById('game-number-label'); const modeTag = document.getElementById('game-mode-tag'); if (label) { if (isDailyMode && gameNumber > 0) { label.textContent = `#${gameNumber}`; label.style.display = 'inline'; } else { label.style.display = 'none'; } } if (modeTag) { modeTag.textContent = isDailyMode ? 'DIARIO' : 'INFINITO'; modeTag.classList.toggle('practice', !isDailyMode); } } function loadGameState(identifier) { const key = isDailyMode ? `MF_STATE_DAILY_${identifier}` : `MF_STATE_PRACTICE`; const savedState = localStorage.getItem(key); if (savedState) { const state = JSON.parse(savedState); // CRITICAL: Restore the secret word for this session if (!isDailyMode && state.secretWord) { CORRECT_WORD = state.secretWord; } attempts = state.attempts || 0; guesses = state.guesses || []; gameWon = state.gameWon || false; startTime = state.startTime || Date.now(); if (guesses.length > 0) { renderGuesses(); } if (gameWon) { triggerWin(); } return true; } else { // Reset local variables if no state for this identifier if (isDailyMode) { attempts = 0; guesses = []; gameWon = false; startTime = Date.now(); renderGuesses(); } return false; } } function saveGameState() { const identifier = isDailyMode ? currentDailyIdentifier : 'practice'; const key = isDailyMode ? `MF_STATE_DAILY_${identifier}` : `MF_STATE_PRACTICE`; // Save last mode to remember on refresh localStorage.setItem('MF_LAST_MODE', isDailyMode ? 'daily' : 'practice'); const state = { attempts, guesses, gameWon, startTime, secretWord: CORRECT_WORD // Store the word so it persists in Infinite mode }; localStorage.setItem(key, JSON.stringify(state)); } async function startNewGame() { input.disabled = true; submitBtn.disabled = true; isDailyMode = false; // Pick a new random word from the API (different from current) try { const newWord = await fetchRandomWord(CORRECT_WORD || ''); CORRECT_WORD = newWord; } catch (e) { console.error('Could not fetch new word:', e); } // Reset game state attempts = 0; guesses = []; gameWon = false; startTime = Date.now(); // Clear UI guessesList.innerHTML = ''; input.value = ''; input.disabled = false; submitBtn.disabled = false; input.focus(); saveGameState(); // Reset practice state in storage updateGameModeUI(); console.log(`🎮 Practice game started!`); } /* --- CUSTOM MODAL HANDLERS --- */ function showMessage(title, text) { return new Promise((resolve) => { document.getElementById('message-title').innerText = title; document.getElementById('message-text').innerText = text; messageModal.classList.remove('hidden'); // Focus the OK button setTimeout(() => btnMessageOk.focus(), 50); // Capture generic close button too const genericClose = document.getElementById('btn-close-message'); const close = () => { messageModal.classList.add('hidden'); cleanup(); resolve(); }; const cleanup = () => { btnMessageOk.removeEventListener('click', close); if (genericClose) genericClose.removeEventListener('click', close); }; btnMessageOk.addEventListener('click', close); if (genericClose) genericClose.addEventListener('click', close); }); } function showConfirm(title, text) { return new Promise((resolve) => { document.getElementById('confirm-title').innerText = title; document.getElementById('confirm-text').innerText = text; confirmModal.classList.remove('hidden'); // Focus the Confirm button by default setTimeout(() => btnConfirmOk.focus(), 50); const onYes = () => { confirmModal.classList.add('hidden'); cleanup(); resolve(true); }; const onNo = () => { confirmModal.classList.add('hidden'); cleanup(); resolve(false); }; const cleanup = () => { btnConfirmOk.removeEventListener('click', onYes); btnConfirmCancel.removeEventListener('click', onNo); }; btnConfirmOk.addEventListener('click', onYes); btnConfirmCancel.addEventListener('click', onNo); }); }