Spaces:
Running
on
Zero
Running
on
Zero
| 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)); } |