from __future__ import annotations import datetime as dt import random import uuid from dataclasses import dataclass from typing import Dict, List, Optional from sqlmodel import select from catan.game import GameConfig from catan.sdk import Action, ActionType, CatanEnv from services.common.db import session_scope from services.game.models import Game, GameEvent, TradeOffer @dataclass class GameRuntime: game: Game env: Optional[CatanEnv] = None action_index: int = 0 def next_action_index(self) -> int: self.action_index += 1 return self.action_index class GameRuntimeManager: def __init__(self) -> None: self._cache: Dict[str, GameRuntime] = {} def get(self, game_id: str) -> GameRuntime: if game_id in self._cache: return self._cache[game_id] runtime = self._load_runtime(game_id) self._cache[game_id] = runtime return runtime def drop(self, game_id: str) -> None: self._cache.pop(game_id, None) def _load_runtime(self, game_id: str) -> GameRuntime: with session_scope() as session: game = session.get(Game, game_id) if not game: raise KeyError(game_id) slots = game.slots.get("slots", []) names = [slot.get("name") for slot in slots if slot.get("name")] colors = [slot.get("color", "player") for slot in slots if slot.get("name")] if game.status == "running" and len(names) >= 2: config = GameConfig(player_names=names, colors=colors, seed=game.seed) env = CatanEnv(config) events = session.exec( select(GameEvent).where(GameEvent.game_id == game_id, GameEvent.applied == True).order_by(GameEvent.idx) ).all() else: env = None events = [] if env is not None: for event in events: action = Action(ActionType(event.action_type), event.payload) env.step(action) action_index = events[-1].idx if events else 0 return GameRuntime(game=game, env=env, action_index=action_index) def create_game(self, name: str, max_players: int, created_by: str) -> Game: now = dt.datetime.now(dt.timezone.utc) game_id = str(uuid.uuid4()) seed = random.randint(0, 2**31 - 1) slots = { "slots": [ { "slot_id": idx + 1, "name": None, "user_id": None, "is_ai": False, "ai_kind": None, "ai_model": None, "ready": False, "color": None, } for idx in range(max_players) ] } game = Game( id=game_id, name=name, status="lobby", max_players=max_players, created_by=created_by, created_at=now, updated_at=now, seed=seed, slots=slots, ) with session_scope() as session: session.add(game) return game def save_game(self, game: Game) -> None: game.updated_at = dt.datetime.now(dt.timezone.utc) with session_scope() as session: session.merge(game) def list_games(self) -> List[Game]: with session_scope() as session: return session.exec(select(Game).order_by(Game.created_at.desc())).all() def record_event( self, game_id: str, actor: str, action: Action, applied: bool = True, debug: Optional[dict] = None, ) -> GameEvent: with session_scope() as session: last_idx = session.exec( select(GameEvent.idx) .where(GameEvent.game_id == game_id) .order_by(GameEvent.idx.desc()) .limit(1) ).first() idx = (last_idx or 0) + 1 event = GameEvent( game_id=game_id, idx=idx, ts=dt.datetime.now(dt.timezone.utc), actor=actor, action_type=action.type.value, payload=action.payload, applied=applied, debug_payload=debug, ) session.add(event) return event def list_events(self, game_id: str) -> List[GameEvent]: with session_scope() as session: return session.exec( select(GameEvent).where(GameEvent.game_id == game_id).order_by(GameEvent.idx) ).all() def create_trade_offer( self, game_id: str, from_player: str, to_player: Optional[str], offer: Dict[str, int], request: Dict[str, int], ) -> TradeOffer: now = dt.datetime.now(dt.timezone.utc) offer_id = str(uuid.uuid4()) trade = TradeOffer( id=offer_id, game_id=game_id, from_player=from_player, to_player=to_player, offer=offer, request=request, status="open", created_at=now, updated_at=now, ) with session_scope() as session: session.add(trade) return trade def update_trade_offer(self, trade: TradeOffer) -> None: trade.updated_at = dt.datetime.now(dt.timezone.utc) with session_scope() as session: session.merge(trade) def list_trade_offers(self, game_id: str, status: Optional[str] = None) -> List[TradeOffer]: with session_scope() as session: query = select(TradeOffer).where(TradeOffer.game_id == game_id) if status: query = query.where(TradeOffer.status == status) return session.exec(query.order_by(TradeOffer.created_at)).all() manager = GameRuntimeManager()