183 lines
5.8 KiB
Python
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()
|