create MemoryGameBoardStore Class and rework Building of Game Board Storage

This commit is contained in:
2026-04-04 12:34:00 +02:00
parent 4547e3443b
commit a1c4a4b68d
5 changed files with 74 additions and 34 deletions
+9 -8
View File
@@ -1,6 +1,6 @@
from server.Files import read_file
from server.game_board_stats import GameBoardStoreBuilder
from server.GameBoard import GameBoard
from server.GameStateStore import GameStateStore
from snakes import SnakeBuilder
from quart_common.web.logger import await_log
from quart_common.web.logger import build_logger
@@ -31,7 +31,7 @@ class Server:
self.check_tls_security = check_tls_security
self.store_game_state = False
self.game_state_store = GameStateStore(
self.game_state_store = GameBoardStoreBuilder.build(
backend=game_state_backend,
redis_url=game_state_redis_url,
ttl_seconds=game_state_ttl_sec,
@@ -104,7 +104,7 @@ class Server:
self._record_http_request('move')
game_state = await request.get_json()
move_started = time.perf_counter()
game_board = await self._get_game_board(game_state)
game_board = cast(GameBoard, await self._get_game_board(game_state))
next_move = game_board.snake_neat_make_a_move()
await self._persist_game_board(game_state['game']['id'], game_board)
elapsed_ms = (time.perf_counter() - move_started) * 1000.0
@@ -133,7 +133,7 @@ class Server:
self._prune_stale_games()
game_state = await request.get_json()
if self.store_game_state:
game_board = await self._get_game_board(game_state, end=True)
game_board = cast(GameBoard, await self._get_game_board(game_state, end=True))
if self.check_tls_security:
await game_board.save(
StorageLoader.build(self.storage_type),
@@ -228,7 +228,7 @@ class Server:
except ValueError:
return 180
async def _create_game_board(self, game_state: dict):
async def _create_game_board(self, game_state:dict) -> GameBoard:
game_id = game_state['game']['id']
new_game_board = GameBoard(
game_id=game_id,
@@ -257,21 +257,22 @@ class Server:
self.running_games[game_id] = game_board
await self.game_state_store.save(game_id, game_board)
async def _delete_game_board(self, game_state: dict):
async def _delete_game_board(self, game_state:dict):
game_id = game_state['game']['id']
self.running_games.pop(game_id, None)
self.game_move_counts.pop(game_id, None)
self.game_last_seen_unix.pop(game_id, None)
await self.game_state_store.delete(game_id)
async def _get_game_board(self, game_state:dict, end:bool=False):
async def _get_game_board(self, game_state:dict, end:bool=False) -> GameBoard:
game_id = game_state['game']['id']
game_board:GameBoard
try:
game_board = self.running_games[game_id]
except KeyError:
persisted_board = await self.game_state_store.load(game_id)
if persisted_board is not None:
game_board = persisted_board
game_board = cast(GameBoard, persisted_board)
self.running_games[game_id] = game_board
else:
game_board = await self._create_game_board(game_state)
@@ -0,0 +1,17 @@
from server.GameBoard import GameBoard
class MemoryGameBoardStore:
def __init__(self, **kwargs):
self._state:dict[str, object] = {}
async def save(self, game_id:str, game_board:GameBoard) -> None:
self._state[game_id] = game_board
async def load(self, game_id:str):
return self._state.get(game_id)
async def delete(self, game_id:str) -> None:
self._state.pop(game_id, None)
async def close(self) -> None:
return None
@@ -1,8 +1,8 @@
from server.GameBoard import GameBoard
import pickle
class GameStateStore:
def __init__(self, backend:str="memory", redis_url:str="redis://localhost:6379/0", key_prefix:str="snake:gameboard", ttl_seconds:int=900,):
self.backend = (backend or "memory").strip().lower()
class RedisGameBoardStore:
def __init__(self, redis_url:str="redis://localhost:6379/0", key_prefix:str="snake:gameboard", ttl_seconds:int=900, **kwargs):
self.redis_url = redis_url
self.key_prefix = key_prefix
self.ttl_seconds = max(60, int(ttl_seconds))
@@ -13,28 +13,22 @@ class GameStateStore:
return self._redis
try:
from redis.asyncio import from_url # type: ignore[import-not-found]
import aioredis # type: ignore[import-not-found]
except ImportError as error: # pragma: no cover
raise RuntimeError("Redis backend selected but 'redis' package is not installed") from error
raise RuntimeError("Redis backend selected but 'aioredis' package is not installed") from error
self._redis = from_url(self.redis_url)
self._redis = aioredis.from_url(self.redis_url)
return self._redis
def _key(self, game_id:str) -> str:
return f"{self.key_prefix}:{game_id}"
async def save(self, game_id:str, game_board) -> None:
if self.backend != "redis":
return
async def save(self, game_id:str, game_board:GameBoard) -> None:
redis = await self._get_redis()
payload = pickle.dumps(game_board, protocol=pickle.HIGHEST_PROTOCOL)
await redis.set(self._key(game_id), payload, ex=self.ttl_seconds)
async def load(self, game_id:str):
if self.backend != "redis":
return None
redis = await self._get_redis()
payload = await redis.get(self._key(game_id))
if payload is None:
@@ -42,14 +36,18 @@ class GameStateStore:
return pickle.loads(payload)
async def delete(self, game_id:str) -> None:
if self.backend != "redis":
return
redis = await self._get_redis()
await redis.delete(self._key(game_id))
async def close(self) -> None:
if self._redis is None:
return
await self._redis.aclose()
if hasattr(self._redis, "aclose"):
await self._redis.aclose()
elif hasattr(self._redis, "close"):
close_result = self._redis.close()
if close_result is not None:
await close_result
self._redis = None
+10
View File
@@ -0,0 +1,10 @@
from server.game_board_stats.MemoryGameBoardStore import MemoryGameBoardStore
from server.game_board_stats.RedisGameBoardStore import RedisGameBoardStore
class GameBoardStoreBuilder:
@classmethod
def build(self, backend:str="memory", **kwargs) -> MemoryGameBoardStore|RedisGameBoardStore:
selected = (backend or "memory").strip().lower()
if selected == "redis":
return RedisGameBoardStore(**kwargs)
return MemoryGameBoardStore(**kwargs)