Files
cambot/spoutnik.py
2025-08-05 21:33:59 +02:00

116 lines
4.6 KiB
Python

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")