Added Spoutnik

This commit is contained in:
Simon Junod
2025-08-05 21:22:51 +02:00
parent 85914c22ac
commit e5e6eb0c53
17 changed files with 707 additions and 43 deletions

116
spoutnik.py Normal file
View File

@@ -0,0 +1,116 @@
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")