Upload 24 files
Browse files- static/admin.js +143 -0
- static/game_images/353ef090-bd5b-425e-aa2f-a662a99779b7.jpg +0 -0
- static/game_images/eabc74c0-d23f-419c-bdfb-039880c44f17.jpg +0 -0
- static/generated_images/009fe859-5eed-453e-8b47-6c29007bd3ee.jpg +0 -0
- static/generated_images/0c9f32bd-bd39-4a38-b906-da2e0f7e228b.jpg +0 -0
- static/generated_images/13615a55-2fa2-454b-bed2-125bd91a1689.jpg +0 -0
- static/generated_images/17a85ef6-86e5-47f9-a5fd-c2a3951cd94a.jpg +0 -0
- static/generated_images/1986875f-ceea-4b53-91cc-f5738edc9c18.jpg +0 -0
- static/generated_images/1b06b0f6-70fb-4a8f-9710-5f219adb4228.jpg +0 -0
- static/generated_images/2151c6ce-a3d2-4385-9c39-5bc2206f9fed.jpg +0 -0
- static/generated_images/3a92192b-7e81-47c5-833f-1c0c0ffb1962.jpg +0 -0
- static/generated_images/9589bfc2-829a-4781-9d05-492bf080fb68.jpg +0 -0
- static/generated_images/a10cecb8-e528-4f34-b0d9-e2f7c73520c2.jpg +0 -0
- static/generated_images/ca32b0af-8b92-4721-b140-3a9dfc21a3ce.jpg +0 -0
- static/generated_images/d002c524-2abc-4fc7-b4ae-0755ceb8b15f.jpg +0 -0
- static/generated_images/d0433004-8658-46a2-a858-49a7c70f45b5.jpg +0 -0
- static/generated_images/db17e1e8-95aa-45ec-b0d1-b466a18f0701.jpg +0 -0
- static/generated_images/e2ebaf71-0d42-4162-b7e2-00f96ccbf7b5.jpg +0 -0
- static/generated_images/e53db263-0b98-4953-8375-71d4db1c1700.jpg +0 -0
- static/generated_images/fe37e7ce-c15b-4d3c-976a-c030e8e6fc13.jpg +0 -0
- static/script.js +412 -0
- static/style.css +169 -0
- templates/admin.html +80 -0
- templates/index.html +54 -0
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>
|