let miniMap, guessMarker, currentRounds = [], currentRoundIdx = 0, aiEventSource = null; function initIndexApp() { hydrateAuth(); document.getElementById('start-btn').addEventListener('click', async () => { const difficulty = document.getElementById('difficulty-select-lobby').value; const url = new URL(window.location.origin + '/game'); url.searchParams.set('difficulty', difficulty); window.location.href = url.toString(); }); } async function initGameApp() { const params = new URLSearchParams(window.location.search); const difficulty = params.get('difficulty') || 'easy'; const res = await fetch('/api/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ difficulty }) }); const data = await res.json(); if (!res.ok) { alert(data.error || 'Failed to start game'); window.location.href = '/'; return; } currentRounds = data.rounds; currentRoundIdx = 0; document.getElementById('game-container').style.display = 'block'; document.getElementById('chat-container').style.display = 'none'; document.getElementById('validate-btn').addEventListener('click', validateGuess); document.getElementById('next-round-btn').addEventListener('click', nextRound); const wrapEl = document.getElementById('mini-map-wrap'); document.getElementById('map-size-plus').addEventListener('click', (e) => { e.stopPropagation(); resizeMiniMap(wrapEl, 1); }); document.getElementById('map-size-minus').addEventListener('click', (e) => { e.stopPropagation(); resizeMiniMap(wrapEl, -1); }); showRound(); } async function hydrateAuth() { const res = await fetch('/api/me'); const me = await res.json(); const signedin = document.getElementById('signedin'); const signinBtn = document.getElementById('signin-btn'); const startBtn = document.getElementById('start-btn'); const limitMsg = document.getElementById('limit-msg'); if (me.authenticated) { signinBtn.style.display = 'none'; signedin.style.display = 'block'; document.getElementById('username').textContent = me.username; if (me.can_play_today) { startBtn.disabled = false; limitMsg.style.display = 'none'; } else { startBtn.disabled = true; limitMsg.style.display = 'block'; limitMsg.textContent = `You already played today. Try again in ${formatCountdown(me.seconds_until_midnight)}.`; startCountdown(limitMsg, me.seconds_until_midnight); } } else { signinBtn.style.display = 'inline-block'; signedin.style.display = 'none'; startBtn.disabled = true; } } function formatCountdown(seconds) { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = seconds % 60; return `${pad(h)}:${pad(m)}:${pad(s)}`; } function pad(n) { return n.toString().padStart(2, '0'); } function startCountdown(el, seconds) { let remaining = seconds; const id = setInterval(() => { remaining -= 1; if (remaining <= 0) { clearInterval(id); location.reload(); return; } el.textContent = `You already played today. Try again in ${formatCountdown(remaining)}.`; }, 1000); } async function startGame() { const difficulty = document.getElementById('difficulty-select-lobby').value; const res = await fetch('/api/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ difficulty }) }); const data = await res.json(); if (!res.ok) { alert(data.error || 'Failed to start game'); return; } currentRounds = data.rounds; currentRoundIdx = 0; document.getElementById('game-container').style.display = 'block'; document.getElementById('chat-container').style.display = 'none'; showRound(); } function showRound() { const round = currentRounds[currentRoundIdx]; const img = document.getElementById('street-image'); img.src = round.image_url; document.getElementById('validate-btn').style.display = 'none'; initMiniMap(); } function initMiniMap() { const miniEl = document.getElementById('mini-map'); miniMap = new google.maps.Map(miniEl, { center: { lat: 0, lng: 0 }, zoom: 1, streetViewControl: false, mapTypeControl: false, fullscreenControl: false, }); miniMap.addListener('click', (e) => { if (miniMap._locked) return; placeGuessMarker(e.latLng); document.getElementById('validate-btn').style.display = 'inline-block'; }); } function resizeMiniMap(el, delta) { const sizes = ['size-small', 'size-medium', 'size-large']; let idx = sizes.findIndex(c => el.classList.contains(c)); if (idx === -1) idx = 1; idx = Math.min(2, Math.max(0, idx + (delta > 0 ? 1 : -1))); sizes.forEach(c => el.classList.remove(c)); el.classList.add(sizes[idx]); if (miniMap) { const miniEl = document.getElementById('mini-map'); google.maps.event.trigger(miniMap, 'resize'); const center = miniMap.getCenter(); miniMap.setCenter(center); } } function placeGuessMarker(latLng) { if (guessMarker) guessMarker.setMap(null); guessMarker = new google.maps.Marker({ position: latLng, map: miniMap }); miniMap.setCenter(latLng); } async function validateGuess() { if (!guessMarker) { alert('Click on the mini-map to pick your guess.'); return; } const round = currentRounds[currentRoundIdx]; const pos = guessMarker.getPosition(); const payload = { round_id: round.id, lat: pos.lat(), lng: pos.lng() }; const res = await fetch('/api/guess', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const result = await res.json(); if (!res.ok) { alert(result.error || 'Guess failed'); return; } miniMap._locked = true; document.getElementById('validate-btn').style.display = 'none'; document.getElementById('chat-container').style.display = 'block'; const chat = document.getElementById('chat-log'); chat.textContent = ''; chat.textContent += 'Analyzing image...\n'; const aiRes = await fetch('/api/ai_analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ round_id: round.id }) }); const aiData = await aiRes.json(); if (!aiRes.ok) { chat.textContent += `[Error] ${aiData.error || 'AI failed'}\n`; document.getElementById('next-round-btn').disabled = false; openResultsPopup({ actual: result.actual_location, human: result.guess_location }); return; } chat.textContent += (aiData.text || '') + '\n'; openResultsPopup({ actual: result.actual_location, human: result.guess_location }); if (aiData.ai_guess) renderAIGuessOnPopup(aiData.ai_guess); document.getElementById('next-round-btn').disabled = false; } function openResultsPopup(points) { const overlay = document.getElementById('popup-overlay'); overlay.style.display = 'flex'; const popupMap = new google.maps.Map(document.getElementById('popup-map'), { zoom: 3, center: points.actual }); new google.maps.Marker({ position: points.actual, map: popupMap, label: 'A' }); new google.maps.Marker({ position: points.human, map: popupMap, label: 'H' }); new google.maps.Polyline({ path: [points.actual, points.human], geodesic: true, strokeColor: '#F97316', strokeOpacity: 1.0, strokeWeight: 2, map: popupMap }); document.getElementById('next-round-btn').disabled = true; } function appendChatToken(t) { const el = document.getElementById('chat-log'); const span = document.createElement('span'); span.textContent = t; el.appendChild(span); el.scrollTop = el.scrollHeight; } function renderAIGuessOnPopup(ai) { const mapEl = document.getElementById('popup-map'); const popupMap = mapEl._map || new google.maps.Map(mapEl, { zoom: 3, center: ai }); mapEl._map = popupMap; new google.maps.Marker({ position: ai, map: popupMap, label: 'AI' }); } async function nextRound() { if (aiEventSource) { try { aiEventSource.close(); } catch (e) {} } currentRoundIdx += 1; if (currentRoundIdx >= currentRounds.length) { await showFinalResults(); return; } document.getElementById('popup-overlay').style.display = 'none'; showRound(); } async function showFinalResults() { const res = await fetch('/api/session_summary'); const data = await res.json(); document.getElementById('popup-overlay').style.display = 'none'; document.getElementById('game-container').style.display = 'none'; const fin = document.getElementById('final-results'); fin.style.display = 'block'; const total = data.total_score?.toFixed ? data.total_score.toFixed(0) : data.total_score; document.getElementById('final-summary').textContent = `Your total score: ${total} / 15000`; } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }