244 lines
8.4 KiB
Python
244 lines
8.4 KiB
Python
# main.py : point d'entree FastAPI (routes + websocket + lancement game loop)
|
|
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from pydantic import BaseModel
|
|
|
|
from game_loop import GameLoop
|
|
from lobby import Lobby, LobbyManager
|
|
from stats import get_leaderboard, init_db, is_discord_taken, save_result
|
|
from websocket import ConnectionManager
|
|
|
|
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
|
|
logging.basicConfig(
|
|
level=getattr(logging, LOG_LEVEL, logging.INFO),
|
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
await init_db()
|
|
logger.info("Base de données initialisée")
|
|
yield
|
|
|
|
|
|
app = FastAPI(title="SOULGATE", lifespan=lifespan)
|
|
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
|
|
|
|
manager = ConnectionManager()
|
|
lobby_manager = LobbyManager()
|
|
active_games: dict[str, tuple[GameLoop, asyncio.Task]] = {}
|
|
|
|
logger.info("Serveur SOULGATE démarré")
|
|
|
|
|
|
# --- routes leaderboard ---
|
|
|
|
class SubmitPayload(BaseModel):
|
|
team_name: str
|
|
score: int
|
|
time_seconds: int
|
|
waves_completed: int
|
|
victory: bool
|
|
lobby_code: str = ""
|
|
submitter_id: str = ""
|
|
p1_username: str = ""
|
|
p1_class: str = ""
|
|
p1_discord: str = ""
|
|
p2_username: str = ""
|
|
p2_class: str = ""
|
|
p2_discord: str = ""
|
|
p3_username: str = ""
|
|
p3_class: str = ""
|
|
p3_discord: str = ""
|
|
|
|
|
|
# lobbies dont le score a deja ete soumis : empeche les doublons
|
|
_submitted_lobbies: set[str] = set()
|
|
|
|
|
|
@app.get("/leaderboard")
|
|
async def route_leaderboard() -> list[dict]:
|
|
return await get_leaderboard()
|
|
|
|
|
|
@app.get("/leaderboard/check-discord/{tag}")
|
|
async def route_check_discord(tag: str) -> dict:
|
|
taken = await is_discord_taken(tag)
|
|
return {"taken": taken}
|
|
|
|
|
|
@app.post("/leaderboard/submit")
|
|
async def route_submit(payload: SubmitPayload) -> dict:
|
|
if not payload.team_name.strip():
|
|
raise HTTPException(400, "Le nom d'équipe est obligatoire.")
|
|
|
|
# une defaite n'a rien a faire dans le leaderboard
|
|
if not payload.victory:
|
|
raise HTTPException(403, "Seules les victoires peuvent être enregistrées.")
|
|
|
|
# seul le host peut soumettre, et 1 seule fois par lobby
|
|
code = payload.lobby_code.upper().strip()
|
|
submitter = payload.submitter_id.strip()
|
|
if not code or not submitter:
|
|
raise HTTPException(400, "Identifiants de session manquants.")
|
|
if code in _submitted_lobbies:
|
|
raise HTTPException(409, "Le score de cette partie a déjà été enregistré.")
|
|
lobby = lobby_manager.lobbies.get(code)
|
|
if not lobby:
|
|
raise HTTPException(404, "Lobby introuvable ou déjà nettoyé.")
|
|
player = lobby.get_player(submitter)
|
|
if not player or not player.is_host:
|
|
raise HTTPException(403, "Seul le chef d'équipe peut enregistrer le score.")
|
|
|
|
# pas de doublon de discord
|
|
|
|
for tag in (payload.p1_discord, payload.p2_discord, payload.p3_discord):
|
|
if tag and await is_discord_taken(tag):
|
|
raise HTTPException(409, f"Le Discord '{tag}' est déjà dans le leaderboard.")
|
|
|
|
rank = await save_result(payload.model_dump(exclude={"lobby_code", "submitter_id"}))
|
|
_submitted_lobbies.add(code)
|
|
return {"rank": rank}
|
|
|
|
|
|
@app.get("/health")
|
|
async def health() -> dict:
|
|
return {"status": "ok"}
|
|
|
|
|
|
@app.websocket("/ws")
|
|
async def websocket_endpoint(websocket: WebSocket) -> None:
|
|
conn_id = await manager.connect(websocket)
|
|
try:
|
|
while True:
|
|
try:
|
|
data = await websocket.receive_json()
|
|
except WebSocketDisconnect:
|
|
raise
|
|
except Exception:
|
|
try:
|
|
await manager.send(conn_id, {"type": "error", "code": "invalid_json", "message": "Message JSON invalide."})
|
|
except Exception:
|
|
pass
|
|
continue
|
|
await _handle_message(conn_id, data.get("type", ""), data)
|
|
except WebSocketDisconnect:
|
|
messages = lobby_manager.disconnect(conn_id)
|
|
await manager.dispatch(messages)
|
|
manager.disconnect(conn_id)
|
|
|
|
|
|
async def _handle_message(conn_id: str, msg_type: str, data: dict) -> None:
|
|
username = manager.get_username(conn_id)
|
|
|
|
if msg_type == "set_username":
|
|
uname = str(data.get("username", "")).strip()
|
|
if not uname:
|
|
await manager.send(conn_id, {"type": "error", "code": "invalid_username", "message": "Le pseudo ne peut pas être vide."})
|
|
return
|
|
manager.set_username(conn_id, uname)
|
|
await manager.send(conn_id, {"type": "username_set", "username": uname, "my_id": conn_id})
|
|
|
|
elif msg_type in ("create_lobby", "join_lobby"):
|
|
if not username:
|
|
await manager.send(conn_id, {"type": "error", "code": "no_username", "message": "Définis ton pseudo d'abord (set_username)."})
|
|
return
|
|
if msg_type == "create_lobby":
|
|
messages = lobby_manager.create_lobby(conn_id, username)
|
|
else:
|
|
messages = lobby_manager.join_lobby(conn_id, username, data.get("code", ""))
|
|
await manager.dispatch(messages)
|
|
|
|
elif msg_type == "leave_lobby":
|
|
await manager.dispatch(lobby_manager.leave_lobby(conn_id))
|
|
|
|
elif msg_type == "select_class":
|
|
await manager.dispatch(lobby_manager.select_class(conn_id, data.get("class", "")))
|
|
|
|
elif msg_type == "ready":
|
|
await manager.dispatch(lobby_manager.set_ready(conn_id))
|
|
|
|
elif msg_type == "start_game":
|
|
messages = lobby_manager.start_game(conn_id)
|
|
await manager.dispatch(messages)
|
|
if messages and messages[0][1].get("type") == "game_starting":
|
|
code = lobby_manager.conn_to_lobby.get(conn_id)
|
|
if code and code in lobby_manager.lobbies:
|
|
await _launch_game(lobby_manager.lobbies[code])
|
|
|
|
elif msg_type == "input":
|
|
code = lobby_manager.conn_to_lobby.get(conn_id)
|
|
if code and code in active_games:
|
|
game, _ = active_games[code]
|
|
try:
|
|
dx = float(data.get("dx", 0))
|
|
dy = float(data.get("dy", 0))
|
|
except (ValueError, TypeError):
|
|
return
|
|
game.handle_input(conn_id, dx, dy)
|
|
|
|
elif msg_type == "attack":
|
|
code = lobby_manager.conn_to_lobby.get(conn_id)
|
|
if code and code in active_games:
|
|
game, _ = active_games[code]
|
|
try:
|
|
tx = float(data.get("tx", 0))
|
|
ty = float(data.get("ty", 0))
|
|
except (ValueError, TypeError):
|
|
return
|
|
game.handle_attack(conn_id, tx, ty)
|
|
|
|
elif msg_type == "ability":
|
|
code = lobby_manager.conn_to_lobby.get(conn_id)
|
|
if code and code in active_games:
|
|
game, _ = active_games[code]
|
|
try:
|
|
ability_id = int(data.get("id", 0))
|
|
tx = float(data.get("tx", 0))
|
|
ty = float(data.get("ty", 0))
|
|
except (ValueError, TypeError):
|
|
return
|
|
if ability_id in (1, 2, 3):
|
|
game.handle_ability(conn_id, ability_id, tx, ty)
|
|
|
|
elif msg_type == "player_upgrade":
|
|
code = lobby_manager.conn_to_lobby.get(conn_id)
|
|
if code and code in active_games:
|
|
game, _ = active_games[code]
|
|
game.handle_upgrade(conn_id, data.get("upgrade_id", ""))
|
|
|
|
elif msg_type == "displacement":
|
|
code = lobby_manager.conn_to_lobby.get(conn_id)
|
|
if code and code in active_games:
|
|
game, _ = active_games[code]
|
|
try:
|
|
tx = float(data.get("tx", 0))
|
|
ty = float(data.get("ty", 0))
|
|
except (ValueError, TypeError):
|
|
return
|
|
game.handle_displacement(conn_id, tx, ty)
|
|
|
|
|
|
else:
|
|
logger.warning("Message inconnu de %s : %s", conn_id, msg_type)
|
|
await manager.send(conn_id, {"type": "error", "code": "unknown_message", "message": f"Type de message inconnu : {msg_type}"})
|
|
|
|
|
|
async def _launch_game(lobby: Lobby) -> None:
|
|
async def broadcast(msg: dict) -> None:
|
|
for conn_id in lobby.get_conn_ids():
|
|
await manager.send(conn_id, msg)
|
|
|
|
game = GameLoop(lobby, broadcast)
|
|
task = asyncio.create_task(game.run())
|
|
active_games[lobby.code] = (game, task)
|
|
logger.info("Game loop lancée — lobby %s", lobby.code)
|