Spaces:
Sleeping
Sleeping
Joffrey Thomas
commited on
Commit
·
860e1a7
1
Parent(s):
e743ebd
merge geoguessr
Browse files- geoguessr/geo_server.py +28 -14
- geoguessr/static/admin.js +139 -0
- geoguessr/static/script.js +243 -0
- geoguessr/static/style.css +171 -0
- geoguessr/templates/admin.html +81 -0
- geoguessr/templates/index.html +45 -0
- geoguessr/web_app.py +431 -0
- geoguessr/zones.json +67 -0
- server.py +4 -0
geoguessr/geo_server.py
CHANGED
@@ -4,6 +4,8 @@ import base64
|
|
4 |
from dotenv import load_dotenv
|
5 |
from mcp.server.fastmcp import FastMCP, Image
|
6 |
|
|
|
|
|
7 |
mcp = FastMCP(name="GeoServer", stateless_http=True)
|
8 |
|
9 |
|
@@ -11,10 +13,22 @@ mcp = FastMCP(name="GeoServer", stateless_http=True)
|
|
11 |
# Store the current game ID and basic state
|
12 |
active_game = {}
|
13 |
|
14 |
-
# ---
|
15 |
-
|
16 |
-
|
17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
try:
|
19 |
if method == 'POST':
|
20 |
response = requests.post(url, json=json_data, headers={'Content-Type': 'application/json'}, timeout=30)
|
@@ -25,14 +39,14 @@ def call_flask_api(endpoint, method='GET', json_data=None):
|
|
25 |
return response.json()
|
26 |
else:
|
27 |
error_msg = f"API call failed: {response.status_code} - {response.text}"
|
28 |
-
print(f"
|
29 |
raise Exception(error_msg)
|
30 |
except requests.exceptions.ConnectionError as e:
|
31 |
-
error_msg = f"Could not connect to
|
32 |
print(f"Connection Error: {error_msg}")
|
33 |
raise Exception(error_msg)
|
34 |
except requests.exceptions.Timeout as e:
|
35 |
-
error_msg = f"Timeout calling
|
36 |
print(f"Timeout Error: {error_msg}")
|
37 |
raise Exception(error_msg)
|
38 |
except Exception as e:
|
@@ -77,8 +91,8 @@ def start_game(difficulty: str = "easy", player_name: str = "MCP Agent") -> Imag
|
|
77 |
"""
|
78 |
global active_game
|
79 |
|
80 |
-
# Call
|
81 |
-
game_data =
|
82 |
'difficulty': difficulty,
|
83 |
'player_name': player_name
|
84 |
})
|
@@ -131,9 +145,9 @@ def move(direction: str = None, degree: float = None, distance: float = 0.1) ->
|
|
131 |
else:
|
132 |
raise ValueError("Must provide either direction or degree parameter.")
|
133 |
|
134 |
-
# Call
|
135 |
game_id = active_game['game_id']
|
136 |
-
move_result =
|
137 |
|
138 |
# Convert base64 image to bytes and return as Image
|
139 |
if move_result.get('streetview_image'):
|
@@ -188,9 +202,9 @@ def make_final_guess() -> dict:
|
|
188 |
# Get the placeholder guess
|
189 |
guess_location = active_game['placeholder_guess']
|
190 |
|
191 |
-
# Call
|
192 |
game_id = active_game['game_id']
|
193 |
-
guess_result =
|
194 |
'lat': guess_location['lat'],
|
195 |
'lng': guess_location['lng']
|
196 |
})
|
@@ -217,7 +231,7 @@ def get_game_state() -> dict:
|
|
217 |
raise ValueError("Game not started.")
|
218 |
|
219 |
game_id = active_game['game_id']
|
220 |
-
game_state =
|
221 |
|
222 |
# Don't expose the actual coordinates - keep the guessing challenge
|
223 |
state_info = {
|
|
|
4 |
from dotenv import load_dotenv
|
5 |
from mcp.server.fastmcp import FastMCP, Image
|
6 |
|
7 |
+
load_dotenv()
|
8 |
+
|
9 |
mcp = FastMCP(name="GeoServer", stateless_http=True)
|
10 |
|
11 |
|
|
|
13 |
# Store the current game ID and basic state
|
14 |
active_game = {}
|
15 |
|
16 |
+
# --- FastAPI endpoint base URL ---
|
17 |
+
PORT = os.environ.get("PORT", "10000")
|
18 |
+
SPACE_HOST = os.environ.get("SPACE_HOST")
|
19 |
+
if SPACE_HOST and SPACE_HOST.strip():
|
20 |
+
BASE_HOST = SPACE_HOST.strip().rstrip("/")
|
21 |
+
else:
|
22 |
+
BASE_HOST = f"http://localhost:{PORT}"
|
23 |
+
|
24 |
+
# Allow override; default to the mounted FastAPI app
|
25 |
+
API_BASE_URL = os.environ.get("GEOGUESSR_API_URL", f"{BASE_HOST}/geoguessr_app")
|
26 |
+
|
27 |
+
|
28 |
+
# --- HTTP Helper Function ---
|
29 |
+
def call_api(endpoint, method='GET', json_data=None):
|
30 |
+
"""Helper function to call GeoGuessr FastAPI endpoints"""
|
31 |
+
url = f"{API_BASE_URL}{endpoint}"
|
32 |
try:
|
33 |
if method == 'POST':
|
34 |
response = requests.post(url, json=json_data, headers={'Content-Type': 'application/json'}, timeout=30)
|
|
|
39 |
return response.json()
|
40 |
else:
|
41 |
error_msg = f"API call failed: {response.status_code} - {response.text}"
|
42 |
+
print(f"Geo API Error: {error_msg}")
|
43 |
raise Exception(error_msg)
|
44 |
except requests.exceptions.ConnectionError as e:
|
45 |
+
error_msg = f"Could not connect to Geo API at {API_BASE_URL}. Make sure the FastAPI server is running. Error: {str(e)}"
|
46 |
print(f"Connection Error: {error_msg}")
|
47 |
raise Exception(error_msg)
|
48 |
except requests.exceptions.Timeout as e:
|
49 |
+
error_msg = f"Timeout calling Geo API at {API_BASE_URL}. Error: {str(e)}"
|
50 |
print(f"Timeout Error: {error_msg}")
|
51 |
raise Exception(error_msg)
|
52 |
except Exception as e:
|
|
|
91 |
"""
|
92 |
global active_game
|
93 |
|
94 |
+
# Call FastAPI to start game
|
95 |
+
game_data = call_api('/start_game', 'POST', {
|
96 |
'difficulty': difficulty,
|
97 |
'player_name': player_name
|
98 |
})
|
|
|
145 |
else:
|
146 |
raise ValueError("Must provide either direction or degree parameter.")
|
147 |
|
148 |
+
# Call FastAPI to move
|
149 |
game_id = active_game['game_id']
|
150 |
+
move_result = call_api(f'/game/{game_id}/move', 'POST', move_data)
|
151 |
|
152 |
# Convert base64 image to bytes and return as Image
|
153 |
if move_result.get('streetview_image'):
|
|
|
202 |
# Get the placeholder guess
|
203 |
guess_location = active_game['placeholder_guess']
|
204 |
|
205 |
+
# Call FastAPI to make the final guess
|
206 |
game_id = active_game['game_id']
|
207 |
+
guess_result = call_api(f'/game/{game_id}/guess', 'POST', {
|
208 |
'lat': guess_location['lat'],
|
209 |
'lng': guess_location['lng']
|
210 |
})
|
|
|
231 |
raise ValueError("Game not started.")
|
232 |
|
233 |
game_id = active_game['game_id']
|
234 |
+
game_state = call_api(f'/game/{game_id}/state')
|
235 |
|
236 |
# Don't expose the actual coordinates - keep the guessing challenge
|
237 |
state_info = {
|
geoguessr/static/admin.js
ADDED
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
let map;
|
2 |
+
let drawingManager;
|
3 |
+
let lastDrawnShape = null;
|
4 |
+
let displayedShapes = [];
|
5 |
+
|
6 |
+
const difficultyColors = {
|
7 |
+
easy: '#34A853',
|
8 |
+
medium: '#F9AB00',
|
9 |
+
hard: '#EA4335'
|
10 |
+
};
|
11 |
+
|
12 |
+
function api(path) {
|
13 |
+
return path.startsWith('/') ? path.slice(1) : path;
|
14 |
+
}
|
15 |
+
|
16 |
+
function initAdminMap() {
|
17 |
+
map = new google.maps.Map(document.getElementById('map'), {
|
18 |
+
center: { lat: 20, lng: 0 },
|
19 |
+
zoom: 2,
|
20 |
+
});
|
21 |
+
|
22 |
+
drawingManager = new google.maps.drawing.DrawingManager({
|
23 |
+
drawingMode: google.maps.drawing.OverlayType.RECTANGLE,
|
24 |
+
drawingControl: true,
|
25 |
+
drawingControlOptions: {
|
26 |
+
position: google.maps.ControlPosition.TOP_CENTER,
|
27 |
+
drawingModes: [google.maps.drawing.OverlayType.RECTANGLE],
|
28 |
+
},
|
29 |
+
rectangleOptions: {
|
30 |
+
fillColor: '#F97316',
|
31 |
+
fillOpacity: 0.3,
|
32 |
+
strokeWeight: 1,
|
33 |
+
clickable: true,
|
34 |
+
editable: true,
|
35 |
+
zIndex: 1,
|
36 |
+
},
|
37 |
+
});
|
38 |
+
|
39 |
+
drawingManager.setMap(map);
|
40 |
+
|
41 |
+
google.maps.event.addListener(drawingManager, 'overlaycomplete', function(event) {
|
42 |
+
if (lastDrawnShape) {
|
43 |
+
lastDrawnShape.setMap(null);
|
44 |
+
}
|
45 |
+
lastDrawnShape = event.overlay;
|
46 |
+
drawingManager.setDrawingMode(null);
|
47 |
+
document.getElementById('save-zone').disabled = false;
|
48 |
+
});
|
49 |
+
|
50 |
+
document.getElementById('save-zone').addEventListener('click', saveLastZone);
|
51 |
+
document.getElementById('new-zone-btn').addEventListener('click', () => {
|
52 |
+
drawingManager.setDrawingMode(google.maps.drawing.OverlayType.RECTANGLE);
|
53 |
+
document.getElementById('save-zone').disabled = true;
|
54 |
+
});
|
55 |
+
|
56 |
+
loadExistingZones();
|
57 |
+
}
|
58 |
+
|
59 |
+
function saveLastZone() {
|
60 |
+
if (!lastDrawnShape) {
|
61 |
+
alert('Please draw a zone first.');
|
62 |
+
return;
|
63 |
+
}
|
64 |
+
|
65 |
+
const difficulty = document.getElementById('difficulty-select').value;
|
66 |
+
const bounds = lastDrawnShape.getBounds().toJSON();
|
67 |
+
|
68 |
+
const zoneData = { type: 'rectangle', bounds: bounds };
|
69 |
+
|
70 |
+
fetch(api('/api/zones'), {
|
71 |
+
method: 'POST',
|
72 |
+
headers: { 'Content-Type': 'application/json' },
|
73 |
+
body: JSON.stringify({ difficulty: difficulty, zone: zoneData }),
|
74 |
+
})
|
75 |
+
.then(response => response.json())
|
76 |
+
.then(data => {
|
77 |
+
const statusMsg = document.getElementById('status-message');
|
78 |
+
statusMsg.textContent = data.message || `Error: ${data.error}`;
|
79 |
+
setTimeout(() => statusMsg.textContent = '', 3000);
|
80 |
+
if (lastDrawnShape) {
|
81 |
+
lastDrawnShape.setMap(null);
|
82 |
+
lastDrawnShape = null;
|
83 |
+
}
|
84 |
+
document.getElementById('save-zone').disabled = true;
|
85 |
+
loadExistingZones();
|
86 |
+
});
|
87 |
+
}
|
88 |
+
|
89 |
+
function loadExistingZones() {
|
90 |
+
displayedShapes.forEach(shape => shape.setMap(null));
|
91 |
+
displayedShapes = [];
|
92 |
+
|
93 |
+
fetch(api('/api/zones'))
|
94 |
+
.then(response => response.json())
|
95 |
+
.then(zones => {
|
96 |
+
for (const difficulty in zones) {
|
97 |
+
zones[difficulty].forEach(zone => {
|
98 |
+
if (zone.type === 'rectangle') {
|
99 |
+
const rectangle = new google.maps.Rectangle({
|
100 |
+
bounds: zone.bounds,
|
101 |
+
map: map,
|
102 |
+
fillColor: difficultyColors[difficulty],
|
103 |
+
fillOpacity: 0.35,
|
104 |
+
strokeColor: difficultyColors[difficulty],
|
105 |
+
strokeWeight: 2,
|
106 |
+
editable: false,
|
107 |
+
clickable: true,
|
108 |
+
});
|
109 |
+
rectangle.zoneId = zone.id;
|
110 |
+
google.maps.event.addListener(rectangle, 'click', function() {
|
111 |
+
if (confirm('Are you sure you want to delete this zone?')) {
|
112 |
+
deleteZone(this.zoneId, this);
|
113 |
+
}
|
114 |
+
});
|
115 |
+
displayedShapes.push(rectangle);
|
116 |
+
}
|
117 |
+
});
|
118 |
+
}
|
119 |
+
});
|
120 |
+
}
|
121 |
+
|
122 |
+
function deleteZone(zoneId, shape) {
|
123 |
+
fetch(api('/api/zones'), {
|
124 |
+
method: 'DELETE',
|
125 |
+
headers: { 'Content-Type': 'application/json' },
|
126 |
+
body: JSON.stringify({ zone_id: zoneId })
|
127 |
+
})
|
128 |
+
.then(response => response.json())
|
129 |
+
.then(data => {
|
130 |
+
const statusMsg = document.getElementById('status-message');
|
131 |
+
statusMsg.textContent = data.message || `Error: ${data.error}`;
|
132 |
+
setTimeout(() => statusMsg.textContent = '', 3000);
|
133 |
+
if (data.message) {
|
134 |
+
shape.setMap(null);
|
135 |
+
}
|
136 |
+
});
|
137 |
+
}
|
138 |
+
|
139 |
+
|
geoguessr/static/script.js
ADDED
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
let map, panorama, guessMarker, gameId, googleMapsApiKey;
|
2 |
+
let startLocation;
|
3 |
+
let onFirstLinksLoaded;
|
4 |
+
|
5 |
+
function api(path) {
|
6 |
+
// Use relative paths so mounting under a subpath works
|
7 |
+
return path.startsWith('/') ? path.slice(1) : path;
|
8 |
+
}
|
9 |
+
|
10 |
+
function initLobby() {
|
11 |
+
const replayForm = document.getElementById('replay-form');
|
12 |
+
if (replayForm) {
|
13 |
+
replayForm.addEventListener('submit', (e) => {
|
14 |
+
e.preventDefault();
|
15 |
+
replayGame();
|
16 |
+
});
|
17 |
+
}
|
18 |
+
const playAgain = document.getElementById('play-again');
|
19 |
+
if (playAgain) {
|
20 |
+
playAgain.addEventListener('click', showLobby);
|
21 |
+
}
|
22 |
+
}
|
23 |
+
|
24 |
+
function showLobby() {
|
25 |
+
document.getElementById('lobby-container').style.display = 'block';
|
26 |
+
document.getElementById('game-container').style.display = 'none';
|
27 |
+
document.getElementById('result-screen').style.display = 'none';
|
28 |
+
}
|
29 |
+
|
30 |
+
function showGame() {
|
31 |
+
document.getElementById('lobby-container').style.display = 'none';
|
32 |
+
document.getElementById('game-container').style.display = 'flex';
|
33 |
+
document.getElementById('result-screen').style.display = 'none';
|
34 |
+
}
|
35 |
+
|
36 |
+
function startGame() {
|
37 |
+
showGame();
|
38 |
+
const difficultyEl = document.getElementById('difficulty-select-lobby');
|
39 |
+
const difficulty = difficultyEl ? difficultyEl.value : 'easy';
|
40 |
+
fetch(api('/start_game'), {
|
41 |
+
method: 'POST',
|
42 |
+
headers: { 'Content-Type': 'application/json' },
|
43 |
+
body: JSON.stringify({ difficulty: difficulty })
|
44 |
+
})
|
45 |
+
.then(response => response.json())
|
46 |
+
.then(data => {
|
47 |
+
if (data.error) {
|
48 |
+
alert(data.error);
|
49 |
+
showLobby();
|
50 |
+
return;
|
51 |
+
}
|
52 |
+
gameId = data.game_id;
|
53 |
+
startLocation = data.start_location || null;
|
54 |
+
googleMapsApiKey = data.google_maps_api_key || null;
|
55 |
+
|
56 |
+
const chatLog = document.getElementById('chat-log');
|
57 |
+
chatLog.innerHTML = '';
|
58 |
+
addChatMessage('Agent', `New game started (ID: ${gameId}). Finding my location...`);
|
59 |
+
|
60 |
+
if (startLocation) {
|
61 |
+
initStreetView(startLocation);
|
62 |
+
initMap();
|
63 |
+
}
|
64 |
+
});
|
65 |
+
}
|
66 |
+
|
67 |
+
function replayGame() {
|
68 |
+
const replayId = document.getElementById('replay-id-input').value;
|
69 |
+
if (!replayId) {
|
70 |
+
alert('Please enter a Game ID to replay.');
|
71 |
+
return;
|
72 |
+
}
|
73 |
+
|
74 |
+
fetch(api(`/game/${replayId}/state`))
|
75 |
+
.then(response => {
|
76 |
+
if (!response.ok) {
|
77 |
+
throw new Error('Game not found.');
|
78 |
+
}
|
79 |
+
return response.json();
|
80 |
+
})
|
81 |
+
.then(data => {
|
82 |
+
if (!data.game_over) {
|
83 |
+
alert('This game has not finished yet.');
|
84 |
+
return;
|
85 |
+
}
|
86 |
+
showGame();
|
87 |
+
gameId = replayId;
|
88 |
+
startLocation = data.start_location;
|
89 |
+
|
90 |
+
const chatLog = document.getElementById('chat-log');
|
91 |
+
chatLog.innerHTML = '';
|
92 |
+
addChatMessage('System', `Replaying game: ${gameId}`);
|
93 |
+
|
94 |
+
initStreetView(startLocation);
|
95 |
+
initMap(true);
|
96 |
+
replayActions(data.actions);
|
97 |
+
})
|
98 |
+
.catch(error => {
|
99 |
+
alert(error.message);
|
100 |
+
});
|
101 |
+
}
|
102 |
+
|
103 |
+
async function replayActions(actions) {
|
104 |
+
for (const action of actions) {
|
105 |
+
await sleep(2000);
|
106 |
+
if (action.type === 'move') {
|
107 |
+
addChatMessage('Agent (Replay)', `Moved to: ${action.location.lat.toFixed(4)}, ${action.location.lng.toFixed(4)}`);
|
108 |
+
panorama.setPosition(action.location);
|
109 |
+
} else if (action.type === 'guess') {
|
110 |
+
addChatMessage('Agent (Replay)', `Guessed: ${action.location.lat.toFixed(4)}, ${action.location.lng.toFixed(4)}`);
|
111 |
+
placeGuessMarker(action.location);
|
112 |
+
await sleep(2000);
|
113 |
+
const resultData = {
|
114 |
+
guess_location: action.location,
|
115 |
+
actual_location: startLocation,
|
116 |
+
distance_km: action.result.distance_km,
|
117 |
+
score: action.result.score
|
118 |
+
};
|
119 |
+
showResultScreen(resultData);
|
120 |
+
}
|
121 |
+
}
|
122 |
+
}
|
123 |
+
|
124 |
+
function initStreetView(location) {
|
125 |
+
onFirstLinksLoaded = new Promise(resolve => {
|
126 |
+
panorama = new google.maps.StreetViewPanorama(
|
127 |
+
document.getElementById('streetview'), {
|
128 |
+
position: location,
|
129 |
+
pov: { heading: 34, pitch: 10 },
|
130 |
+
visible: true,
|
131 |
+
linksControl: true,
|
132 |
+
clickToGo: true,
|
133 |
+
}
|
134 |
+
);
|
135 |
+
|
136 |
+
const linksChangedListener = panorama.addListener('links_changed', () => {
|
137 |
+
google.maps.event.removeListener(linksChangedListener);
|
138 |
+
resolve();
|
139 |
+
});
|
140 |
+
|
141 |
+
panorama.addListener('position_changed', function() {
|
142 |
+
const newLocation = panorama.getPosition();
|
143 |
+
updateAgentLocation(newLocation.lat(), newLocation.lng());
|
144 |
+
});
|
145 |
+
});
|
146 |
+
}
|
147 |
+
|
148 |
+
function initMap(isReplay = false) {
|
149 |
+
map = new google.maps.Map(document.getElementById('map'), {
|
150 |
+
center: { lat: 0, lng: 0 },
|
151 |
+
zoom: 1,
|
152 |
+
});
|
153 |
+
|
154 |
+
if (!isReplay) {
|
155 |
+
map.addListener('click', function(e) {
|
156 |
+
placeGuessMarker(e.latLng);
|
157 |
+
makeGuess(e.latLng.lat(), e.latLng.lng());
|
158 |
+
});
|
159 |
+
}
|
160 |
+
}
|
161 |
+
|
162 |
+
function placeGuessMarker(location) {
|
163 |
+
if (guessMarker) {
|
164 |
+
guessMarker.setMap(null);
|
165 |
+
}
|
166 |
+
guessMarker = new google.maps.Marker({
|
167 |
+
position: location,
|
168 |
+
map: map
|
169 |
+
});
|
170 |
+
map.setCenter(location);
|
171 |
+
}
|
172 |
+
|
173 |
+
function addChatMessage(sender, message) {
|
174 |
+
const chatLog = document.getElementById('chat-log');
|
175 |
+
const messageElement = document.createElement('div');
|
176 |
+
messageElement.innerHTML = `<strong>${sender}:</strong> ${message}`;
|
177 |
+
chatLog.appendChild(messageElement);
|
178 |
+
chatLog.scrollTop = chatLog.scrollHeight;
|
179 |
+
}
|
180 |
+
|
181 |
+
async function runFakeAgent() {}
|
182 |
+
|
183 |
+
async function takeActionWithScreenshot(actionMessage) {}
|
184 |
+
|
185 |
+
async function updateAgentLocation(lat, lng) {
|
186 |
+
await fetch(api(`/game/${gameId}/move`), {
|
187 |
+
method: 'POST',
|
188 |
+
headers: { 'Content-Type': 'application/json' },
|
189 |
+
body: JSON.stringify({ lat: lat, lng: lng }),
|
190 |
+
});
|
191 |
+
}
|
192 |
+
|
193 |
+
async function makeGuess(lat, lng) {
|
194 |
+
addChatMessage('You', `Guessed: ${lat.toFixed(4)}, ${lng.toFixed(4)}`);
|
195 |
+
const response = await fetch(api(`/game/${gameId}/guess`), {
|
196 |
+
method: 'POST',
|
197 |
+
headers: { 'Content-Type': 'application/json' },
|
198 |
+
body: JSON.stringify({ lat: lat, lng: lng }),
|
199 |
+
});
|
200 |
+
const result = await response.json();
|
201 |
+
showResultScreen(result);
|
202 |
+
}
|
203 |
+
|
204 |
+
function showResultScreen(result) {
|
205 |
+
document.getElementById('game-container').style.display = 'none';
|
206 |
+
document.getElementById('result-screen').style.display = 'block';
|
207 |
+
|
208 |
+
const resultSummary = document.getElementById('result-summary');
|
209 |
+
resultSummary.innerHTML = `
|
210 |
+
<p>Your guess was ${result.distance_km.toFixed(2)} km away.</p>
|
211 |
+
<p>You scored ${result.score.toFixed(0)} points.</p>
|
212 |
+
`;
|
213 |
+
|
214 |
+
const resultMap = new google.maps.Map(document.getElementById('result-map'), {
|
215 |
+
zoom: 3,
|
216 |
+
center: result.actual_location
|
217 |
+
});
|
218 |
+
|
219 |
+
new google.maps.Marker({
|
220 |
+
position: result.actual_location,
|
221 |
+
map: resultMap,
|
222 |
+
label: 'A'
|
223 |
+
});
|
224 |
+
new google.maps.Marker({
|
225 |
+
position: result.guess_location,
|
226 |
+
map: resultMap,
|
227 |
+
label: 'G'
|
228 |
+
});
|
229 |
+
new google.maps.Polyline({
|
230 |
+
path: [result.actual_location, result.guess_location],
|
231 |
+
geodesic: true,
|
232 |
+
strokeColor: '#F97316',
|
233 |
+
strokeOpacity: 1.0,
|
234 |
+
strokeWeight: 2,
|
235 |
+
map: resultMap
|
236 |
+
});
|
237 |
+
}
|
238 |
+
|
239 |
+
function sleep(ms) {
|
240 |
+
return new Promise(resolve => setTimeout(resolve, ms));
|
241 |
+
}
|
242 |
+
|
243 |
+
|
geoguessr/static/style.css
ADDED
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
}
|
170 |
+
|
171 |
+
|
geoguessr/templates/admin.html
ADDED
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html>
|
3 |
+
<head>
|
4 |
+
<title>Admin Panel - LLM GeoGuessr</title>
|
5 |
+
<link rel="stylesheet" href="{{ url_for('static', path='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', path='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>
|
81 |
+
|
geoguessr/templates/index.html
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html>
|
3 |
+
<head>
|
4 |
+
<title>LLM GeoGuessr</title>
|
5 |
+
<link rel="stylesheet" href="{{ url_for('static', path='style.css') }}">
|
6 |
+
</head>
|
7 |
+
<body>
|
8 |
+
<h1>LLM GeoGuessr</h1>
|
9 |
+
|
10 |
+
<div id="lobby-container">
|
11 |
+
<h2>Welcome</h2>
|
12 |
+
<form id="replay-form">
|
13 |
+
<p>Replay a previous game:</p>
|
14 |
+
<input type="text" id="replay-id-input" placeholder="Enter Game ID">
|
15 |
+
<button type="submit">Replay Game</button>
|
16 |
+
</form>
|
17 |
+
</div>
|
18 |
+
|
19 |
+
<div id="game-container" style="display: none;">
|
20 |
+
<div id="streetview-container">
|
21 |
+
<div id="streetview"></div>
|
22 |
+
</div>
|
23 |
+
<div id="map-container">
|
24 |
+
<div id="map"></div>
|
25 |
+
</div>
|
26 |
+
<div id="chat-container">
|
27 |
+
<div id="chat-log"></div>
|
28 |
+
<div id="controls">
|
29 |
+
<!-- In-game controls can go here -->
|
30 |
+
</div>
|
31 |
+
</div>
|
32 |
+
</div>
|
33 |
+
|
34 |
+
<div id="result-screen" style="display: none;">
|
35 |
+
<h2>Results</h2>
|
36 |
+
<div id="result-map"></div>
|
37 |
+
<div id="result-summary"></div>
|
38 |
+
<button id="play-again">Back to Lobby</button>
|
39 |
+
</div>
|
40 |
+
|
41 |
+
<script src="{{ url_for('static', path='script.js') }}"></script>
|
42 |
+
<script async defer src="https://maps.googleapis.com/maps/api/js?key={{ google_maps_api_key }}&callback=initLobby"></script>
|
43 |
+
</body>
|
44 |
+
</html>
|
45 |
+
|
geoguessr/web_app.py
ADDED
@@ -0,0 +1,431 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import json
|
3 |
+
import uuid
|
4 |
+
import math
|
5 |
+
import random
|
6 |
+
import base64
|
7 |
+
import io
|
8 |
+
import requests
|
9 |
+
|
10 |
+
from fastapi import FastAPI, Request, Depends, HTTPException, status
|
11 |
+
from fastapi.responses import JSONResponse, HTMLResponse
|
12 |
+
from fastapi.staticfiles import StaticFiles
|
13 |
+
from fastapi.templating import Jinja2Templates
|
14 |
+
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
15 |
+
from dotenv import load_dotenv
|
16 |
+
from PIL import Image, ImageDraw, ImageFont
|
17 |
+
|
18 |
+
|
19 |
+
load_dotenv()
|
20 |
+
|
21 |
+
|
22 |
+
# Paths
|
23 |
+
BASE_DIR = os.path.dirname(__file__)
|
24 |
+
TEMPLATES_DIR = os.path.join(BASE_DIR, "templates")
|
25 |
+
STATIC_DIR = os.path.join(BASE_DIR, "static")
|
26 |
+
ZONES_FILE = os.path.join(BASE_DIR, "zones.json")
|
27 |
+
|
28 |
+
|
29 |
+
app = FastAPI()
|
30 |
+
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
31 |
+
templates = Jinja2Templates(directory=TEMPLATES_DIR)
|
32 |
+
|
33 |
+
|
34 |
+
# In-memory state
|
35 |
+
games = {}
|
36 |
+
zones = {"easy": [], "medium": [], "hard": []}
|
37 |
+
|
38 |
+
|
39 |
+
# --- Zone Persistence Functions ---
|
40 |
+
def save_zones_to_file() -> None:
|
41 |
+
try:
|
42 |
+
with open(ZONES_FILE, 'w') as f:
|
43 |
+
json.dump(zones, f, indent=4)
|
44 |
+
except Exception as e:
|
45 |
+
print(f"Error saving zones: {e}")
|
46 |
+
|
47 |
+
|
48 |
+
def load_zones_from_file() -> None:
|
49 |
+
global zones
|
50 |
+
if os.path.exists(ZONES_FILE):
|
51 |
+
try:
|
52 |
+
with open(ZONES_FILE, 'r') as f:
|
53 |
+
loaded_zones = json.load(f)
|
54 |
+
|
55 |
+
if not (isinstance(loaded_zones, dict) and all(k in loaded_zones for k in ["easy", "medium", "hard"])):
|
56 |
+
raise ValueError("Invalid zones format")
|
57 |
+
|
58 |
+
migrated = False
|
59 |
+
for difficulty in loaded_zones:
|
60 |
+
for zone in loaded_zones[difficulty]:
|
61 |
+
if 'id' not in zone:
|
62 |
+
zone['id'] = uuid.uuid4().hex
|
63 |
+
migrated = True
|
64 |
+
|
65 |
+
zones = loaded_zones
|
66 |
+
if migrated:
|
67 |
+
save_zones_to_file()
|
68 |
+
except Exception as e:
|
69 |
+
print(f"Warning: '{ZONES_FILE}' is corrupted or invalid ({e}). Recreating with empty zones.")
|
70 |
+
save_zones_to_file()
|
71 |
+
else:
|
72 |
+
save_zones_to_file()
|
73 |
+
|
74 |
+
|
75 |
+
# Predefined fallback locations
|
76 |
+
LOCATIONS = [
|
77 |
+
{'lat': 48.85824, 'lng': 2.2945}, # Eiffel Tower, Paris
|
78 |
+
{'lat': 40.748440, 'lng': -73.985664}, # Empire State Building, New York
|
79 |
+
{'lat': 35.689487, 'lng': 139.691711}, # Tokyo, Japan
|
80 |
+
{'lat': -33.856784, 'lng': 151.215297} # Sydney Opera House, Australia
|
81 |
+
]
|
82 |
+
|
83 |
+
|
84 |
+
def generate_game_id() -> str:
|
85 |
+
return ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=10))
|
86 |
+
|
87 |
+
|
88 |
+
def draw_compass_on_image(image_data_base64: str, heading: int) -> str:
|
89 |
+
try:
|
90 |
+
img = Image.open(io.BytesIO(base64.b64decode(image_data_base64)))
|
91 |
+
|
92 |
+
img_with_compass = img.copy()
|
93 |
+
draw = ImageDraw.Draw(img_with_compass)
|
94 |
+
|
95 |
+
compass_size = 80
|
96 |
+
margin = 20
|
97 |
+
x = img.width - compass_size - margin
|
98 |
+
y = margin
|
99 |
+
center_x = x + compass_size // 2
|
100 |
+
center_y = y + compass_size // 2
|
101 |
+
|
102 |
+
draw.ellipse([x, y, x + compass_size, y + compass_size], fill=(240, 240, 240), outline=(249, 115, 22), width=3)
|
103 |
+
|
104 |
+
font_size = 12
|
105 |
+
try:
|
106 |
+
font = ImageFont.truetype("/System/Library/Fonts/Arial.ttf", font_size)
|
107 |
+
except Exception:
|
108 |
+
try:
|
109 |
+
font = ImageFont.truetype("arial.ttf", font_size)
|
110 |
+
except Exception:
|
111 |
+
font = ImageFont.load_default()
|
112 |
+
|
113 |
+
directions = [
|
114 |
+
("N", center_x, y + 8, (220, 38, 38)),
|
115 |
+
("E", x + compass_size - 15, center_y, (249, 115, 22)),
|
116 |
+
("S", center_x, y + compass_size - 20, (249, 115, 22)),
|
117 |
+
("W", x + 8, center_y, (249, 115, 22)),
|
118 |
+
]
|
119 |
+
for text, text_x, text_y, color in directions:
|
120 |
+
bbox = draw.textbbox((0, 0), text, font=font)
|
121 |
+
text_width = bbox[2] - bbox[0]
|
122 |
+
text_height = bbox[3] - bbox[1]
|
123 |
+
|
124 |
+
circle_radius = 10
|
125 |
+
draw.ellipse([text_x - circle_radius, text_y - circle_radius, text_x + circle_radius, text_y + circle_radius], fill=(255, 255, 255), outline=color, width=1)
|
126 |
+
draw.text((text_x - text_width // 2, text_y - text_height // 2), text, font=font, fill=color)
|
127 |
+
|
128 |
+
needle_length = compass_size // 2 - 15
|
129 |
+
needle_angle = math.radians(heading)
|
130 |
+
end_x = center_x + needle_length * math.sin(needle_angle)
|
131 |
+
end_y = center_y - needle_length * math.cos(needle_angle)
|
132 |
+
draw.line([center_x, center_y, end_x, end_y], fill=(220, 38, 38), width=4)
|
133 |
+
tip_radius = 3
|
134 |
+
draw.ellipse([end_x - tip_radius, end_y - tip_radius, end_x + tip_radius, end_y + tip_radius], fill=(220, 38, 38))
|
135 |
+
center_radius = 4
|
136 |
+
draw.ellipse([center_x - center_radius, center_y - center_radius, center_x + center_radius, center_y + center_radius], fill=(249, 115, 22))
|
137 |
+
|
138 |
+
label_y = y + compass_size + 5
|
139 |
+
label_text = f"{heading}°"
|
140 |
+
bbox = draw.textbbox((0, 0), label_text, font=font)
|
141 |
+
label_width = bbox[2] - bbox[0]
|
142 |
+
draw.text((center_x - label_width // 2, label_y), label_text, font=font, fill=(249, 115, 22))
|
143 |
+
|
144 |
+
buffer = io.BytesIO()
|
145 |
+
img_with_compass.save(buffer, format='JPEG', quality=85)
|
146 |
+
return base64.b64encode(buffer.getvalue()).decode('utf-8')
|
147 |
+
except Exception as e:
|
148 |
+
print(f"Error drawing compass: {e}")
|
149 |
+
return image_data_base64
|
150 |
+
|
151 |
+
|
152 |
+
# --- Auth ---
|
153 |
+
security = HTTPBasic()
|
154 |
+
|
155 |
+
|
156 |
+
def verify_basic_auth(credentials: HTTPBasicCredentials = Depends(security)) -> None:
|
157 |
+
admin_user = os.getenv('ADMIN_USERNAME', 'admin')
|
158 |
+
admin_pass = os.getenv('ADMIN_PASSWORD', 'password')
|
159 |
+
if not (credentials.username == admin_user and credentials.password == admin_pass):
|
160 |
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized", headers={"WWW-Authenticate": "Basic"})
|
161 |
+
|
162 |
+
|
163 |
+
@app.get("/", response_class=HTMLResponse)
|
164 |
+
def index(request: Request):
|
165 |
+
google_maps_api_key = os.getenv('GOOGLE_MAPS_API_KEY')
|
166 |
+
if not google_maps_api_key:
|
167 |
+
return HTMLResponse("Error: GOOGLE_MAPS_API_KEY not set. Please set it in a .env file.", status_code=500)
|
168 |
+
base_path = request.scope.get('root_path', '')
|
169 |
+
return templates.TemplateResponse("index.html", {"request": request, "google_maps_api_key": google_maps_api_key, "base_path": base_path})
|
170 |
+
|
171 |
+
|
172 |
+
@app.get("/admin", response_class=HTMLResponse)
|
173 |
+
def admin(request: Request, _: None = Depends(verify_basic_auth)):
|
174 |
+
google_maps_api_key = os.getenv('GOOGLE_MAPS_API_KEY')
|
175 |
+
if not google_maps_api_key:
|
176 |
+
return HTMLResponse("Error: GOOGLE_MAPS_API_KEY not set. Please set it in a .env file.", status_code=500)
|
177 |
+
base_path = request.scope.get('root_path', '')
|
178 |
+
return templates.TemplateResponse("admin.html", {"request": request, "google_maps_api_key": google_maps_api_key, "base_path": base_path})
|
179 |
+
|
180 |
+
|
181 |
+
@app.get("/api/zones")
|
182 |
+
def get_zones():
|
183 |
+
return JSONResponse(zones)
|
184 |
+
|
185 |
+
|
186 |
+
@app.post("/api/zones")
|
187 |
+
def create_zone(payload: dict):
|
188 |
+
difficulty = payload.get('difficulty')
|
189 |
+
zone_data = payload.get('zone')
|
190 |
+
if difficulty and zone_data and difficulty in zones:
|
191 |
+
zone_data['id'] = uuid.uuid4().hex
|
192 |
+
zones[difficulty].append(zone_data)
|
193 |
+
save_zones_to_file()
|
194 |
+
return {"message": "Zone saved successfully"}
|
195 |
+
raise HTTPException(status_code=400, detail="Invalid data")
|
196 |
+
|
197 |
+
|
198 |
+
@app.delete("/api/zones")
|
199 |
+
def delete_zone(payload: dict):
|
200 |
+
zone_id = payload.get('zone_id')
|
201 |
+
if not zone_id:
|
202 |
+
raise HTTPException(status_code=400, detail="Zone ID is required")
|
203 |
+
for difficulty in zones:
|
204 |
+
zones[difficulty] = [z for z in zones[difficulty] if z.get('id') != zone_id]
|
205 |
+
save_zones_to_file()
|
206 |
+
return {"message": "Zone deleted successfully"}
|
207 |
+
|
208 |
+
|
209 |
+
def direction_to_degree(direction: str):
|
210 |
+
directions = {
|
211 |
+
'N': 0, 'NORTH': 0,
|
212 |
+
'NE': 45, 'NORTHEAST': 45,
|
213 |
+
'E': 90, 'EAST': 90,
|
214 |
+
'SE': 135, 'SOUTHEAST': 135,
|
215 |
+
'S': 180, 'SOUTH': 180,
|
216 |
+
'SW': 225, 'SOUTHWEST': 225,
|
217 |
+
'W': 270, 'WEST': 270,
|
218 |
+
'NW': 315, 'NORTHWEST': 315
|
219 |
+
}
|
220 |
+
return directions.get(direction.upper()) if isinstance(direction, str) else None
|
221 |
+
|
222 |
+
|
223 |
+
def calculate_new_location(current_lat: float, current_lng: float, degree: float, distance_km: float = 0.1):
|
224 |
+
lat_rad = math.radians(current_lat)
|
225 |
+
lng_rad = math.radians(current_lng)
|
226 |
+
bearing_rad = math.radians(degree)
|
227 |
+
R = 6371.0
|
228 |
+
|
229 |
+
new_lat_rad = math.asin(
|
230 |
+
math.sin(lat_rad) * math.cos(distance_km / R) +
|
231 |
+
math.cos(lat_rad) * math.sin(distance_km / R) * math.cos(bearing_rad)
|
232 |
+
)
|
233 |
+
|
234 |
+
new_lng_rad = lng_rad + math.atan2(
|
235 |
+
math.sin(bearing_rad) * math.sin(distance_km / R) * math.cos(lat_rad),
|
236 |
+
math.cos(distance_km / R) - math.sin(lat_rad) * math.sin(new_lat_rad)
|
237 |
+
)
|
238 |
+
|
239 |
+
new_lat = math.degrees(new_lat_rad)
|
240 |
+
new_lng = math.degrees(new_lng_rad)
|
241 |
+
new_lng = ((new_lng + 180) % 360) - 180
|
242 |
+
return new_lat, new_lng
|
243 |
+
|
244 |
+
|
245 |
+
@app.post("/start_game")
|
246 |
+
def start_game(payload: dict):
|
247 |
+
difficulty = payload.get('difficulty', 'easy') if payload else 'easy'
|
248 |
+
player_name = payload.get('player_name', 'Anonymous Player') if payload else 'Anonymous Player'
|
249 |
+
player_google_api_key = payload.get('google_api_key') if payload else None
|
250 |
+
|
251 |
+
start_location = None
|
252 |
+
if difficulty in zones and zones[difficulty]:
|
253 |
+
selected_zone = random.choice(zones[difficulty])
|
254 |
+
if selected_zone.get('type') == 'rectangle':
|
255 |
+
bounds = selected_zone['bounds']
|
256 |
+
north, south, east, west = bounds['north'], bounds['south'], bounds['east'], bounds['west']
|
257 |
+
if west > east:
|
258 |
+
east += 360
|
259 |
+
rand_lng = random.uniform(west, east)
|
260 |
+
if rand_lng > 180:
|
261 |
+
rand_lng -= 360
|
262 |
+
rand_lat = random.uniform(south, north)
|
263 |
+
start_location = {'lat': rand_lat, 'lng': rand_lng}
|
264 |
+
|
265 |
+
if not start_location:
|
266 |
+
start_location = random.choice(LOCATIONS)
|
267 |
+
|
268 |
+
game_id = generate_game_id()
|
269 |
+
games[game_id] = {
|
270 |
+
'start_location': start_location,
|
271 |
+
'current_location': start_location,
|
272 |
+
'guesses': [],
|
273 |
+
'moves': 0,
|
274 |
+
'actions': [],
|
275 |
+
'game_over': False,
|
276 |
+
'player_name': player_name,
|
277 |
+
'player_google_api_key': player_google_api_key,
|
278 |
+
'created_at': __import__('datetime').datetime.now().isoformat()
|
279 |
+
}
|
280 |
+
|
281 |
+
google_maps_api_key = player_google_api_key or os.getenv('GOOGLE_MAPS_API_KEY')
|
282 |
+
|
283 |
+
streetview_image = None
|
284 |
+
compass_heading = random.randint(0, 359)
|
285 |
+
if google_maps_api_key:
|
286 |
+
try:
|
287 |
+
lat, lng = start_location['lat'], start_location['lng']
|
288 |
+
streetview_url = f"https://maps.googleapis.com/maps/api/streetview?size=640x400&location={lat},{lng}&heading={compass_heading}&pitch=0&fov=90&key={google_maps_api_key}"
|
289 |
+
response = requests.get(streetview_url, timeout=20)
|
290 |
+
if response.status_code == 200:
|
291 |
+
base_image = base64.b64encode(response.content).decode('utf-8')
|
292 |
+
streetview_image = draw_compass_on_image(base_image, compass_heading)
|
293 |
+
except Exception as e:
|
294 |
+
print(f"Error fetching Street View image: {e}")
|
295 |
+
|
296 |
+
return {
|
297 |
+
'game_id': game_id,
|
298 |
+
'player_name': player_name,
|
299 |
+
'streetview_image': streetview_image,
|
300 |
+
'compass_heading': compass_heading
|
301 |
+
}
|
302 |
+
|
303 |
+
|
304 |
+
@app.get("/game/{game_id}/state")
|
305 |
+
def get_game_state(game_id: str):
|
306 |
+
game = games.get(game_id)
|
307 |
+
if not game:
|
308 |
+
raise HTTPException(status_code=404, detail='Game not found')
|
309 |
+
return game
|
310 |
+
|
311 |
+
|
312 |
+
@app.post("/game/{game_id}/move")
|
313 |
+
def move(game_id: str, payload: dict):
|
314 |
+
game = games.get(game_id)
|
315 |
+
if not game:
|
316 |
+
raise HTTPException(status_code=404, detail='Game not found')
|
317 |
+
if game['game_over']:
|
318 |
+
raise HTTPException(status_code=400, detail='Game is over')
|
319 |
+
|
320 |
+
direction = payload.get('direction') if payload else None
|
321 |
+
degree = payload.get('degree') if payload else None
|
322 |
+
distance = payload.get('distance', 0.1) if payload else 0.1
|
323 |
+
|
324 |
+
if direction is None and degree is None:
|
325 |
+
raise HTTPException(status_code=400, detail='Must provide either direction (N, NE, E, etc.) or degree (0-360)')
|
326 |
+
|
327 |
+
if direction is not None:
|
328 |
+
degree = direction_to_degree(direction)
|
329 |
+
if degree is None:
|
330 |
+
raise HTTPException(status_code=400, detail='Invalid direction. Use N, NE, E, SE, S, SW, W, NW or their full names')
|
331 |
+
|
332 |
+
if not (0 <= degree <= 360):
|
333 |
+
raise HTTPException(status_code=400, detail='Degree must be between 0 and 360')
|
334 |
+
|
335 |
+
if not (0.01 <= distance <= 10):
|
336 |
+
raise HTTPException(status_code=400, detail='Distance must be between 0.01 and 10 km')
|
337 |
+
|
338 |
+
current_lat = game['current_location']['lat']
|
339 |
+
current_lng = game['current_location']['lng']
|
340 |
+
new_lat, new_lng = calculate_new_location(current_lat, current_lng, degree, distance)
|
341 |
+
|
342 |
+
game['current_location'] = {'lat': new_lat, 'lng': new_lng}
|
343 |
+
game['moves'] += 1
|
344 |
+
game['actions'].append({
|
345 |
+
'type': 'move',
|
346 |
+
'location': {'lat': new_lat, 'lng': new_lng},
|
347 |
+
'direction': direction,
|
348 |
+
'degree': degree,
|
349 |
+
'distance_km': distance
|
350 |
+
})
|
351 |
+
|
352 |
+
google_maps_api_key = game.get('player_google_api_key') or os.getenv('GOOGLE_MAPS_API_KEY')
|
353 |
+
streetview_image = None
|
354 |
+
compass_heading = random.randint(0, 359)
|
355 |
+
if google_maps_api_key:
|
356 |
+
try:
|
357 |
+
streetview_url = f"https://maps.googleapis.com/maps/api/streetview?size=640x400&location={new_lat},{new_lng}&heading={compass_heading}&pitch=0&fov=90&key={google_maps_api_key}"
|
358 |
+
response = requests.get(streetview_url, timeout=20)
|
359 |
+
if response.status_code == 200:
|
360 |
+
base_image = base64.b64encode(response.content).decode('utf-8')
|
361 |
+
streetview_image = draw_compass_on_image(base_image, compass_heading)
|
362 |
+
except Exception as e:
|
363 |
+
print(f"Error fetching Street View image: {e}")
|
364 |
+
|
365 |
+
return {
|
366 |
+
'message': 'Move successful',
|
367 |
+
'streetview_image': streetview_image,
|
368 |
+
'compass_heading': compass_heading,
|
369 |
+
'moved_direction': direction or f"{degree}°",
|
370 |
+
'distance_moved_km': distance
|
371 |
+
}
|
372 |
+
|
373 |
+
|
374 |
+
@app.post("/game/{game_id}/guess")
|
375 |
+
def guess(game_id: str, payload: dict):
|
376 |
+
game = games.get(game_id)
|
377 |
+
if not game:
|
378 |
+
raise HTTPException(status_code=404, detail='Game not found')
|
379 |
+
if game['game_over']:
|
380 |
+
raise HTTPException(status_code=400, detail='Game is over')
|
381 |
+
|
382 |
+
guess_lat = payload.get('lat') if payload else None
|
383 |
+
guess_lng = payload.get('lng') if payload else None
|
384 |
+
if guess_lat is None or guess_lng is None:
|
385 |
+
raise HTTPException(status_code=400, detail='Missing lat/lng for guess')
|
386 |
+
|
387 |
+
guess_location = {'lat': guess_lat, 'lng': guess_lng}
|
388 |
+
game['guesses'].append(guess_location)
|
389 |
+
|
390 |
+
from math import radians, cos, sin, asin, sqrt
|
391 |
+
|
392 |
+
def haversine(lat1, lon1, lat2, lon2):
|
393 |
+
lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])
|
394 |
+
dlon = lon2 - lon1
|
395 |
+
dlat = lat2 - lat1
|
396 |
+
a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
|
397 |
+
c = 2 * asin(sqrt(a))
|
398 |
+
r = 6371
|
399 |
+
return c * r
|
400 |
+
|
401 |
+
distance = haversine(
|
402 |
+
game['start_location']['lat'], game['start_location']['lng'],
|
403 |
+
guess_lat, guess_lng
|
404 |
+
)
|
405 |
+
|
406 |
+
max_score = 5000
|
407 |
+
score = max(0, max_score - distance)
|
408 |
+
|
409 |
+
game['actions'].append({
|
410 |
+
'type': 'guess',
|
411 |
+
'location': guess_location,
|
412 |
+
'result': {
|
413 |
+
'distance_km': distance,
|
414 |
+
'score': score
|
415 |
+
}
|
416 |
+
})
|
417 |
+
game['game_over'] = True
|
418 |
+
|
419 |
+
return {
|
420 |
+
'message': 'Guess received',
|
421 |
+
'guess_location': guess_location,
|
422 |
+
'actual_location': game['start_location'],
|
423 |
+
'distance_km': distance,
|
424 |
+
'score': score
|
425 |
+
}
|
426 |
+
|
427 |
+
|
428 |
+
# Load zones at startup
|
429 |
+
load_zones_from_file()
|
430 |
+
|
431 |
+
|
geoguessr/zones.json
ADDED
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"easy": [
|
3 |
+
{
|
4 |
+
"type": "rectangle",
|
5 |
+
"bounds": {
|
6 |
+
"south": 48.8336736971006,
|
7 |
+
"west": 2.28515625,
|
8 |
+
"north": 48.87885155432368,
|
9 |
+
"east": 2.39776611328125
|
10 |
+
},
|
11 |
+
"id": "fc849008dae8481092eb79750ffb29b3"
|
12 |
+
},
|
13 |
+
{
|
14 |
+
"type": "rectangle",
|
15 |
+
"bounds": {
|
16 |
+
"south": 35.66847408359237,
|
17 |
+
"west": 139.62718704877395,
|
18 |
+
"north": 35.737615509324385,
|
19 |
+
"east": 139.8218510502388
|
20 |
+
},
|
21 |
+
"id": "898886b44cad43b9abfec296a553daea"
|
22 |
+
},
|
23 |
+
{
|
24 |
+
"type": "rectangle",
|
25 |
+
"bounds": {
|
26 |
+
"south": 37.536938882023136,
|
27 |
+
"west": 126.94755460738277,
|
28 |
+
"north": 37.557898527241505,
|
29 |
+
"east": 127.01244260787105
|
30 |
+
},
|
31 |
+
"id": "ba27424f2f584ed0b238795608503149"
|
32 |
+
},
|
33 |
+
{
|
34 |
+
"type": "rectangle",
|
35 |
+
"bounds": {
|
36 |
+
"south": 18.953448012353313,
|
37 |
+
"west": 72.81336899465802,
|
38 |
+
"north": 18.976825403712557,
|
39 |
+
"east": 72.83911820120099
|
40 |
+
},
|
41 |
+
"id": "71ec558ad7d7476297452801a170f10c"
|
42 |
+
},
|
43 |
+
{
|
44 |
+
"type": "rectangle",
|
45 |
+
"bounds": {
|
46 |
+
"south": 40.713060179679026,
|
47 |
+
"west": -74.00079248380419,
|
48 |
+
"north": 40.76040587151275,
|
49 |
+
"east": -73.97933481168505
|
50 |
+
},
|
51 |
+
"id": "c03e32aeb33a444f8eac20b24d946b34"
|
52 |
+
},
|
53 |
+
{
|
54 |
+
"type": "rectangle",
|
55 |
+
"bounds": {
|
56 |
+
"south": 37.77180843179515,
|
57 |
+
"west": -122.4442846812313,
|
58 |
+
"north": 37.80328200680126,
|
59 |
+
"east": -122.40857911482505
|
60 |
+
},
|
61 |
+
"id": "41ce7507e12a44b8b964d9c1d2755c42"
|
62 |
+
}
|
63 |
+
],
|
64 |
+
"medium": [],
|
65 |
+
"hard": []
|
66 |
+
}
|
67 |
+
|
server.py
CHANGED
@@ -5,6 +5,7 @@ from fastapi.staticfiles import StaticFiles
|
|
5 |
from fastapi.templating import Jinja2Templates
|
6 |
from pokemon.pokemon_server import mcp as pokemon_mcp
|
7 |
from geoguessr.geo_server import mcp as geogussr_mcp
|
|
|
8 |
import os
|
9 |
|
10 |
|
@@ -42,6 +43,9 @@ async def index(request: Request):
|
|
42 |
app.mount("/geoguessr", geogussr_mcp.streamable_http_app())
|
43 |
app.mount("/Pokemon", pokemon_mcp.streamable_http_app())
|
44 |
|
|
|
|
|
|
|
45 |
PORT = int(os.environ.get("PORT", "10000"))
|
46 |
|
47 |
if __name__ == "__main__":
|
|
|
5 |
from fastapi.templating import Jinja2Templates
|
6 |
from pokemon.pokemon_server import mcp as pokemon_mcp
|
7 |
from geoguessr.geo_server import mcp as geogussr_mcp
|
8 |
+
from geoguessr.web_app import app as geoguessr_app
|
9 |
import os
|
10 |
|
11 |
|
|
|
43 |
app.mount("/geoguessr", geogussr_mcp.streamable_http_app())
|
44 |
app.mount("/Pokemon", pokemon_mcp.streamable_http_app())
|
45 |
|
46 |
+
# Mount GeoGuessr FastAPI web app (UI + API)
|
47 |
+
app.mount("/geoguessr_app", geoguessr_app)
|
48 |
+
|
49 |
PORT = int(os.environ.get("PORT", "10000"))
|
50 |
|
51 |
if __name__ == "__main__":
|