Jofthomas commited on
Commit
1ac84c3
·
verified ·
1 Parent(s): ac79576

Upload 24 files

Browse files
static/admin.js ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ let map;
2
+ let drawingManager;
3
+ let lastDrawnShape = null;
4
+ let displayedShapes = [];
5
+
6
+ const difficultyColors = {
7
+ easy: '#34A853', // Green
8
+ medium: '#F9AB00', // Yellow
9
+ hard: '#EA4335' // Red
10
+ };
11
+
12
+ function initAdminMap() {
13
+ map = new google.maps.Map(document.getElementById('map'), {
14
+ center: { lat: 20, lng: 0 },
15
+ zoom: 2,
16
+ });
17
+
18
+ drawingManager = new google.maps.drawing.DrawingManager({
19
+ drawingMode: google.maps.drawing.OverlayType.RECTANGLE,
20
+ drawingControl: true,
21
+ drawingControlOptions: {
22
+ position: google.maps.ControlPosition.TOP_CENTER,
23
+ drawingModes: [google.maps.drawing.OverlayType.RECTANGLE],
24
+ },
25
+ rectangleOptions: {
26
+ fillColor: '#F97316',
27
+ fillOpacity: 0.3,
28
+ strokeWeight: 1,
29
+ clickable: true,
30
+ editable: true,
31
+ zIndex: 1,
32
+ },
33
+ });
34
+
35
+ drawingManager.setMap(map);
36
+
37
+ google.maps.event.addListener(drawingManager, 'overlaycomplete', function(event) {
38
+ if (lastDrawnShape) {
39
+ lastDrawnShape.setMap(null);
40
+ }
41
+ lastDrawnShape = event.overlay;
42
+ drawingManager.setDrawingMode(null); // Exit drawing mode
43
+ document.getElementById('save-zone').disabled = false;
44
+ });
45
+
46
+ document.getElementById('save-zone').addEventListener('click', saveLastZone);
47
+ document.getElementById('new-zone-btn').addEventListener('click', () => {
48
+ drawingManager.setDrawingMode(google.maps.drawing.OverlayType.RECTANGLE);
49
+ document.getElementById('save-zone').disabled = true;
50
+ });
51
+
52
+ loadExistingZones();
53
+ }
54
+
55
+ function saveLastZone() {
56
+ if (!lastDrawnShape) {
57
+ alert('Please draw a zone first.');
58
+ return;
59
+ }
60
+
61
+ const difficulty = document.getElementById('difficulty-select').value;
62
+ const bounds = lastDrawnShape.getBounds().toJSON();
63
+
64
+ const zoneData = {
65
+ type: 'rectangle',
66
+ bounds: bounds,
67
+ };
68
+
69
+ fetch('/api/zones', {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json' },
72
+ body: JSON.stringify({ difficulty: difficulty, zone: zoneData }),
73
+ })
74
+ .then(response => response.json())
75
+ .then(data => {
76
+ const statusMsg = document.getElementById('status-message');
77
+ statusMsg.textContent = data.message || `Error: ${data.error}`;
78
+ setTimeout(() => statusMsg.textContent = '', 3000);
79
+
80
+ // Clean up the drawn shape and reload all zones to get the new one with its listener
81
+ if (lastDrawnShape) {
82
+ lastDrawnShape.setMap(null);
83
+ lastDrawnShape = null;
84
+ }
85
+ document.getElementById('save-zone').disabled = true;
86
+ loadExistingZones();
87
+ });
88
+ }
89
+
90
+ function loadExistingZones() {
91
+ // Clear existing shapes from the map
92
+ displayedShapes.forEach(shape => shape.setMap(null));
93
+ displayedShapes = [];
94
+
95
+ fetch('/api/zones')
96
+ .then(response => response.json())
97
+ .then(zones => {
98
+ for (const difficulty in zones) {
99
+ zones[difficulty].forEach(zone => {
100
+ if (zone.type === 'rectangle') {
101
+ const rectangle = new google.maps.Rectangle({
102
+ bounds: zone.bounds,
103
+ map: map,
104
+ fillColor: difficultyColors[difficulty],
105
+ fillOpacity: 0.35,
106
+ strokeColor: difficultyColors[difficulty],
107
+ strokeWeight: 2,
108
+ editable: false,
109
+ clickable: true,
110
+ });
111
+
112
+ rectangle.zoneId = zone.id;
113
+
114
+ google.maps.event.addListener(rectangle, 'click', function() {
115
+ if (confirm('Are you sure you want to delete this zone?')) {
116
+ deleteZone(this.zoneId, this);
117
+ }
118
+ });
119
+
120
+ displayedShapes.push(rectangle);
121
+ }
122
+ });
123
+ }
124
+ });
125
+ }
126
+
127
+ function deleteZone(zoneId, shape) {
128
+ fetch('/api/zones', {
129
+ method: 'DELETE',
130
+ headers: { 'Content-Type': 'application/json' },
131
+ body: JSON.stringify({ zone_id: zoneId })
132
+ })
133
+ .then(response => response.json())
134
+ .then(data => {
135
+ const statusMsg = document.getElementById('status-message');
136
+ statusMsg.textContent = data.message || `Error: ${data.error}`;
137
+ setTimeout(() => statusMsg.textContent = '', 3000);
138
+
139
+ if (data.message) {
140
+ shape.setMap(null);
141
+ }
142
+ });
143
+ }
static/game_images/353ef090-bd5b-425e-aa2f-a662a99779b7.jpg ADDED
static/game_images/eabc74c0-d23f-419c-bdfb-039880c44f17.jpg ADDED
static/generated_images/009fe859-5eed-453e-8b47-6c29007bd3ee.jpg ADDED
static/generated_images/0c9f32bd-bd39-4a38-b906-da2e0f7e228b.jpg ADDED
static/generated_images/13615a55-2fa2-454b-bed2-125bd91a1689.jpg ADDED
static/generated_images/17a85ef6-86e5-47f9-a5fd-c2a3951cd94a.jpg ADDED
static/generated_images/1986875f-ceea-4b53-91cc-f5738edc9c18.jpg ADDED
static/generated_images/1b06b0f6-70fb-4a8f-9710-5f219adb4228.jpg ADDED
static/generated_images/2151c6ce-a3d2-4385-9c39-5bc2206f9fed.jpg ADDED
static/generated_images/3a92192b-7e81-47c5-833f-1c0c0ffb1962.jpg ADDED
static/generated_images/9589bfc2-829a-4781-9d05-492bf080fb68.jpg ADDED
static/generated_images/a10cecb8-e528-4f34-b0d9-e2f7c73520c2.jpg ADDED
static/generated_images/ca32b0af-8b92-4721-b140-3a9dfc21a3ce.jpg ADDED
static/generated_images/d002c524-2abc-4fc7-b4ae-0755ceb8b15f.jpg ADDED
static/generated_images/d0433004-8658-46a2-a858-49a7c70f45b5.jpg ADDED
static/generated_images/db17e1e8-95aa-45ec-b0d1-b466a18f0701.jpg ADDED
static/generated_images/e2ebaf71-0d42-4162-b7e2-00f96ccbf7b5.jpg ADDED
static/generated_images/e53db263-0b98-4953-8375-71d4db1c1700.jpg ADDED
static/generated_images/fe37e7ce-c15b-4d3c-976a-c030e8e6fc13.jpg ADDED
static/script.js ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ let map, panorama, guessMarker, gameId, googleMapsApiKey;
2
+ let startLocation;
3
+ let onFirstLinksLoaded; // Promise that resolves when the first panorama links are loaded
4
+
5
+ function initLobby() {
6
+ document.getElementById('new-game-form').addEventListener('submit', (e) => {
7
+ e.preventDefault();
8
+ startGame();
9
+ });
10
+ document.getElementById('replay-form').addEventListener('submit', (e) => {
11
+ e.preventDefault();
12
+ replayGame();
13
+ });
14
+ document.getElementById('play-again').addEventListener('click', showLobby);
15
+ }
16
+
17
+ function showLobby() {
18
+ document.getElementById('lobby-container').style.display = 'block';
19
+ document.getElementById('game-container').style.display = 'none';
20
+ document.getElementById('result-screen').style.display = 'none';
21
+ }
22
+
23
+ function showGame() {
24
+ document.getElementById('lobby-container').style.display = 'none';
25
+ document.getElementById('game-container').style.display = 'flex';
26
+ document.getElementById('result-screen').style.display = 'none';
27
+ }
28
+
29
+ function startGame() {
30
+ showGame();
31
+ const difficulty = document.getElementById('difficulty-select-lobby').value;
32
+ fetch('/start_game', {
33
+ method: 'POST',
34
+ headers: { 'Content-Type': 'application/json' },
35
+ body: JSON.stringify({ difficulty: difficulty })
36
+ })
37
+ .then(response => response.json())
38
+ .then(data => {
39
+ if (data.error) {
40
+ alert(data.error);
41
+ showLobby();
42
+ return;
43
+ }
44
+ gameId = data.game_id;
45
+ startLocation = data.start_location;
46
+ googleMapsApiKey = data.google_maps_api_key;
47
+
48
+ const chatLog = document.getElementById('chat-log');
49
+ chatLog.innerHTML = '';
50
+ addChatMessage('Agent', `New game started (ID: ${gameId}). Finding my location...`);
51
+
52
+ initStreetView(startLocation);
53
+ initMap();
54
+
55
+ //runFakeAgent();
56
+ });
57
+ }
58
+
59
+ function replayGame() {
60
+ const replayId = document.getElementById('replay-id-input').value;
61
+ if (!replayId) {
62
+ alert('Please enter a Game ID to replay.');
63
+ return;
64
+ }
65
+
66
+ fetch(`/game/${replayId}/state`)
67
+ .then(response => {
68
+ if (!response.ok) {
69
+ throw new Error('Game not found.');
70
+ }
71
+ return response.json();
72
+ })
73
+ .then(data => {
74
+ if (!data.game_over) {
75
+ alert('This game has not finished yet.');
76
+ return;
77
+ }
78
+ showGame();
79
+ gameId = replayId;
80
+ startLocation = data.start_location;
81
+
82
+ const chatLog = document.getElementById('chat-log');
83
+ chatLog.innerHTML = '';
84
+ addChatMessage('System', `Replaying game: ${gameId}`);
85
+
86
+ initStreetView(startLocation);
87
+ initMap(true); // isReplay = true
88
+
89
+ replayActions(data.actions);
90
+ })
91
+ .catch(error => {
92
+ alert(error.message);
93
+ });
94
+ }
95
+
96
+ async function replayActions(actions) {
97
+ for (const action of actions) {
98
+ await sleep(2000);
99
+ if (action.type === 'move') {
100
+ addChatMessage('Agent (Replay)', `Moved to: ${action.location.lat.toFixed(4)}, ${action.location.lng.toFixed(4)}`);
101
+ panorama.setPosition(action.location);
102
+ } else if (action.type === 'guess') {
103
+ addChatMessage('Agent (Replay)', `Guessed: ${action.location.lat.toFixed(4)}, ${action.location.lng.toFixed(4)}`);
104
+ placeGuessMarker(action.location);
105
+
106
+ await sleep(2000);
107
+ const resultData = {
108
+ guess_location: action.location,
109
+ actual_location: startLocation,
110
+ distance_km: action.result.distance_km,
111
+ score: action.result.score
112
+ };
113
+ showResultScreen(resultData);
114
+ }
115
+ }
116
+ }
117
+
118
+ function initStreetView(location) {
119
+ onFirstLinksLoaded = new Promise(resolve => {
120
+ panorama = new google.maps.StreetViewPanorama(
121
+ document.getElementById('streetview'), {
122
+ position: location,
123
+ pov: { heading: 34, pitch: 10 },
124
+ visible: true,
125
+ linksControl: true, // Ensure links are enabled
126
+ clickToGo: true, // Ensure click-to-go is enabled
127
+ }
128
+ );
129
+
130
+ const linksChangedListener = panorama.addListener('links_changed', () => {
131
+ console.log("links_changed event fired for the first time.");
132
+ // We got links, resolve the promise, and remove the one-time listener.
133
+ google.maps.event.removeListener(linksChangedListener);
134
+ resolve();
135
+ });
136
+
137
+ panorama.addListener('position_changed', function() {
138
+ const newLocation = panorama.getPosition();
139
+ updateAgentLocation(newLocation.lat(), newLocation.lng());
140
+ });
141
+ });
142
+ }
143
+
144
+ function initMap(isReplay = false) {
145
+ map = new google.maps.Map(document.getElementById('map'), {
146
+ center: { lat: 0, lng: 0 },
147
+ zoom: 1,
148
+ });
149
+
150
+ if (!isReplay) {
151
+ map.addListener('click', function(e) {
152
+ placeGuessMarker(e.latLng);
153
+ makeGuess(e.latLng.lat(), e.latLng.lng());
154
+ });
155
+ }
156
+ }
157
+
158
+ function placeGuessMarker(location) {
159
+ if (guessMarker) {
160
+ guessMarker.setMap(null);
161
+ }
162
+ guessMarker = new google.maps.Marker({
163
+ position: location,
164
+ map: map
165
+ });
166
+ map.setCenter(location);
167
+ }
168
+
169
+ function addChatMessage(sender, message) {
170
+ const chatLog = document.getElementById('chat-log');
171
+ const messageElement = document.createElement('div');
172
+ messageElement.innerHTML = `<strong>${sender}:</strong> ${message}`;
173
+ chatLog.appendChild(messageElement);
174
+ chatLog.scrollTop = chatLog.scrollHeight;
175
+ }
176
+
177
+ // --- Fake LLM Agent Logic ---
178
+
179
+ async function runFakeAgent() {
180
+ addChatMessage('Agent', 'Initializing...');
181
+ console.log('runFakeAgent: Waiting for initial Street View data...');
182
+
183
+ // Wait for the first links_changed event to ensure panorama is ready
184
+ await onFirstLinksLoaded;
185
+ await sleep(1000); // Wait a bit for the panorama to be fully rendered
186
+
187
+ console.log('runFakeAgent: Initial data loaded. Starting navigation.');
188
+ await takeActionWithScreenshot('I have my bearings. Starting to move.');
189
+ await sleep(2000);
190
+
191
+ const numberOfMoves = 3;
192
+ let moved = false;
193
+
194
+ for (let i = 0; i < numberOfMoves; i++) {
195
+ addChatMessage('Agent', `Planning move ${i + 1}/${numberOfMoves}...`);
196
+ await sleep(1000);
197
+
198
+ // Let the agent look around
199
+ await takeActionWithScreenshot(`Let me check my surroundings...`);
200
+ await sleep(1000);
201
+ await turnCamera(-90); // Look 90 degrees left
202
+ await sleep(1500);
203
+ await turnCamera(180); // Look 180 degrees right (which is 90 deg right from origin)
204
+ await sleep(1500);
205
+ await turnCamera(-90); // Turn back to the front
206
+ await sleep(1000);
207
+
208
+ // The agent will always try to move forward in this simplified logic
209
+ moved = await agentMove('forward');
210
+
211
+ console.log(`runFakeAgent: Move ${i + 1} result (moved):`, moved);
212
+ if (!moved) {
213
+ break; // Stop if we hit a dead end
214
+ }
215
+
216
+ // After moving, we need to wait for the new panorama to load its links
217
+ await new Promise(resolve => {
218
+ const listener = panorama.addListener('links_changed', () => {
219
+ google.maps.event.removeListener(listener);
220
+ resolve();
221
+ });
222
+ });
223
+ await sleep(2000); // Pause for the user to see the new location
224
+ }
225
+
226
+ addChatMessage('Agent', `I'm done moving. Now I will make a guess.`);
227
+ console.log('runFakeAgent: Finished moving, now making a guess.');
228
+ await makeEducatedGuess();
229
+ }
230
+
231
+ /**
232
+ * Moves the Street View agent in a specified direction.
233
+ * @param {('forward'|'backward'|'left'|'right'|number)} direction - The direction to move.
234
+ * @returns {Promise<boolean>} - True if the move was successful, false otherwise.
235
+ */
236
+ async function agentMove(direction = 'forward') {
237
+ await takeActionWithScreenshot(`Trying to move ${direction}...`);
238
+
239
+ const links = panorama.getLinks();
240
+ if (!links || links.length === 0) {
241
+ addChatMessage('Agent', "I'm at a dead end, can't move from here.");
242
+ console.log('agentMove: No links found.');
243
+ return false;
244
+ }
245
+
246
+ const currentPov = panorama.getPov();
247
+ let targetHeading;
248
+
249
+ switch(direction) {
250
+ case 'forward':
251
+ targetHeading = currentPov.heading;
252
+ break;
253
+ case 'backward':
254
+ targetHeading = (currentPov.heading + 180) % 360;
255
+ break;
256
+ case 'left':
257
+ targetHeading = (currentPov.heading - 90 + 360) % 360;
258
+ break;
259
+ case 'right':
260
+ targetHeading = (currentPov.heading + 90) % 360;
261
+ break;
262
+ default:
263
+ if (typeof direction === 'number' && direction >= 0 && direction < 360) {
264
+ targetHeading = direction;
265
+ } else {
266
+ addChatMessage('Agent', `Unknown direction: ${direction}. Defaulting to forward.`);
267
+ targetHeading = currentPov.heading;
268
+ }
269
+ break;
270
+ }
271
+
272
+ let bestLink = null;
273
+ let minAngleDiff = 360;
274
+
275
+ await sleep(1000);
276
+
277
+ links.forEach(link => {
278
+ let diff = Math.abs(targetHeading - link.heading);
279
+ if (diff > 180) diff = 360 - diff; // Find the shortest angle
280
+
281
+ if (diff < minAngleDiff) {
282
+ minAngleDiff = diff;
283
+ bestLink = link;
284
+ }
285
+ });
286
+
287
+ if (bestLink) {
288
+ console.log(`agentMove: Best link found:`, bestLink, `with angle diff ${minAngleDiff}`);
289
+ await sleep(1500);
290
+ await takeActionWithScreenshot(`Best path is at ${bestLink.heading.toFixed(1)}° (a ${minAngleDiff.toFixed(1)}° turn). Moving...`);
291
+
292
+ panorama.setPano(bestLink.pano);
293
+ return true;
294
+ } else {
295
+ // This case should be rare if there are any links at all
296
+ addChatMessage('Agent', "Couldn't find a suitable path in that direction.");
297
+ console.log('agentMove: No suitable link found.');
298
+ return false;
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Turns the camera view relative to the current heading.
304
+ * @param {number} angle - The angle in degrees to turn. Negative values turn left, positive values turn right.
305
+ */
306
+ async function turnCamera(angle) {
307
+ const currentPov = panorama.getPov();
308
+ const newHeading = (currentPov.heading + angle % 360 + 360) % 360;
309
+
310
+ const direction = angle < 0 ? 'left' : 'right';
311
+ await takeActionWithScreenshot(`Turning ${direction} by ${Math.abs(angle)} degrees.`);
312
+
313
+ panorama.setPov({ heading: newHeading, pitch: currentPov.pitch });
314
+
315
+ await sleep(1000); // Simulate action time
316
+ }
317
+
318
+ async function makeEducatedGuess() {
319
+ // 2. Guess (a bit off from the start location to simulate a guess)
320
+ await takeActionWithScreenshot('Okay, I think I have an idea. I will make a guess in 10 seconds...');
321
+ await sleep(10000);
322
+ addChatMessage('Agent', 'Making my guess now!');
323
+
324
+ const guessLat = startLocation.lat + (Math.random() - 0.5) * 0.1;
325
+ const guessLng = startLocation.lng + (Math.random() - 0.5) * 0.1;
326
+
327
+ placeGuessMarker({ lat: guessLat, lng: guessLng });
328
+ makeGuess(guessLat, guessLng);
329
+ }
330
+
331
+ async function takeActionWithScreenshot(actionMessage) {
332
+ if (!googleMapsApiKey) {
333
+ addChatMessage('Agent', actionMessage); // Fallback if key is not available
334
+ console.warn("Google Maps API key not available for screenshot.");
335
+ return;
336
+ }
337
+
338
+ await sleep(500); // Give panorama a moment to update image
339
+
340
+ const pov = panorama.getPov();
341
+ const position = panorama.getPosition();
342
+ const location = `${position.lat()},${position.lng()}`;
343
+
344
+ const imageUrl = `https://maps.googleapis.com/maps/api/streetview?size=400x250&location=${location}&heading=${pov.heading}&pitch=${pov.pitch}&fov=90&key=${googleMapsApiKey}`;
345
+
346
+ const message = `
347
+ ${actionMessage}<br>
348
+ <img src="${imageUrl}" alt="Agent's view" style="width: 100%; border-radius: 5px; margin-top: 5px; border: 1px solid #ccc;">
349
+ `;
350
+ addChatMessage('Agent', message);
351
+ console.log(`Action taken: ${actionMessage}`);
352
+ }
353
+
354
+ async function updateAgentLocation(lat, lng) {
355
+ await fetch(`/game/${gameId}/move`, {
356
+ method: 'POST',
357
+ headers: { 'Content-Type': 'application/json' },
358
+ body: JSON.stringify({ lat: lat, lng: lng }),
359
+ });
360
+ }
361
+
362
+ async function makeGuess(lat, lng) {
363
+ addChatMessage('You', `Guessed: ${lat.toFixed(4)}, ${lng.toFixed(4)}`);
364
+ const response = await fetch(`/game/${gameId}/guess`, {
365
+ method: 'POST',
366
+ headers: { 'Content-Type': 'application/json' },
367
+ body: JSON.stringify({ lat: lat, lng: lng }),
368
+ });
369
+ const result = await response.json();
370
+ showResultScreen(result);
371
+ }
372
+
373
+ function showResultScreen(result) {
374
+ document.getElementById('game-container').style.display = 'none';
375
+ document.getElementById('result-screen').style.display = 'block';
376
+
377
+ const resultSummary = document.getElementById('result-summary');
378
+ resultSummary.innerHTML = `
379
+ <p>Your guess was ${result.distance_km.toFixed(2)} km away.</p>
380
+ <p>You scored ${result.score.toFixed(0)} points.</p>
381
+ `;
382
+
383
+ const resultMap = new google.maps.Map(document.getElementById('result-map'), {
384
+ zoom: 3,
385
+ center: result.actual_location
386
+ });
387
+
388
+ new google.maps.Marker({
389
+ position: result.actual_location,
390
+ map: resultMap,
391
+ label: 'A' // Actual
392
+ });
393
+
394
+ new google.maps.Marker({
395
+ position: result.guess_location,
396
+ map: resultMap,
397
+ label: 'G' // Guess
398
+ });
399
+
400
+ new google.maps.Polyline({
401
+ path: [result.actual_location, result.guess_location],
402
+ geodesic: true,
403
+ strokeColor: '#F97316',
404
+ strokeOpacity: 1.0,
405
+ strokeWeight: 2,
406
+ map: resultMap
407
+ });
408
+ }
409
+
410
+ function sleep(ms) {
411
+ return new Promise(resolve => setTimeout(resolve, ms));
412
+ }
static/style.css ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --primary-color: #F97316; /* Orange */
3
+ --secondary-color: #EA580C; /* Darker Orange */
4
+ --accent-color: #FB923C; /* Lighter Orange */
5
+ --dark-color: #202124;
6
+ --light-color: #FFFBEB; /* Creamy White */
7
+ --border-radius: 8px;
8
+ }
9
+
10
+ body {
11
+ font-family: 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
12
+ margin: 0;
13
+ padding: 0;
14
+ background-color: var(--light-color);
15
+ color: var(--dark-color);
16
+ }
17
+
18
+ h1 {
19
+ text-align: center;
20
+ margin: 20px 0;
21
+ color: var(--primary-color);
22
+ font-size: 2.5rem;
23
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
24
+ }
25
+
26
+ #game-container {
27
+ display: flex;
28
+ justify-content: space-around;
29
+ margin: 20px;
30
+ height: 80vh;
31
+ gap: 20px;
32
+ display: none; /* Hidden by default */
33
+ }
34
+
35
+ #streetview-container {
36
+ width: 50%;
37
+ height: 100%;
38
+ border-radius: var(--border-radius);
39
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
40
+ }
41
+
42
+ #map-container {
43
+ width: 30%;
44
+ height: 100%;
45
+ border-radius: var(--border-radius);
46
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
47
+ }
48
+
49
+ #chat-container {
50
+ width: 20%;
51
+ height: 100%;
52
+ display: flex;
53
+ flex-direction: column;
54
+ border-radius: var(--border-radius);
55
+ background-color: white;
56
+ padding: 15px;
57
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
58
+ }
59
+
60
+ #streetview, #map, #result-map {
61
+ height: 100%;
62
+ width: 100%;
63
+ border-radius: var(--border-radius);
64
+ }
65
+
66
+ #chat-log {
67
+ flex-grow: 1;
68
+ overflow-y: auto;
69
+ margin-bottom: 15px;
70
+ padding: 10px;
71
+ background-color: #f8f9fa;
72
+ border-radius: var(--border-radius);
73
+ }
74
+
75
+ #chat-log div {
76
+ margin-bottom: 10px;
77
+ padding: 8px 12px;
78
+ background-color: white;
79
+ border-radius: 18px;
80
+ box-shadow: 0 1px 2px rgba(0,0,0,0.1);
81
+ }
82
+
83
+ #chat-log div strong {
84
+ color: var(--primary-color);
85
+ }
86
+
87
+ #controls {
88
+ display: flex;
89
+ justify-content: center;
90
+ }
91
+
92
+ button {
93
+ background: linear-gradient(135deg, var(--secondary-color), var(--primary-color));
94
+ color: white;
95
+ border: none;
96
+ padding: 10px 20px;
97
+ border-radius: var(--border-radius);
98
+ cursor: pointer;
99
+ font-weight: 500;
100
+ transition: all 0.2s;
101
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
102
+ }
103
+
104
+ button:hover {
105
+ background: linear-gradient(135deg, #D9500B, #E56A15); /* Slightly darker gradient on hover */
106
+ transform: translateY(-1px);
107
+ box-shadow: 0 4px 8px rgba(0,0,0,0.15);
108
+ }
109
+
110
+ #result-screen {
111
+ margin: 20px auto;
112
+ max-width: 800px;
113
+ text-align: center;
114
+ background-color: white;
115
+ padding: 30px;
116
+ border-radius: var(--border-radius);
117
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
118
+ }
119
+
120
+ #result-summary {
121
+ margin: 20px 0;
122
+ font-size: 1.2rem;
123
+ }
124
+
125
+ #result-summary p {
126
+ margin: 10px 0;
127
+ }
128
+
129
+ .marker-label {
130
+ color: white;
131
+ font-weight: bold;
132
+ text-align: center;
133
+ padding: 2px 6px;
134
+ border-radius: 50%;
135
+ }
136
+
137
+ #lobby-container {
138
+ max-width: 500px;
139
+ margin: 40px auto;
140
+ padding: 30px;
141
+ background-color: white;
142
+ border-radius: var(--border-radius);
143
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
144
+ text-align: center;
145
+ }
146
+
147
+ #lobby-container h2 {
148
+ margin-top: 0;
149
+ margin-bottom: 20px;
150
+ }
151
+
152
+ #lobby-container hr {
153
+ margin: 20px 0;
154
+ border: 0;
155
+ border-top: 1px solid #eee;
156
+ }
157
+
158
+ #lobby-container input {
159
+ width: calc(100% - 22px);
160
+ padding: 10px;
161
+ margin-bottom: 10px;
162
+ border: 1px solid #ccc;
163
+ border-radius: var(--border-radius);
164
+ }
165
+
166
+ /* Hide the address text in Street View */
167
+ .gm-iv-address {
168
+ display: none !important;
169
+ }
templates/admin.html ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Admin Panel - LLM GeoGuessr</title>
5
+ <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
6
+ <style>
7
+ h1 {
8
+ color: var(--dark-color);
9
+ text-align: center;
10
+ }
11
+ #controls {
12
+ width: 90%;
13
+ max-width: 800px;
14
+ margin: 20px auto;
15
+ padding: 20px;
16
+ background-color: white;
17
+ border-radius: var(--border-radius);
18
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: space-around;
22
+ flex-wrap: wrap;
23
+ }
24
+ #controls h2 {
25
+ display: none; /* Title is obvious from context */
26
+ }
27
+ #controls .control-item {
28
+ margin: 5px 10px;
29
+ }
30
+ #controls .control-item p {
31
+ margin: 0;
32
+ font-size: 0.9rem;
33
+ color: #555;
34
+ }
35
+ #controls .control-item label {
36
+ margin-right: 5px;
37
+ }
38
+ #controls .control-item select,
39
+ #controls .control-item button {
40
+ width: 100%;
41
+ padding: 10px;
42
+ box-sizing: border-box; /* Ensures padding is included in the width */
43
+ }
44
+ #map {
45
+ height: 75vh;
46
+ margin: 0 auto;
47
+ width: calc(100% - 40px);
48
+ border-radius: var(--border-radius);
49
+ }
50
+ </style>
51
+ </head>
52
+ <body>
53
+ <h1>Admin Panel</h1>
54
+ <div id="controls">
55
+ <div class="control-item">
56
+ <p>Select "Draw" then use the rectangle tool on the map.</p>
57
+ </div>
58
+ <div class="control-item">
59
+ <button id="new-zone-btn">Draw New Zone</button>
60
+ </div>
61
+ <div class="control-item">
62
+ <label for="difficulty-select">Difficulty:</label>
63
+ <select id="difficulty-select">
64
+ <option value="easy">Easy</option>
65
+ <option value="medium">Medium</option>
66
+ <option value="hard">Hard</option>
67
+ </select>
68
+ </div>
69
+ <div class="control-item">
70
+ <button id="save-zone" disabled>Save Zone</button>
71
+ </div>
72
+ </div>
73
+ <div id="status-message-container" style="text-align: center; margin-bottom: 10px;">
74
+ <p id="status-message"></p>
75
+ </div>
76
+ <div id="map"></div>
77
+ <script src="{{ url_for('static', filename='admin.js') }}"></script>
78
+ <script async defer src="https://maps.googleapis.com/maps/api/js?key={{ google_maps_api_key }}&libraries=drawing&callback=initAdminMap"></script>
79
+ </body>
80
+ </html>
templates/index.html ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>LLM GeoGuessr</title>
5
+ <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
6
+ </head>
7
+ <body>
8
+ <h1>LLM GeoGuessr</h1>
9
+
10
+ <div id="lobby-container">
11
+ <h2>Welcome</h2>
12
+ <form id="new-game-form">
13
+ <label for="difficulty-select-lobby">Choose a difficulty:</label>
14
+ <select id="difficulty-select-lobby">
15
+ <option value="easy">Easy</option>
16
+ <option value="medium">Medium</option>
17
+ <option value="hard">Hard</option>
18
+ </select>
19
+ <button type="submit">Start New Game</button>
20
+ </form>
21
+ <hr>
22
+ <form id="replay-form">
23
+ <p>Or replay a previous game:</p>
24
+ <input type="text" id="replay-id-input" placeholder="Enter Game ID">
25
+ <button type="submit">Replay Game</button>
26
+ </form>
27
+ </div>
28
+
29
+ <div id="game-container" style="display: none;">
30
+ <div id="streetview-container">
31
+ <div id="streetview"></div>
32
+ </div>
33
+ <div id="map-container">
34
+ <div id="map"></div>
35
+ </div>
36
+ <div id="chat-container">
37
+ <div id="chat-log"></div>
38
+ <div id="controls">
39
+ <!-- In-game controls can go here -->
40
+ </div>
41
+ </div>
42
+ </div>
43
+
44
+ <div id="result-screen" style="display: none;">
45
+ <h2>Results</h2>
46
+ <div id="result-map"></div>
47
+ <div id="result-summary"></div>
48
+ <button id="play-again">Back to Lobby</button>
49
+ </div>
50
+
51
+ <script src="{{ url_for('static', filename='script.js') }}"></script>
52
+ <script async defer src="https://maps.googleapis.com/maps/api/js?key={{ google_maps_api_key }}&callback=initLobby"></script>
53
+ </body>
54
+ </html>