# Backend - Paper Dynasty Game Engine ## Overview FastAPI-based real-time game backend handling WebSocket communication, game state management, and database persistence for both SBA and PD leagues. ## Technology Stack - **Framework**: FastAPI (Python 3.13) - **WebSocket**: Socket.io (python-socketio) - **Database**: PostgreSQL 14+ with SQLAlchemy 2.0 (async) - **ORM**: SQLAlchemy with asyncpg driver - **Validation**: Pydantic v2 - **DateTime**: Pendulum 3.0 (replaces Python's datetime module) - **Testing**: pytest with pytest-asyncio - **Code Quality**: black, flake8, mypy ## Project Structure ``` backend/ ├── app/ │ ├── main.py # FastAPI app + Socket.io initialization │ ├── config.py # Settings with pydantic-settings │ │ │ ├── core/ # Game logic (Phase 2+) │ │ ├── game_engine.py # Main game simulation │ │ ├── state_manager.py # In-memory state │ │ ├── play_resolver.py # Play outcome resolution │ │ ├── dice.py # Secure random rolls │ │ └── validators.py # Rule validation │ │ │ ├── config/ # League configurations (Phase 2+) │ │ ├── base_config.py # Shared configuration │ │ ├── league_configs.py # SBA/PD specific │ │ └── result_charts.py # d20 outcome tables │ │ │ ├── models/ # Data models │ │ ├── db_models.py # SQLAlchemy ORM models │ │ ├── game_models.py # Pydantic game state models (Phase 2+) │ │ └── player_models.py # Polymorphic player models (Phase 2+) │ │ │ ├── websocket/ # WebSocket handling │ │ ├── connection_manager.py # Connection lifecycle │ │ ├── handlers.py # Event handlers │ │ └── events.py # Event definitions (Phase 2+) │ │ │ ├── api/ # REST API │ │ ├── routes/ │ │ │ ├── health.py # Health check endpoints │ │ │ ├── auth.py # Discord OAuth (Phase 1) │ │ │ └── games.py # Game CRUD (Phase 2+) │ │ └── dependencies.py # FastAPI dependencies │ │ │ ├── database/ # Database layer │ │ ├── session.py # Async session management │ │ └── operations.py # DB operations (Phase 2+) │ │ │ ├── data/ # External data (Phase 2+) │ │ ├── api_client.py # League REST API client │ │ └── cache.py # Caching layer │ │ │ └── utils/ # Utilities │ ├── logging.py # Logging setup │ └── auth.py # JWT utilities (Phase 1) │ ├── tests/ │ ├── unit/ # Unit tests │ ├── integration/ # Integration tests │ └── e2e/ # End-to-end tests │ ├── logs/ # Application logs (gitignored) ├── venv/ # Virtual environment (gitignored) ├── .env # Environment variables (gitignored) ├── .env.example # Environment template ├── requirements.txt # Production dependencies ├── requirements-dev.txt # Dev dependencies ├── Dockerfile # Container definition ├── docker-compose.yml # Redis for local dev └── pytest.ini # Pytest configuration ``` ## Key Architectural Patterns ### 1. Hybrid State Management - **In-Memory**: Active game states for fast access (<500ms response) - **PostgreSQL**: Persistent storage for recovery and history - **Pattern**: Write-through cache (update memory + async DB write) ### 2. Polymorphic Player Models ```python # Base class with abstract methods class BasePlayer(BaseModel, ABC): @abstractmethod def get_image_url(self) -> str: ... # League-specific implementations class SbaPlayer(BasePlayer): ... class PdPlayer(BasePlayer): ... # Factory pattern for instantiation Lineup.from_api_data(config, data) ``` ### 3. League-Agnostic Core - Game engine works for any league - League-specific logic in config classes - Result charts loaded per league ### 4. Async-First - All database operations use `async/await` - Database writes don't block game logic - Connection pooling for efficiency ## Development Workflow ### Daily Development ```bash # Activate virtual environment source venv/bin/activate # Start Redis (in separate terminal or use -d for detached) docker compose up -d # Run backend with hot-reload python -m app.main # Backend available at http://localhost:8000 # API docs at http://localhost:8000/docs ``` ### Testing ```bash # Run all tests pytest tests/ -v # Run with coverage pytest tests/ --cov=app --cov-report=html # Run specific test file pytest tests/unit/test_game_engine.py -v # Type checking mypy app/ # Code formatting black app/ tests/ # Linting flake8 app/ tests/ ``` ## Coding Standards ### Python Style - **Formatting**: Black with default settings - **Line Length**: 88 characters (black default) - **Imports**: Group stdlib, third-party, local (isort compatible) - **Type Hints**: Required for all public functions - **Docstrings**: Google style for classes and public methods ### Logging Pattern ```python import logging logger = logging.getLogger(f'{__name__}.ClassName') # Usage logger.info(f"User {user_id} connected") logger.error(f"Failed to process action: {error}", exc_info=True) ``` ### DateTime Handling **ALWAYS use Pendulum, NEVER use Python's datetime module:** ```python import pendulum # Get current UTC time now = pendulum.now('UTC') # Format for display formatted = now.format('YYYY-MM-DD HH:mm:ss') formatted_iso = now.to_iso8601_string() # Parse dates parsed = pendulum.parse('2025-10-21') # Timezones eastern = pendulum.now('America/New_York') utc = eastern.in_timezone('UTC') # Database defaults (in models) created_at = Column(DateTime, default=lambda: pendulum.now('UTC')) ``` ### Error Handling - **Raise or Return**: Never return `Optional` unless specifically required - **Custom Exceptions**: Use for domain-specific errors - **Logging**: Always log exceptions with context ### Dataclasses ```python from dataclasses import dataclass @dataclass class GameState: game_id: str inning: int outs: int # ... fields ``` ## Database Models Our database schema is designed based on the proven Discord game implementation, with enhancements for web real-time gameplay. ### Core Tables #### **Game** (`games`) Primary game container with state tracking. **Key Fields:** - `id` (UUID): Primary key, prevents ID collisions across distributed systems - `league_id` (String): 'sba' or 'pd', determines league-specific behavior - `status` (String): 'pending', 'active', 'completed' - `game_mode` (String): 'ranked', 'friendly', 'practice' - `visibility` (String): 'public', 'private' - `current_inning`, `current_half`: Current game state - `home_score`, `away_score`: Running scores **AI Support:** - `home_team_is_ai` (Boolean): Home team controlled by AI - `away_team_is_ai` (Boolean): Away team controlled by AI - `ai_difficulty` (String): 'balanced', 'yolo', 'safe' **Relationships:** - `plays`: All plays in the game (cascade delete) - `lineups`: All lineup entries (cascade delete) - `cardset_links`: PD only - approved cardsets (cascade delete) - `roster_links`: Roster tracking - cards (PD) or players (SBA) (cascade delete) - `session`: Real-time WebSocket session (cascade delete) --- #### **Play** (`plays`) Records every at-bat with full statistics and game state. **Game State Snapshot:** - `play_number`: Sequential play counter - `inning`, `half`: Inning state - `outs_before`: Outs at start of play - `batting_order`: Current spot in order - `away_score`, `home_score`: Score at play start **Player References (FKs to Lineup):** - `batter_id`, `pitcher_id`, `catcher_id`: Required players - `defender_id`, `runner_id`: Optional for specific plays - `on_first_id`, `on_second_id`, `on_third_id`: Base runners **Runner Outcomes:** - `on_first_final`, `on_second_final`, `on_third_final`: Final base (None = out, 1-4 = base) - `batter_final`: Where batter ended up - `on_base_code` (Integer): Bit field for efficient queries (1=1st, 2=2nd, 4=3rd, 7=loaded) **Strategic Decisions:** - `defensive_choices` (JSON): Alignment, holds, shifts - `offensive_choices` (JSON): Steal attempts, bunts, hit-and-run **Play Result:** - `dice_roll` (String): Dice notation (e.g., "14+6") - `hit_type` (String): GB, FB, LD, etc. - `result_description` (Text): Human-readable result - `outs_recorded`, `runs_scored`: Play outcome - `check_pos` (String): Defensive position for X-check **Batting Statistics (25+ fields):** - `pa`, `ab`, `hit`, `double`, `triple`, `homerun` - `bb`, `so`, `hbp`, `rbi`, `sac`, `ibb`, `gidp` - `sb`, `cs`: Base stealing - `wild_pitch`, `passed_ball`, `pick_off`, `balk` - `bphr`, `bpfo`, `bp1b`, `bplo`: Ballpark power events - `run`, `e_run`: Earned/unearned runs **Advanced Analytics:** - `wpa` (Float): Win Probability Added - `re24` (Float): Run Expectancy 24 base-out states **Game Situation Flags:** - `is_tied`, `is_go_ahead`, `is_new_inning`: Context flags - `in_pow`: Pitcher over workload - `complete`, `locked`: Workflow state **Helper Properties:** ```python @property def ai_is_batting(self) -> bool: """True if batting team is AI-controlled""" return (self.half == 'top' and self.game.away_team_is_ai) or \ (self.half == 'bot' and self.game.home_team_is_ai) @property def ai_is_fielding(self) -> bool: """True if fielding team is AI-controlled""" return not self.ai_is_batting ``` --- #### **Lineup** (`lineups`) Tracks player assignments and substitutions. **Key Fields:** - `game_id`, `team_id`, `card_id`: Links to game/team/card - `position` (String): P, C, 1B, 2B, 3B, SS, LF, CF, RF, DH - `batting_order` (Integer): 1-9 **Substitution Tracking:** - `is_starter` (Boolean): Original lineup vs substitute - `is_active` (Boolean): Currently in game - `entered_inning` (Integer): When player entered - `replacing_id` (Integer): Lineup ID of replaced player - `after_play` (Integer): Exact play number of substitution **Pitcher Management:** - `is_fatigued` (Boolean): Triggers bullpen decisions --- #### **GameCardsetLink** (`game_cardset_links`) PD league only - defines legal cardsets for a game. **Key Fields:** - `game_id`, `cardset_id`: Composite primary key - `priority` (Integer): 1 = primary, 2+ = backup **Usage:** - SBA games: Empty (no cardset restrictions) - PD games: Required (validates card eligibility) --- #### **RosterLink** (`roster_links`) Tracks eligible cards (PD) or players (SBA) for a game. **Polymorphic Design**: Single table supporting both leagues with application-layer type safety. **Key Fields:** - `id` (Integer): Surrogate primary key (auto-increment) - `game_id` (UUID): Foreign key to games table - `card_id` (Integer, nullable): PD league - card identifier - `player_id` (Integer, nullable): SBA league - player identifier - `team_id` (Integer): Which team owns this entity in this game **Constraints:** - `roster_link_one_id_required`: CHECK constraint ensures exactly one of `card_id` or `player_id` is populated (XOR logic) - `uq_game_card`: UNIQUE constraint on (game_id, card_id) for PD - `uq_game_player`: UNIQUE constraint on (game_id, player_id) for SBA **Usage Pattern:** ```python # PD league - add card to roster roster_data = await db_ops.add_pd_roster_card( game_id=game_id, card_id=123, team_id=1 ) # SBA league - add player to roster roster_data = await db_ops.add_sba_roster_player( game_id=game_id, player_id=456, team_id=2 ) # Get roster (league-specific) pd_roster = await db_ops.get_pd_roster(game_id, team_id=1) sba_roster = await db_ops.get_sba_roster(game_id, team_id=2) ``` **Design Rationale:** - Single table avoids complex joins and simplifies queries - Nullable columns with CHECK constraint ensures data integrity at database level - Pydantic models (`PdRosterLinkData`, `SbaRosterLinkData`) provide type safety at application layer - Surrogate key allows nullable columns (can't use nullable columns in composite PK) --- #### **GameSession** (`game_sessions`) Real-time WebSocket state tracking. **Key Fields:** - `game_id` (UUID): Primary key, one-to-one with Game - `connected_users` (JSON): Active WebSocket connections - `last_action_at` (DateTime): Last activity timestamp - `state_snapshot` (JSON): In-memory game state cache --- ### Database Patterns ### Async Session Usage ```python from app.database.session import get_session async def some_function(): async with get_session() as session: result = await session.execute(query) # session.commit() happens automatically ``` ### Model Definitions ```python from app.database.session import Base from sqlalchemy import Column, String, Integer class Game(Base): __tablename__ = "games" id = Column(UUID(as_uuid=True), primary_key=True) # ... columns ``` ### Relationship Patterns **Using Lazy Loading:** ```python # In Play model - common players loaded automatically batter = relationship("Lineup", foreign_keys=[batter_id], lazy="joined") pitcher = relationship("Lineup", foreign_keys=[pitcher_id], lazy="joined") # Rare players loaded on demand defender = relationship("Lineup", foreign_keys=[defender_id]) # lazy="select" (default) ``` **Querying with Relationships:** ```python from sqlalchemy.orm import joinedload # Efficient loading for broadcasting play = await session.execute( select(Play) .options( joinedload(Play.batter), joinedload(Play.pitcher), joinedload(Play.on_first), joinedload(Play.on_second), joinedload(Play.on_third) ) .where(Play.id == play_id) ) ``` ### Common Query Patterns **Find plays with bases loaded:** ```python # Using on_base_code bit field bases_loaded_plays = await session.execute( select(Play).where(Play.on_base_code == 7) # 1+2+4 = 7 ) ``` **Get active pitcher for team:** ```python pitcher = await session.execute( select(Lineup) .where( Lineup.game_id == game_id, Lineup.team_id == team_id, Lineup.position == 'P', Lineup.is_active == True ) ) ``` **Calculate box score stats:** ```python # Player batting stats for game stats = await session.execute( select( func.sum(Play.ab).label('ab'), func.sum(Play.hit).label('hits'), func.sum(Play.homerun).label('hr'), func.sum(Play.rbi).label('rbi') ) .where( Play.game_id == game_id, Play.batter_id == lineup_id ) ) ``` ## WebSocket Patterns ### Event Handler Registration ```python @sio.event async def some_event(sid, data): """Handle some_event from client""" try: # Validate data # Process action # Emit response await sio.emit('response_event', result, room=sid) except Exception as e: logger.error(f"Error handling event: {e}") await sio.emit('error', {'message': str(e)}, room=sid) ``` ### Broadcasting ```python # To specific game room await connection_manager.broadcast_to_game( game_id, 'game_state_update', state_data ) # To specific user await connection_manager.emit_to_user( sid, 'decision_required', decision_data ) ``` ## Environment Variables Required in `.env`: ```bash # Database DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/dbname # Application SECRET_KEY=your-secret-key-at-least-32-chars # Discord OAuth DISCORD_CLIENT_ID=your-client-id DISCORD_CLIENT_SECRET=your-client-secret # League APIs SBA_API_URL=https://sba-api.example.com SBA_API_KEY=your-api-key PD_API_URL=https://pd-api.example.com PD_API_KEY=your-api-key ``` ## Performance Targets - **Action Response**: < 500ms from user action to state update - **WebSocket Delivery**: < 200ms - **Database Write**: < 100ms (async, non-blocking) - **State Recovery**: < 2 seconds - **Concurrent Games**: Support 10+ simultaneous games - **Memory**: < 1GB with 10 active games ## Security Considerations - **Authentication**: All WebSocket connections require valid JWT - **Authorization**: Verify team ownership before allowing actions - **Input Validation**: Pydantic models validate all inputs - **SQL Injection**: Prevented by SQLAlchemy ORM - **Dice Rolls**: Cryptographically secure random generation - **Server-Side Logic**: All game rules enforced server-side ## Common Tasks ### Adding a New API Endpoint 1. Create route in `app/api/routes/` 2. Define Pydantic request/response models 3. Add dependency injection if needed 4. Register router in `app/main.py` ### Adding a New WebSocket Event 1. Define event handler in `app/websocket/handlers.py` 2. Register with `@sio.event` decorator 3. Validate data with Pydantic 4. Add corresponding client handling in frontend ### Adding a New Database Model 1. Define SQLAlchemy model in `app/models/db_models.py` 2. Create Alembic migration: `alembic revision --autogenerate -m "description"` 3. Apply migration: `alembic upgrade head` ## Troubleshooting ### Import Errors - Ensure virtual environment is activated - Check `PYTHONPATH` if using custom structure - Verify all `__init__.py` files exist ### Database Connection Issues - Verify `DATABASE_URL` in `.env` is correct - Test connection: `psql $DATABASE_URL` - Check firewall/network access - Verify database exists ### WebSocket Not Connecting - Check CORS settings in `config.py` - Verify token is being sent from client - Check logs for connection errors - Ensure Socket.io versions match (client/server) ## Environment-Specific Configuration ### Database Connection - **Dev Server**: PostgreSQL at `10.10.0.42:5432` - **Database Name**: `paperdynasty_dev` - **User**: `paperdynasty` - **Connection String Format**: `postgresql+asyncpg://paperdynasty:PASSWORD@10.10.0.42:5432/paperdynasty_dev` ### Docker Compose Commands This system uses **Docker Compose V2** (not legacy docker-compose): ```bash # Correct (with space) docker compose up -d docker compose down docker compose logs # Incorrect (will not work) docker-compose up -d # Old command not available ``` ### Python Environment - **Version**: Python 3.13.3 (not 3.11 as originally planned) - **Virtual Environment**: Located at `backend/venv/` - **Activation**: `source venv/bin/activate` (from backend directory) ### Critical Dependencies - **greenlet**: Required for SQLAlchemy async support (must be explicitly installed) - **Pendulum**: Used for ALL datetime operations (replaces Python's datetime module) ```python import pendulum # Always use Pendulum now = pendulum.now('UTC') # Never use from datetime import datetime # ❌ Don't import this ``` ### Environment Variable Format **IMPORTANT**: Pydantic Settings requires specific formats for complex types: ```bash # Lists must be JSON arrays CORS_ORIGINS=["http://localhost:3000", "http://localhost:3001"] # ✅ Correct # NOT comma-separated strings CORS_ORIGINS=http://localhost:3000,http://localhost:3001 # ❌ Will fail ``` ### SQLAlchemy Reserved Names The following column names are **reserved** in SQLAlchemy and will cause errors: ```python # ❌ NEVER use these as column names metadata = Column(JSON) # RESERVED - will fail # ✅ Use descriptive alternatives game_metadata = Column(JSON) play_metadata = Column(JSON) lineup_metadata = Column(JSON) ``` **Other reserved names to avoid**: `metadata`, `registry`, `__tablename__`, `__mapper__`, `__table__` ### Package Version Notes Due to Python 3.13 compatibility, we use newer versions than originally planned: ```txt # Updated versions (from requirements.txt) fastapi==0.115.6 # (was 0.104.1) uvicorn==0.34.0 # (was 0.24.0) pydantic==2.10.6 # (was 2.5.0) sqlalchemy==2.0.36 # (was 2.0.23) asyncpg==0.30.0 # (was 0.29.0) pendulum==3.0.0 # (new addition) ``` ### Server Startup ```bash # From backend directory with venv activated python -m app.main # Server runs at: # - Main API: http://localhost:8000 # - Swagger UI: http://localhost:8000/docs # - ReDoc: http://localhost:8000/redoc # With hot-reload enabled by default ``` ### Logs Directory - Auto-created at `backend/logs/` - Daily rotating logs: `app_YYYYMMDD.log` - 10MB max size, 5 backup files - Gitignored ## Week 4 Implementation (Phase 2 - State Management) **Completed**: 2025-10-22 **Status**: ✅ **COMPLETE** - All deliverables achieved ### Components Implemented #### 1. Pydantic Game State Models (`app/models/game_models.py`) **Purpose**: Type-safe models for in-memory game state representation **Key Models**: - `GameState`: Core game state (inning, score, runners, decisions) - `RunnerState`: Base runner tracking - `LineupPlayerState` / `TeamLineupState`: Lineup management - `DefensiveDecision` / `OffensiveDecision`: Strategic decisions **Features**: - Full Pydantic v2 validation with field validators - 20+ helper methods on GameState - Optimized for fast serialization (WebSocket broadcasts) - Game over logic, runner advancement, scoring **Usage Example**: ```python from app.models.game_models import GameState, RunnerState # Create game state state = GameState( game_id=uuid4(), league_id="sba", home_team_id=1, away_team_id=2 ) # Add runner state.add_runner(lineup_id=1, card_id=101, base=1) # Advance runner and score state.advance_runner(from_base=1, to_base=4) # Scores run ``` #### 2. State Manager (`app/core/state_manager.py`) **Purpose**: In-memory state management with O(1) lookups **Key Features**: - Dictionary-based storage: `_states: Dict[UUID, GameState]` - Lineup caching per game - Last access tracking with Pendulum timestamps - Idle game eviction (configurable timeout) - State recovery from database - Statistics tracking **Usage Example**: ```python from app.core.state_manager import state_manager # Create game state = await state_manager.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2 ) # Get state (fast O(1) lookup) state = state_manager.get_state(game_id) # Update state state.inning = 5 state_manager.update_state(game_id, state) # Recover from database recovered = await state_manager.recover_game(game_id) ``` **Performance**: - State access: O(1) via dictionary lookup - Memory per game: ~1KB (just state, no player data yet) - Target response time: <500ms ✅ #### 3. Database Operations (`app/database/operations.py`) **Purpose**: Async PostgreSQL persistence layer **Key Methods**: - `create_game()`: Create game in database - `update_game_state()`: Update inning, score, status - `create_lineup_entry()` / `get_active_lineup()`: Lineup persistence - `save_play()` / `get_plays()`: Play recording - `load_game_state()`: Complete state loading for recovery - `create_game_session()` / `update_session_snapshot()`: WebSocket state **Usage Example**: ```python from app.database.operations import DatabaseOperations db_ops = DatabaseOperations() # Create game await db_ops.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) # Update state (async, non-blocking) await db_ops.update_game_state( game_id=game_id, inning=5, half="bottom", home_score=3, away_score=2 ) # Load for recovery game_data = await db_ops.load_game_state(game_id) ``` **Pattern**: All operations use async/await with proper error handling ### Testing **Unit Tests** (86 tests, 100% passing): - `tests/unit/models/test_game_models.py` (60 tests) - `tests/unit/core/test_state_manager.py` (26 tests) **Integration Tests** (29 tests written): - `tests/integration/database/test_operations.py` (21 tests) - `tests/integration/test_state_persistence.py` (8 tests) **Run Tests**: ```bash # All unit tests pytest tests/unit/ -v # Integration tests (requires database) pytest tests/integration/ -v -m integration # Specific file pytest tests/unit/models/test_game_models.py -v ``` ### Patterns Established #### 1. Pydantic Field Validation ```python @field_validator('position') @classmethod def validate_position(cls, v: str) -> str: valid_positions = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH'] if v not in valid_positions: raise ValueError(f"Position must be one of {valid_positions}") return v ``` #### 2. Async Database Session Management ```python async with AsyncSessionLocal() as session: try: # database operations await session.commit() except Exception as e: await session.rollback() logger.error(f"Error: {e}") raise ``` #### 3. State Recovery Pattern ```python # 1. Load from database game_data = await db_ops.load_game_state(game_id) # 2. Rebuild in-memory state state = await state_manager._rebuild_state_from_data(game_data) # 3. Cache in memory state_manager._states[game_id] = state ``` #### 4. Helper Methods on Models ```python # GameState helper methods def get_batting_team_id(self) -> int: return self.away_team_id if self.half == "top" else self.home_team_id def is_runner_on_first(self) -> bool: return any(r.on_base == 1 for r in self.runners) def advance_runner(self, from_base: int, to_base: int) -> None: # Auto-score when to_base == 4 if to_base == 4: if self.half == "top": self.away_score += 1 else: self.home_score += 1 ``` ### Configuration **pytest.ini** (created): ```ini [pytest] asyncio_mode = auto asyncio_default_fixture_loop_scope = function ``` **Fixed Warnings**: - ✅ Pydantic v2 config deprecation (migrated to `model_config = ConfigDict()`) - ✅ pytest-asyncio loop scope configuration ### Key Files ``` app/models/game_models.py (492 lines) - Pydantic state models app/core/state_manager.py (296 lines) - In-memory state management app/database/operations.py (362 lines) - Async database operations tests/unit/models/test_game_models.py (788 lines, 60 tests) tests/unit/core/test_state_manager.py (447 lines, 26 tests) tests/integration/database/test_operations.py (438 lines, 21 tests) tests/integration/test_state_persistence.py (290 lines, 8 tests) pytest.ini (23 lines) - Test configuration ``` ### Next Phase (Week 5) Week 5 will build game logic on top of this state management foundation: 1. Dice system (cryptographic d20 rolls) 2. Play resolver (result charts and outcome determination) 3. Game engine (orchestrate complete at-bat flow) 4. Rule validators (enforce baseball rules) 5. Enhanced state recovery (replay plays to rebuild complete state) --- ## References - **Implementation Guide**: `../.claude/implementation/01-infrastructure.md` - **Backend Architecture**: `../.claude/implementation/backend-architecture.md` - **Week 4 Plan**: `../.claude/implementation/02-week4-state-management.md` - **Week 5 Plan**: `../.claude/implementation/02-week5-game-logic.md` - **Player Data Catalog**: `../.claude/implementation/player-data-catalog.md` - **WebSocket Protocol**: `../.claude/implementation/websocket-protocol.md` - **Database Design**: `../.claude/implementation/database-design.md` - **Full PRD**: `../prd-web-scorecard-1.1.md` --- **Current Phase**: Phase 2 - Game Engine Core (Week 4 ✅ Complete) **Next Phase**: Phase 2 - Game Logic (Week 5) **Phase 1 Completed**: 2025-10-21 **Week 4 Completed**: 2025-10-22 **Python Version**: 3.13.3 **Database Server**: 10.10.0.42:5432 ## Database Model Updates (2025-10-21) Enhanced all database models based on proven Discord game implementation: ### Changes from Initial Design: - ✅ Added `GameCardsetLink` and `RosterLink` tables for PD league cardset management - ✅ Enhanced `Game` model with AI opponent support (`home_team_is_ai`, `away_team_is_ai`, `ai_difficulty`) - ✅ Added 25+ statistic fields to `Play` model (pa, ab, hit, hr, rbi, sb, wpa, re24, etc.) - ✅ Added player reference FKs to `Play` (batter, pitcher, catcher, defender, runner) - ✅ Added base runner tracking with `on_base_code` bit field for efficient queries - ✅ Added game situation flags to `Play` (is_tied, is_go_ahead, is_new_inning, in_pow) - ✅ Added play workflow flags (complete, locked) - ✅ Enhanced `Lineup` with substitution tracking (replacing_id, after_play, is_fatigued) - ✅ Changed strategic decisions to JSON (defensive_choices, offensive_choices) - ✅ Added helper properties for AI decision-making (ai_is_batting, ai_is_fielding) ### Design Decisions: - **UUID vs BigInteger**: Kept UUIDs for Game primary key (better for distributed systems) - **AI Tracking**: Per-team booleans instead of single `ai_team` field (supports AI vs AI simulations) - **Runner Tracking**: Removed JSON fields, using FKs + `on_base_code` for type safety - **Cardsets**: Optional relationships - empty for SBA, required for PD - **Relationships**: Using SQLAlchemy relationships with strategic lazy loading