Removed outdated TODO comments and updated documentation to reflect work completed in Phase 3E (Position Ratings and WebSocket Handlers). Changes: - Removed 2 stale WebSocket emission TODOs in game_engine.py (lines 319, 387) These referenced Phase 3E-Final work completed on 2025-01-10 - Updated backend/CLAUDE.md Phase 3E status section Marked defender retrieval as COMPLETE (Phase 3E-Main) Clarified SPD test still pending (needs batter speed rating) Marked runner advancement as COMPLETE (Phase 3D) - Updated TODO_RESOLUTION_SUMMARY.md Marked defender lookup TODO as resolved with implementation details Documentation: - Created TODO_AUDIT_2025-01-14.md - Complete TODO audit (53 items) - Created TODO_VERIFICATION_RESULTS.md - Verification of resolved items - Created TODO_SUMMARY.md - Quick reference priority matrix - Created TODO_CLEANUP_COMPLETE.md - Cleanup work summary Test Status: - Backend: 9/9 PlayResolver tests passing - No regressions introduced Remaining Work: - 41 legitimate TODOs properly categorized for future phases - 8 Phase F6 TODOs (game page integration) - 5 Quick win TODOs (optional polish) - 28 Future phase TODOs (auth, AI, advanced features) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2468 lines
76 KiB
Markdown
2468 lines
76 KiB
Markdown
# 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
|
||
│
|
||
├── terminal_client/ # Interactive testing REPL
|
||
│ ├── __init__.py # Package marker
|
||
│ ├── __main__.py # Entry point
|
||
│ ├── repl.py # Interactive REPL (cmd module)
|
||
│ ├── main.py # Click CLI commands
|
||
│ ├── display.py # Rich formatting
|
||
│ ├── config.py # Persistent config file
|
||
│ └── CLAUDE.md # Terminal client docs
|
||
│
|
||
├── 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
|
||
|
||
### Package Management
|
||
|
||
This project uses **UV** for fast, reliable package management.
|
||
|
||
**Installing UV** (one-time setup):
|
||
```bash
|
||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||
```
|
||
|
||
**Setting up the project**:
|
||
```bash
|
||
cd backend
|
||
uv sync # Creates .venv and installs all dependencies
|
||
```
|
||
|
||
### Daily Development
|
||
```bash
|
||
# Start Redis (in separate terminal or use -d for detached)
|
||
docker compose up -d
|
||
|
||
# Run backend with hot-reload (UV auto-activates .venv)
|
||
uv run python -m app.main
|
||
|
||
# Backend available at http://localhost:8000
|
||
# API docs at http://localhost:8000/docs
|
||
|
||
# Alternative: Activate .venv manually
|
||
source .venv/bin/activate
|
||
python -m app.main
|
||
```
|
||
|
||
### Testing
|
||
|
||
#### Terminal Client (Interactive Game Engine Testing)
|
||
|
||
Test the game engine directly without needing a frontend:
|
||
|
||
```bash
|
||
# Start interactive REPL (recommended for rapid testing)
|
||
uv run python -m terminal_client
|
||
|
||
# Then interact:
|
||
⚾ > new_game
|
||
⚾ > defensive
|
||
⚾ > offensive
|
||
⚾ > resolve
|
||
⚾ > quick_play 10
|
||
⚾ > status
|
||
⚾ > quit
|
||
```
|
||
|
||
**Features**:
|
||
- ✅ Persistent in-memory state throughout session
|
||
- ✅ Direct GameEngine access (no WebSocket overhead)
|
||
- ✅ Beautiful Rich formatting
|
||
- ✅ Auto-generated test lineups
|
||
- ✅ Perfect for rapid iteration
|
||
|
||
See `terminal_client/CLAUDE.md` for full documentation.
|
||
|
||
#### Unit & Integration Tests
|
||
|
||
```bash
|
||
# Run all tests
|
||
uv run pytest tests/ -v
|
||
|
||
# Run with coverage
|
||
uv run pytest tests/ --cov=app --cov-report=html
|
||
|
||
# Run specific test file
|
||
uv run pytest tests/unit/test_game_engine.py -v
|
||
|
||
# Type checking
|
||
uv run mypy app/
|
||
|
||
# Code formatting
|
||
uv run black app/ tests/
|
||
|
||
# Linting
|
||
uv run flake8 app/ tests/
|
||
```
|
||
|
||
## Testing Policy
|
||
|
||
**REQUIRED: 100% unit tests passing before any commit to feature branches.**
|
||
|
||
### Commit Requirements
|
||
|
||
**Feature Branches:**
|
||
- ✅ **REQUIRED**: All unit tests must pass (609/609)
|
||
- ✅ **REQUIRED**: Run tests before every commit
|
||
- ⚠️ **ALLOWED**: `[WIP]` commits with `--no-verify` (feature branches only)
|
||
- ✅ **REQUIRED**: 100% pass before merge to main
|
||
|
||
**Main/Master Branch:**
|
||
- ✅ **REQUIRED**: 100% unit tests passing
|
||
- ✅ **REQUIRED**: Code review approval
|
||
- ✅ **REQUIRED**: CI/CD green build
|
||
- ❌ **NEVER**: Commit with failing tests
|
||
|
||
**Integration Tests:**
|
||
- ⚠️ Known infrastructure issues (asyncpg connection pooling)
|
||
- ℹ️ Run individually during development
|
||
- ℹ️ Fix infrastructure as separate task
|
||
|
||
### Quick Test Commands
|
||
|
||
```bash
|
||
# Before every commit - REQUIRED
|
||
uv run pytest tests/unit/ -q
|
||
|
||
# Expected output:
|
||
# ======================= 609 passed, 3 warnings in 0.91s ========================
|
||
|
||
# If tests fail, fix them before committing!
|
||
```
|
||
|
||
### Git Hook Setup (Automated Enforcement)
|
||
|
||
We provide a pre-commit hook that automatically runs unit tests before each commit.
|
||
|
||
**Installation:**
|
||
```bash
|
||
# From backend directory
|
||
cd /mnt/NV2/Development/strat-gameplay-webapp/backend
|
||
|
||
# Copy the pre-commit hook
|
||
cp .git-hooks/pre-commit .git/hooks/pre-commit
|
||
|
||
# Make it executable
|
||
chmod +x .git/hooks/pre-commit
|
||
```
|
||
|
||
**What it does:**
|
||
- ✅ Runs all unit tests automatically before commit
|
||
- ✅ Prevents commits if tests fail
|
||
- ✅ Shows clear error messages
|
||
- ✅ Fast execution (~1 second)
|
||
|
||
**Bypassing the hook (use sparingly):**
|
||
```bash
|
||
# For WIP commits on feature branches ONLY
|
||
git commit -m "[WIP] Work in progress" --no-verify
|
||
|
||
# NEVER bypass on main branch
|
||
# ALWAYS fix tests before final commit
|
||
```
|
||
|
||
### Test Troubleshooting
|
||
|
||
**If tests fail:**
|
||
```bash
|
||
# Run with verbose output to see which test failed
|
||
uv run pytest tests/unit/ -v
|
||
|
||
# Run with full traceback
|
||
uv run pytest tests/unit/ -v --tb=long
|
||
|
||
# Run specific failing test
|
||
uv run pytest tests/unit/path/to/test.py::test_name -v
|
||
```
|
||
|
||
**Common issues:**
|
||
- Import errors → Check PYTHONPATH or use `uv run`
|
||
- Module not found → Run `uv sync` to install dependencies
|
||
- Type errors → Check SQLAlchemy Column vs runtime types (use `# type: ignore[assignment]`)
|
||
|
||
### Why This Matters
|
||
|
||
**Benefits:**
|
||
- 🛡️ **Prevents regressions** - Broken code never reaches main
|
||
- ⚡ **Fast feedback** - Know immediately if you broke something
|
||
- 📜 **Clean history** - Main branch always deployable
|
||
- 🎯 **High confidence** - 609 tests verify core behavior
|
||
- 🔄 **Forces good habits** - Write tests as you code
|
||
|
||
**The cost of skipping:**
|
||
- 🐛 Bugs reach production
|
||
- ⏰ Hours debugging what tests would catch instantly
|
||
- 😰 Fear of refactoring (might break something)
|
||
- 🔥 Emergency hotfixes instead of planned releases
|
||
|
||
### Test Coverage Baseline
|
||
|
||
Current baseline (must maintain or improve):
|
||
- ✅ Unit tests: 609/609 passing (100%)
|
||
- ⏱️ Execution time: <1 second
|
||
- 📊 Coverage: High coverage of core game engine, state management, models
|
||
|
||
## 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
|
||
```
|
||
|
||
## Type Checking & Common False Positives
|
||
|
||
### Overview
|
||
|
||
This project uses **Pylance** (Pyright) for real-time type checking in VS Code and **mypy** for validation. Due to SQLAlchemy's ORM magic and Pydantic's settings pattern, we encounter known false positives that must be handled strategically.
|
||
|
||
**Critical**: Do NOT disable type checking globally. Use targeted suppressions only where needed.
|
||
|
||
### 🔴 Known False Positive #1: SQLAlchemy Model Attributes
|
||
|
||
**Problem**: SQLAlchemy model instances have `.id`, `.position`, etc. that are `int`/`str` at runtime but typed as `Column[int]`/`Column[str]` for type checkers.
|
||
|
||
**Symptom**:
|
||
```
|
||
Cannot assign to attribute "current_batter_lineup_id" for class "GameState"
|
||
Type "Column[int]" is not assignable to type "int | None"
|
||
```
|
||
|
||
**Solution**: Use targeted `# type: ignore[assignment]` comments:
|
||
|
||
```python
|
||
# ❌ DON'T: Disable all type checking
|
||
lineup_player.id # type: ignore
|
||
|
||
# ❌ DON'T: Runtime conversion (unnecessary overhead)
|
||
int(lineup_player.id)
|
||
|
||
# ✅ DO: Targeted suppression with explanation
|
||
lineup_player.id # type: ignore[assignment] # SQLAlchemy Column is int at runtime
|
||
```
|
||
|
||
**When to Apply**:
|
||
- Assigning SQLAlchemy model attributes to Pydantic model fields
|
||
- Common locations: `game_engine.py`, any code interfacing between ORM and Pydantic
|
||
|
||
**Example**:
|
||
```python
|
||
# In game_engine.py
|
||
state.current_batter_lineup_id = batting_order[current_idx].id # type: ignore[assignment]
|
||
state.current_pitcher_lineup_id = pitcher.id if pitcher else None # type: ignore[assignment]
|
||
```
|
||
|
||
### 🔴 Known False Positive #2: SQLAlchemy Declarative Base
|
||
|
||
**Problem**: mypy doesn't understand SQLAlchemy's `declarative_base()` pattern.
|
||
|
||
**Symptom**:
|
||
```
|
||
app/models/db_models.py:10: error: Variable "app.database.session.Base" is not valid as a type
|
||
app/models/db_models.py:10: error: Invalid base class "Base"
|
||
```
|
||
|
||
**Solution**: Configure `mypy.ini` to disable strict checking for ORM files:
|
||
|
||
```ini
|
||
[mypy-app.models.db_models]
|
||
disallow_untyped_defs = False
|
||
warn_return_any = False
|
||
|
||
[mypy-app.database.operations]
|
||
disallow_untyped_defs = False
|
||
```
|
||
|
||
**Status**: Already configured in `mypy.ini`. These warnings are expected and safe to ignore.
|
||
|
||
### 🔴 Known False Positive #3: Pydantic Settings Constructor
|
||
|
||
**Problem**: Pydantic `BaseSettings` loads from environment variables, not constructor arguments.
|
||
|
||
**Symptom**:
|
||
```
|
||
app/config.py:48: error: Missing named argument "secret_key" for "Settings"
|
||
app/config.py:48: error: Missing named argument "database_url" for "Settings"
|
||
```
|
||
|
||
**Solution**: Configure `mypy.ini` to disable checks for config module:
|
||
|
||
```ini
|
||
[mypy-app.config]
|
||
disallow_untyped_defs = False
|
||
```
|
||
|
||
**Status**: Already configured. This is expected Pydantic-settings behavior.
|
||
|
||
### 🟡 Legitimate Type Issues to Fix
|
||
|
||
#### Missing Type Annotations
|
||
|
||
**Problem**: Dictionary or variable without type hint.
|
||
|
||
**Symptom**:
|
||
```
|
||
Need type annotation for "position_counts"
|
||
```
|
||
|
||
**Solution**: Add explicit type annotation:
|
||
|
||
```python
|
||
# ❌ Missing type hint
|
||
position_counts = {}
|
||
|
||
# ✅ With type hint
|
||
position_counts: dict[str, int] = {}
|
||
```
|
||
|
||
#### Optional in Lambda/Sorted
|
||
|
||
**Problem**: Using `Optional[int]` in comparison context.
|
||
|
||
**Symptom**:
|
||
```
|
||
Argument "key" to "sorted" has incompatible type "Callable[[LineupPlayerState], int | None]"
|
||
```
|
||
|
||
**Solution**: Provide fallback value in lambda:
|
||
|
||
```python
|
||
# ❌ Optional can be None
|
||
sorted(players, key=lambda x: x.batting_order)
|
||
|
||
# ✅ With fallback (we already filtered None above)
|
||
sorted(players, key=lambda x: x.batting_order or 0)
|
||
```
|
||
|
||
### Best Practices
|
||
|
||
#### ✅ DO: Use Specific Type Ignore Codes
|
||
|
||
```python
|
||
# Specific - only ignores assignment mismatch
|
||
value = something # type: ignore[assignment]
|
||
|
||
# Specific - only ignores argument type
|
||
func(arg) # type: ignore[arg-type]
|
||
```
|
||
|
||
#### ✅ DO: Add Explanatory Comments
|
||
|
||
```python
|
||
# SQLAlchemy model .id is int at runtime, but typed as Column[int]
|
||
state.current_batter_lineup_id = lineup_player.id # type: ignore[assignment]
|
||
```
|
||
|
||
#### ✅ DO: Keep Type Checking Strict Elsewhere
|
||
|
||
Only suppress where SQLAlchemy/Pydantic cause false positives. New code should pass type checking without suppressions.
|
||
|
||
#### ❌ DON'T: Disable Type Checking Globally
|
||
|
||
```python
|
||
# ❌ Too broad - hides real errors
|
||
# type: ignore at top of file
|
||
|
||
# ❌ Disables all checks for file
|
||
# In pyrightconfig.json: "ignore": ["app/core/game_engine.py"]
|
||
```
|
||
|
||
#### ❌ DON'T: Use Unnecessary Runtime Conversions
|
||
|
||
```python
|
||
# ❌ int() is unnecessary (already int at runtime) and adds overhead
|
||
state.current_batter_lineup_id = int(lineup_player.id)
|
||
|
||
# ✅ Direct assignment with targeted suppression
|
||
state.current_batter_lineup_id = lineup_player.id # type: ignore[assignment]
|
||
```
|
||
|
||
### Configuration Files
|
||
|
||
#### mypy.ini
|
||
|
||
Manages mypy-specific type checking configuration:
|
||
- Disables strict checks for SQLAlchemy ORM files (`db_models.py`, `operations.py`)
|
||
- Disables checks for Pydantic Settings (`config.py`)
|
||
- Enables SQLAlchemy plugin support
|
||
|
||
#### pyrightconfig.json
|
||
|
||
Manages Pylance/Pyright configuration in VS Code:
|
||
- Sets `typeCheckingMode: "basic"` (not too strict)
|
||
- Suppresses some SQLAlchemy-related global warnings
|
||
- Keeps strict checking for application logic
|
||
|
||
### Common Error Codes Reference
|
||
|
||
| Code | Meaning | Common Fix |
|
||
|------|---------|-----------|
|
||
| `[assignment]` | Type mismatch in assignment | SQLAlchemy Column → use `# type: ignore[assignment]` |
|
||
| `[arg-type]` | Argument type mismatch | SQLAlchemy Column in function call → use `# type: ignore[arg-type]` |
|
||
| `[attr-defined]` | Attribute doesn't exist | Usually a real error - check for typos |
|
||
| `[var-annotated]` | Missing type annotation | Add `: dict[str, int]` etc. |
|
||
| `[return-value]` | Return type mismatch | Usually a real error - fix return value |
|
||
| `[operator]` | Unsupported operation | Check if operation makes sense |
|
||
|
||
### Verification Checklist
|
||
|
||
Before considering type warnings as false positives:
|
||
|
||
1. ✅ Is it a SQLAlchemy model attribute? → Use `# type: ignore[assignment]`
|
||
2. ✅ Is it in `db_models.py` or `operations.py`? → Expected, configured in `mypy.ini`
|
||
3. ✅ Is it in `config.py` (Pydantic Settings)? → Expected, configured in `mypy.ini`
|
||
4. ❌ Is it in game logic (`game_engine.py`, `validators.py`, etc.)? → **FIX IT** - likely a real issue
|
||
|
||
### Example: Correct SQLAlchemy-Pydantic Bridging
|
||
|
||
```python
|
||
async def _prepare_next_play(self, state: GameState) -> None:
|
||
"""Prepare snapshot for the next play."""
|
||
|
||
# Fetch SQLAlchemy models from database
|
||
batting_lineup = await self.db_ops.get_active_lineup(state.game_id, batting_team)
|
||
fielding_lineup = await self.db_ops.get_active_lineup(state.game_id, fielding_team)
|
||
|
||
# Extract values for Pydantic model
|
||
# SQLAlchemy model .id is int at runtime, but typed as Column[int]
|
||
state.current_batter_lineup_id = batting_order[current_idx].id # type: ignore[assignment]
|
||
|
||
pitcher = next((p for p in fielding_lineup if p.position == "P"), None)
|
||
state.current_pitcher_lineup_id = pitcher.id if pitcher else None # type: ignore[assignment]
|
||
```
|
||
|
||
### Resources
|
||
|
||
- **Type Checking Guide**: `.claude/type-checking-guide.md` (comprehensive documentation)
|
||
- **mypy Configuration**: `mypy.ini` (type checker settings)
|
||
- **Pylance Configuration**: `pyrightconfig.json` (VS Code settings)
|
||
|
||
---
|
||
|
||
## 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)
|
||
- **Package Manager**: UV (v0.9.7+) - Fast, reliable Python package management
|
||
- **Virtual Environment**: Located at `backend/.venv/` (UV default)
|
||
- **Activation**: `source .venv/bin/activate` (from backend directory)
|
||
- Or use `uv run <command>` to auto-activate
|
||
|
||
### Package Management with UV
|
||
|
||
**Add a new dependency**:
|
||
```bash
|
||
# Production dependency
|
||
uv add package-name==1.2.3
|
||
|
||
# Development dependency
|
||
uv add --dev package-name==1.2.3
|
||
|
||
# With extras
|
||
uv add "package[extra]==1.2.3"
|
||
```
|
||
|
||
**Update dependencies**:
|
||
```bash
|
||
# Update a specific package
|
||
uv add package-name@latest
|
||
|
||
# Sync dependencies (after pulling pyproject.toml changes)
|
||
uv sync
|
||
```
|
||
|
||
**Remove a dependency**:
|
||
```bash
|
||
uv remove package-name
|
||
```
|
||
|
||
**Key Files**:
|
||
- `pyproject.toml` - Project metadata and dependencies (commit to git)
|
||
- `uv.lock` - Locked dependency versions (commit to git)
|
||
- `.venv/` - Virtual environment (gitignored)
|
||
|
||
### 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
|
||
uv run pytest tests/unit/ -v
|
||
|
||
# Integration tests (requires database)
|
||
uv run pytest tests/integration/ -v -m integration
|
||
|
||
# Specific file
|
||
uv run 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)
|
||
|
||
---
|
||
|
||
## Player Models (2025-10-28)
|
||
|
||
### Overview
|
||
|
||
Polymorphic player model system that supports both SBA and PD leagues with different data complexity levels. Uses abstract base class pattern to ensure league-agnostic game engine.
|
||
|
||
### Architecture
|
||
|
||
```
|
||
BasePlayer (Abstract)
|
||
├── SbaPlayer (Simple)
|
||
└── PdPlayer (Complex with scouting data)
|
||
```
|
||
|
||
**Key Design Decisions**:
|
||
- Abstract base ensures consistent interface across leagues
|
||
- Factory methods for easy creation from API responses
|
||
- Pydantic validation for type safety
|
||
- Optional scouting data for PD league (loaded separately)
|
||
|
||
### Models
|
||
|
||
#### BasePlayer (Abstract Base Class)
|
||
|
||
**Location**: `app/models/player_models.py:18-44`
|
||
|
||
Common interface for all player types. Game engine uses this abstraction.
|
||
|
||
**Required Abstract Methods**:
|
||
- `get_image_url() -> str`: Get player image with fallback logic
|
||
- `get_positions() -> List[str]`: Get all playable positions
|
||
- `get_display_name() -> str`: Get formatted name for UI
|
||
|
||
**Common Fields**:
|
||
- `id`: Player ID (SBA) or Card ID (PD)
|
||
- `name`: Player display name
|
||
- `image`: Primary image URL
|
||
|
||
#### SbaPlayer Model
|
||
|
||
**Location**: `app/models/player_models.py:49-145`
|
||
|
||
Simple model for SBA league with minimal data.
|
||
|
||
**Key Fields**:
|
||
- Basic: `id`, `name`, `image`, `wara`
|
||
- Team: `team_id`, `team_name`, `season`
|
||
- Positions: `pos_1` through `pos_8` (up to 8 positions)
|
||
- References: `headshot`, `vanity_card`, `strat_code`, `bbref_id`, `injury_rating`
|
||
|
||
**Usage**:
|
||
```python
|
||
from app.models import SbaPlayer
|
||
|
||
# Create from API response
|
||
player = SbaPlayer.from_api_response(api_data)
|
||
|
||
# Use common interface
|
||
positions = player.get_positions() # ['RF', 'CF']
|
||
image = player.get_image_url() # With fallback logic
|
||
name = player.get_display_name() # 'Ronald Acuna Jr'
|
||
```
|
||
|
||
**API Mapping**:
|
||
- Endpoint: `{{baseUrl}}/players/:player_id`
|
||
- Maps API response fields to model fields
|
||
- Extracts nested team data automatically
|
||
|
||
#### PdPlayer Model
|
||
|
||
**Location**: `app/models/player_models.py:254-495`
|
||
|
||
Complex model for PD league with detailed scouting data.
|
||
|
||
**Key Fields**:
|
||
- Basic: `player_id`, `name`, `cost`, `description`
|
||
- Card: `cardset`, `rarity`, `set_num`, `quantity`
|
||
- Team: `mlbclub`, `franchise`
|
||
- Positions: `pos_1` through `pos_8`
|
||
- References: `headshot`, `vanity_card`, `strat_code`, `bbref_id`, `fangr_id`
|
||
- **Scouting**: `batting_card`, `pitching_card` (optional)
|
||
|
||
**Scouting Data Structure**:
|
||
|
||
```python
|
||
# Batting Card (loaded from /api/v2/battingcardratings/player/:id)
|
||
batting_card:
|
||
- steal_low, steal_high, steal_auto, steal_jump
|
||
- bunting, hit_and_run, running ratings
|
||
- hand (L/R), offense_col (1/2)
|
||
- ratings: Dict[str, PdBattingRating]
|
||
- 'L': vs Left-handed pitchers
|
||
- 'R': vs Right-handed pitchers
|
||
|
||
# Pitching Card (loaded from /api/v2/pitchingcardratings/player/:id)
|
||
pitching_card:
|
||
- balk, wild_pitch, hold
|
||
- starter_rating, relief_rating, closer_rating
|
||
- hand (L/R), offense_col (1/2)
|
||
- ratings: Dict[str, PdPitchingRating]
|
||
- 'L': vs Left-handed batters
|
||
- 'R': vs Right-handed batters
|
||
```
|
||
|
||
**PdBattingRating** (per handedness matchup):
|
||
- Hit location: `pull_rate`, `center_rate`, `slap_rate`
|
||
- Outcomes: `homerun`, `triple`, `double_*`, `single_*`, `walk`, `strikeout`, etc.
|
||
- Summary: `avg`, `obp`, `slg`
|
||
|
||
**PdPitchingRating** (per handedness matchup):
|
||
- Outcomes: `homerun`, `triple`, `double_*`, `single_*`, `walk`, `strikeout`, etc.
|
||
- X-checks: `xcheck_p`, `xcheck_c`, `xcheck_1b`, etc. (defensive play probabilities)
|
||
- Summary: `avg`, `obp`, `slg`
|
||
|
||
**Usage**:
|
||
```python
|
||
from app.models import PdPlayer
|
||
|
||
# Create from API responses (scouting data optional)
|
||
player = PdPlayer.from_api_response(
|
||
player_data=player_api_response,
|
||
batting_data=batting_api_response, # Optional
|
||
pitching_data=pitching_api_response # Optional
|
||
)
|
||
|
||
# Use common interface
|
||
positions = player.get_positions() # ['2B', 'SS']
|
||
name = player.get_display_name() # 'Chuck Knoblauch (1998 Season)'
|
||
|
||
# Access scouting data
|
||
rating_vs_lhp = player.get_batting_rating('L')
|
||
if rating_vs_lhp:
|
||
print(f"HR rate vs LHP: {rating_vs_lhp.homerun}%")
|
||
print(f"Walk rate: {rating_vs_lhp.walk}%")
|
||
print(f"Strikeout rate: {rating_vs_lhp.strikeout}%")
|
||
|
||
rating_vs_rhb = player.get_pitching_rating('R')
|
||
if rating_vs_rhb:
|
||
print(f"X-check SS: {rating_vs_rhb.xcheck_ss}%")
|
||
```
|
||
|
||
**API Mapping**:
|
||
- Player data: `{{baseUrl}}/api/v2/players/:player_id`
|
||
- Batting data: `{{baseUrl}}/api/v2/battingcardratings/player/:player_id`
|
||
- Pitching data: `{{baseUrl}}/api/v2/pitchingcardratings/player/:player_id`
|
||
|
||
### Supporting Models
|
||
|
||
#### PdCardset
|
||
Card set information: `id`, `name`, `description`, `ranked_legal`
|
||
|
||
#### PdRarity
|
||
Card rarity: `id`, `value`, `name` (MVP, Starter, Replacement), `color` (hex)
|
||
|
||
#### PdBattingCard
|
||
Container for batting statistics with ratings for both vs LHP and vs RHP
|
||
|
||
#### PdPitchingCard
|
||
Container for pitching statistics with ratings for both vs LHB and vs RHB
|
||
|
||
### Integration Points
|
||
|
||
**With Game Engine**:
|
||
```python
|
||
# Game engine uses BasePlayer interface
|
||
def process_at_bat(batter: BasePlayer, pitcher: BasePlayer):
|
||
# Works for both SBA and PD players
|
||
print(f"{batter.get_display_name()} batting")
|
||
print(f"Positions: {batter.get_positions()}")
|
||
```
|
||
|
||
**With API Client** (future):
|
||
```python
|
||
# API client will fetch and parse player data
|
||
async def get_player(league: str, player_id: int) -> BasePlayer:
|
||
if league == "sba":
|
||
data = await fetch_sba_player(player_id)
|
||
return SbaPlayer.from_api_response(data)
|
||
else: # PD league
|
||
player_data = await fetch_pd_player(player_id)
|
||
batting_data = await fetch_pd_batting(player_id)
|
||
pitching_data = await fetch_pd_pitching(player_id)
|
||
return PdPlayer.from_api_response(player_data, batting_data, pitching_data)
|
||
```
|
||
|
||
### Testing
|
||
|
||
**Unit Tests**: `tests/unit/models/test_player_models.py`
|
||
- BasePlayer abstract methods
|
||
- SbaPlayer creation and methods
|
||
- PdPlayer creation with/without scouting data
|
||
- Factory method validation
|
||
- Edge cases (missing positions, partial data)
|
||
|
||
**Test Data**: Uses real API response examples from `app/models/player_model_info.md`
|
||
|
||
### Key Files
|
||
|
||
```
|
||
app/models/player_models.py (516 lines) - Player model implementations
|
||
tests/unit/models/test_player_models.py - Comprehensive unit tests
|
||
app/models/player_model_info.md (540 lines) - API response examples
|
||
```
|
||
|
||
---
|
||
|
||
## Recent Optimizations & Bug Fixes (2025-10-28)
|
||
|
||
### Performance Optimizations - 60% Query Reduction
|
||
|
||
Optimized play resolution to eliminate unnecessary database queries:
|
||
|
||
**Before Optimization**: 5 queries per play
|
||
1. INSERT INTO plays (necessary)
|
||
2. SELECT plays with LEFT JOINs (refresh - unnecessary)
|
||
3. SELECT games (for update - inefficient)
|
||
4. SELECT lineups team 1 (unnecessary - should use cache)
|
||
5. SELECT lineups team 2 (unnecessary - should use cache)
|
||
|
||
**After Optimization**: 2 queries per play (60% reduction)
|
||
1. INSERT INTO plays (necessary)
|
||
2. UPDATE games (necessary, now uses direct UPDATE)
|
||
|
||
**Changes Made**:
|
||
|
||
1. **Lineup Caching** (`app/core/game_engine.py:384-425`)
|
||
- `_prepare_next_play()` now checks `state_manager.get_lineup()` cache first
|
||
- Only fetches from database if not cached
|
||
- Cache persists for entire game lifecycle
|
||
- **Impact**: Eliminates 2 SELECT queries per play
|
||
|
||
2. **Removed Unnecessary Refresh** (`app/database/operations.py:281-302`)
|
||
- `save_play()` no longer calls `session.refresh(play)`
|
||
- Play ID is available after commit without refresh
|
||
- Returns just the ID instead of full Play object with relationships
|
||
- **Impact**: Eliminates 1 SELECT with 3 expensive LEFT JOINs per play
|
||
|
||
3. **Direct UPDATE Statement** (`app/database/operations.py:109-165`)
|
||
- `update_game_state()` now uses direct UPDATE statement
|
||
- No longer does SELECT + modify + commit
|
||
- Uses `result.rowcount` to verify game exists
|
||
- **Impact**: Cleaner code, slightly faster (was already a simple SELECT)
|
||
|
||
4. **Conditional Game State Updates** (`app/core/game_engine.py:200-217`)
|
||
- Only UPDATE games table when score/inning/status actually changes
|
||
- Captures state before/after play resolution and compares
|
||
- Many plays don't change game state (outs without runs, singles without scoring)
|
||
- **Impact**: ~40-60% fewer UPDATE queries (depends on scoring frequency)
|
||
|
||
**Performance Impact**:
|
||
- Typical play resolution: ~50-100ms (down from ~150-200ms)
|
||
- Only necessary write operations remain
|
||
- Scalable for high-throughput gameplay
|
||
- Combined with conditional updates: ~70% fewer queries in low-scoring innings
|
||
|
||
### Critical Bug Fixes
|
||
|
||
#### 1. Fixed outs_before Tracking (`app/core/game_engine.py:551`)
|
||
|
||
**Issue**: Play records had incorrect `outs_before` values due to wrong calculation.
|
||
|
||
**Root Cause**:
|
||
```python
|
||
# WRONG - calculated after outs were applied
|
||
"outs_before": state.outs - result.outs_recorded
|
||
```
|
||
|
||
**Fix**:
|
||
```python
|
||
# CORRECT - captures outs BEFORE applying result
|
||
"outs_before": state.outs
|
||
```
|
||
|
||
**Why It Works**:
|
||
- `_save_play_to_db()` is called in STEP 2 (before result is applied)
|
||
- `_apply_play_result()` is called in STEP 3 (after save)
|
||
- `state.outs` already contains the correct "outs before" value at save time
|
||
|
||
**Impact**: All play records now have accurate out counts for historical analysis.
|
||
|
||
#### 2. Fixed Game Recovery (`app/core/state_manager.py:312-314`)
|
||
|
||
**Issue**: Recovered games crashed with `AttributeError: 'GameState' object has no attribute 'runners'`
|
||
|
||
**Root Cause**: Logging statement tried to access non-existent `state.runners` attribute.
|
||
|
||
**Fix**:
|
||
```python
|
||
# Count runners on base using the correct method
|
||
runners_on_base = len(state.get_all_runners())
|
||
logger.info(f"Rebuilt state for game {state.game_id}: {state.play_count} plays, {runners_on_base} runners")
|
||
```
|
||
|
||
**Impact**: Games can now be properly recovered from database after server restart or REPL exit.
|
||
|
||
### Testing Notes
|
||
|
||
**Integration Tests**:
|
||
- Known issue: Integration tests in `tests/integration/test_game_engine.py` must be run individually
|
||
- Reason: Database connection pooling conflicts when running in parallel
|
||
- Workaround: `uv run pytest tests/integration/test_game_engine.py::TestClassName::test_method -v`
|
||
- All tests pass when run individually
|
||
|
||
**Terminal Client**:
|
||
- Best tool for testing game engine optimizations
|
||
- REPL mode maintains persistent state and event loop
|
||
- See `terminal_client/CLAUDE.md` for usage
|
||
|
||
### Game Models Refactor
|
||
|
||
**Simplified Runner Management** (`app/models/game_models.py`)
|
||
|
||
**Problem**: `RunnerState` class was redundant and created complexity.
|
||
- Had separate `List[RunnerState]` for tracking runners
|
||
- Required list management operations (add, remove, filter)
|
||
- Didn't match database structure (plays table has on_first_id, on_second_id, on_third_id)
|
||
- Extra layer of indirection
|
||
|
||
**Solution**: Direct base references in GameState
|
||
```python
|
||
# Before (redundant)
|
||
runners: List[RunnerState] = Field(default_factory=list)
|
||
|
||
# After (direct)
|
||
on_first: Optional[LineupPlayerState] = None
|
||
on_second: Optional[LineupPlayerState] = None
|
||
on_third: Optional[LineupPlayerState] = None
|
||
```
|
||
|
||
**Benefits**:
|
||
- ✅ Matches database structure exactly
|
||
- ✅ Simpler state management (direct assignment vs list operations)
|
||
- ✅ Better type safety (LineupPlayerState vs generic runner)
|
||
- ✅ Easier to work with in game engine
|
||
- ✅ Fewer lines of code
|
||
|
||
**Updated Methods**:
|
||
- `get_runner_at_base(base: int) -> Optional[LineupPlayerState]` - Returns direct reference
|
||
- `get_all_runners() -> List[Tuple[int, LineupPlayerState]]` - Returns list when needed
|
||
- `is_runner_on_first/second/third()` - Simple `is not None` checks
|
||
|
||
**Impact**: All tests updated and passing. Game engine logic simplified.
|
||
|
||
### Terminal Client Modularization
|
||
|
||
**Problem**: Code duplication between CLI (`main.py`) and REPL (`repl.py`)
|
||
- Same command logic in two places
|
||
- Hard to maintain consistency
|
||
- Difficult to test
|
||
|
||
**Solution**: Modular architecture with shared modules
|
||
|
||
**New Modules Created**:
|
||
|
||
1. **`terminal_client/commands.py`** (10,243 bytes)
|
||
- Shared command functions: `submit_defensive_decision`, `submit_offensive_decision`, `resolve_play`
|
||
- Used by both CLI and REPL
|
||
- Single source of truth for command logic
|
||
- Fully tested independently
|
||
|
||
2. **`terminal_client/arg_parser.py`** (7,280 bytes)
|
||
- Centralized argument parsing and validation
|
||
- Handles defensive/offensive decision arguments
|
||
- Validates formats (alignment, depths, hold runners, steal attempts)
|
||
- Reusable across both interfaces
|
||
|
||
3. **`terminal_client/completions.py`** (10,357 bytes)
|
||
- TAB completion support for REPL mode
|
||
- Command completions, option completions
|
||
- Dynamic completions (game IDs, defensive/offensive options)
|
||
- Improves REPL user experience
|
||
|
||
4. **`terminal_client/help_text.py`** (10,839 bytes)
|
||
- Centralized help text and command documentation
|
||
- Detailed command descriptions and usage examples
|
||
- Consistent help across CLI and REPL
|
||
- Easy to update in one place
|
||
|
||
**Benefits**:
|
||
- ✅ DRY principle - no code duplication
|
||
- ✅ Behavior consistent between CLI and REPL modes
|
||
- ✅ Easier to maintain (changes in one place)
|
||
- ✅ Better testability (modules tested independently)
|
||
- ✅ Clear separation of concerns
|
||
- ✅ Improved user experience (completions, better help)
|
||
|
||
**Test Coverage**:
|
||
- `tests/unit/terminal_client/test_commands.py`
|
||
- `tests/unit/terminal_client/test_arg_parser.py`
|
||
- `tests/unit/terminal_client/test_completions.py`
|
||
- `tests/unit/terminal_client/test_help_text.py`
|
||
|
||
**File Structure**:
|
||
```
|
||
terminal_client/
|
||
├── __init__.py
|
||
├── main.py # CLI entry point (simplified)
|
||
├── repl.py # REPL mode (simplified)
|
||
├── display.py # Display formatting
|
||
├── config.py # Configuration
|
||
├── commands.py # NEW - Shared command logic
|
||
├── arg_parser.py # NEW - Argument parsing
|
||
├── completions.py # NEW - TAB completions
|
||
└── help_text.py # NEW - Help documentation
|
||
```
|
||
|
||
### Summary of 2025-10-28 Updates
|
||
|
||
**Total Changes**:
|
||
- 36 files modified/created
|
||
- +9,034 lines added
|
||
- -645 lines removed
|
||
- 2 git commits
|
||
|
||
**Major Improvements**:
|
||
|
||
1. **Player Models** (Week 6 - 50% Complete)
|
||
- BasePlayer, SbaPlayer, PdPlayer with factory methods
|
||
- 32 comprehensive tests (all passing)
|
||
- Single-layer architecture (simpler than planned two-layer)
|
||
- Ready for API integration
|
||
|
||
2. **Performance Optimizations**
|
||
- 60-70% database query reduction
|
||
- Lineup caching eliminates redundant SELECTs
|
||
- Conditional updates only when state changes
|
||
- ~50-100ms play resolution (was ~150-200ms)
|
||
|
||
3. **Model Simplification**
|
||
- Removed redundant RunnerState class
|
||
- Direct base references match DB structure
|
||
- Cleaner game engine logic
|
||
|
||
4. **Terminal Client Enhancement**
|
||
- Modularized into 4 shared modules
|
||
- DRY principle - no duplication between CLI/REPL
|
||
- TAB completions for better UX
|
||
- Comprehensive test coverage
|
||
|
||
5. **Bug Fixes**
|
||
- outs_before tracking corrected
|
||
- Game recovery AttributeError fixed
|
||
- Enhanced status display with action guidance
|
||
|
||
**Test Status**:
|
||
- All existing tests passing
|
||
- 32 new player model tests
|
||
- 4 new terminal client test suites
|
||
- Integration tests verified
|
||
|
||
**Documentation**:
|
||
- Player models documented in CLAUDE.md
|
||
- Week 6 status assessment created
|
||
- Terminal client modularization documented
|
||
- Architecture decisions explained
|
||
|
||
**Next Priorities** (Week 6 Remaining):
|
||
1. Configuration system (BaseConfig, SbaConfig, PdConfig)
|
||
2. Result charts & PD play resolution with ratings
|
||
3. API client (deferred for now)
|
||
|
||
---
|
||
|
||
## 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 - Week 6 (Player Models & League Integration)
|
||
|
||
**Completion Status**:
|
||
- ✅ Phase 1 (Infrastructure): Complete (2025-10-21)
|
||
- ✅ Week 4 (State Management): Complete (2025-10-22)
|
||
- ✅ Week 5 (Game Logic): Complete (2025-10-26)
|
||
- Game engine orchestration
|
||
- Play resolver with dice system
|
||
- Full at-bat flow working
|
||
- Terminal client for testing
|
||
- 🟡 Week 6 (Player Models & League Features): ~50% Complete (2025-10-28)
|
||
- ✅ Player models (BasePlayer, SbaPlayer, PdPlayer)
|
||
- ✅ Factory methods for API parsing
|
||
- ✅ Comprehensive test coverage (32/32 tests passing)
|
||
- ⏳ Configuration system (not started)
|
||
- ⏳ Result charts & PD integration (not started)
|
||
- ⏳ API client (deferred)
|
||
|
||
---
|
||
|
||
## Week 6: League Configuration & Play Outcome System (2025-10-28)
|
||
|
||
**Status**: 75% Complete
|
||
**Phase**: Phase 2 - Week 6 (League Features & Integration)
|
||
|
||
### Overview
|
||
|
||
Implemented foundational configuration and outcome systems for both SBA and PD leagues, establishing the framework for card-based play resolution.
|
||
|
||
### Components Implemented
|
||
|
||
#### 1. League Configuration System ✅
|
||
|
||
**Location**: `app/config/`
|
||
|
||
Provides immutable, league-specific configuration objects for game rules and API endpoints.
|
||
|
||
**Files Created**:
|
||
- `app/config/base_config.py` - Abstract base configuration
|
||
- `app/config/league_configs.py` - SBA and PD implementations
|
||
- `app/config/__init__.py` - Public API
|
||
- `tests/unit/config/test_league_configs.py` - 28 tests (all passing)
|
||
|
||
**Key Features**:
|
||
```python
|
||
from app.config import get_league_config
|
||
|
||
# Get league-specific config
|
||
config = get_league_config("sba")
|
||
api_url = config.get_api_base_url() # "https://api.sba.manticorum.com"
|
||
chart = config.get_result_chart_name() # "sba_standard_v1"
|
||
|
||
# Configs are immutable (frozen=True)
|
||
# config.innings = 7 # Raises ValidationError
|
||
```
|
||
|
||
**League Differences**:
|
||
- **SBA**: Manual result selection, simple player data
|
||
- **PD**: Flexible selection (manual or auto), detailed scouting data, cardset validation, advanced analytics
|
||
|
||
**Config Registry**:
|
||
```python
|
||
LEAGUE_CONFIGS = {
|
||
"sba": SbaConfig(),
|
||
"pd": PdConfig()
|
||
}
|
||
```
|
||
|
||
#### 2. PlayOutcome Enum ✅
|
||
|
||
**Location**: `app/config/result_charts.py`
|
||
|
||
Universal enum defining all possible play outcomes for both leagues.
|
||
|
||
**Files Created**:
|
||
- `app/config/result_charts.py` - PlayOutcome enum with helpers
|
||
- `tests/unit/config/test_play_outcome.py` - 30 tests (all passing)
|
||
|
||
**Outcome Categories**:
|
||
```python
|
||
from app.config import PlayOutcome
|
||
|
||
# Standard outcomes
|
||
PlayOutcome.STRIKEOUT
|
||
PlayOutcome.SINGLE
|
||
PlayOutcome.HOMERUN
|
||
|
||
# Uncapped hits (pitching cards only)
|
||
PlayOutcome.SINGLE_UNCAPPED # si(cf) - triggers advancement decisions
|
||
PlayOutcome.DOUBLE_UNCAPPED # do(cf) - triggers advancement decisions
|
||
|
||
# Interrupt plays (logged with pa=0)
|
||
PlayOutcome.WILD_PITCH # Play.wp = 1
|
||
PlayOutcome.PASSED_BALL # Play.pb = 1
|
||
PlayOutcome.STOLEN_BASE # Play.sb = 1
|
||
PlayOutcome.CAUGHT_STEALING # Play.cs = 1
|
||
|
||
# Ballpark power (PD specific)
|
||
PlayOutcome.BP_HOMERUN # Play.bphr = 1
|
||
PlayOutcome.BP_SINGLE # Play.bp1b = 1
|
||
```
|
||
|
||
**Helper Methods**:
|
||
```python
|
||
outcome = PlayOutcome.SINGLE_UNCAPPED
|
||
|
||
outcome.is_hit() # True
|
||
outcome.is_out() # False
|
||
outcome.is_uncapped() # True - requires decision tree
|
||
outcome.is_interrupt() # False
|
||
outcome.get_bases_advanced() # 1
|
||
```
|
||
|
||
#### 3. Card-Based Resolution System
|
||
|
||
**Resolution Mechanics** (Both SBA and PD):
|
||
1. Roll 1d6 → determines column (1-3: batter card, 4-6: pitcher card)
|
||
2. Roll 2d6 → selects row 2-12 on that card
|
||
3. Roll 1d20 → resolves split results (e.g., 1-16: HR, 17-20: 2B)
|
||
4. Outcome from card = `PlayOutcome` enum value
|
||
|
||
**League Differences**:
|
||
- **PD**: Card data digitized in `PdBattingRating`/`PdPitchingRating`
|
||
- Can auto-resolve using probabilities
|
||
- OR manual selection by players
|
||
- **SBA**: Physical cards only (not digitized)
|
||
- Players read physical cards and manually enter outcomes
|
||
- System validates and records the human-selected outcome
|
||
|
||
### Testing
|
||
|
||
**Test Coverage**:
|
||
- 28 config tests (league configs, registry, immutability)
|
||
- 30 PlayOutcome tests (helpers, categorization, edge cases)
|
||
- **Total: 58 tests, all passing**
|
||
|
||
**Test Files**:
|
||
```
|
||
tests/unit/config/
|
||
├── test_league_configs.py # Config system tests
|
||
└── test_play_outcome.py # Outcome enum tests
|
||
```
|
||
|
||
### Architecture Decisions
|
||
|
||
**1. Immutable Configs**
|
||
- Used Pydantic `frozen=True` to prevent accidental modification
|
||
- Configs are singletons in registry
|
||
- Type-safe with full validation
|
||
|
||
**2. Universal PlayOutcome Enum**
|
||
- Single source of truth for all possible outcomes
|
||
- Works for both SBA and PD leagues
|
||
- Helper methods reduce duplicate logic in resolvers
|
||
|
||
**3. No Static Result Charts**
|
||
- Originally planned d20 charts for SBA
|
||
- Realized both leagues use same card-based mechanics
|
||
- Charts come from player card data (PD) or manual entry (SBA)
|
||
|
||
### Integration Points
|
||
|
||
**With Game Engine**:
|
||
```python
|
||
from app.config import get_league_config, PlayOutcome
|
||
|
||
# Get league config
|
||
config = get_league_config(state.league_id)
|
||
|
||
# Resolve outcome (from card or manual entry)
|
||
outcome = PlayOutcome.SINGLE_UNCAPPED
|
||
|
||
# Handle based on outcome type
|
||
if outcome.is_uncapped() and state.on_base_code > 0:
|
||
# Trigger advancement decision tree
|
||
present_advancement_options()
|
||
elif outcome.is_interrupt():
|
||
# Log interrupt play (pa=0)
|
||
log_interrupt_play(outcome)
|
||
else:
|
||
# Standard play resolution
|
||
resolve_standard_play(outcome)
|
||
```
|
||
|
||
### Remaining Work (Week 6)
|
||
|
||
**1. Dice System Update** ⏳
|
||
- Rename `check_d20` → `chaos_d20` in AbRoll
|
||
- Update all references
|
||
|
||
**2. PlayResolver Integration** ⏳
|
||
- Replace old `PlayOutcome` enum with new one
|
||
- Use `PlayOutcome` throughout resolution logic
|
||
- Handle uncapped hit decision trees
|
||
|
||
**3. Play.metadata Support** ⏳
|
||
- Add JSON metadata field for uncapped hit tracking
|
||
- Log `{"uncapped": true}` when applicable
|
||
|
||
### Key Files
|
||
|
||
```
|
||
app/config/
|
||
├── __init__.py # Public API
|
||
├── base_config.py # Abstract base config
|
||
├── league_configs.py # SBA/PD implementations
|
||
└── result_charts.py # PlayOutcome enum
|
||
|
||
tests/unit/config/
|
||
├── test_league_configs.py # 28 tests
|
||
└── test_play_outcome.py # 30 tests
|
||
```
|
||
|
||
---
|
||
|
||
**Next Priorities**:
|
||
1. Update dice system (chaos_d20)
|
||
2. Integrate PlayOutcome into PlayResolver
|
||
3. Add Play.metadata support for uncapped hits
|
||
4. Complete week 6 remaining work
|
||
|
||
**Python Version**: 3.13.3
|
||
**Database Server**: 10.10.0.42:5432
|
||
|
||
**Implementation Status**: Phase 3E-Final ✅ Complete (WebSocket handlers, Position ratings, Statistics system)
|
||
|
||
## 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
|
||
|
||
## Lineup Polymorphic Migration (2025-10-23)
|
||
|
||
Updated `Lineup` model to support both PD and SBA leagues using polymorphic `card_id`/`player_id` fields, matching the `RosterLink` pattern.
|
||
|
||
### Changes:
|
||
- ✅ Made `card_id` nullable (PD league)
|
||
- ✅ Added `player_id` nullable (SBA league)
|
||
- ✅ Added XOR CHECK constraint: exactly one ID must be populated
|
||
- ✅ Created league-specific methods: `add_pd_lineup_card()` and `add_sba_lineup_player()`
|
||
- ✅ Fixed Pendulum DateTime + asyncpg compatibility issue with `.naive()`
|
||
|
||
### Archived Files:
|
||
- Migration documentation: `../../.claude/archive/LINEUP_POLYMORPHIC_MIGRATION.md`
|
||
- Migration script: `../../.claude/archive/migrate_lineup_schema.py`
|
||
|
||
**Note**: Migration has been applied to database. Script archived for reference only.
|
||
|
||
## Phase 3B: X-Check League Config Tables (2025-11-01)
|
||
|
||
Implemented complete X-Check resolution table system for defensive play outcomes.
|
||
|
||
**Status**: ✅ Complete
|
||
|
||
### Components Implemented
|
||
|
||
1. **Defense Range Tables** (`app/config/common_x_check_tables.py`)
|
||
- Complete 20×5 tables for infield, outfield, and catcher positions
|
||
- Maps d20 roll × defense range (1-5) → result code
|
||
- Result codes: G1-G3, G2#/G3# (holding), SI1-SI2, F1-F3, DO2-DO3, TR3, SPD, FO, PO
|
||
|
||
2. **Error Charts** (3d6 by error rating 0-25)
|
||
- ✅ Complete: LF/RF and CF error charts (26 ratings each)
|
||
- ⏳ Placeholders: P, C, 1B, 2B, 3B, SS (empty dicts awaiting data)
|
||
- Error types: RP (replay), E1-E3 (severity), NO (no error)
|
||
|
||
3. **Helper Functions**
|
||
- `get_fielders_holding_runners(runner_bases, batter_handedness)` - Complete implementation
|
||
- Tracks all fielders holding runners by position
|
||
- R1: 1B + middle infielder (2B for RHB, SS for LHB)
|
||
- R2: Middle infielder (if not already added)
|
||
- R3: 3B
|
||
- `get_error_chart_for_position(position)` - Maps all 9 positions to error charts
|
||
|
||
4. **League Config Integration** (`app/config/league_configs.py`)
|
||
- Both SbaConfig and PdConfig include X-Check tables
|
||
- Attributes: `x_check_defense_tables`, `x_check_error_charts`, `x_check_holding_runners`
|
||
- Shared common tables for both leagues
|
||
|
||
5. **X-Check Placeholder Functions** (`app/core/runner_advancement.py`)
|
||
- 6 placeholder functions: `x_check_g1`, `x_check_g2`, `x_check_g3`, `x_check_f1`, `x_check_f2`, `x_check_f3`
|
||
- All return valid `AdvancementResult` structures
|
||
- Ready for Phase 3C implementation
|
||
|
||
### Test Coverage
|
||
|
||
- ✅ 36 tests for X-Check tables (`tests/unit/config/test_x_check_tables.py`)
|
||
- Defense table dimensions and valid result codes
|
||
- Error chart structure validation
|
||
- Helper function behavior
|
||
- Integration workflows
|
||
- ✅ 9 tests for X-Check placeholders (`tests/unit/core/test_runner_advancement.py`)
|
||
- Function signatures and return types
|
||
- Error type acceptance
|
||
- On-base code support
|
||
|
||
**Total**: 45/45 tests passing
|
||
|
||
### What's Pending
|
||
|
||
**Infield Error Charts** - 6 positions awaiting actual data:
|
||
- PITCHER_ERROR_CHART
|
||
- CATCHER_ERROR_CHART
|
||
- FIRST_BASE_ERROR_CHART
|
||
- SECOND_BASE_ERROR_CHART
|
||
- THIRD_BASE_ERROR_CHART
|
||
- SHORTSTOP_ERROR_CHART
|
||
|
||
Once data is provided, these empty dicts will be populated with the same structure as outfield charts.
|
||
|
||
### Next Phase
|
||
|
||
✅ **COMPLETED** - Phase 3C implemented full defensive play resolution
|
||
|
||
---
|
||
|
||
## Phase 3C: X-Check Resolution Logic (2025-11-02)
|
||
|
||
Implemented complete X-Check resolution system in PlayResolver with full integration of Phase 3B tables.
|
||
|
||
**Status**: ✅ Complete
|
||
|
||
### Components Implemented
|
||
|
||
1. **Main Resolution Method** (`_resolve_x_check()` in `app/core/play_resolver.py`)
|
||
- 10-step resolution process from dice rolls to final outcome
|
||
- Rolls 1d20 for defense table + 3d6 for error chart
|
||
- Adjusts range if defender playing in
|
||
- Looks up base result from defense table
|
||
- Applies SPD test if needed (placeholder)
|
||
- Converts G2#/G3# to SI2 based on conditions
|
||
- Looks up error result from error chart
|
||
- Determines final outcome with error overrides
|
||
- Creates XCheckResult audit trail
|
||
- Returns PlayResult with full details
|
||
|
||
2. **Helper Methods** (6 new methods in PlayResolver)
|
||
- `_adjust_range_for_defensive_position()` - Range +1 if playing in (max 5)
|
||
- `_lookup_defense_table()` - Maps d20 + range → result code
|
||
- `_apply_hash_conversion()` - G2#/G3# → SI2 if playing in OR holding runner
|
||
- `_lookup_error_chart()` - Maps 3d6 + error rating → error type
|
||
- `_determine_final_x_check_outcome()` - Maps result + error → PlayOutcome
|
||
|
||
3. **Integration Points**
|
||
- Added X_CHECK case to `resolve_outcome()` method
|
||
- Extended PlayResult dataclass with `x_check_details: Optional[XCheckResult]`
|
||
- Imported all Phase 3B tables: INFIELD/OUTFIELD/CATCHER defense tables
|
||
- Imported helper functions: `get_error_chart_for_position()`, `get_fielders_holding_runners()`
|
||
|
||
### Key Features
|
||
|
||
**Defense Table Lookup**:
|
||
- Selects correct table based on position (infield/outfield/catcher)
|
||
- 0-indexed lookup: `table[d20_roll - 1][defense_range - 1]`
|
||
- Returns result codes: G1-G3, G2#/G3#, F1-F3, SI1-SI2, DO2-DO3, TR3, SPD, FO, PO
|
||
|
||
**Range Adjustment**:
|
||
- Corners in: +1 range for 1B, 3B, P, C
|
||
- Infield in: +1 range for 1B, 2B, 3B, SS, P, C
|
||
- Maximum range capped at 5
|
||
|
||
**Hash Conversion Logic**:
|
||
```python
|
||
G2# or G3# → SI2 if:
|
||
a) Playing in (adjusted_range > base_range), OR
|
||
b) Holding runner (position in holding_positions list)
|
||
Otherwise: G2# → G2, G3# → G3
|
||
```
|
||
|
||
**Error Chart Lookup**:
|
||
- Priority order: RP > E3 > E2 > E1 > NO
|
||
- Uses 3d6 sum (3-18) against defender's error rating
|
||
- Returns: 'RP', 'E3', 'E2', 'E1', or 'NO'
|
||
|
||
**Final Outcome Determination**:
|
||
```python
|
||
If error_result == 'NO':
|
||
outcome = base_outcome, hit_type = "{result}_no_error"
|
||
|
||
If error_result == 'RP':
|
||
outcome = ERROR, hit_type = "{result}_rare_play"
|
||
|
||
If error_result in ['E1', 'E2', 'E3']:
|
||
If base_outcome is out:
|
||
outcome = ERROR # Error overrides
|
||
Else:
|
||
outcome = base_outcome # Hit + error keeps hit
|
||
hit_type = "{result}_plus_error_{n}"
|
||
```
|
||
|
||
### Phase 3E Integration Status
|
||
|
||
1. **Defender Retrieval** - ✅ **COMPLETE** (Phase 3E-Main)
|
||
- GameState.get_defender_for_position() implemented
|
||
- Uses StateManager lineup cache for O(1) lookups
|
||
- Position ratings integrated from PD API
|
||
- Falls back to defaults (range=3, error=15) for SBA or missing ratings
|
||
|
||
2. **SPD Test** - ⏳ **PENDING** (Phase 3E-Final or F6)
|
||
- Currently defaults to G3 fail
|
||
- Needs batter speed rating in player model
|
||
- Line 715 in play_resolver.py
|
||
|
||
3. **Batter Handedness** - ⏳ **PENDING** (Future enhancement)
|
||
- Currently hardcoded to 'R'
|
||
- Needs player model handedness field
|
||
|
||
4. **Runner Advancement** - ✅ **COMPLETE** (Phase 3D)
|
||
- Full X-Check advancement tables implemented
|
||
- Groundball and flyball advancement working
|
||
- 59 X-Check advancement tests passing
|
||
|
||
### Testing
|
||
|
||
**Test Coverage**:
|
||
- ✅ All 9 PlayResolver tests passing
|
||
- ✅ All 36 X-Check table tests passing
|
||
- ✅ All 51 runner advancement tests passing
|
||
- ✅ 325/327 total tests passing (99.4%)
|
||
- ⚠️ 2 pre-existing failures (unrelated: dice history, config URL)
|
||
|
||
### Files Modified
|
||
|
||
```
|
||
app/core/play_resolver.py (+397 lines, -2 lines)
|
||
- Added X_CHECK resolution case
|
||
- Added 6 helper methods (397 lines)
|
||
- Extended PlayResult with x_check_details
|
||
- Imported Phase 3B tables and helpers
|
||
```
|
||
|
||
### Next Phase
|
||
|
||
**Phase 3D**: X-Check Runner Advancement Tables
|
||
- Implement groundball advancement (G1, G2, G3)
|
||
- Implement flyball advancement (F1, F2, F3)
|
||
- Implement hit advancement with errors (SI1, SI2, DO2, DO3, TR3)
|
||
- Implement out advancement with errors (FO, PO)
|
||
- Fill in placeholder `_get_x_check_advancement()` method
|
||
|
||
---
|
||
|
||
## Phase 3E-Main: Position Ratings Integration (2025-11-03)
|
||
|
||
Integrated position ratings system enabling X-Check defensive plays to use actual player ratings from PD API with intelligent fallbacks for SBA.
|
||
|
||
**Status**: ✅ Complete - Live API verified with player 8807
|
||
|
||
### Components Implemented
|
||
|
||
1. **PD API Client** (`app/services/pd_api_client.py`)
|
||
- Endpoint: `GET /api/v2/cardpositions?player_id={id}&position={pos}`
|
||
- Async HTTP client using httpx
|
||
- Optional position filtering: `get_position_ratings(8807, ['SS', '2B'])`
|
||
- Returns `List[PositionRating]` for all positions
|
||
- Handles both list and dict response formats
|
||
- Comprehensive error handling
|
||
|
||
2. **Position Rating Service** (`app/services/position_rating_service.py`)
|
||
- In-memory caching (16,601x performance improvement)
|
||
- `get_ratings_for_card(card_id, league_id)` - All positions
|
||
- `get_rating_for_position(card_id, position, league_id)` - Specific position
|
||
- Singleton pattern: `position_rating_service` instance
|
||
- TODO Phase 3E-Final: Upgrade to Redis
|
||
|
||
3. **GameState Integration** (`app/models/game_models.py`)
|
||
- LineupPlayerState: Added `position_rating` field (Optional[PositionRating])
|
||
- GameState: Added `get_defender_for_position(position, state_manager)` method
|
||
- Uses StateManager's lineup cache to find active defender
|
||
- No database lookups during play resolution
|
||
|
||
4. **League Configuration** (`app/config/league_configs.py`)
|
||
- SbaConfig: `supports_position_ratings()` → False
|
||
- PdConfig: `supports_position_ratings()` → True
|
||
- Enables league-specific behavior without hardcoded conditionals
|
||
|
||
5. **PlayResolver Integration** (`app/core/play_resolver.py`)
|
||
- Added `state_manager` parameter to constructor
|
||
- `_resolve_x_check()`: Replaced placeholder ratings with actual lookup
|
||
- Uses league config check: `config.supports_position_ratings()`
|
||
- Falls back to defaults (range=3, error=15) if unavailable
|
||
|
||
6. **Game Start Rating Loader** (`app/core/game_engine.py`)
|
||
- `_load_position_ratings_for_lineup()` method
|
||
- Loads all position ratings at game start for PD league
|
||
- Skips loading for SBA (league config check)
|
||
- Called in `start_game()` for both teams
|
||
- Logs: "Loaded X/9 position ratings for team Y"
|
||
|
||
### Live API Testing
|
||
|
||
**Verified with Player 8807** (7 positions):
|
||
```
|
||
Position Range Error Innings
|
||
CF 3 2 372
|
||
2B 3 8 212
|
||
SS 4 12 159
|
||
RF 2 2 74
|
||
LF 3 2 62
|
||
1B 4 0 46
|
||
3B 3 65 34
|
||
```
|
||
|
||
**Performance**:
|
||
- API call: 0.214s
|
||
- Cache hit: 0.000s
|
||
- Speedup: 16,601x
|
||
|
||
### X-Check Resolution Flow
|
||
|
||
1. Check league config: `supports_position_ratings()`?
|
||
2. Get defender: `state.get_defender_for_position(pos, state_manager)`
|
||
3. If PD + `defender.position_rating` exists: Use actual range/error
|
||
4. Else if defender found: Use defaults (range=3, error=15)
|
||
5. Else: Log warning, use defaults
|
||
|
||
### Testing
|
||
|
||
**Live Integration**:
|
||
- ✅ Real API: Player 8807 → 7 positions retrieved
|
||
- ✅ Caching: 16,601x performance improvement
|
||
- ✅ League configs: SBA skips API, PD fetches ratings
|
||
- ✅ GameState: Defender lookup working
|
||
- ✅ Existing tests: 27/28 config tests passing
|
||
|
||
**Test Files Created**:
|
||
- `test_pd_api_live.py` - Live API integration test
|
||
- `test_pd_api_mock.py` - Mock test for CI/CD
|
||
- `tests/integration/test_position_ratings_api.py` - Pytest suite
|
||
|
||
### Files Created/Modified
|
||
|
||
**Created**:
|
||
```
|
||
app/services/__init__.py - Package exports
|
||
app/services/pd_api_client.py - PD API client (97 lines)
|
||
app/services/position_rating_service.py - Caching service (120 lines)
|
||
```
|
||
|
||
**Modified**:
|
||
```
|
||
app/models/game_models.py - Added position_rating field, get_defender_for_position()
|
||
app/config/league_configs.py - Added supports_position_ratings()
|
||
app/core/play_resolver.py - Integrated actual ratings lookup
|
||
app/core/game_engine.py - Load ratings at game start
|
||
```
|
||
|
||
### Key Features
|
||
|
||
**League-Aware Behavior**:
|
||
- PD: Fetches ratings from API with caching
|
||
- SBA: Skips API calls, uses defaults
|
||
|
||
**Self-Contained GameState**:
|
||
- All X-Check data in memory (no lookups during resolution)
|
||
- Direct access: `defender.position_rating.range`
|
||
|
||
**Graceful Degradation**:
|
||
- API unavailable → Use defaults
|
||
- Player has no rating → Use defaults
|
||
- Defaults: range=3 (average), error=15 (average)
|
||
|
||
## Phase 3E-Final: WebSocket Event Handlers (2025-01-10)
|
||
|
||
Completed WebSocket event handlers enabling real-time gameplay communication between frontend and backend.
|
||
|
||
**Status**: ✅ **COMPLETE**
|
||
|
||
### Components Implemented
|
||
|
||
**New Event Handlers** (3 added to complete the system):
|
||
1. `submit_defensive_decision` - Receive defensive strategy from client
|
||
2. `submit_offensive_decision` - Receive offensive strategy from client
|
||
3. `get_box_score` - Return box score data from materialized views
|
||
|
||
**Previously Completed Handlers**:
|
||
- `roll_dice` / `submit_manual_outcome` - Manual outcome workflow
|
||
- Substitution handlers (3) - Player substitutions
|
||
- `get_lineup` - Lineup retrieval
|
||
|
||
**Total**: 15 WebSocket event handlers implemented
|
||
|
||
### Testing
|
||
|
||
**Test Results**: 730/731 passing (99.9%)
|
||
- All WebSocket handlers passing
|
||
- No regressions introduced
|
||
- 1 pre-existing failure in terminal_client (unrelated)
|
||
|
||
### Documentation
|
||
|
||
See `app/websocket/CLAUDE.md` for complete event handler documentation and `.claude/WEBSOCKET_HANDLERS_COMPLETE.md` for implementation details.
|
||
|
||
### Next Phase
|
||
|
||
**Frontend Implementation**: Vue 3 + Nuxt 3 client with Socket.io integration
|
||
- WebSocket connection manager
|
||
- Reactive game state (Pinia)
|
||
- UI components for gameplay
|
||
- Event handling and broadcasting
|
||
|
||
---
|
||
|
||
**Updated**: 2025-01-10
|
||
**Total Unit Tests**: 730 passing (99.9%)
|
||
**Live API**: Verified with PD player 8807 |