Geogussr / app.py
Jofthomas's picture
Upload 4 files
8398097 verified
raw
history blame
9.94 kB
import os
import random
import json
import uuid
import requests
import base64
from flask import Flask, jsonify, render_template, request
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24)
# In-memory "database"
ZONES_FILE = 'zones.json'
games = {}
zones = {
"easy": [],
"medium": [],
"hard": []
}
# --- Zone Persistence Functions ---
def save_zones_to_file():
with open(ZONES_FILE, 'w') as f:
json.dump(zones, f, indent=4)
def load_zones_from_file():
global zones
if os.path.exists(ZONES_FILE):
try:
with open(ZONES_FILE, 'r') as f:
loaded_zones = json.load(f)
# Basic format validation
if not (isinstance(loaded_zones, dict) and all(k in loaded_zones for k in ["easy", "medium", "hard"])):
raise ValueError("Invalid format")
migrated = False
for difficulty in loaded_zones:
for zone in loaded_zones[difficulty]:
# Assign ID if missing
if 'id' not in zone:
zone['id'] = uuid.uuid4().hex
migrated = True
zones = loaded_zones
print(zones)
if migrated:
print("Info: Migrated old zone data by adding unique IDs.")
save_zones_to_file()
except (json.JSONDecodeError, IOError, ValueError):
print(f"Warning: '{ZONES_FILE}' is corrupted or invalid. Recreating with empty zones.")
save_zones_to_file() # This creates a fresh, empty, and valid file
else:
# If file doesn't exist, create an empty one.
save_zones_to_file()
# Predefined locations for the game, used as a fallback
LOCATIONS = [
{'lat': 48.85824, 'lng': 2.2945}, # Eiffel Tower, Paris
{'lat': 40.748440, 'lng': -73.985664}, # Empire State Building, New York
{'lat': 35.689487, 'lng': 139.691711}, # Tokyo, Japan
{'lat': -33.856784, 'lng': 151.215297} # Sydney Opera House, Australia
]
def generate_game_id():
return ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=10))
@app.route('/')
def index():
google_maps_api_key = os.getenv('GOOGLE_MAPS_API_KEY')
if not google_maps_api_key:
return "Error: GOOGLE_MAPS_API_KEY not set. Please set it in a .env file.", 500
return render_template('index.html', google_maps_api_key=google_maps_api_key)
@app.route('/admin')
def admin():
google_maps_api_key = os.getenv('GOOGLE_MAPS_API_KEY')
if not google_maps_api_key:
return "Error: GOOGLE_MAPS_API_KEY not set. Please set it in a .env file.", 500
return render_template('admin.html', google_maps_api_key=google_maps_api_key)
@app.route('/api/zones', methods=['GET', 'POST', 'DELETE'])
def handle_zones():
if request.method == 'POST':
data = request.json
difficulty = data.get('difficulty')
zone_data = data.get('zone')
if difficulty and zone_data and difficulty in zones:
zone_data['id'] = uuid.uuid4().hex
zones[difficulty].append(zone_data)
save_zones_to_file()
return jsonify({'message': 'Zone saved successfully'}), 201
return jsonify({'error': 'Invalid data'}), 400
if request.method == 'DELETE':
data = request.json
zone_id = data.get('zone_id')
if not zone_id:
return jsonify({'error': 'Zone ID is required'}), 400
for difficulty in zones:
zones[difficulty] = [z for z in zones[difficulty] if z.get('id') != zone_id]
save_zones_to_file()
return jsonify({'message': 'Zone deleted successfully'})
# GET request
return jsonify(zones)
@app.route('/start_game', methods=['POST'])
def start_game():
data = request.json or {}
difficulty = data.get('difficulty', 'easy')
start_location = None
if difficulty in zones and zones[difficulty]:
selected_zone_list = zones[difficulty]
print("selected_zone_list", selected_zone_list)
selected_zone = random.choice(selected_zone_list)
print("selected_zone", selected_zone)
if selected_zone['type'] == 'rectangle':
bounds = selected_zone['bounds']
north, south, east, west = bounds['north'], bounds['south'], bounds['east'], bounds['west']
# Handle antimeridian crossing
if west > east:
east += 360
rand_lng = random.uniform(west, east)
if rand_lng > 180:
rand_lng -= 360
rand_lat = random.uniform(south, north)
start_location = {'lat': rand_lat, 'lng': rand_lng}
# Fallback to predefined locations if no zones are defined
if not start_location:
start_location = random.choice(LOCATIONS)
game_id = generate_game_id()
games[game_id] = {
'start_location': start_location,
'current_location': start_location,
'guesses': [],
'moves': 0,
'actions': [],
'game_over': False
}
google_maps_api_key = os.getenv('GOOGLE_MAPS_API_KEY')
# Fetch Street View image
streetview_image = None
compass_heading = random.randint(0, 359) # Random compass direction
if google_maps_api_key:
try:
lat, lng = start_location['lat'], start_location['lng']
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}"
response = requests.get(streetview_url)
if response.status_code == 200:
# Convert image to base64
streetview_image = base64.b64encode(response.content).decode('utf-8')
except Exception as e:
print(f"Error fetching Street View image: {e}")
return jsonify({
'game_id': game_id,
'streetview_image': streetview_image,
'compass_heading': compass_heading
})
@app.route('/game/<game_id>/state', methods=['GET'])
def get_game_state(game_id):
game = games.get(game_id)
if not game:
return jsonify({'error': 'Game not found'}), 404
return jsonify(game)
@app.route('/game/<game_id>/move', methods=['POST'])
def move(game_id):
game = games.get(game_id)
if not game:
return jsonify({'error': 'Game not found'}), 404
if game['game_over']:
return jsonify({'error': 'Game is over'}), 400
data = request.json
new_lat = data.get('lat')
new_lng = data.get('lng')
if new_lat is None or new_lng is None:
return jsonify({'error': 'Missing lat/lng for move'}), 400
game['current_location'] = {'lat': new_lat, 'lng': new_lng}
game['moves'] += 1
game['actions'].append({'type': 'move', 'location': {'lat': new_lat, 'lng': new_lng}})
# Fetch Street View image for the new location
google_maps_api_key = os.getenv('GOOGLE_MAPS_API_KEY')
streetview_image = None
compass_heading = random.randint(0, 359) # Random compass direction
if google_maps_api_key:
try:
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}"
response = requests.get(streetview_url)
if response.status_code == 200:
# Convert image to base64
streetview_image = base64.b64encode(response.content).decode('utf-8')
except Exception as e:
print(f"Error fetching Street View image: {e}")
return jsonify({
'message': 'Move successful',
'streetview_image': streetview_image,
'compass_heading': compass_heading
})
@app.route('/game/<game_id>/guess', methods=['POST'])
def guess(game_id):
game = games.get(game_id)
if not game:
return jsonify({'error': 'Game not found'}), 404
if game['game_over']:
return jsonify({'error': 'Game is over'}), 400
data = request.json
guess_lat = data.get('lat')
guess_lng = data.get('lng')
if guess_lat is None or guess_lng is None:
return jsonify({'error': 'Missing lat/lng for guess'}), 400
guess_location = {'lat': guess_lat, 'lng': guess_lng}
game['guesses'].append(guess_location)
# Calculate score (simple distance for now)
# This is a placeholder for a more complex scoring function.
from math import radians, cos, sin, asin, sqrt
def haversine(lat1, lon1, lat2, lon2):
# convert decimal degrees to radians
lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])
# haversine formula
dlon = lon2 - lon1
dlat = lat2 - lat1
a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
c = 2 * asin(sqrt(a))
r = 6371 # Radius of earth in kilometers.
return c * r
distance = haversine(
game['start_location']['lat'], game['start_location']['lng'],
guess_lat, guess_lng
)
# Simple scoring
max_score = 5000
score = max(0, max_score - distance) # The closer, the higher the score
game['actions'].append({
'type': 'guess',
'location': guess_location,
'result': {
'distance_km': distance,
'score': score
}
})
game['game_over'] = True # For now, one guess ends the game.
return jsonify({
'message': 'Guess received',
'guess_location': guess_location,
'actual_location': game['start_location'],
'distance_km': distance,
'score': score
})
# Load zones at startup
load_zones_from_file()
if __name__ == '__main__':
app.run(debug=True)