mantimon-tcg/backend/app/main.py
Cal Corum 0c810e5b30 Add Phase 4 WebSocket infrastructure (WS-001 through GS-001)
WebSocket Message Schemas (WS-002):
- Add Pydantic models for all client/server WebSocket messages
- Implement discriminated unions for message type parsing
- Include JoinGame, Action, Resign, Heartbeat client messages
- Include GameState, ActionResult, Error, TurnStart server messages

Connection Manager (WS-003):
- Add Redis-backed WebSocket connection tracking
- Implement user-to-sid mapping with TTL management
- Support game room association and opponent lookup
- Add heartbeat tracking for connection health

Socket.IO Authentication (WS-004):
- Add JWT-based authentication middleware
- Support token extraction from multiple formats
- Implement session setup with ConnectionManager integration
- Add require_auth helper for event handlers

Socket.IO Server Setup (WS-001):
- Configure AsyncServer with ASGI mode
- Register /game namespace with event handlers
- Integrate with FastAPI via ASGIApp wrapper
- Configure CORS from application settings

Game Service (GS-001):
- Add stateless GameService for game lifecycle orchestration
- Create engine per-operation using rules from GameState
- Implement action-based RNG seeding for deterministic replay
- Add rng_seed field to GameState for replay support

Architecture verified:
- Core module independence (no forbidden imports)
- Config from request pattern (rules in GameState)
- Dependency injection (constructor deps, method config)
- All 1090 tests passing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 22:21:20 -06:00

190 lines
5.3 KiB
Python

"""Mantimon TCG - FastAPI Application Entry Point.
This module configures and starts the FastAPI application with:
- Database initialization and cleanup
- Redis connection management
- Card service loading
- CORS middleware
- API routers
Usage:
uvicorn app.main:app --reload
"""
import logging
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.auth import router as auth_router
from app.api.collections import router as collections_router
from app.api.decks import router as decks_router
from app.api.users import router as users_router
from app.config import settings
from app.db import close_db, init_db
from app.db.redis import close_redis, init_redis
from app.services import get_card_service
from app.socketio import create_socketio_app
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
"""Application lifespan context manager.
Handles startup and shutdown events:
- Startup: Initialize DB, Redis, and load cards
- Shutdown: Close DB and Redis connections
Args:
app: The FastAPI application instance.
Yields:
None - Control returns to the application during its lifetime.
"""
# === STARTUP ===
logger.info("Starting Mantimon TCG server...")
# Initialize database connection pool
logger.info("Initializing database...")
await init_db()
logger.info("Database initialized")
# Initialize Redis connection pool
logger.info("Initializing Redis...")
await init_redis()
logger.info("Redis initialized")
# Load card definitions into memory
logger.info("Loading card definitions...")
card_service = get_card_service()
await card_service.load_all()
card_count = len(card_service.get_all_cards())
logger.info(f"Loaded {card_count} card definitions")
logger.info("Mantimon TCG server started successfully")
yield # Application runs here
# === SHUTDOWN ===
logger.info("Shutting down Mantimon TCG server...")
# Close Redis connections
logger.info("Closing Redis connections...")
await close_redis()
logger.info("Redis connections closed")
# Close database connections
logger.info("Closing database connections...")
await close_db()
logger.info("Database connections closed")
logger.info("Mantimon TCG server shutdown complete")
# Create FastAPI application with lifespan
app = FastAPI(
title="Mantimon TCG",
description="A home-rule-modified Pokemon Trading Card Game API",
version="0.1.0",
lifespan=lifespan,
docs_url="/docs" if settings.is_development else None,
redoc_url="/redoc" if settings.is_development else None,
)
# Configure CORS
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# === Health Check Endpoints ===
@app.get("/health")
async def health_check() -> dict[str, str]:
"""Basic health check endpoint.
Returns:
Health status indicating the server is running.
"""
return {"status": "healthy"}
@app.get("/health/ready")
async def readiness_check() -> dict[str, str | int]:
"""Readiness check with service status.
Verifies that all required services are available:
- Database connection
- Redis connection
- Card service loaded
Returns:
Detailed status of each service.
"""
from app.db import get_engine
from app.db.redis import get_pool
status: dict[str, str | int] = {"status": "ready"}
# Check database
try:
get_engine() # Raises RuntimeError if not initialized
status["database"] = "connected"
except RuntimeError:
status["database"] = "not initialized"
status["status"] = "not ready"
# Check Redis
try:
get_pool() # Raises RuntimeError if not initialized
status["redis"] = "connected"
except RuntimeError:
status["redis"] = "not initialized"
status["status"] = "not ready"
# Check card service
card_service = get_card_service()
card_count = len(card_service.get_all_cards())
if card_count > 0:
status["cards_loaded"] = card_count
else:
status["cards_loaded"] = 0
status["status"] = "not ready"
return status
# === API Routers ===
app.include_router(auth_router, prefix="/api")
app.include_router(users_router, prefix="/api")
app.include_router(collections_router, prefix="/api")
app.include_router(decks_router, prefix="/api")
# TODO: Add remaining routers in future phases
# from app.api import cards, games, campaign
# app.include_router(cards.router, prefix="/api/cards", tags=["cards"])
# app.include_router(games.router, prefix="/api/games", tags=["games"])
# app.include_router(campaign.router, prefix="/api/campaign", tags=["campaign"])
# === Socket.IO Integration ===
# Wrap FastAPI with Socket.IO ASGI app for WebSocket support.
# The combined app handles:
# - WebSocket connections at /socket.io/
# - HTTP requests passed through to FastAPI
#
# Note: The module-level 'app' variable is now the combined ASGI app.
# This is intentional - uvicorn imports 'app' and will serve both
# FastAPI HTTP routes and Socket.IO WebSocket connections.
_fastapi_app = app
app = create_socketio_app(_fastapi_app)