let map, panorama, guessMarker, gameId, googleMapsApiKey; let startLocation; let onFirstLinksLoaded; // Promise that resolves when the first panorama links are loaded function initLobby() { document.getElementById('new-game-form').addEventListener('submit', (e) => { e.preventDefault(); startGame(); }); document.getElementById('replay-form').addEventListener('submit', (e) => { e.preventDefault(); replayGame(); }); document.getElementById('play-again').addEventListener('click', showLobby); } function showLobby() { document.getElementById('lobby-container').style.display = 'block'; document.getElementById('game-container').style.display = 'none'; document.getElementById('result-screen').style.display = 'none'; } function showGame() { document.getElementById('lobby-container').style.display = 'none'; document.getElementById('game-container').style.display = 'flex'; document.getElementById('result-screen').style.display = 'none'; } function startGame() { showGame(); const difficulty = document.getElementById('difficulty-select-lobby').value; fetch('/start_game', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ difficulty: difficulty }) }) .then(response => response.json()) .then(data => { if (data.error) { alert(data.error); showLobby(); return; } gameId = data.game_id; startLocation = data.start_location; googleMapsApiKey = data.google_maps_api_key; const chatLog = document.getElementById('chat-log'); chatLog.innerHTML = ''; addChatMessage('Agent', `New game started (ID: ${gameId}). Finding my location...`); initStreetView(startLocation); initMap(); //runFakeAgent(); }); } function replayGame() { const replayId = document.getElementById('replay-id-input').value; if (!replayId) { alert('Please enter a Game ID to replay.'); return; } fetch(`/game/${replayId}/state`) .then(response => { if (!response.ok) { throw new Error('Game not found.'); } return response.json(); }) .then(data => { if (!data.game_over) { alert('This game has not finished yet.'); return; } showGame(); gameId = replayId; startLocation = data.start_location; const chatLog = document.getElementById('chat-log'); chatLog.innerHTML = ''; addChatMessage('System', `Replaying game: ${gameId}`); initStreetView(startLocation); initMap(true); // isReplay = true replayActions(data.actions); }) .catch(error => { alert(error.message); }); } async function replayActions(actions) { for (const action of actions) { await sleep(2000); if (action.type === 'move') { addChatMessage('Agent (Replay)', `Moved to: ${action.location.lat.toFixed(4)}, ${action.location.lng.toFixed(4)}`); panorama.setPosition(action.location); } else if (action.type === 'guess') { addChatMessage('Agent (Replay)', `Guessed: ${action.location.lat.toFixed(4)}, ${action.location.lng.toFixed(4)}`); placeGuessMarker(action.location); await sleep(2000); const resultData = { guess_location: action.location, actual_location: startLocation, distance_km: action.result.distance_km, score: action.result.score }; showResultScreen(resultData); } } } function initStreetView(location) { onFirstLinksLoaded = new Promise(resolve => { panorama = new google.maps.StreetViewPanorama( document.getElementById('streetview'), { position: location, pov: { heading: 34, pitch: 10 }, visible: true, linksControl: true, // Ensure links are enabled clickToGo: true, // Ensure click-to-go is enabled } ); const linksChangedListener = panorama.addListener('links_changed', () => { console.log("links_changed event fired for the first time."); // We got links, resolve the promise, and remove the one-time listener. google.maps.event.removeListener(linksChangedListener); resolve(); }); panorama.addListener('position_changed', function() { const newLocation = panorama.getPosition(); updateAgentLocation(newLocation.lat(), newLocation.lng()); }); }); } function initMap(isReplay = false) { map = new google.maps.Map(document.getElementById('map'), { center: { lat: 0, lng: 0 }, zoom: 1, }); if (!isReplay) { map.addListener('click', function(e) { placeGuessMarker(e.latLng); makeGuess(e.latLng.lat(), e.latLng.lng()); }); } } function placeGuessMarker(location) { if (guessMarker) { guessMarker.setMap(null); } guessMarker = new google.maps.Marker({ position: location, map: map }); map.setCenter(location); } function addChatMessage(sender, message) { const chatLog = document.getElementById('chat-log'); const messageElement = document.createElement('div'); messageElement.innerHTML = `${sender}: ${message}`; chatLog.appendChild(messageElement); chatLog.scrollTop = chatLog.scrollHeight; } // --- Fake LLM Agent Logic --- async function runFakeAgent() { addChatMessage('Agent', 'Initializing...'); console.log('runFakeAgent: Waiting for initial Street View data...'); // Wait for the first links_changed event to ensure panorama is ready await onFirstLinksLoaded; await sleep(1000); // Wait a bit for the panorama to be fully rendered console.log('runFakeAgent: Initial data loaded. Starting navigation.'); await takeActionWithScreenshot('I have my bearings. Starting to move.'); await sleep(2000); const numberOfMoves = 3; let moved = false; for (let i = 0; i < numberOfMoves; i++) { addChatMessage('Agent', `Planning move ${i + 1}/${numberOfMoves}...`); await sleep(1000); // Let the agent look around await takeActionWithScreenshot(`Let me check my surroundings...`); await sleep(1000); await turnCamera(-90); // Look 90 degrees left await sleep(1500); await turnCamera(180); // Look 180 degrees right (which is 90 deg right from origin) await sleep(1500); await turnCamera(-90); // Turn back to the front await sleep(1000); // The agent will always try to move forward in this simplified logic moved = await agentMove('forward'); console.log(`runFakeAgent: Move ${i + 1} result (moved):`, moved); if (!moved) { break; // Stop if we hit a dead end } // After moving, we need to wait for the new panorama to load its links await new Promise(resolve => { const listener = panorama.addListener('links_changed', () => { google.maps.event.removeListener(listener); resolve(); }); }); await sleep(2000); // Pause for the user to see the new location } addChatMessage('Agent', `I'm done moving. Now I will make a guess.`); console.log('runFakeAgent: Finished moving, now making a guess.'); await makeEducatedGuess(); } /** * Moves the Street View agent in a specified direction. * @param {('forward'|'backward'|'left'|'right'|number)} direction - The direction to move. * @returns {Promise} - True if the move was successful, false otherwise. */ async function agentMove(direction = 'forward') { await takeActionWithScreenshot(`Trying to move ${direction}...`); const links = panorama.getLinks(); if (!links || links.length === 0) { addChatMessage('Agent', "I'm at a dead end, can't move from here."); console.log('agentMove: No links found.'); return false; } const currentPov = panorama.getPov(); let targetHeading; switch(direction) { case 'forward': targetHeading = currentPov.heading; break; case 'backward': targetHeading = (currentPov.heading + 180) % 360; break; case 'left': targetHeading = (currentPov.heading - 90 + 360) % 360; break; case 'right': targetHeading = (currentPov.heading + 90) % 360; break; default: if (typeof direction === 'number' && direction >= 0 && direction < 360) { targetHeading = direction; } else { addChatMessage('Agent', `Unknown direction: ${direction}. Defaulting to forward.`); targetHeading = currentPov.heading; } break; } let bestLink = null; let minAngleDiff = 360; await sleep(1000); links.forEach(link => { let diff = Math.abs(targetHeading - link.heading); if (diff > 180) diff = 360 - diff; // Find the shortest angle if (diff < minAngleDiff) { minAngleDiff = diff; bestLink = link; } }); if (bestLink) { console.log(`agentMove: Best link found:`, bestLink, `with angle diff ${minAngleDiff}`); await sleep(1500); await takeActionWithScreenshot(`Best path is at ${bestLink.heading.toFixed(1)}° (a ${minAngleDiff.toFixed(1)}° turn). Moving...`); panorama.setPano(bestLink.pano); return true; } else { // This case should be rare if there are any links at all addChatMessage('Agent', "Couldn't find a suitable path in that direction."); console.log('agentMove: No suitable link found.'); return false; } } /** * Turns the camera view relative to the current heading. * @param {number} angle - The angle in degrees to turn. Negative values turn left, positive values turn right. */ async function turnCamera(angle) { const currentPov = panorama.getPov(); const newHeading = (currentPov.heading + angle % 360 + 360) % 360; const direction = angle < 0 ? 'left' : 'right'; await takeActionWithScreenshot(`Turning ${direction} by ${Math.abs(angle)} degrees.`); panorama.setPov({ heading: newHeading, pitch: currentPov.pitch }); await sleep(1000); // Simulate action time } async function makeEducatedGuess() { // 2. Guess (a bit off from the start location to simulate a guess) await takeActionWithScreenshot('Okay, I think I have an idea. I will make a guess in 10 seconds...'); await sleep(10000); addChatMessage('Agent', 'Making my guess now!'); const guessLat = startLocation.lat + (Math.random() - 0.5) * 0.1; const guessLng = startLocation.lng + (Math.random() - 0.5) * 0.1; placeGuessMarker({ lat: guessLat, lng: guessLng }); makeGuess(guessLat, guessLng); } async function takeActionWithScreenshot(actionMessage) { if (!googleMapsApiKey) { addChatMessage('Agent', actionMessage); // Fallback if key is not available console.warn("Google Maps API key not available for screenshot."); return; } await sleep(500); // Give panorama a moment to update image const pov = panorama.getPov(); const position = panorama.getPosition(); const location = `${position.lat()},${position.lng()}`; const imageUrl = `https://maps.googleapis.com/maps/api/streetview?size=400x250&location=${location}&heading=${pov.heading}&pitch=${pov.pitch}&fov=90&key=${googleMapsApiKey}`; const message = ` ${actionMessage}
Agent's view `; addChatMessage('Agent', message); console.log(`Action taken: ${actionMessage}`); } async function updateAgentLocation(lat, lng) { await fetch(`/game/${gameId}/move`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lat: lat, lng: lng }), }); } async function makeGuess(lat, lng) { addChatMessage('You', `Guessed: ${lat.toFixed(4)}, ${lng.toFixed(4)}`); const response = await fetch(`/game/${gameId}/guess`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lat: lat, lng: lng }), }); const result = await response.json(); showResultScreen(result); } function showResultScreen(result) { document.getElementById('game-container').style.display = 'none'; document.getElementById('result-screen').style.display = 'block'; const resultSummary = document.getElementById('result-summary'); resultSummary.innerHTML = `

Your guess was ${result.distance_km.toFixed(2)} km away.

You scored ${result.score.toFixed(0)} points.

`; const resultMap = new google.maps.Map(document.getElementById('result-map'), { zoom: 3, center: result.actual_location }); new google.maps.Marker({ position: result.actual_location, map: resultMap, label: 'A' // Actual }); new google.maps.Marker({ position: result.guess_location, map: resultMap, label: 'G' // Guess }); new google.maps.Polyline({ path: [result.actual_location, result.guess_location], geodesic: true, strokeColor: '#F97316', strokeOpacity: 1.0, strokeWeight: 2, map: resultMap }); } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }