import random import re import requests from nextcord import File from unidecode import unidecode from collections import defaultdict from datetime import datetime from math import ceil from settings import * import geopandas as gpd from shapely.geometry import Point from geopy.distance import geodesic from math import sinh, atan, pi, log, tan, cos, ceil def tile_to_wgs84(x, y, z): return atan(sinh(pi * (1 - 2 * y / 2 ** z))) * 180 / pi, (x / 2 ** z - 0.5) * 360 def wgs84_to_tile(lat, lon, z): return int((lon + 180) / 360 * 2 ** z), int((1 - (log(tan(lat * pi / 180) + 1 / cos(lat * pi / 180))) / pi) / 2 * 2 ** z) def random_point(filename): shape = gpd.read_file(filename).union_all() min_x, min_y, max_x, max_y = shape.bounds while True: random_x = random.uniform(min_x, max_x) random_y = random.uniform(min_y, max_y) point = Point(random_x, random_y) if shape.contains(point): return point.coords[0][::-1] class Game: def __init__(self, channel): self.channel = channel self.target = None self.file = f"/tmp/spoutnik_{channel}.jpg" self.winner = None self.last_player = None self.last_datetime = None self.resetters = set() async def reset(self): output = "" if self.target and not self.winner: output = f"Le lieu précédent était : `{self.target}`\n\n" random_lat, random_lon = random_point(SPOUTNIK_FILE) tile_x, tile_y = wgs84_to_tile(random_lat, random_lon, SPOUTNIK_ZOOM) req = requests.get(f"https://khms2.google.com/kh/v=1000?x={tile_x}&y={tile_y}&z={SPOUTNIK_ZOOM}") with open(self.file, "wb") as f: f.write(req.content) self.target = tile_to_wgs84(tile_x + 0.5, tile_y + 0.5, SPOUTNIK_ZOOM) self.winner = None self.last_player = None self.last_datetime = None self.resetters = set() output += f"Il y a un nouveau lieu à trouver ! Donnez-moi les coordonnées GPS du centre de l'image, à {SPOUTNIK_MAX_DISTANCE} m près (sur Google Maps, clic droit puis clic sur les coordonnées)." await self.channel.send(output, file=File(self.file)) async def parse(self, message): guess = unidecode(message.content.strip()).upper() # if it's the special keyword 'reset', consider resetting if guess == "RESET" and SPOUTNIK_RESETTERS_NEEDED != 0: if message.author in self.resetters: return self.resetters.add(message.author) await self.channel.send(f"Réinitialisation : {len(self.resetters)}/{SPOUTNIK_RESETTERS_NEEDED}") if len(self.resetters) >= SPOUTNIK_RESETTERS_NEEDED: await self.reset() return # if somebody has already won, return silently if self.winner: return # same if the game was never initialized if not self.target: return # same if the message is not WGS 84 coordinates if not (regex := re.match(r"^(\d{1,2}[.,]\d+) *[ ,.;:] *(\d{1,3}[.,]\d+)$", guess)): return # check for errors and react accordingly error = False if SPOUTNIK_FORCE_ALTERNATION and message.author == self.last_player and (elapsed := (datetime.now() - self.last_datetime).total_seconds()) < SPOUTNIK_ALTERNATION_TIMEOUT: await message.add_reaction("\N{BUSTS IN SILHOUETTE}") error = True remaining = ceil(SPOUTNIK_ALTERNATION_TIMEOUT - elapsed) await self.channel.send(f"{message.author.mention} : tu pourras rejouer dans {remaining} seconde{'s' if remaining > 1 else ''}") if error: return # everything is OK, validate the proposal self.last_player = message.author self.last_datetime = datetime.now() lat = float(regex.group(1).replace(",", ".")) lon = float(regex.group(2).replace(",", ".")) distance = geodesic((lat, lon), self.target).meters blurred_distance = ceil(geodesic((lat, lon), self.target).meters / 1000) if distance <= SPOUTNIK_MAX_DISTANCE: self.winner = message.author self.resetters = set() output = f":trophy: YOUPI ! {message.author.mention} a trouvé le lieu ! :trophy:" output += f"\n\nLieu exact : https://www.google.ch/maps/@{self.target[0]},{self.target[1]},{SPOUTNIK_ZOOM}z" await self.channel.send(output) else: await self.channel.send(f"{message.author.mention} : c'est pas ça chef, mais tu es à moins de {blurred_distance} km de la cible")