Files
catan/services/game/runtime.py
dan 2499deb071
All checks were successful
ci / tests (push) Successful in 21s
Refresh web UI and make ML imports optional
2025-12-25 09:15:10 +03:00

183 lines
5.8 KiB
Python

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) >= 3:
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()