change that GameplayDatabase can have different backends, sqlite and postgresql with a Template example backend
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
class GameplayBackendTemplate:
|
||||
"""Abstract base for gameplay database backends.
|
||||
|
||||
Subclasses must override every method that raises NotImplementedError.
|
||||
Shared pure-Python helpers (_utc_now, _to_json, etc.) live here so they
|
||||
are available to both SQLite and PostgreSQL implementations.
|
||||
"""
|
||||
|
||||
# ── public async interface ─────────────────────────────────────────────────
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""Called once on server startup. Backends that need eager connection
|
||||
(pool creation, schema init, migration) should override this."""
|
||||
return None
|
||||
|
||||
async def record_game_start(self, game_state:dict, snake_type:str|None=None, snake_version:str|None=None) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def record_turn(self, game_state:dict, my_move:str|None, my_thinking:dict|None=None) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def record_game_end(self, game_state:dict) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def get_summary(self, recent_limit:int=15) -> dict:
|
||||
raise NotImplementedError
|
||||
|
||||
async def list_games(self, limit:int=50) -> list[dict]:
|
||||
raise NotImplementedError
|
||||
|
||||
async def finalize_stale_running_games(self, stale_after_seconds:int=600) -> int:
|
||||
raise NotImplementedError
|
||||
|
||||
async def get_game_replay(self, game_id:str) -> dict|None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def close(self) -> None:
|
||||
return None
|
||||
|
||||
# ── shared pure-python helpers ─────────────────────────────────────────────
|
||||
|
||||
def _utc_now(self) -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
def _parse_utc_timestamp(self, value:str|None) -> datetime|None:
|
||||
if not value:
|
||||
return None
|
||||
normalized = value.strip()
|
||||
if normalized.endswith("Z"):
|
||||
normalized = normalized[:-1] + "+00:00"
|
||||
try:
|
||||
parsed = datetime.fromisoformat(normalized)
|
||||
except ValueError:
|
||||
return None
|
||||
if parsed.tzinfo is None:
|
||||
return parsed.replace(tzinfo=timezone.utc)
|
||||
return parsed.astimezone(timezone.utc)
|
||||
|
||||
def _to_json(self, payload:object) -> str:
|
||||
return json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
|
||||
|
||||
def _from_json(self, payload:str|None) -> Any:
|
||||
if payload is None or payload == "":
|
||||
return None
|
||||
try:
|
||||
return json.loads(payload)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return None
|
||||
|
||||
def _extract_snakes(self, game_state:dict) -> list[dict]:
|
||||
return list(game_state.get("board", {}).get("snakes", []))
|
||||
|
||||
def _extract_you(self, game_state:dict) -> dict:
|
||||
return dict(game_state.get("you", {}))
|
||||
|
||||
def _infer_direction(self, old_head:tuple[int, int]|None, new_head:tuple[int, int]|None) -> str|None:
|
||||
if old_head is None or new_head is None:
|
||||
return None
|
||||
|
||||
dx = new_head[0] - old_head[0]
|
||||
dy = new_head[1] - old_head[1]
|
||||
if dx == 1 and dy == 0:
|
||||
return "right"
|
||||
if dx == -1 and dy == 0:
|
||||
return "left"
|
||||
if dx == 0 and dy == 1:
|
||||
return "up"
|
||||
if dx == 0 and dy == -1:
|
||||
return "down"
|
||||
return None
|
||||
|
||||
def _derive_game_type(self, board:dict, ruleset:dict) -> str:
|
||||
if len(board.get("snakes", [])) == 2:
|
||||
return "duel"
|
||||
|
||||
return ruleset.get("name") or "standard"
|
||||
Reference in New Issue
Block a user