Jofthomas commited on
Commit
8398097
·
verified ·
1 Parent(s): d7d9310

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +26 -0
  2. app.py +289 -0
  3. requirements.txt +5 -0
  4. zones.json +66 -0
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.9-slim
3
+
4
+ # Set the working directory in the container
5
+ WORKDIR /app
6
+
7
+ # Copy the requirements file into the container at /app
8
+ COPY requirements.txt .
9
+
10
+ # Install any needed packages specified in requirements.txt
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ # Copy the rest of the application's code into the container at /app
14
+ COPY . .
15
+
16
+ # Make port 7860 available to the world outside this container
17
+ # Hugging Face Spaces uses this port by default
18
+ EXPOSE 7860
19
+
20
+ # Define environment variable for the port
21
+ ENV PORT=7860
22
+
23
+ # Run app.py when the container launches using Gunicorn
24
+ # Gunicorn is a production-ready web server.
25
+ # We bind to 0.0.0.0 to allow external connections.
26
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860", "--workers", "1", "app:app"]
app.py ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import random
3
+ import json
4
+ import uuid
5
+ import requests
6
+ import base64
7
+ from flask import Flask, jsonify, render_template, request
8
+ from dotenv import load_dotenv
9
+
10
+ load_dotenv()
11
+
12
+ app = Flask(__name__)
13
+ app.config['SECRET_KEY'] = os.urandom(24)
14
+
15
+ # In-memory "database"
16
+ ZONES_FILE = 'zones.json'
17
+ games = {}
18
+ zones = {
19
+ "easy": [],
20
+ "medium": [],
21
+ "hard": []
22
+ }
23
+
24
+ # --- Zone Persistence Functions ---
25
+ def save_zones_to_file():
26
+ with open(ZONES_FILE, 'w') as f:
27
+ json.dump(zones, f, indent=4)
28
+
29
+ def load_zones_from_file():
30
+ global zones
31
+ if os.path.exists(ZONES_FILE):
32
+ try:
33
+ with open(ZONES_FILE, 'r') as f:
34
+ loaded_zones = json.load(f)
35
+
36
+ # Basic format validation
37
+ if not (isinstance(loaded_zones, dict) and all(k in loaded_zones for k in ["easy", "medium", "hard"])):
38
+ raise ValueError("Invalid format")
39
+
40
+ migrated = False
41
+ for difficulty in loaded_zones:
42
+ for zone in loaded_zones[difficulty]:
43
+ # Assign ID if missing
44
+ if 'id' not in zone:
45
+ zone['id'] = uuid.uuid4().hex
46
+ migrated = True
47
+
48
+ zones = loaded_zones
49
+ print(zones)
50
+ if migrated:
51
+ print("Info: Migrated old zone data by adding unique IDs.")
52
+ save_zones_to_file()
53
+
54
+ except (json.JSONDecodeError, IOError, ValueError):
55
+ print(f"Warning: '{ZONES_FILE}' is corrupted or invalid. Recreating with empty zones.")
56
+ save_zones_to_file() # This creates a fresh, empty, and valid file
57
+ else:
58
+ # If file doesn't exist, create an empty one.
59
+ save_zones_to_file()
60
+
61
+ # Predefined locations for the game, used as a fallback
62
+ LOCATIONS = [
63
+ {'lat': 48.85824, 'lng': 2.2945}, # Eiffel Tower, Paris
64
+ {'lat': 40.748440, 'lng': -73.985664}, # Empire State Building, New York
65
+ {'lat': 35.689487, 'lng': 139.691711}, # Tokyo, Japan
66
+ {'lat': -33.856784, 'lng': 151.215297} # Sydney Opera House, Australia
67
+ ]
68
+
69
+ def generate_game_id():
70
+ return ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=10))
71
+
72
+ @app.route('/')
73
+ def index():
74
+ google_maps_api_key = os.getenv('GOOGLE_MAPS_API_KEY')
75
+ if not google_maps_api_key:
76
+ return "Error: GOOGLE_MAPS_API_KEY not set. Please set it in a .env file.", 500
77
+ return render_template('index.html', google_maps_api_key=google_maps_api_key)
78
+
79
+ @app.route('/admin')
80
+ def admin():
81
+ google_maps_api_key = os.getenv('GOOGLE_MAPS_API_KEY')
82
+ if not google_maps_api_key:
83
+ return "Error: GOOGLE_MAPS_API_KEY not set. Please set it in a .env file.", 500
84
+ return render_template('admin.html', google_maps_api_key=google_maps_api_key)
85
+
86
+ @app.route('/api/zones', methods=['GET', 'POST', 'DELETE'])
87
+ def handle_zones():
88
+ if request.method == 'POST':
89
+ data = request.json
90
+ difficulty = data.get('difficulty')
91
+ zone_data = data.get('zone')
92
+ if difficulty and zone_data and difficulty in zones:
93
+ zone_data['id'] = uuid.uuid4().hex
94
+ zones[difficulty].append(zone_data)
95
+ save_zones_to_file()
96
+ return jsonify({'message': 'Zone saved successfully'}), 201
97
+ return jsonify({'error': 'Invalid data'}), 400
98
+
99
+ if request.method == 'DELETE':
100
+ data = request.json
101
+ zone_id = data.get('zone_id')
102
+ if not zone_id:
103
+ return jsonify({'error': 'Zone ID is required'}), 400
104
+
105
+ for difficulty in zones:
106
+ zones[difficulty] = [z for z in zones[difficulty] if z.get('id') != zone_id]
107
+
108
+ save_zones_to_file()
109
+ return jsonify({'message': 'Zone deleted successfully'})
110
+
111
+ # GET request
112
+ return jsonify(zones)
113
+
114
+ @app.route('/start_game', methods=['POST'])
115
+ def start_game():
116
+ data = request.json or {}
117
+ difficulty = data.get('difficulty', 'easy')
118
+
119
+ start_location = None
120
+
121
+ if difficulty in zones and zones[difficulty]:
122
+ selected_zone_list = zones[difficulty]
123
+ print("selected_zone_list", selected_zone_list)
124
+ selected_zone = random.choice(selected_zone_list)
125
+ print("selected_zone", selected_zone)
126
+
127
+ if selected_zone['type'] == 'rectangle':
128
+ bounds = selected_zone['bounds']
129
+ north, south, east, west = bounds['north'], bounds['south'], bounds['east'], bounds['west']
130
+
131
+ # Handle antimeridian crossing
132
+ if west > east:
133
+ east += 360
134
+
135
+ rand_lng = random.uniform(west, east)
136
+ if rand_lng > 180:
137
+ rand_lng -= 360
138
+
139
+ rand_lat = random.uniform(south, north)
140
+ start_location = {'lat': rand_lat, 'lng': rand_lng}
141
+
142
+ # Fallback to predefined locations if no zones are defined
143
+ if not start_location:
144
+ start_location = random.choice(LOCATIONS)
145
+
146
+ game_id = generate_game_id()
147
+ games[game_id] = {
148
+ 'start_location': start_location,
149
+ 'current_location': start_location,
150
+ 'guesses': [],
151
+ 'moves': 0,
152
+ 'actions': [],
153
+ 'game_over': False
154
+ }
155
+
156
+ google_maps_api_key = os.getenv('GOOGLE_MAPS_API_KEY')
157
+
158
+ # Fetch Street View image
159
+ streetview_image = None
160
+ compass_heading = random.randint(0, 359) # Random compass direction
161
+ if google_maps_api_key:
162
+ try:
163
+ lat, lng = start_location['lat'], start_location['lng']
164
+ 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}"
165
+
166
+ response = requests.get(streetview_url)
167
+ if response.status_code == 200:
168
+ # Convert image to base64
169
+ streetview_image = base64.b64encode(response.content).decode('utf-8')
170
+ except Exception as e:
171
+ print(f"Error fetching Street View image: {e}")
172
+
173
+ return jsonify({
174
+ 'game_id': game_id,
175
+ 'streetview_image': streetview_image,
176
+ 'compass_heading': compass_heading
177
+ })
178
+
179
+ @app.route('/game/<game_id>/state', methods=['GET'])
180
+ def get_game_state(game_id):
181
+ game = games.get(game_id)
182
+ if not game:
183
+ return jsonify({'error': 'Game not found'}), 404
184
+ return jsonify(game)
185
+
186
+ @app.route('/game/<game_id>/move', methods=['POST'])
187
+ def move(game_id):
188
+ game = games.get(game_id)
189
+ if not game:
190
+ return jsonify({'error': 'Game not found'}), 404
191
+ if game['game_over']:
192
+ return jsonify({'error': 'Game is over'}), 400
193
+
194
+ data = request.json
195
+ new_lat = data.get('lat')
196
+ new_lng = data.get('lng')
197
+
198
+ if new_lat is None or new_lng is None:
199
+ return jsonify({'error': 'Missing lat/lng for move'}), 400
200
+
201
+ game['current_location'] = {'lat': new_lat, 'lng': new_lng}
202
+ game['moves'] += 1
203
+ game['actions'].append({'type': 'move', 'location': {'lat': new_lat, 'lng': new_lng}})
204
+
205
+ # Fetch Street View image for the new location
206
+ google_maps_api_key = os.getenv('GOOGLE_MAPS_API_KEY')
207
+ streetview_image = None
208
+ compass_heading = random.randint(0, 359) # Random compass direction
209
+ if google_maps_api_key:
210
+ try:
211
+ 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}"
212
+
213
+ response = requests.get(streetview_url)
214
+ if response.status_code == 200:
215
+ # Convert image to base64
216
+ streetview_image = base64.b64encode(response.content).decode('utf-8')
217
+ except Exception as e:
218
+ print(f"Error fetching Street View image: {e}")
219
+
220
+ return jsonify({
221
+ 'message': 'Move successful',
222
+ 'streetview_image': streetview_image,
223
+ 'compass_heading': compass_heading
224
+ })
225
+
226
+ @app.route('/game/<game_id>/guess', methods=['POST'])
227
+ def guess(game_id):
228
+ game = games.get(game_id)
229
+ if not game:
230
+ return jsonify({'error': 'Game not found'}), 404
231
+ if game['game_over']:
232
+ return jsonify({'error': 'Game is over'}), 400
233
+
234
+ data = request.json
235
+ guess_lat = data.get('lat')
236
+ guess_lng = data.get('lng')
237
+
238
+ if guess_lat is None or guess_lng is None:
239
+ return jsonify({'error': 'Missing lat/lng for guess'}), 400
240
+
241
+ guess_location = {'lat': guess_lat, 'lng': guess_lng}
242
+ game['guesses'].append(guess_location)
243
+
244
+ # Calculate score (simple distance for now)
245
+ # This is a placeholder for a more complex scoring function.
246
+ from math import radians, cos, sin, asin, sqrt
247
+ def haversine(lat1, lon1, lat2, lon2):
248
+ # convert decimal degrees to radians
249
+ lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])
250
+ # haversine formula
251
+ dlon = lon2 - lon1
252
+ dlat = lat2 - lat1
253
+ a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
254
+ c = 2 * asin(sqrt(a))
255
+ r = 6371 # Radius of earth in kilometers.
256
+ return c * r
257
+
258
+ distance = haversine(
259
+ game['start_location']['lat'], game['start_location']['lng'],
260
+ guess_lat, guess_lng
261
+ )
262
+
263
+ # Simple scoring
264
+ max_score = 5000
265
+ score = max(0, max_score - distance) # The closer, the higher the score
266
+
267
+ game['actions'].append({
268
+ 'type': 'guess',
269
+ 'location': guess_location,
270
+ 'result': {
271
+ 'distance_km': distance,
272
+ 'score': score
273
+ }
274
+ })
275
+ game['game_over'] = True # For now, one guess ends the game.
276
+
277
+ return jsonify({
278
+ 'message': 'Guess received',
279
+ 'guess_location': guess_location,
280
+ 'actual_location': game['start_location'],
281
+ 'distance_km': distance,
282
+ 'score': score
283
+ })
284
+
285
+ # Load zones at startup
286
+ load_zones_from_file()
287
+
288
+ if __name__ == '__main__':
289
+ app.run(debug=True)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Flask
2
+ python-dotenv
3
+ requests
4
+ gunicorn
5
+ mcp[cli]
zones.json ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }