Added league-agnostic roster tracking with single-table design: Database Changes: - Modified RosterLink model with surrogate primary key (id) - Added nullable card_id (PD) and player_id (SBA) columns - Added CHECK constraint ensuring exactly one ID populated (XOR logic) - Added unique constraints for (game_id, card_id) and (game_id, player_id) - Imported CheckConstraint and UniqueConstraint from SQLAlchemy New Files: - app/models/roster_models.py: Pydantic models for type safety - BaseRosterLinkData: Abstract base class - PdRosterLinkData: PD league card-based rosters - SbaRosterLinkData: SBA league player-based rosters - RosterLinkCreate: Request validation model - tests/unit/models/test_roster_models.py: 24 unit tests (all passing) - Tests for PD/SBA roster link creation and validation - Tests for RosterLinkCreate XOR validation - Tests for polymorphic behavior Database Operations: - add_pd_roster_card(): Add PD card to game roster - add_sba_roster_player(): Add SBA player to game roster - get_pd_roster(): Get PD cards with optional team filter - get_sba_roster(): Get SBA players with optional team filter - remove_roster_entry(): Remove roster entry by ID Tests: - Added 12 integration tests for roster operations - Fixed setup_database fixture scope (module → function) Documentation: - Updated backend/CLAUDE.md with RosterLink documentation - Added usage examples and design rationale - Updated Game model relationship description Design Pattern: Single table with application-layer type safety rather than SQLAlchemy polymorphic inheritance. Simpler queries, database-enforced integrity, and Pydantic type safety at application layer. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
29 KiB
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
# 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
# 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
# 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
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:
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
Optionalunless specifically required - Custom Exceptions: Use for domain-specific errors
- Logging: Always log exceptions with context
Dataclasses
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 systemsleague_id(String): 'sba' or 'pd', determines league-specific behaviorstatus(String): 'pending', 'active', 'completed'game_mode(String): 'ranked', 'friendly', 'practice'visibility(String): 'public', 'private'current_inning,current_half: Current game statehome_score,away_score: Running scores
AI Support:
home_team_is_ai(Boolean): Home team controlled by AIaway_team_is_ai(Boolean): Away team controlled by AIai_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 counterinning,half: Inning stateouts_before: Outs at start of playbatting_order: Current spot in orderaway_score,home_score: Score at play start
Player References (FKs to Lineup):
batter_id,pitcher_id,catcher_id: Required playersdefender_id,runner_id: Optional for specific playson_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 upon_base_code(Integer): Bit field for efficient queries (1=1st, 2=2nd, 4=3rd, 7=loaded)
Strategic Decisions:
defensive_choices(JSON): Alignment, holds, shiftsoffensive_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 resultouts_recorded,runs_scored: Play outcomecheck_pos(String): Defensive position for X-check
Batting Statistics (25+ fields):
pa,ab,hit,double,triple,homerunbb,so,hbp,rbi,sac,ibb,gidpsb,cs: Base stealingwild_pitch,passed_ball,pick_off,balkbphr,bpfo,bp1b,bplo: Ballpark power eventsrun,e_run: Earned/unearned runs
Advanced Analytics:
wpa(Float): Win Probability Addedre24(Float): Run Expectancy 24 base-out states
Game Situation Flags:
is_tied,is_go_ahead,is_new_inning: Context flagsin_pow: Pitcher over workloadcomplete,locked: Workflow state
Helper Properties:
@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/cardposition(String): P, C, 1B, 2B, 3B, SS, LF, CF, RF, DHbatting_order(Integer): 1-9
Substitution Tracking:
is_starter(Boolean): Original lineup vs substituteis_active(Boolean): Currently in gameentered_inning(Integer): When player enteredreplacing_id(Integer): Lineup ID of replaced playerafter_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 keypriority(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 tablecard_id(Integer, nullable): PD league - card identifierplayer_id(Integer, nullable): SBA league - player identifierteam_id(Integer): Which team owns this entity in this game
Constraints:
roster_link_one_id_required: CHECK constraint ensures exactly one ofcard_idorplayer_idis populated (XOR logic)uq_game_card: UNIQUE constraint on (game_id, card_id) for PDuq_game_player: UNIQUE constraint on (game_id, player_id) for SBA
Usage Pattern:
# 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 Gameconnected_users(JSON): Active WebSocket connectionslast_action_at(DateTime): Last activity timestampstate_snapshot(JSON): In-memory game state cache
Database Patterns
Async Session Usage
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
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:
# 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:
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:
# 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:
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:
# 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
@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
# 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:
# 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
- Create route in
app/api/routes/ - Define Pydantic request/response models
- Add dependency injection if needed
- Register router in
app/main.py
Adding a New WebSocket Event
- Define event handler in
app/websocket/handlers.py - Register with
@sio.eventdecorator - Validate data with Pydantic
- Add corresponding client handling in frontend
Adding a New Database Model
- Define SQLAlchemy model in
app/models/db_models.py - Create Alembic migration:
alembic revision --autogenerate -m "description" - Apply migration:
alembic upgrade head
Troubleshooting
Import Errors
- Ensure virtual environment is activated
- Check
PYTHONPATHif using custom structure - Verify all
__init__.pyfiles exist
Database Connection Issues
- Verify
DATABASE_URLin.envis 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):
# 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)
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:
# 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:
# ❌ 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:
# 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
# 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 trackingLineupPlayerState/TeamLineupState: Lineup managementDefensiveDecision/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:
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:
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 databaseupdate_game_state(): Update inning, score, statuscreate_lineup_entry()/get_active_lineup(): Lineup persistencesave_play()/get_plays(): Play recordingload_game_state(): Complete state loading for recoverycreate_game_session()/update_session_snapshot(): WebSocket state
Usage Example:
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:
# 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
@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
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
# 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
# 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):
[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:
- Dice system (cryptographic d20 rolls)
- Play resolver (result charts and outcome determination)
- Game engine (orchestrate complete at-bat flow)
- Rule validators (enforce baseball rules)
- 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
GameCardsetLinkandRosterLinktables for PD league cardset management - ✅ Enhanced
Gamemodel with AI opponent support (home_team_is_ai,away_team_is_ai,ai_difficulty) - ✅ Added 25+ statistic fields to
Playmodel (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_codebit 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
Lineupwith 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_teamfield (supports AI vs AI simulations) - Runner Tracking: Removed JSON fields, using FKs +
on_base_codefor type safety - Cardsets: Optional relationships - empty for SBA, required for PD
- Relationships: Using SQLAlchemy relationships with strategic lazy loading