Add PostgreSQL game logging system

- Add GameLogger service for move logging to PostgreSQL
- Add moves table to event_store.py for AI decision analysis
- Update main.py to initialize GameLogger in lifespan
- Update game_analyzer.py to query PostgreSQL instead of SQLite
- Add VDD documentation V2_08_GAME_LOGGING.md

Replaces SQLite game_log.py with unified PostgreSQL backend.
See docs/v2/V2_08_GAME_LOGGING.md for architecture and API.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-02-14 08:42:49 -05:00
parent 7d28e83a49
commit 49b2490c25
5 changed files with 1003 additions and 66 deletions

View File

@@ -16,7 +16,7 @@ from config import config
from room import RoomManager, Room
from game import GamePhase, GameOptions
from ai import GolfAI, process_cpu_turn, get_all_profiles, reset_all_profiles, cleanup_room_profiles
from game_log import get_logger
from services.game_logger import GameLogger, get_logger, set_logger
# Import production components
from logging_config import setup_logging
@@ -142,6 +142,12 @@ async def lifespan(app: FastAPI):
set_stats_auth_service(_auth_service)
logger.info("Stats services initialized successfully")
# Initialize game logger (uses event_store for move logging)
logger.info("Initializing game logger...")
_game_logger = GameLogger(_event_store)
set_logger(_game_logger)
logger.info("Game logger initialized with PostgreSQL backend")
# Initialize replay service
logger.info("Initializing replay services...")
from services.replay_service import get_replay_service, set_replay_service
@@ -343,9 +349,8 @@ app.add_middleware(LazyRateLimitMiddleware)
room_manager = RoomManager()
# Initialize game logger database at startup
_game_logger = get_logger()
logger.info(f"Game analytics database initialized at: {_game_logger.db_path}")
# Game logger is initialized in lifespan after event_store is available
# The get_logger() function returns None until set_logger() is called
# =============================================================================
@@ -692,11 +697,12 @@ async def websocket_endpoint(websocket: WebSocket):
# Log game start for AI analysis
game_logger = get_logger()
current_room.game_log_id = game_logger.log_game_start(
room_code=current_room.code,
num_players=len(current_room.players),
options=options,
)
if game_logger:
current_room.game_log_id = game_logger.log_game_start(
room_code=current_room.code,
num_players=len(current_room.players),
options=options,
)
# CPU players do their initial flips immediately (if required)
if options.initial_flips > 0:
@@ -740,8 +746,8 @@ async def websocket_endpoint(websocket: WebSocket):
if card:
# Log draw decision for human player
if current_room.game_log_id:
game_logger = get_logger()
game_logger = get_logger()
if game_logger and current_room.game_log_id:
player = current_room.game.get_player(player_id)
if player:
reason = f"took {discard_before_draw.rank.value} from discard" if source == "discard" else "drew from deck"
@@ -779,8 +785,8 @@ async def websocket_endpoint(websocket: WebSocket):
if discarded:
# Log swap decision for human player
if current_room.game_log_id and drawn_card and player:
game_logger = get_logger()
game_logger = get_logger()
if game_logger and current_room.game_log_id and drawn_card and player:
old_rank = old_card.rank.value if old_card else "?"
game_logger.log_move(
game_id=current_room.game_log_id,
@@ -810,8 +816,8 @@ async def websocket_endpoint(websocket: WebSocket):
if current_room.game.discard_drawn(player_id):
# Log discard decision for human player
if current_room.game_log_id and drawn_card and player:
game_logger = get_logger()
game_logger = get_logger()
if game_logger and current_room.game_log_id and drawn_card and player:
game_logger.log_move(
game_id=current_room.game_log_id,
player=player,
@@ -864,8 +870,8 @@ async def websocket_endpoint(websocket: WebSocket):
current_room.game.flip_and_end_turn(player_id, position)
# Log flip decision for human player
if current_room.game_log_id and player and 0 <= position < len(player.cards):
game_logger = get_logger()
game_logger = get_logger()
if game_logger and current_room.game_log_id and player and 0 <= position < len(player.cards):
flipped_card = player.cards[position]
game_logger.log_move(
game_id=current_room.game_log_id,
@@ -889,8 +895,8 @@ async def websocket_endpoint(websocket: WebSocket):
player = current_room.game.get_player(player_id)
if current_room.game.skip_flip_and_end_turn(player_id):
# Log skip flip decision for human player
if current_room.game_log_id and player:
game_logger = get_logger()
game_logger = get_logger()
if game_logger and current_room.game_log_id and player:
game_logger.log_move(
game_id=current_room.game_log_id,
player=player,
@@ -913,8 +919,8 @@ async def websocket_endpoint(websocket: WebSocket):
player = current_room.game.get_player(player_id)
if current_room.game.flip_card_as_action(player_id, position):
# Log flip-as-action for human player
if current_room.game_log_id and player and 0 <= position < len(player.cards):
game_logger = get_logger()
game_logger = get_logger()
if game_logger and current_room.game_log_id and player and 0 <= position < len(player.cards):
flipped_card = player.cards[position]
game_logger.log_move(
game_id=current_room.game_log_id,
@@ -938,8 +944,8 @@ async def websocket_endpoint(websocket: WebSocket):
player = current_room.game.get_player(player_id)
if current_room.game.knock_early(player_id):
# Log knock early for human player
if current_room.game_log_id and player:
game_logger = get_logger()
game_logger = get_logger()
if game_logger and current_room.game_log_id and player:
face_down_count = sum(1 for c in player.cards if not c.face_up)
game_logger.log_move(
game_id=current_room.game_log_id,
@@ -1098,8 +1104,8 @@ async def broadcast_game_state(room: Room):
# Check for game over
elif room.game.phase == GamePhase.GAME_OVER:
# Log game end
if room.game_log_id:
game_logger = get_logger()
game_logger = get_logger()
if game_logger and room.game_log_id:
game_logger.log_game_end(room.game_log_id)
room.game_log_id = None # Clear to avoid duplicate logging