CLAUDE: Initialize Paper Dynasty web app with Model/Service Architecture
Establishes foundation for migrating baseball simulation from Discord bot to web application using service-oriented architecture pattern. Key components: - FastAPI application structure with dependency injection - Service layer foundation with base classes and container - Comprehensive directory documentation with README files - PostgreSQL containerization with Docker Compose - Testing structure for unit/integration/e2e tests - Migration planning documentation - Rotating log configuration per user requirements Architecture follows Model/Service/Controller pattern to improve testability, maintainability, and scalability over original monolithic Discord app. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a2042401a7
commit
c09f9d1302
235
.claude/model-migration-plan.md
Normal file
235
.claude/model-migration-plan.md
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
# Model Migration Plan: Data Models + Business Logic Extraction
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document tracks the migration of models from Discord app to web app, with careful separation of data models from business logic. All extracted business logic is tracked to ensure proper implementation in services.
|
||||||
|
|
||||||
|
## Migration Principle
|
||||||
|
|
||||||
|
**Models = Pure Data | Services = Business Logic**
|
||||||
|
|
||||||
|
- Models: Field definitions, relationships, basic validators
|
||||||
|
- Services: Complex logic, UI formatting, game management, AI decisions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Foundation Data Models
|
||||||
|
|
||||||
|
### 1. `ManagerAi` - AI Configuration Data
|
||||||
|
|
||||||
|
**Model Migration**:
|
||||||
|
- ✅ Keep: AI parameter fields (steal, running, hold, etc.)
|
||||||
|
- ✅ Keep: Database relationships
|
||||||
|
- ❌ Remove: All decision methods
|
||||||
|
|
||||||
|
**Business Logic to Extract**:
|
||||||
|
|
||||||
|
| Original Method | Target Service | New Method | Status |
|
||||||
|
|-----------------|---------------|------------|---------|
|
||||||
|
| `check_jump()` | AIService | `check_steal_opportunity()` | 📋 TODO |
|
||||||
|
| `tag_from_second()` | AIService | `check_tag_from_second()` | 📋 TODO |
|
||||||
|
| `tag_from_third()` | AIService | `check_tag_from_third()` | 📋 TODO |
|
||||||
|
| `throw_at_uncapped()` | AIService | `decide_throw_target()` | 📋 TODO |
|
||||||
|
| `uncapped_advance()` | AIService | `decide_runner_advance()` | 📋 TODO |
|
||||||
|
| `defense_alignment()` | AIService | `set_defensive_alignment()` | 📋 TODO |
|
||||||
|
| `gb_decide_run()` | AIService | `decide_groundball_running()` | 📋 TODO |
|
||||||
|
| `gb_decide_throw()` | AIService | `decide_groundball_throw()` | 📋 TODO |
|
||||||
|
| `replace_pitcher()` | AIService | `should_replace_pitcher()` | 📋 TODO |
|
||||||
|
|
||||||
|
### 2. `Cardset` - Card Set Metadata
|
||||||
|
|
||||||
|
**Model Migration**:
|
||||||
|
- ✅ Keep: Basic metadata (id, name, ranked_legal)
|
||||||
|
- ✅ Keep: Relationships to games/players
|
||||||
|
|
||||||
|
**Business Logic to Extract**: None (pure data model)
|
||||||
|
|
||||||
|
### 3. `Team` - Team Identity Data
|
||||||
|
|
||||||
|
**Model Migration**:
|
||||||
|
- ✅ Keep: Team data fields (abbrev, names, wallet, etc.)
|
||||||
|
- ✅ Keep: Simple `description` property
|
||||||
|
- ❌ Remove: `embed` property (Discord UI)
|
||||||
|
|
||||||
|
**Business Logic to Extract**:
|
||||||
|
|
||||||
|
| Original Method/Property | Target Service | New Method | Status |
|
||||||
|
|-------------------------|---------------|------------|---------|
|
||||||
|
| `embed` property | UIService | `format_team_display()` | 📋 TODO |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Player and Card Data
|
||||||
|
|
||||||
|
### 4. `Player` - Player Metadata
|
||||||
|
|
||||||
|
**Model Migration**:
|
||||||
|
- ✅ Keep: Player metadata (name, cost, positions, etc.)
|
||||||
|
- ✅ Keep: Simple `name_with_desc` property
|
||||||
|
- ❌ Remove: `name_card_link()` method (Discord markdown)
|
||||||
|
|
||||||
|
**Business Logic to Extract**:
|
||||||
|
|
||||||
|
| Original Method/Property | Target Service | New Method | Status |
|
||||||
|
|-------------------------|---------------|------------|---------|
|
||||||
|
| `name_card_link()` | UIService | `format_player_link()` | 📋 TODO |
|
||||||
|
| `batter_card_url` | UIService | `get_batter_card_image()` | 📋 TODO |
|
||||||
|
| `pitcher_card_url` | UIService | `get_pitcher_card_image()` | 📋 TODO |
|
||||||
|
|
||||||
|
### 5-8. Card and Rating Models
|
||||||
|
|
||||||
|
**Models**: `BattingCard`, `PitchingCard`, `BattingRatings`, `PitchingRatings`, `BatterScouting`, `PitcherScouting`, `Card`, `PositionRating`
|
||||||
|
|
||||||
|
**Migration**: Pure data models - no business logic extraction needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Game Structure (Critical)
|
||||||
|
|
||||||
|
### 9. `Game` - Game State and Management ⭐
|
||||||
|
|
||||||
|
**Model Migration**:
|
||||||
|
- ✅ Keep: Game metadata (teams, season, settings)
|
||||||
|
- ✅ Keep: Database relationships
|
||||||
|
- ❌ Remove: `channel_id` (Discord-specific)
|
||||||
|
- ❌ Extract: All business logic methods
|
||||||
|
|
||||||
|
**Business Logic to Extract**:
|
||||||
|
|
||||||
|
| Original Method/Property | Target Service | New Method | Status |
|
||||||
|
|-------------------------|---------------|------------|---------|
|
||||||
|
| `initialize_play()` | GameService | `initialize_game()` | 📋 TODO |
|
||||||
|
| `current_play_or_none()` | GameService | `get_current_play()` | 📋 TODO |
|
||||||
|
| `team_lineup()` | GameService | `format_team_lineup()` | 📋 TODO |
|
||||||
|
| `cardset_param_string` | GameService | `build_cardset_params()` | 📋 TODO |
|
||||||
|
| `human_team` property | GameService | `get_human_team()` | 📋 TODO |
|
||||||
|
| `league_name` property | GameService | `get_league_name()` | 📋 TODO |
|
||||||
|
|
||||||
|
**Critical Implementation Notes**:
|
||||||
|
- `initialize_play()` contains complex game setup logic (lines 171-230)
|
||||||
|
- `team_lineup()` handles lineup formatting with Discord links
|
||||||
|
- Game state management methods are core to gameplay
|
||||||
|
|
||||||
|
### 10-11. Link Models
|
||||||
|
|
||||||
|
**Models**: `GameCardsetLink`, `RosterLink`, `Lineup`
|
||||||
|
|
||||||
|
**Migration**: Pure relationship models - no extraction needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: Gameplay State (Heaviest Extraction)
|
||||||
|
|
||||||
|
### 12. `Play` - Gameplay State and Statistics ⭐
|
||||||
|
|
||||||
|
**Model Migration**:
|
||||||
|
- ✅ Keep: Game state data (scores, outs, runners)
|
||||||
|
- ✅ Keep: Statistical tracking fields
|
||||||
|
- ✅ Keep: Relationships to lineups/game
|
||||||
|
- ❌ Extract: All computed properties and methods
|
||||||
|
|
||||||
|
**Business Logic to Extract**:
|
||||||
|
|
||||||
|
| Original Method/Property | Target Service | New Method | Status |
|
||||||
|
|-------------------------|---------------|------------|---------|
|
||||||
|
| `init_ai()` | GameplayService | `initialize_play_ai()` | 📋 TODO |
|
||||||
|
| `ai_run_diff` property | GameplayService | `calculate_run_differential()` | 📋 TODO |
|
||||||
|
| `ai_is_batting` property | GameplayService | `is_ai_batting()` | 📋 TODO |
|
||||||
|
| `could_walkoff` property | GameplayService | `check_walkoff_situation()` | 📋 TODO |
|
||||||
|
| `scorebug_ascii` property | UIService | `format_scoreboard()` | 📋 TODO |
|
||||||
|
|
||||||
|
**Critical Implementation Notes**:
|
||||||
|
- `scorebug_ascii` creates ASCII scoreboard display (lines 1223-1243)
|
||||||
|
- AI integration properties are used throughout gameplay
|
||||||
|
- Statistical calculations affect game flow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## New Web-Specific Models to Add
|
||||||
|
|
||||||
|
### Web Session and User Management
|
||||||
|
|
||||||
|
| Model | Purpose | Status |
|
||||||
|
|-------|---------|---------|
|
||||||
|
| `WebSession` | User session management | 📋 TODO |
|
||||||
|
| `UserPreferences` | User settings and preferences | 📋 TODO |
|
||||||
|
| `GameViewState` | UI state for live game viewing | 📋 TODO |
|
||||||
|
| `Notification` | Web notification queue | 📋 TODO |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Service Implementation Tracker
|
||||||
|
|
||||||
|
### AIService - AI Decision Making
|
||||||
|
- [ ] `check_steal_opportunity()` - Complex steal decision logic
|
||||||
|
- [ ] `check_tag_from_second()` - Tag-up decisions from 2nd
|
||||||
|
- [ ] `check_tag_from_third()` - Tag-up decisions from 3rd
|
||||||
|
- [ ] `decide_throw_target()` - Uncapped runner throw decisions
|
||||||
|
- [ ] `decide_runner_advance()` - Uncapped advance decisions
|
||||||
|
- [ ] `set_defensive_alignment()` - Defensive positioning
|
||||||
|
- [ ] `decide_groundball_running()` - Groundball running decisions
|
||||||
|
- [ ] `decide_groundball_throw()` - Groundball throw decisions
|
||||||
|
- [ ] `should_replace_pitcher()` - Pitcher replacement logic
|
||||||
|
|
||||||
|
### GameService - Game Management
|
||||||
|
- [ ] `initialize_game()` - Game setup and first play creation
|
||||||
|
- [ ] `get_current_play()` - Current play retrieval
|
||||||
|
- [ ] `format_team_lineup()` - Lineup display formatting
|
||||||
|
- [ ] `build_cardset_params()` - Cardset parameter building
|
||||||
|
- [ ] `get_human_team()` - Human team identification
|
||||||
|
- [ ] `get_league_name()` - League name formatting
|
||||||
|
|
||||||
|
### GameplayService - Live Game Logic
|
||||||
|
- [ ] `initialize_play_ai()` - AI setup for plays
|
||||||
|
- [ ] `calculate_run_differential()` - Score differential calculation
|
||||||
|
- [ ] `is_ai_batting()` - AI batting turn detection
|
||||||
|
- [ ] `check_walkoff_situation()` - Walkoff scenario detection
|
||||||
|
|
||||||
|
### UIService - User Interface Formatting
|
||||||
|
- [ ] `format_team_display()` - Team display for web UI
|
||||||
|
- [ ] `format_player_link()` - Player links for web
|
||||||
|
- [ ] `get_batter_card_image()` - Batter card image URLs
|
||||||
|
- [ ] `get_pitcher_card_image()` - Pitcher card image URLs
|
||||||
|
- [ ] `format_scoreboard()` - Live scoreboard display
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Validation Checklist
|
||||||
|
|
||||||
|
### ✅ Model Purity
|
||||||
|
- [ ] No business logic methods in models
|
||||||
|
- [ ] Only field definitions and relationships
|
||||||
|
- [ ] Basic validators for data integrity only
|
||||||
|
- [ ] No Discord dependencies
|
||||||
|
|
||||||
|
### ✅ Service Completeness
|
||||||
|
- [ ] All extracted methods implemented in services
|
||||||
|
- [ ] Services have comprehensive unit tests
|
||||||
|
- [ ] Service methods handle all edge cases from original
|
||||||
|
- [ ] Service integration tested
|
||||||
|
|
||||||
|
### ✅ Functionality Preservation
|
||||||
|
- [ ] All game mechanics preserved
|
||||||
|
- [ ] AI decision-making logic intact
|
||||||
|
- [ ] Statistical tracking complete
|
||||||
|
- [ ] UI formatting adapted for web
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes and Decisions
|
||||||
|
|
||||||
|
### Key Architectural Decisions:
|
||||||
|
1. **AI Logic Location**: All AI decision-making moves to AIService to enable easy testing and modification
|
||||||
|
2. **UI Separation**: All formatting logic moves to UIService to support multiple interfaces
|
||||||
|
3. **Game State Management**: Core game logic moves to GameService/GameplayService for business rule centralization
|
||||||
|
|
||||||
|
### Risk Areas:
|
||||||
|
1. **Game.initialize_play()**: Complex method with lineup validation and AI setup
|
||||||
|
2. **Play model**: Heavily used throughout gameplay with many computed properties
|
||||||
|
3. **AI integration**: AI methods are called throughout game flow and must be properly service-integrated
|
||||||
|
|
||||||
|
### Success Metrics:
|
||||||
|
- All models are pure data containers
|
||||||
|
- All business logic has corresponding service methods
|
||||||
|
- No functionality lost in migration
|
||||||
|
- Services are independently testable
|
||||||
525
.claude/web-migration-v2.md
Normal file
525
.claude/web-migration-v2.md
Normal file
@ -0,0 +1,525 @@
|
|||||||
|
# Paper Dynasty Web App Migration Plan v2.0
|
||||||
|
## Model/Service Architecture Edition
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
Migrating the baseball gameplay functionality from the Discord app (`../discord-app/`) to a standalone web application using **FastAPI + Model/Service Architecture**. This approach provides better separation of concerns, testability, and scalability compared to the original monolithic migration plan.
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
### Why Model/Service Architecture?
|
||||||
|
Based on analysis of the Discord app complexity (15K+ line files), a service-oriented approach will:
|
||||||
|
- **Improve Testability**: Services can be unit tested independently
|
||||||
|
- **Enhance Maintainability**: Clear separation between business logic and web framework
|
||||||
|
- **Enable Scalability**: Easy to add caching, background tasks, API versioning
|
||||||
|
- **Future-Proof**: Ready for mobile API, microservices, or different frontends
|
||||||
|
|
||||||
|
### Technology Stack
|
||||||
|
- **Backend**: FastAPI + SQLModel + PostgreSQL
|
||||||
|
- **Architecture**: Model/Service/Controller pattern
|
||||||
|
- **Frontend**: Jinja2 templates + HTMX + TailwindCSS
|
||||||
|
- **Testing**: pytest with comprehensive service unit tests
|
||||||
|
- **Real-time**: HTMX polling with WebSocket upgrade path
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Progress Tracking
|
||||||
|
|
||||||
|
### 🚧 Current Phase
|
||||||
|
- [ ] **Phase 1: Service Foundation & Models** (Days 1-4)
|
||||||
|
|
||||||
|
### 📋 Upcoming Phases
|
||||||
|
- [ ] **Phase 2: Core Game Services** (Days 5-9)
|
||||||
|
- [ ] **Phase 3: Web Interface with Services** (Days 10-14)
|
||||||
|
- [ ] **Phase 4: Advanced Features & Optimization** (Days 15-17)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Service Foundation & Models (Days 1-4)
|
||||||
|
|
||||||
|
### Day 1: Project Setup & Service Foundation
|
||||||
|
#### Tasks
|
||||||
|
- [ ] Initialize FastAPI project with service-oriented structure
|
||||||
|
- [ ] Set up dependency injection patterns for services
|
||||||
|
- [ ] Create base service classes and interfaces
|
||||||
|
- [ ] Configure logging with rotating handlers
|
||||||
|
- [ ] Set up virtual environment and requirements
|
||||||
|
|
||||||
|
#### Service Architecture Setup
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── services/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── base_service.py # Base service class
|
||||||
|
│ └── service_container.py # Dependency injection
|
||||||
|
├── models/
|
||||||
|
├── routers/
|
||||||
|
├── engine/
|
||||||
|
└── repositories/ # Optional data access layer
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Code Review Checklist - Foundation
|
||||||
|
- [ ] Service base classes follow single responsibility principle
|
||||||
|
- [ ] Dependency injection properly configured
|
||||||
|
- [ ] Logging follows rotating pattern from user's CLAUDE.md
|
||||||
|
- [ ] Virtual environment properly configured
|
||||||
|
- [ ] Project structure supports service-oriented development
|
||||||
|
|
||||||
|
#### Success Criteria
|
||||||
|
- [ ] FastAPI server starts without errors
|
||||||
|
- [ ] Base service classes created and testable
|
||||||
|
- [ ] Dependency injection working
|
||||||
|
- [ ] Service unit test framework established
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Day 2: Database Models Migration
|
||||||
|
#### Tasks
|
||||||
|
- [ ] Migrate core gameplay models from `../discord-app/in_game/gameplay_models.py`
|
||||||
|
- [ ] Remove Discord dependencies (discord.Embed, etc.)
|
||||||
|
- [ ] Add web-specific models (sessions, preferences, notifications)
|
||||||
|
- [ ] Set up PostgreSQL with Docker for testing
|
||||||
|
- [ ] Create model validation and relationship tests
|
||||||
|
|
||||||
|
#### Models to Migrate
|
||||||
|
- `Game`, `Team`, `Player`, `Lineup`, `Play` (core gameplay)
|
||||||
|
- `Card`, `Cardset`, `ManagerAi` (game content)
|
||||||
|
- `WebSession`, `UserPreferences`, `GameViewState` (web-specific)
|
||||||
|
|
||||||
|
#### Code Review Checklist - Models
|
||||||
|
- [ ] All Discord imports removed
|
||||||
|
- [ ] Field types and constraints validated for PostgreSQL
|
||||||
|
- [ ] Relationships properly defined with foreign keys
|
||||||
|
- [ ] Web-specific models added for session management
|
||||||
|
- [ ] Model tests cover creation, relationships, and validation
|
||||||
|
|
||||||
|
#### Success Criteria
|
||||||
|
- [ ] All models work with PostgreSQL
|
||||||
|
- [ ] 90%+ test coverage on model functionality
|
||||||
|
- [ ] Foreign key relationships properly defined
|
||||||
|
- [ ] Web session models support user management
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Day 3: Core Engine Migration (Stateless)
|
||||||
|
#### Tasks
|
||||||
|
- [ ] Migrate `../discord-app/dice.py` → `engine/dice.py`
|
||||||
|
- [ ] Migrate `../discord-app/in_game/simulations.py` → `engine/simulation.py`
|
||||||
|
- [ ] Create `engine/calculations.py` for statistics and WPA
|
||||||
|
- [ ] Ensure engine components are stateless and pure
|
||||||
|
- [ ] Add comprehensive engine unit tests
|
||||||
|
|
||||||
|
#### Engine Components
|
||||||
|
- **dice.py**: Dice rolling, fielding checks, X-chart integration
|
||||||
|
- **simulation.py**: Pitcher vs batter mechanics, outcome calculation
|
||||||
|
- **calculations.py**: Statistics, WPA, game metrics
|
||||||
|
|
||||||
|
#### Code Review Checklist - Engine
|
||||||
|
- [ ] All engine functions are pure (no side effects)
|
||||||
|
- [ ] Random operations use proper seeding for testing
|
||||||
|
- [ ] No Discord dependencies remain
|
||||||
|
- [ ] Type hints added for all functions
|
||||||
|
- [ ] Edge cases in game rules properly handled
|
||||||
|
|
||||||
|
#### Success Criteria
|
||||||
|
- [ ] Engine components are stateless and testable
|
||||||
|
- [ ] Dice probability distributions validated
|
||||||
|
- [ ] Pitcher vs batter simulation accuracy confirmed
|
||||||
|
- [ ] 95%+ test coverage on core engine logic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Day 4: Service Layer Foundation
|
||||||
|
#### Tasks
|
||||||
|
- [ ] Create `services/game_service.py` with basic game management
|
||||||
|
- [ ] Create `services/user_service.py` for session management
|
||||||
|
- [ ] Create `services/auth_service.py` for Discord OAuth
|
||||||
|
- [ ] Implement service dependency injection in FastAPI
|
||||||
|
- [ ] Add service unit tests with mocked dependencies
|
||||||
|
|
||||||
|
#### Service Interfaces
|
||||||
|
```python
|
||||||
|
class GameService:
|
||||||
|
async def create_game(self, away_team_id: int, home_team_id: int) -> Game
|
||||||
|
async def get_game(self, game_id: int) -> Game
|
||||||
|
async def list_active_games(self) -> List[Game]
|
||||||
|
|
||||||
|
class UserService:
|
||||||
|
async def create_session(self, discord_user_id: int) -> WebSession
|
||||||
|
async def get_user_preferences(self, session_id: str) -> UserPreferences
|
||||||
|
|
||||||
|
class AuthService:
|
||||||
|
async def authenticate_discord(self, oauth_code: str) -> WebSession
|
||||||
|
async def validate_session(self, session_id: str) -> bool
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Code Review Checklist - Services
|
||||||
|
- [ ] Services have clear, single responsibilities
|
||||||
|
- [ ] All service methods have type hints
|
||||||
|
- [ ] Services use dependency injection for database sessions
|
||||||
|
- [ ] Service unit tests use mocked dependencies
|
||||||
|
- [ ] Business logic separated from data access
|
||||||
|
|
||||||
|
#### Success Criteria
|
||||||
|
- [ ] Basic services implement core functionality
|
||||||
|
- [ ] Services can be unit tested independently
|
||||||
|
- [ ] Dependency injection working in FastAPI routes
|
||||||
|
- [ ] Service interfaces support expected web app features
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Core Game Services (Days 5-9)
|
||||||
|
|
||||||
|
### Day 5: Gameplay Service Architecture
|
||||||
|
#### Tasks
|
||||||
|
- [ ] Extract complex gameplay logic from `../discord-app/command_logic/logic_gameplay.py`
|
||||||
|
- [ ] Create `services/gameplay_service.py` for core game flow
|
||||||
|
- [ ] Implement play execution and validation
|
||||||
|
- [ ] Add inning management and state transitions
|
||||||
|
- [ ] Create comprehensive gameplay service tests
|
||||||
|
|
||||||
|
#### GameplayService Responsibilities
|
||||||
|
- Play execution and validation
|
||||||
|
- Game state management
|
||||||
|
- Inning progression logic
|
||||||
|
- Score calculation and updates
|
||||||
|
- Game completion detection
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Day 6: AI Service Implementation
|
||||||
|
#### Tasks
|
||||||
|
- [ ] Migrate `../discord-app/in_game/ai_manager.py` → `services/ai_service.py`
|
||||||
|
- [ ] Remove Discord interaction dependencies
|
||||||
|
- [ ] Implement AI decision-making for automated teams
|
||||||
|
- [ ] Add AI personality and difficulty settings
|
||||||
|
- [ ] Create AI behavior validation tests
|
||||||
|
|
||||||
|
#### AIService Responsibilities
|
||||||
|
- Manager AI decision making
|
||||||
|
- Lineup optimization
|
||||||
|
- Pitching decisions
|
||||||
|
- Defensive positioning
|
||||||
|
- Strategic game management
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Day 7: Advanced Game Services
|
||||||
|
#### Tasks
|
||||||
|
- [ ] Create `services/simulation_service.py` for game simulation orchestration
|
||||||
|
- [ ] Implement `services/statistics_service.py` for player/team stats
|
||||||
|
- [ ] Add `services/notification_service.py` for real-time updates
|
||||||
|
- [ ] Integrate services with each other using dependency injection
|
||||||
|
- [ ] Add integration tests for service interactions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Days 8-9: Service Integration & Testing
|
||||||
|
#### Tasks
|
||||||
|
- [ ] Complete end-to-end service integration
|
||||||
|
- [ ] Add performance optimization for service calls
|
||||||
|
- [ ] Implement caching strategies for expensive operations
|
||||||
|
- [ ] Create comprehensive integration test suite
|
||||||
|
- [ ] Validate service architecture with complex game scenarios
|
||||||
|
|
||||||
|
#### Code Review Checklist - Service Integration
|
||||||
|
- [ ] Services interact cleanly without tight coupling
|
||||||
|
- [ ] Database queries optimized (no N+1 issues)
|
||||||
|
- [ ] Error handling propagates correctly through service layers
|
||||||
|
- [ ] Performance acceptable for concurrent users
|
||||||
|
- [ ] All service interactions have integration tests
|
||||||
|
|
||||||
|
#### Success Criteria
|
||||||
|
- [ ] Complete game can be simulated using services
|
||||||
|
- [ ] All services have 90%+ unit test coverage
|
||||||
|
- [ ] Integration tests validate full game flows
|
||||||
|
- [ ] Performance benchmarks met for service calls
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Web Interface with Services (Days 10-14)
|
||||||
|
|
||||||
|
### Day 10: FastAPI Route Architecture
|
||||||
|
#### Tasks
|
||||||
|
- [ ] Create `routers/games.py` using GameService
|
||||||
|
- [ ] Create `routers/auth.py` using AuthService
|
||||||
|
- [ ] Create `routers/api.py` for HTMX endpoints
|
||||||
|
- [ ] Implement dependency injection for services in routes
|
||||||
|
- [ ] Add route-level error handling and validation
|
||||||
|
|
||||||
|
#### Route Design Pattern
|
||||||
|
```python
|
||||||
|
@router.post("/games/start")
|
||||||
|
async def start_game(
|
||||||
|
request: StartGameRequest,
|
||||||
|
game_service: GameService = Depends(get_game_service),
|
||||||
|
user_service: UserService = Depends(get_user_service)
|
||||||
|
):
|
||||||
|
# Thin controller - delegate to services
|
||||||
|
session = await user_service.validate_session(request.session_id)
|
||||||
|
game = await game_service.create_game(
|
||||||
|
away_team_id=request.away_team_id,
|
||||||
|
home_team_id=request.home_team_id
|
||||||
|
)
|
||||||
|
return {"game_id": game.id}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Day 11: Authentication & Session Management
|
||||||
|
#### Tasks
|
||||||
|
- [ ] Implement Discord OAuth flow using AuthService
|
||||||
|
- [ ] Create session management with UserService
|
||||||
|
- [ ] Add user preference handling
|
||||||
|
- [ ] Implement authentication middleware
|
||||||
|
- [ ] Add authentication integration tests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Day 12-13: Game Interface Implementation
|
||||||
|
#### Tasks
|
||||||
|
- [ ] Create game lobby system using GameService
|
||||||
|
- [ ] Implement real-time game board with GameplayService
|
||||||
|
- [ ] Add player action forms with service validation
|
||||||
|
- [ ] Create scoreboard using StatisticsService
|
||||||
|
- [ ] Implement HTMX polling for live updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Day 14: Web Interface Polish
|
||||||
|
#### Tasks
|
||||||
|
- [ ] Add comprehensive error handling in routes
|
||||||
|
- [ ] Implement responsive design with TailwindCSS
|
||||||
|
- [ ] Add client-side validation with HTMX
|
||||||
|
- [ ] Create user preference management interface
|
||||||
|
- [ ] Add end-to-end web interface tests
|
||||||
|
|
||||||
|
#### Code Review Checklist - Web Interface
|
||||||
|
- [ ] Routes are thin, delegating business logic to services
|
||||||
|
- [ ] HTMX requests handle errors gracefully
|
||||||
|
- [ ] Forms have proper validation (client and server-side)
|
||||||
|
- [ ] UI is responsive and mobile-friendly
|
||||||
|
- [ ] Game state updates are atomic through services
|
||||||
|
|
||||||
|
#### Success Criteria
|
||||||
|
- [ ] Complete single-player game flow working
|
||||||
|
- [ ] Discord OAuth login/logout functional
|
||||||
|
- [ ] Real-time updates working via HTMX polling
|
||||||
|
- [ ] Responsive design works on mobile and desktop
|
||||||
|
- [ ] All web interfaces use services (no direct database access)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: Advanced Features & Optimization (Days 15-17)
|
||||||
|
|
||||||
|
### Day 15: Repository Layer (Optional)
|
||||||
|
#### Tasks
|
||||||
|
- [ ] Evaluate need for repository pattern for complex queries
|
||||||
|
- [ ] Implement `repositories/game_repository.py` if needed
|
||||||
|
- [ ] Add query optimization and caching
|
||||||
|
- [ ] Refactor services to use repositories
|
||||||
|
- [ ] Add repository unit tests
|
||||||
|
|
||||||
|
#### Repository Pattern (If Needed)
|
||||||
|
```python
|
||||||
|
class GameRepository:
|
||||||
|
def __init__(self, session: Session):
|
||||||
|
self.session = session
|
||||||
|
|
||||||
|
async def get_games_with_stats(self, filters: GameFilters) -> List[Game]:
|
||||||
|
# Complex query logic separated from service
|
||||||
|
pass
|
||||||
|
|
||||||
|
class GameService:
|
||||||
|
def __init__(self, session: Session, game_repo: GameRepository):
|
||||||
|
self.session = session
|
||||||
|
self.game_repo = game_repo
|
||||||
|
|
||||||
|
async def get_leaderboard(self, filters: GameFilters) -> List[GameStats]:
|
||||||
|
# Service uses repository for complex queries
|
||||||
|
games = await self.game_repo.get_games_with_stats(filters)
|
||||||
|
return self._calculate_leaderboard(games)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Day 16: Performance & Caching
|
||||||
|
#### Tasks
|
||||||
|
- [ ] Add Redis caching for expensive service operations
|
||||||
|
- [ ] Implement database query optimization
|
||||||
|
- [ ] Add performance monitoring and metrics
|
||||||
|
- [ ] Optimize service call patterns
|
||||||
|
- [ ] Add load testing for concurrent users
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Day 17: Final Polish & Documentation
|
||||||
|
#### Tasks
|
||||||
|
- [ ] Complete comprehensive test suite (unit, integration, e2e)
|
||||||
|
- [ ] Add API documentation with FastAPI auto-generation
|
||||||
|
- [ ] Performance optimization and final benchmarking
|
||||||
|
- [ ] Code review and refactoring
|
||||||
|
- [ ] Deployment preparation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Architecture
|
||||||
|
|
||||||
|
### Test Structure
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── unit/ # Fast, isolated unit tests
|
||||||
|
│ ├── services/ # Service unit tests (most important)
|
||||||
|
│ │ ├── test_game_service.py
|
||||||
|
│ │ ├── test_gameplay_service.py
|
||||||
|
│ │ ├── test_ai_service.py
|
||||||
|
│ │ └── test_user_service.py
|
||||||
|
│ ├── engine/ # Engine unit tests
|
||||||
|
│ │ ├── test_dice.py
|
||||||
|
│ │ ├── test_simulation.py
|
||||||
|
│ │ └── test_calculations.py
|
||||||
|
│ └── models/ # Model validation tests
|
||||||
|
│ └── test_models.py
|
||||||
|
├── integration/ # Service + database integration
|
||||||
|
│ ├── test_game_flow.py
|
||||||
|
│ ├── test_auth_flow.py
|
||||||
|
│ └── test_service_integration.py
|
||||||
|
└── e2e/ # Full application tests
|
||||||
|
├── test_game_creation.py
|
||||||
|
├── test_gameplay_flow.py
|
||||||
|
└── test_user_journey.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Priorities
|
||||||
|
|
||||||
|
#### Unit Tests (90%+ Coverage Required)
|
||||||
|
- **Critical**: All service methods, core engine functions
|
||||||
|
- **Important**: Model validation, business logic edge cases
|
||||||
|
- **Nice to have**: Repository methods (if implemented)
|
||||||
|
|
||||||
|
#### Integration Tests (80%+ Coverage)
|
||||||
|
- **Critical**: Service interactions, database transactions
|
||||||
|
- **Important**: Authentication flows, game state persistence
|
||||||
|
- **Nice to have**: Performance characteristics
|
||||||
|
|
||||||
|
#### E2E Tests (Happy Path Coverage)
|
||||||
|
- **Critical**: Complete game creation and play flow
|
||||||
|
- **Important**: User authentication and session management
|
||||||
|
- **Nice to have**: Error handling and edge cases
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Decisions
|
||||||
|
|
||||||
|
### Service Layer Benefits
|
||||||
|
- **Testability**: Services can be unit tested with mocked dependencies
|
||||||
|
- **Maintainability**: Business logic centralized, not scattered across routes
|
||||||
|
- **Scalability**: Easy to add caching, background tasks, monitoring
|
||||||
|
- **Flexibility**: Services can be used by different interfaces (web, API, CLI)
|
||||||
|
|
||||||
|
### Repository Pattern (Optional)
|
||||||
|
- **When to use**: Complex queries, multiple data sources, query optimization
|
||||||
|
- **When to skip**: Simple CRUD operations, small project scope
|
||||||
|
- **Decision**: Evaluate during Day 15 based on query complexity
|
||||||
|
|
||||||
|
### Dependency Injection Strategy
|
||||||
|
- **FastAPI Depends()**: For HTTP request-scoped dependencies
|
||||||
|
- **Service constructors**: For service-to-service dependencies
|
||||||
|
- **Configuration**: Environment-based configuration injection
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Notes & Decisions
|
||||||
|
|
||||||
|
### Service Extraction Strategy
|
||||||
|
1. **Identify Business Logic**: Extract from Discord command handlers
|
||||||
|
2. **Create Service Interface**: Define clear method signatures
|
||||||
|
3. **Write Tests First**: Test-driven service development
|
||||||
|
4. **Implement Service**: Pure business logic, no framework dependencies
|
||||||
|
5. **Integrate with Routes**: Thin controllers using services
|
||||||
|
|
||||||
|
### Discord App Analysis for Service Extraction
|
||||||
|
|
||||||
|
#### High-Value Extractions
|
||||||
|
- `command_logic/logic_gameplay.py` (4,186 lines) → Multiple services
|
||||||
|
- `in_game/ai_manager.py` → `services/ai_service.py`
|
||||||
|
- Game creation/management → `services/game_service.py`
|
||||||
|
- User/session management → `services/user_service.py`
|
||||||
|
|
||||||
|
#### Keep as Stateless Engine
|
||||||
|
- `dice.py` → `engine/dice.py` (pure functions)
|
||||||
|
- `in_game/simulations.py` → `engine/simulation.py` (calculations)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
### Architecture Quality
|
||||||
|
- [ ] Clear separation: Models → Services → Controllers (Routes)
|
||||||
|
- [ ] Services have single, well-defined responsibilities
|
||||||
|
- [ ] Business logic not mixed with web framework code
|
||||||
|
- [ ] Dependency injection working throughout application
|
||||||
|
|
||||||
|
### Code Quality Standards
|
||||||
|
- [ ] Services have 90%+ unit test coverage
|
||||||
|
- [ ] Integration tests cover service interactions
|
||||||
|
- [ ] Type hints on all service methods
|
||||||
|
- [ ] Error handling appropriate for each layer
|
||||||
|
- [ ] Performance acceptable for multiple concurrent users
|
||||||
|
|
||||||
|
### Migration Success
|
||||||
|
- [ ] Working single-player baseball game with Discord OAuth
|
||||||
|
- [ ] Live scoreboard with HTMX real-time updates
|
||||||
|
- [ ] Clean, service-oriented codebase ready for scaling
|
||||||
|
- [ ] Comprehensive test coverage across all layers
|
||||||
|
- [ ] Responsive web interface (mobile + desktop)
|
||||||
|
- [ ] No Discord-specific dependencies remaining
|
||||||
|
|
||||||
|
### Service Architecture Validation
|
||||||
|
- [ ] Services can be unit tested independently
|
||||||
|
- [ ] Business logic easily modifiable without touching routes
|
||||||
|
- [ ] New features can be added by extending services
|
||||||
|
- [ ] Multiple interfaces (web, API) can use same services
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Enhancements (Post-MVP)
|
||||||
|
|
||||||
|
### Microservices Evolution
|
||||||
|
- Services are designed to be extracted into microservices later
|
||||||
|
- Clean service interfaces support API versioning
|
||||||
|
- Database access patterns support service splitting
|
||||||
|
|
||||||
|
### Advanced Features
|
||||||
|
- Background task processing using service layer
|
||||||
|
- Multiple client support (mobile app, desktop)
|
||||||
|
- Real-time multiplayer using service orchestration
|
||||||
|
- Analytics and monitoring built into service layer
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes for Future AI Agents
|
||||||
|
|
||||||
|
### Service-First Development
|
||||||
|
1. **Always start with services** when adding new features
|
||||||
|
2. **Write service tests first** before implementation
|
||||||
|
3. **Keep routes thin** - delegate to services
|
||||||
|
4. **Test services independently** with mocked dependencies
|
||||||
|
5. **Use dependency injection** for all service interactions
|
||||||
|
|
||||||
|
### Migration Best Practices
|
||||||
|
1. **Extract business logic** from Discord command handlers
|
||||||
|
2. **Remove all Discord dependencies** before service implementation
|
||||||
|
3. **Test service logic** independently of web framework
|
||||||
|
4. **Integrate services** into routes using FastAPI Depends()
|
||||||
|
5. **Validate service architecture** with integration tests
|
||||||
|
|
||||||
|
### Maintenance Guidelines
|
||||||
|
- Update service tests when changing business logic
|
||||||
|
- Keep service interfaces stable for backward compatibility
|
||||||
|
- Add new features by extending services, not routes
|
||||||
|
- Monitor service performance and add caching as needed
|
||||||
|
- Document service interactions and dependencies
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Remember: This migration uses **Model/Service Architecture** to create a scalable, maintainable web application. Always prioritize clean service design and comprehensive testing over speed of development. The service layer is the foundation that will support future growth and feature additions.
|
||||||
21
.env.example
Normal file
21
.env.example
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Environment configuration for Paper Dynasty Web App
|
||||||
|
# Copy this file to .env and update values as needed
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DATABASE_URL=postgresql+psycopg://paper_dynasty_user:paper_dynasty_dev_password@localhost:5432/paper_dynasty
|
||||||
|
DATABASE_TEST_URL=postgresql+psycopg://paper_dynasty_user:paper_dynasty_dev_password@localhost:5432/paper_dynasty_test
|
||||||
|
|
||||||
|
# Application Settings
|
||||||
|
DEBUG=true
|
||||||
|
ENVIRONMENT=development
|
||||||
|
SECRET_KEY=development-secret-key-change-in-production
|
||||||
|
SESSION_EXPIRE_HOURS=24
|
||||||
|
|
||||||
|
# Discord OAuth (fill in when implementing authentication)
|
||||||
|
DISCORD_CLIENT_ID=
|
||||||
|
DISCORD_CLIENT_SECRET=
|
||||||
|
DISCORD_REDIRECT_URI=http://localhost:8001/auth/callback
|
||||||
|
|
||||||
|
# Game Settings
|
||||||
|
MAX_CONCURRENT_GAMES=100
|
||||||
|
GAME_TIMEOUT_HOURS=4
|
||||||
193
CLAUDE.md
Normal file
193
CLAUDE.md
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This is the **Paper Dynasty Web App**, a baseball simulation game migrated from a Discord bot (`../discord-app/`) to a standalone web application using **FastAPI + Model/Service Architecture**. The migration follows a service-oriented approach that prioritizes testability, maintainability, and scalability.
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Environment Setup
|
||||||
|
```bash
|
||||||
|
# Start PostgreSQL database container
|
||||||
|
docker compose up -d postgres
|
||||||
|
|
||||||
|
# Activate virtual environment
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Install dependencies (including PostgreSQL driver)
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Copy environment configuration
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env file if needed
|
||||||
|
|
||||||
|
# Run development server
|
||||||
|
python -m app.main
|
||||||
|
# Server runs on http://localhost:8001
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
pytest
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
pytest --cov=app
|
||||||
|
|
||||||
|
# Run specific test module
|
||||||
|
pytest tests/unit/services/test_game_service.py
|
||||||
|
|
||||||
|
# Run specific test
|
||||||
|
pytest tests/unit/services/test_game_service.py::test_create_game_success
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database
|
||||||
|
```bash
|
||||||
|
# Start/stop PostgreSQL container
|
||||||
|
docker compose up -d postgres # Start pddev-postgres container
|
||||||
|
docker compose stop postgres # Stop container
|
||||||
|
docker compose down -v # Stop and remove data (fresh start)
|
||||||
|
|
||||||
|
# Create database tables (once models are implemented)
|
||||||
|
python -c "from app.services.service_container import create_db_and_tables; create_db_and_tables()"
|
||||||
|
|
||||||
|
# Generate migration (once Alembic is configured)
|
||||||
|
alembic revision --autogenerate -m "Description"
|
||||||
|
|
||||||
|
# Apply migrations
|
||||||
|
alembic upgrade head
|
||||||
|
|
||||||
|
# Connect to database directly
|
||||||
|
docker compose exec postgres psql -U paper_dynasty_user -d paper_dynasty
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
### Model/Service Architecture Pattern
|
||||||
|
|
||||||
|
This project implements a **service-oriented architecture** with clear separation of concerns:
|
||||||
|
|
||||||
|
- **Models** (`app/models/`): Pure data models using SQLModel
|
||||||
|
- **Services** (`app/services/`): Business logic layer with dependency injection
|
||||||
|
- **Controllers** (`app/routers/`): Thin FastAPI route handlers that delegate to services
|
||||||
|
- **Engine** (`app/engine/`): Stateless game simulation functions
|
||||||
|
- **Repositories** (`app/repositories/`): Optional data access layer for complex queries
|
||||||
|
|
||||||
|
### Service Layer Design
|
||||||
|
|
||||||
|
All services inherit from `BaseService` and follow these patterns:
|
||||||
|
- Constructor dependency injection via SQLModel Session
|
||||||
|
- Standardized logging with `f'{__name__}.{self.__class__.__name__}'` format
|
||||||
|
- Business logic separated from HTTP concerns
|
||||||
|
- Unit testable with mocked dependencies
|
||||||
|
|
||||||
|
### Dependency Injection
|
||||||
|
|
||||||
|
Services are injected into routes using FastAPI's `Depends()` system via `service_container.py`:
|
||||||
|
```python
|
||||||
|
from app.services.service_container import GameServiceDep
|
||||||
|
|
||||||
|
@router.post("/games/start")
|
||||||
|
async def start_game(game_service: GameServiceDep):
|
||||||
|
# Service contains business logic, route handles HTTP concerns
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration from Discord App
|
||||||
|
|
||||||
|
When migrating code from `../discord-app/`:
|
||||||
|
1. **Extract business logic** from Discord command handlers into services
|
||||||
|
2. **Remove Discord dependencies** (discord.py, interaction objects)
|
||||||
|
3. **Create service methods** with pure business logic
|
||||||
|
4. **Add comprehensive unit tests** for service methods
|
||||||
|
5. **Create thin route handlers** that delegate to services
|
||||||
|
|
||||||
|
## Current Migration Status
|
||||||
|
|
||||||
|
**Phase 1: Service Foundation** ✅ COMPLETE
|
||||||
|
- FastAPI project structure established
|
||||||
|
- Base service classes and dependency injection configured
|
||||||
|
- Logging with rotating handlers implemented
|
||||||
|
- Virtual environment and core dependencies installed
|
||||||
|
|
||||||
|
**Phase 2: Core Game Services** 🚧 NEXT
|
||||||
|
- Extract business logic from `../discord-app/command_logic/logic_gameplay.py`
|
||||||
|
- Implement GameService, GameplayService, AIService
|
||||||
|
- Migrate core gameplay models from Discord app
|
||||||
|
- Add comprehensive service unit tests
|
||||||
|
|
||||||
|
## Key Files and Patterns
|
||||||
|
|
||||||
|
### Service Container (`app/services/service_container.py`)
|
||||||
|
- Manages database sessions and service lifecycles
|
||||||
|
- Provides dependency injection for FastAPI routes
|
||||||
|
- Configure SQLModel engine for PostgreSQL/SQLite
|
||||||
|
|
||||||
|
### Base Service (`app/services/base_service.py`)
|
||||||
|
- Abstract base class for all services
|
||||||
|
- Standardized logging and validation patterns
|
||||||
|
- Session management and error handling
|
||||||
|
|
||||||
|
### Constants (`app/config/constants.py`)
|
||||||
|
- Migrated constants from Discord app (not assumed values)
|
||||||
|
- Environment-based configuration via Settings class
|
||||||
|
- Game constants, MLB teams, rarity values from original source
|
||||||
|
|
||||||
|
### Logging Configuration (`app/config/logging_config.py`)
|
||||||
|
- Rotating file handlers per user requirements
|
||||||
|
- Console and file output with structured formatting
|
||||||
|
- Service-specific logger naming patterns
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Unit Tests (`tests/unit/`)
|
||||||
|
- **Services**: Test business logic with mocked database sessions
|
||||||
|
- **Engine**: Test stateless game simulation functions
|
||||||
|
- **Models**: Test data validation and relationships
|
||||||
|
|
||||||
|
### Integration Tests (`tests/integration/`)
|
||||||
|
- Service interactions with real database (isolated transactions)
|
||||||
|
- Authentication flows and session management
|
||||||
|
|
||||||
|
### End-to-End Tests (`tests/e2e/`)
|
||||||
|
- Complete user journeys through web interface
|
||||||
|
- Game creation and gameplay flows
|
||||||
|
|
||||||
|
## Development Guidelines
|
||||||
|
|
||||||
|
### Service Development
|
||||||
|
- All services extend `BaseService`
|
||||||
|
- Use dependency injection for service-to-service communication
|
||||||
|
- Log operations with `self._log_operation()` and `self._log_error()`
|
||||||
|
- Follow "Raise or Return" pattern (no optional return values)
|
||||||
|
|
||||||
|
### Documentation Maintenance
|
||||||
|
- **Update README.md files** when making substantial changes to any directory
|
||||||
|
- Each directory with significant content has a README.md explaining its purpose and patterns
|
||||||
|
- Keep documentation current with architecture decisions and new patterns
|
||||||
|
- Update relevant README when adding new services, routes, or changing patterns
|
||||||
|
- **Reference per-directory README.md files** - Future AI agents should read the README in each directory to understand its purpose, patterns, and responsibilities before making changes
|
||||||
|
|
||||||
|
### File Size Management
|
||||||
|
- **Keep all files as small as feasibly possible** for maximum maintainability
|
||||||
|
- Break large files into smaller, focused modules
|
||||||
|
- Single responsibility principle applies to files as well as classes
|
||||||
|
- Prefer multiple small service files over monolithic service classes
|
||||||
|
- Split complex models into separate files when they serve different domains
|
||||||
|
|
||||||
|
### Code Migration
|
||||||
|
- Always check `../discord-app/` for actual implementation before assuming logic
|
||||||
|
- Extract Discord command handlers into service methods
|
||||||
|
- Test service logic independently of web framework
|
||||||
|
- Keep routes thin - delegate business logic to services
|
||||||
|
|
||||||
|
### Database
|
||||||
|
- Currently configured for SQLite development
|
||||||
|
- PostgreSQL driver (`psycopg2-binary`) commented out until proper DB setup
|
||||||
|
- Use SQLModel for all data models
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
- Use rotating file handlers as per user requirements
|
||||||
|
- Format: `f'{__name__}.{className or functionName}'`
|
||||||
|
- Log service operations and errors consistently
|
||||||
573
DEVELOPMENT_GUIDE_V2.md
Normal file
573
DEVELOPMENT_GUIDE_V2.md
Normal file
@ -0,0 +1,573 @@
|
|||||||
|
# Paper Dynasty Web App Development Guide v2.0
|
||||||
|
## Model/Service Architecture Edition
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This guide provides instructions for AI agents working on the Paper Dynasty web app migration project using a **Model/Service Architecture**. This approach separates business logic into services, improving testability, maintainability, and scalability over the original monolithic approach.
|
||||||
|
|
||||||
|
## Quick Start for AI Agents
|
||||||
|
|
||||||
|
### Before Starting Any Session:
|
||||||
|
1. **Check Progress**: Read `./.claude/plans/web-migration-v2.md` to understand current status
|
||||||
|
2. **Review TODO List**: Use TodoWrite tool to track your progress throughout the session
|
||||||
|
3. **Understand Context**: This is a migration from `../discord-app/` to a web interface using **Model/Service Architecture**
|
||||||
|
4. **Follow Standards**: Use the code review methodology defined below
|
||||||
|
|
||||||
|
### Current Project Status
|
||||||
|
- **Migration Type**: Fork approach with **Model/Service Architecture**
|
||||||
|
- **Current Phase**: Check `./.claude/plans/web-migration-v2.md` for latest status
|
||||||
|
- **Architecture**: Clean separation between Models, Services, Controllers (FastAPI routes)
|
||||||
|
- **Next Tasks**: Always work from the current phase checklist in the migration plan
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
### Model/Service Architecture Benefits
|
||||||
|
- **Separation of Concerns**: Clear boundaries between data, business logic, and web interface
|
||||||
|
- **Testability**: Services can be unit tested independently of database and web framework
|
||||||
|
- **Scalability**: Easy to add caching, background tasks, API versioning
|
||||||
|
- **Maintainability**: Business logic centralized in services, not scattered across routes
|
||||||
|
- **Future-Proofing**: Ready for mobile API, microservices, or different frontends
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
```
|
||||||
|
gameplay-website/
|
||||||
|
├── .claude/
|
||||||
|
│ └── web-migration-v2.md
|
||||||
|
├── app/
|
||||||
|
│ ├── main.py # FastAPI application setup
|
||||||
|
│ ├── models/ # Pure data models (SQLModel)
|
||||||
|
│ │ ├── game.py # Game, Team, Player, Lineup, Play
|
||||||
|
│ │ ├── web_models.py # Session, Preferences, Notifications
|
||||||
|
│ │ └── exceptions.py # Custom exceptions
|
||||||
|
│ ├── services/ # Business logic layer ⭐ NEW
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── game_service.py # Game creation, management, queries
|
||||||
|
│ │ ├── gameplay_service.py # Core gameplay simulation & flow
|
||||||
|
│ │ ├── ai_service.py # AI decision making
|
||||||
|
│ │ ├── user_service.py # User sessions, preferences
|
||||||
|
│ │ ├── notification_service.py # Web notifications
|
||||||
|
│ │ └── auth_service.py # Discord OAuth, session management
|
||||||
|
│ ├── repositories/ # Data access layer ⭐ NEW (optional)
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── game_repository.py # Complex game queries
|
||||||
|
│ │ └── user_repository.py # User/session queries
|
||||||
|
│ ├── routers/ # FastAPI route handlers ⭐ ENHANCED
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── games.py # Game-related endpoints
|
||||||
|
│ │ ├── auth.py # Authentication routes
|
||||||
|
│ │ ├── api.py # JSON API for HTMX
|
||||||
|
│ │ └── pages.py # HTML page routes
|
||||||
|
│ ├── engine/ # Core simulation engine (stateless)
|
||||||
|
│ │ ├── dice.py # Dice rolling, fielding checks
|
||||||
|
│ │ ├── simulation.py # Pitcher vs batter mechanics
|
||||||
|
│ │ └── calculations.py # Statistics, WPA calculations
|
||||||
|
│ ├── config/
|
||||||
|
│ │ └── constants.py
|
||||||
|
│ ├── static/
|
||||||
|
│ └── templates/
|
||||||
|
├── tests/ # Comprehensive test structure
|
||||||
|
│ ├── unit/ # Unit tests for services
|
||||||
|
│ ├── integration/ # Integration tests
|
||||||
|
│ └── e2e/ # End-to-end tests
|
||||||
|
├── requirements.txt
|
||||||
|
└── DEVELOPMENT_GUIDE_V2.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Review Methodology
|
||||||
|
|
||||||
|
### When Migrating Code from Discord App:
|
||||||
|
|
||||||
|
#### 1. Service Extraction First
|
||||||
|
```python
|
||||||
|
# ❌ Old approach: Business logic in routes
|
||||||
|
@app.post("/games/start")
|
||||||
|
async def start_game(request: Request):
|
||||||
|
# 50+ lines of game creation logic mixed with HTTP handling
|
||||||
|
|
||||||
|
# ✅ New approach: Clean service separation
|
||||||
|
@router.post("/games/start")
|
||||||
|
async def start_game(request: StartGameRequest):
|
||||||
|
game = await game_service.create_game(
|
||||||
|
away_team_id=request.away_team_id,
|
||||||
|
home_team_id=request.home_team_id,
|
||||||
|
game_type=request.game_type
|
||||||
|
)
|
||||||
|
return {"game_id": game.id, "status": "created"}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Service Layer Design
|
||||||
|
```python
|
||||||
|
# ✅ Service pattern example
|
||||||
|
from sqlmodel import Session
|
||||||
|
from typing import Optional, List
|
||||||
|
from ..models.game import Game, Team
|
||||||
|
from ..repositories.game_repository import GameRepository
|
||||||
|
|
||||||
|
class GameService:
|
||||||
|
def __init__(self, session: Session):
|
||||||
|
self.session = session
|
||||||
|
self.game_repo = GameRepository(session)
|
||||||
|
|
||||||
|
async def create_game(
|
||||||
|
self,
|
||||||
|
away_team_id: int,
|
||||||
|
home_team_id: int,
|
||||||
|
game_type: str
|
||||||
|
) -> Game:
|
||||||
|
"""Create a new game with validation and business rules"""
|
||||||
|
# Validation
|
||||||
|
await self._validate_teams_available(away_team_id, home_team_id)
|
||||||
|
|
||||||
|
# Business logic
|
||||||
|
game = Game(
|
||||||
|
away_team_id=away_team_id,
|
||||||
|
home_team_id=home_team_id,
|
||||||
|
game_type=game_type,
|
||||||
|
season=self._get_current_season()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Persistence
|
||||||
|
return await self.game_repo.create(game)
|
||||||
|
|
||||||
|
async def _validate_teams_available(self, away_id: int, home_id: int):
|
||||||
|
"""Private method for business rule validation"""
|
||||||
|
# Implementation here
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Dependency Cleanup
|
||||||
|
```python
|
||||||
|
# ❌ Remove these Discord-specific imports
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
# ✅ Replace with web-appropriate imports
|
||||||
|
from fastapi import FastAPI, Request, Response, Depends
|
||||||
|
from sqlmodel import Session
|
||||||
|
from typing import List, Optional
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Error Handling Review
|
||||||
|
```python
|
||||||
|
# ❌ Discord-specific error handling
|
||||||
|
await interaction.response.send_message("Error occurred", ephemeral=True)
|
||||||
|
|
||||||
|
# ✅ Service-layer error handling
|
||||||
|
from ..models.exceptions import GameNotFoundException
|
||||||
|
|
||||||
|
class GameService:
|
||||||
|
async def get_game(self, game_id: int) -> Game:
|
||||||
|
game = await self.game_repo.get_by_id(game_id)
|
||||||
|
if not game:
|
||||||
|
raise GameNotFoundException(f"Game {game_id} not found")
|
||||||
|
return game
|
||||||
|
|
||||||
|
# ✅ Route-layer error handling
|
||||||
|
@router.get("/games/{game_id}")
|
||||||
|
async def get_game(game_id: int, game_service: GameService = Depends()):
|
||||||
|
try:
|
||||||
|
game = await game_service.get_game(game_id)
|
||||||
|
return game
|
||||||
|
except GameNotFoundException as e:
|
||||||
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Logging Standards
|
||||||
|
Follow the user's CLAUDE.md requirements:
|
||||||
|
```python
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(f'{__name__}.GameService')
|
||||||
|
|
||||||
|
class GameService:
|
||||||
|
async def create_game(self, away_team_id: int, home_team_id: int) -> Game:
|
||||||
|
logger.info(f'GameService.create_game - Creating game: {away_team_id} vs {home_team_id}')
|
||||||
|
try:
|
||||||
|
# Business logic
|
||||||
|
result = await self._create_game_logic(away_team_id, home_team_id)
|
||||||
|
logger.info(f'GameService.create_game - Game created successfully: {result.id}')
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'GameService.create_game - Failed to create game: {str(e)}')
|
||||||
|
raise
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. Service Testing Pattern
|
||||||
|
```python
|
||||||
|
# ✅ Unit test services independently
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import Mock
|
||||||
|
from app.services.game_service import GameService
|
||||||
|
from app.models.game import Game
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_session():
|
||||||
|
return Mock()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def game_service(mock_session):
|
||||||
|
return GameService(mock_session)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_game_success(game_service):
|
||||||
|
# Test business logic without database
|
||||||
|
result = await game_service.create_game(
|
||||||
|
away_team_id=1,
|
||||||
|
home_team_id=2,
|
||||||
|
game_type="ranked"
|
||||||
|
)
|
||||||
|
assert result.away_team_id == 1
|
||||||
|
assert result.home_team_id == 2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Review Checklist Template
|
||||||
|
For each migrated file, check:
|
||||||
|
- [ ] Business logic extracted into appropriate service
|
||||||
|
- [ ] Service has clear, single responsibility
|
||||||
|
- [ ] All Discord imports removed
|
||||||
|
- [ ] Error handling appropriate for service layer
|
||||||
|
- [ ] Logging follows project standards
|
||||||
|
- [ ] Services have comprehensive unit tests
|
||||||
|
- [ ] Routes are thin, delegating to services
|
||||||
|
- [ ] Database access goes through repositories (if using repository pattern)
|
||||||
|
- [ ] Type hints on all service methods
|
||||||
|
- [ ] No obvious performance issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Service Layer Guidelines
|
||||||
|
|
||||||
|
### Service Responsibilities
|
||||||
|
|
||||||
|
#### GameService
|
||||||
|
- Game creation and validation
|
||||||
|
- Game state management
|
||||||
|
- Game queries and filtering
|
||||||
|
- Game lifecycle (start, pause, complete)
|
||||||
|
|
||||||
|
#### GameplayService
|
||||||
|
- Core gameplay simulation
|
||||||
|
- Play execution and validation
|
||||||
|
- Inning management
|
||||||
|
- Score calculation
|
||||||
|
|
||||||
|
#### AIService
|
||||||
|
- AI decision making
|
||||||
|
- Manager AI logic
|
||||||
|
- Automated gameplay
|
||||||
|
|
||||||
|
#### UserService
|
||||||
|
- User session management
|
||||||
|
- User preferences
|
||||||
|
- Authentication state
|
||||||
|
|
||||||
|
#### NotificationService
|
||||||
|
- Web notifications
|
||||||
|
- Game event notifications
|
||||||
|
- Real-time updates
|
||||||
|
|
||||||
|
### Service Design Principles
|
||||||
|
|
||||||
|
1. **Single Responsibility**: Each service has one clear purpose
|
||||||
|
2. **Dependency Injection**: Services receive dependencies via constructor
|
||||||
|
3. **Stateless**: Services don't maintain instance state between calls
|
||||||
|
4. **Testable**: Services can be unit tested with mocked dependencies
|
||||||
|
5. **Pure Business Logic**: No HTTP, database, or UI concerns in services
|
||||||
|
|
||||||
|
### Service Interaction Patterns
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ✅ Service-to-service communication
|
||||||
|
class GameplayService:
|
||||||
|
def __init__(self, session: Session, ai_service: AIService):
|
||||||
|
self.session = session
|
||||||
|
self.ai_service = ai_service
|
||||||
|
|
||||||
|
async def execute_play(self, game_id: int, play_data: dict) -> PlayResult:
|
||||||
|
# Get current game state
|
||||||
|
game = await self.game_repo.get_by_id(game_id)
|
||||||
|
|
||||||
|
# Check if AI needs to make decisions
|
||||||
|
if game.ai_team:
|
||||||
|
ai_decision = await self.ai_service.get_defensive_decision(game)
|
||||||
|
play_data.update(ai_decision)
|
||||||
|
|
||||||
|
# Execute the play
|
||||||
|
return await self._execute_play_logic(game, play_data)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Architecture
|
||||||
|
|
||||||
|
### Test Structure
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── unit/ # Fast, isolated unit tests
|
||||||
|
│ ├── services/
|
||||||
|
│ │ ├── test_game_service.py
|
||||||
|
│ │ ├── test_gameplay_service.py
|
||||||
|
│ │ └── test_ai_service.py
|
||||||
|
│ ├── engine/
|
||||||
|
│ │ ├── test_dice.py
|
||||||
|
│ │ └── test_simulation.py
|
||||||
|
│ └── models/
|
||||||
|
│ └── test_game_models.py
|
||||||
|
├── integration/ # Database + service integration
|
||||||
|
│ ├── test_game_flow.py
|
||||||
|
│ └── test_auth_flow.py
|
||||||
|
└── e2e/ # Full application tests
|
||||||
|
├── test_game_creation.py
|
||||||
|
└── test_gameplay_flow.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Patterns
|
||||||
|
|
||||||
|
#### Unit Testing Services
|
||||||
|
```python
|
||||||
|
# test_game_service.py
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_game_repo():
|
||||||
|
return Mock(spec=GameRepository)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def game_service(mock_game_repo):
|
||||||
|
return GameService(session=Mock(), game_repo=mock_game_repo)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_game_validates_teams(game_service, mock_game_repo):
|
||||||
|
# Test business logic without database
|
||||||
|
mock_game_repo.get_by_id.return_value = None # Team not found
|
||||||
|
|
||||||
|
with pytest.raises(TeamNotFoundException):
|
||||||
|
await game_service.create_game(999, 1000, "ranked")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Integration Testing
|
||||||
|
```python
|
||||||
|
# test_game_flow.py
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_complete_game_creation_flow(db_session):
|
||||||
|
# Test with real database but isolated transaction
|
||||||
|
game_service = GameService(db_session)
|
||||||
|
|
||||||
|
# Create test data
|
||||||
|
team1 = await create_test_team(db_session)
|
||||||
|
team2 = await create_test_team(db_session)
|
||||||
|
|
||||||
|
# Test full flow
|
||||||
|
game = await game_service.create_game(team1.id, team2.id, "ranked")
|
||||||
|
|
||||||
|
assert game.id is not None
|
||||||
|
assert game.away_team_id == team1.id
|
||||||
|
```
|
||||||
|
|
||||||
|
### What to Test by Layer
|
||||||
|
|
||||||
|
#### Services (Unit Tests)
|
||||||
|
- **Critical**: Business logic validation, error handling, service interactions
|
||||||
|
- **Important**: Edge cases, complex calculations
|
||||||
|
- **Nice to have**: Performance characteristics
|
||||||
|
|
||||||
|
#### Engine (Unit Tests)
|
||||||
|
- **Critical**: Game simulation accuracy, dice probability, calculation correctness
|
||||||
|
- **Important**: Edge cases in game rules, random seed consistency
|
||||||
|
- **Nice to have**: Performance optimization
|
||||||
|
|
||||||
|
#### Integration Tests
|
||||||
|
- **Critical**: Database operations, service composition, authentication flow
|
||||||
|
- **Important**: Error propagation, transaction handling
|
||||||
|
- **Nice to have**: Caching behavior
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Strategy
|
||||||
|
|
||||||
|
### Phase 1: Service Foundation
|
||||||
|
1. Create basic service interfaces
|
||||||
|
2. Extract simple business logic from Discord app
|
||||||
|
3. Set up dependency injection patterns
|
||||||
|
4. Create service unit tests
|
||||||
|
|
||||||
|
### Phase 2: Core Service Implementation
|
||||||
|
1. Implement GameService with full game management
|
||||||
|
2. Implement GameplayService with simulation logic
|
||||||
|
3. Implement AIService with decision making
|
||||||
|
4. Add comprehensive service tests
|
||||||
|
|
||||||
|
### Phase 3: Web Integration
|
||||||
|
1. Create FastAPI routes that use services
|
||||||
|
2. Add authentication and session services
|
||||||
|
3. Implement HTMX endpoints with service calls
|
||||||
|
4. Integration testing
|
||||||
|
|
||||||
|
### Phase 4: Advanced Features
|
||||||
|
1. Add repository layer if needed for complex queries
|
||||||
|
2. Implement notification service
|
||||||
|
3. Add caching and performance optimization
|
||||||
|
4. End-to-end testing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Service Layer Performance
|
||||||
|
- Use connection pooling for database sessions
|
||||||
|
- Implement caching at service level for expensive operations
|
||||||
|
- Consider async/await for I/O bound operations
|
||||||
|
- Monitor service call patterns for optimization opportunities
|
||||||
|
|
||||||
|
### Database Optimization
|
||||||
|
- Repositories handle complex queries and optimization
|
||||||
|
- Use eager loading for frequently accessed relationships
|
||||||
|
- Implement query result caching where appropriate
|
||||||
|
- Monitor N+1 query patterns
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Starting a New Session
|
||||||
|
1. Read current progress in `./.claude/web-migration-v2.md`
|
||||||
|
2. Update TodoWrite with current tasks focusing on service implementation
|
||||||
|
3. Work on the current phase tasks in order
|
||||||
|
4. Follow the service-first approach for any new business logic
|
||||||
|
5. Update progress in migration plan when completing major milestones
|
||||||
|
|
||||||
|
### Service Development Process
|
||||||
|
1. **Design**: Define service interface and responsibilities
|
||||||
|
2. **Test**: Write unit tests for expected behavior
|
||||||
|
3. **Implement**: Build service following single responsibility principle
|
||||||
|
4. **Integrate**: Connect service to routes and other services
|
||||||
|
5. **Validate**: Run full test suite and integration tests
|
||||||
|
|
||||||
|
### Virtual Environment Setup
|
||||||
|
Always check for virtual environment:
|
||||||
|
```bash
|
||||||
|
# Check for existing venv
|
||||||
|
ls -la | grep venv
|
||||||
|
|
||||||
|
# Create if doesn't exist (user works with discord.py, wants venv)
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # Linux/Mac
|
||||||
|
# or
|
||||||
|
venv\Scripts\activate # Windows
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Requirements
|
||||||
|
User's CLAUDE.md specifies:
|
||||||
|
- "You do not need to ask for permission to run tests"
|
||||||
|
- Use pytest for all testing
|
||||||
|
- Include unit tests for services, integration tests for full flows
|
||||||
|
- Test critical game logic thoroughly
|
||||||
|
- Aim for 80%+ test coverage on services
|
||||||
|
|
||||||
|
### Git Commit Standards
|
||||||
|
User's CLAUDE.md specifies:
|
||||||
|
- Prefix commit messages with "CLAUDE: "
|
||||||
|
- Focus on the "why" rather than the "what"
|
||||||
|
- Example: "CLAUDE: Extract game creation logic into GameService for better testability"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration-Specific Guidelines
|
||||||
|
|
||||||
|
### Files to Prioritize for Service Extraction
|
||||||
|
|
||||||
|
**High Priority (Extract to Services First)**:
|
||||||
|
- `../discord-app/command_logic/logic_gameplay.py` → `services/gameplay_service.py`
|
||||||
|
- `../discord-app/in_game/ai_manager.py` → `services/ai_service.py`
|
||||||
|
- Game creation/management logic → `services/game_service.py`
|
||||||
|
|
||||||
|
**Medium Priority (Core Engine - Keep Stateless)**:
|
||||||
|
- `../discord-app/in_game/simulations.py` → `engine/simulation.py`
|
||||||
|
- `../discord-app/dice.py` → `engine/dice.py`
|
||||||
|
|
||||||
|
**Lower Priority (Supporting Services)**:
|
||||||
|
- Authentication logic → `services/auth_service.py`
|
||||||
|
- User management → `services/user_service.py`
|
||||||
|
- Notifications → `services/notification_service.py`
|
||||||
|
|
||||||
|
### Service Extraction Patterns
|
||||||
|
|
||||||
|
#### Pattern 1: Discord Command → Service Method
|
||||||
|
```python
|
||||||
|
# ❌ Discord app pattern
|
||||||
|
@app_commands.command(name="start_game")
|
||||||
|
async def start_game(interaction: discord.Interaction):
|
||||||
|
# 50+ lines of business logic mixed with Discord API calls
|
||||||
|
|
||||||
|
# ✅ Web app pattern
|
||||||
|
class GameService:
|
||||||
|
async def create_game(self, away_team_id: int, home_team_id: int) -> Game:
|
||||||
|
# Pure business logic, no Discord dependencies
|
||||||
|
|
||||||
|
@router.post("/games/start")
|
||||||
|
async def start_game(request: StartGameRequest, game_service: GameService = Depends()):
|
||||||
|
return await game_service.create_game(request.away_team_id, request.home_team_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Pattern 2: Game Logic → Service + Engine
|
||||||
|
```python
|
||||||
|
# ❌ Mixed business logic and game engine
|
||||||
|
def process_at_bat(interaction, game_id, batter_decision):
|
||||||
|
# Mixed: Discord UI + game rules + database + simulation
|
||||||
|
|
||||||
|
# ✅ Separated concerns
|
||||||
|
class GameplayService:
|
||||||
|
async def process_at_bat(self, game_id: int, batter_decision: str) -> PlayResult:
|
||||||
|
# Business logic: validation, state management
|
||||||
|
|
||||||
|
def _simulate_at_bat(self, pitcher_stats, batter_stats) -> SimulationResult:
|
||||||
|
# Pure game engine logic
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Pattern 3: AI Decision Making → AIService
|
||||||
|
```python
|
||||||
|
# ✅ Clean AI service
|
||||||
|
class AIService:
|
||||||
|
async def get_pitching_decision(self, game_state: GameState) -> PitchingDecision:
|
||||||
|
# AI logic without Discord dependencies
|
||||||
|
|
||||||
|
async def get_batting_decision(self, game_state: GameState) -> BattingDecision:
|
||||||
|
# AI logic without Discord dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
### Architecture Quality
|
||||||
|
- Clear separation between models, services, routes
|
||||||
|
- Services have single responsibilities
|
||||||
|
- Business logic not mixed with web framework code
|
||||||
|
- Comprehensive service unit tests
|
||||||
|
|
||||||
|
### Code Quality Standards
|
||||||
|
- All services have 90%+ test coverage
|
||||||
|
- Type hints required for all service methods
|
||||||
|
- Error handling appropriate for service layer
|
||||||
|
- Performance acceptable for multiple concurrent users
|
||||||
|
- No Discord-specific dependencies remaining
|
||||||
|
|
||||||
|
### Migration Success
|
||||||
|
- Working single-player baseball game with Discord OAuth
|
||||||
|
- Live scoreboard with news ticker
|
||||||
|
- Clean, service-oriented codebase ready for scaling
|
||||||
|
- Comprehensive test coverage across all layers
|
||||||
|
- Responsive web interface (mobile + desktop)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Emergency Contacts & Resources
|
||||||
|
|
||||||
|
- **Project Owner**: Check project README for contact info
|
||||||
|
- **Discord App Source**: `../discord-app/` directory
|
||||||
|
- **Migration Plan**: `./.claude/web-migration-v2.md`
|
||||||
|
- **User Preferences**: `/home/cal/.claude/CLAUDE.md`
|
||||||
|
- **Architecture Reference**: This file (DEVELOPMENT_GUIDE_V2.md)
|
||||||
|
|
||||||
|
Remember: This is a migration project with the goal of creating a scalable, maintainable web application using Model/Service Architecture. Always prioritize clean architecture and comprehensive testing over speed of development.
|
||||||
46
app/README.md
Normal file
46
app/README.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# App Directory
|
||||||
|
|
||||||
|
This directory contains the main application code for the Paper Dynasty web app, following the **Model/Service Architecture** pattern.
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
The app follows a clean separation of concerns:
|
||||||
|
|
||||||
|
- **Models** (`models/`) - Pure data models using SQLModel
|
||||||
|
- **Services** (`services/`) - Business logic layer with dependency injection
|
||||||
|
- **Controllers** (`routers/`) - Thin FastAPI route handlers
|
||||||
|
- **Engine** (`engine/`) - Stateless game simulation functions
|
||||||
|
- **Configuration** (`config/`) - Application settings and constants
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- `main.py` - FastAPI application setup and configuration
|
||||||
|
- Entry point for the web application
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── main.py # FastAPI application setup
|
||||||
|
├── config/ # Configuration and constants
|
||||||
|
├── models/ # SQLModel data models
|
||||||
|
├── services/ # Business logic layer (⭐ Core of architecture)
|
||||||
|
├── repositories/ # Optional data access layer
|
||||||
|
├── routers/ # FastAPI route handlers
|
||||||
|
├── engine/ # Stateless game simulation
|
||||||
|
├── static/ # Static web assets
|
||||||
|
└── templates/ # Jinja2 HTML templates
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service-First Development
|
||||||
|
|
||||||
|
When adding new features:
|
||||||
|
|
||||||
|
1. **Start with services** - implement business logic in the service layer
|
||||||
|
2. **Test services independently** - unit test with mocked dependencies
|
||||||
|
3. **Create thin routes** - delegate to services via dependency injection
|
||||||
|
4. **Keep engine stateless** - pure functions for game calculations
|
||||||
|
|
||||||
|
## Migration Context
|
||||||
|
|
||||||
|
This application is migrated from a Discord bot (`../discord-app/`) with the goal of extracting Discord-specific business logic into clean, testable services that can support multiple interfaces (web, API, mobile, etc.).
|
||||||
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
51
app/config/README.md
Normal file
51
app/config/README.md
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Config Directory
|
||||||
|
|
||||||
|
This directory contains application configuration, constants, and settings management.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
### `constants.py`
|
||||||
|
- **Settings class**: Environment-based configuration using `os.getenv()`
|
||||||
|
- **GameConstants**: Migrated constants from Discord app (`../discord-app/constants.py`)
|
||||||
|
- **MLB Teams**: Team lookup tables and metadata
|
||||||
|
- **Database URLs**: PostgreSQL connection strings for development and testing
|
||||||
|
|
||||||
|
### `logging_config.py`
|
||||||
|
- **Rotating file handlers**: Per user's CLAUDE.md requirements
|
||||||
|
- **Console and file output**: Structured logging with proper formatting
|
||||||
|
- **Service-specific loggers**: Standardized naming patterns
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### Environment Configuration
|
||||||
|
The `Settings` class provides environment-based configuration:
|
||||||
|
```python
|
||||||
|
from app.config.constants import settings
|
||||||
|
|
||||||
|
# Database connection
|
||||||
|
DATABASE_URL = settings.DATABASE_URL
|
||||||
|
|
||||||
|
# Discord OAuth
|
||||||
|
DISCORD_CLIENT_ID = settings.DISCORD_CLIENT_ID
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migrated Constants
|
||||||
|
All game constants are migrated from the actual Discord app source, not assumed values:
|
||||||
|
- Season configuration (SBA_SEASON, PD_SEASON)
|
||||||
|
- Cardset configurations and rankings
|
||||||
|
- Player rarity values
|
||||||
|
- MLB team lookup tables
|
||||||
|
|
||||||
|
### Logging Standards
|
||||||
|
Follows rotating log pattern with service-specific naming:
|
||||||
|
```python
|
||||||
|
logger = logging.getLogger(f'{__name__}.{self.__class__.__name__}')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Copy `.env.example` to `.env` and configure:
|
||||||
|
- `DATABASE_URL` - PostgreSQL connection string
|
||||||
|
- `DISCORD_CLIENT_ID` / `DISCORD_CLIENT_SECRET` - OAuth credentials
|
||||||
|
- `DEBUG` - Enable debug mode
|
||||||
|
- `SECRET_KEY` - Application secret for sessions
|
||||||
0
app/config/__init__.py
Normal file
0
app/config/__init__.py
Normal file
122
app/config/constants.py
Normal file
122
app/config/constants.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
"""
|
||||||
|
Application constants and configuration values.
|
||||||
|
Migrated from Discord app constants.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Optional, Literal
|
||||||
|
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
"""Application settings and configuration."""
|
||||||
|
|
||||||
|
# Database settings
|
||||||
|
DATABASE_URL: str = os.getenv(
|
||||||
|
"DATABASE_URL",
|
||||||
|
"postgresql+psycopg://paper_dynasty_user:paper_dynasty_dev_password@localhost:5432/paper_dynasty"
|
||||||
|
)
|
||||||
|
DATABASE_TEST_URL: str = os.getenv(
|
||||||
|
"DATABASE_TEST_URL",
|
||||||
|
"postgresql+psycopg://paper_dynasty_user:paper_dynasty_dev_password@localhost:5432/paper_dynasty_test"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Discord OAuth settings
|
||||||
|
DISCORD_CLIENT_ID: Optional[str] = os.getenv("DISCORD_CLIENT_ID")
|
||||||
|
DISCORD_CLIENT_SECRET: Optional[str] = os.getenv("DISCORD_CLIENT_SECRET")
|
||||||
|
DISCORD_REDIRECT_URI: Optional[str] = os.getenv("DISCORD_REDIRECT_URI")
|
||||||
|
|
||||||
|
# Security settings
|
||||||
|
SECRET_KEY: str = os.getenv("SECRET_KEY", "development-secret-key-change-in-production")
|
||||||
|
SESSION_EXPIRE_HOURS: int = int(os.getenv("SESSION_EXPIRE_HOURS", "24"))
|
||||||
|
|
||||||
|
# Application settings
|
||||||
|
DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true"
|
||||||
|
ENVIRONMENT: str = os.getenv("ENVIRONMENT", "development")
|
||||||
|
|
||||||
|
# Game settings
|
||||||
|
MAX_CONCURRENT_GAMES: int = int(os.getenv("MAX_CONCURRENT_GAMES", "100"))
|
||||||
|
GAME_TIMEOUT_HOURS: int = int(os.getenv("GAME_TIMEOUT_HOURS", "4"))
|
||||||
|
|
||||||
|
|
||||||
|
# Global settings instance
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
|
||||||
|
# Constants migrated from Discord app
|
||||||
|
class GameConstants:
|
||||||
|
"""Game constants migrated from Discord app constants.py"""
|
||||||
|
|
||||||
|
# Season Configuration (from Discord app)
|
||||||
|
SBA_SEASON = 11
|
||||||
|
PD_SEASON = 9
|
||||||
|
LIVE_CARDSET_ID = 24
|
||||||
|
LIVE_PROMO_CARDSET_ID = 25
|
||||||
|
MAX_CARDSET_ID = 30
|
||||||
|
|
||||||
|
# Ranked cardsets for web app
|
||||||
|
RANKED_CARDSETS = [20, 21, 22, 17, 18, 19]
|
||||||
|
|
||||||
|
# Application Configuration
|
||||||
|
SBA_COLOR = 'a6ce39'
|
||||||
|
PD_PLAYERS = 'Paper Dynasty Players'
|
||||||
|
|
||||||
|
# External URLs and Resources
|
||||||
|
PD_IMAGE_BUCKET = 'https://paper-dynasty.s3.us-east-1.amazonaws.com/static-images'
|
||||||
|
|
||||||
|
# Player Rarity Values (from Discord app)
|
||||||
|
RARITY = {
|
||||||
|
'HoF': 8,
|
||||||
|
'MVP': 5,
|
||||||
|
'All-Star': 3,
|
||||||
|
'Starter': 2,
|
||||||
|
'Reserve': 1,
|
||||||
|
'Replacement': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Color Definitions
|
||||||
|
COLORS = {
|
||||||
|
'sba': int('a6ce39', 16),
|
||||||
|
'yellow': int('FFEA00', 16),
|
||||||
|
'red': int('C70039', 16),
|
||||||
|
'white': int('FFFFFF', 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# MLB Teams Lookup (migrated from Discord app)
|
||||||
|
ALL_MLB_TEAMS = {
|
||||||
|
'Arizona Diamondbacks': ['ARI', 'Diamondbacks'],
|
||||||
|
'Atlanta Braves': ['ATL', 'MLN', 'Braves'],
|
||||||
|
'Baltimore Orioles': ['BAL', 'Orioles'],
|
||||||
|
'Boston Red Sox': ['BOS', 'Red Sox'],
|
||||||
|
'Chicago Cubs': ['CHC', 'Cubs'],
|
||||||
|
'Chicago White Sox': ['CHW', 'White Sox'],
|
||||||
|
'Cincinnati Reds': ['CIN', 'Reds'],
|
||||||
|
'Cleveland Guardians': ['CLE', 'Guardians'],
|
||||||
|
'Colorado Rockies': ['COL', 'Rockies'],
|
||||||
|
'Detroit Tigers': ['DET', 'Tigers'],
|
||||||
|
'Houston Astros': ['HOU', 'Astros'],
|
||||||
|
'Kansas City Royals': ['KCR', 'Royals'],
|
||||||
|
'Los Angeles Angels': ['LAA', 'CAL', 'Angels'],
|
||||||
|
'Los Angeles Dodgers': ['LAD', 'Dodgers'],
|
||||||
|
'Miami Marlins': ['MIA', 'Marlins'],
|
||||||
|
'Milwaukee Brewers': ['MIL', 'MKE', 'Brewers'],
|
||||||
|
'Minnesota Twins': ['MIN', 'Twins'],
|
||||||
|
'New York Mets': ['NYM', 'Mets'],
|
||||||
|
'New York Yankees': ['NYY', 'Yankees'],
|
||||||
|
'Oakland Athletics': ['OAK', 'Athletics'],
|
||||||
|
'Philadelphia Phillies': ['PHI', 'Phillies'],
|
||||||
|
'Pittsburgh Pirates': ['PIT', 'Pirates'],
|
||||||
|
'San Diego Padres': ['SDP', 'Padres'],
|
||||||
|
'Seattle Mariners': ['SEA', 'Mariners'],
|
||||||
|
'San Francisco Giants': ['SFG', 'Giants'],
|
||||||
|
'St Louis Cardinals': ['STL', 'Cardinals'],
|
||||||
|
'Tampa Bay Rays': ['TBR', 'Rays'],
|
||||||
|
'Texas Rangers': ['TEX', 'Senators', 'Rangers'],
|
||||||
|
'Toronto Blue Jays': ['TOR', 'Jays'],
|
||||||
|
'Washington Nationals': ['WSN', 'WAS', 'Nationals'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Type Definitions (from Discord app)
|
||||||
|
DEFENSE_LITERAL = Literal['Pitcher', 'Catcher', 'First Base', 'Second Base', 'Third Base', 'Shortstop', 'Left Field', 'Center Field', 'Right Field']
|
||||||
|
DEFENSE_NO_PITCHER_LITERAL = Literal['Catcher', 'First Base', 'Second Base', 'Third Base', 'Shortstop', 'Left Field', 'Center Field', 'Right Field']
|
||||||
62
app/config/logging_config.py
Normal file
62
app/config/logging_config.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
"""
|
||||||
|
Logging configuration with rotating file handlers.
|
||||||
|
Follows user's CLAUDE.md requirements for rotating loggers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import logging.handlers
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging(
|
||||||
|
level: int = logging.INFO,
|
||||||
|
max_bytes: int = 10 * 1024 * 1024, # 10MB
|
||||||
|
backup_count: int = 5
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Set up rotating file logging for the application.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
level: Logging level (default: INFO)
|
||||||
|
max_bytes: Maximum bytes per log file before rotation
|
||||||
|
backup_count: Number of backup files to keep
|
||||||
|
"""
|
||||||
|
# Create logs directory if it doesn't exist
|
||||||
|
log_dir = Path("logs")
|
||||||
|
log_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# Configure root logger
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
root_logger.setLevel(level)
|
||||||
|
|
||||||
|
# Remove existing handlers to avoid duplicates
|
||||||
|
for handler in root_logger.handlers[:]:
|
||||||
|
root_logger.removeHandler(handler)
|
||||||
|
|
||||||
|
# Console handler for development
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_formatter = logging.Formatter(
|
||||||
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
console_handler.setFormatter(console_formatter)
|
||||||
|
root_logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
# Rotating file handler
|
||||||
|
file_handler = logging.handlers.RotatingFileHandler(
|
||||||
|
filename=log_dir / "paper_dynasty.log",
|
||||||
|
maxBytes=max_bytes,
|
||||||
|
backupCount=backup_count
|
||||||
|
)
|
||||||
|
file_formatter = logging.Formatter(
|
||||||
|
'%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s'
|
||||||
|
)
|
||||||
|
file_handler.setFormatter(file_formatter)
|
||||||
|
root_logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
# Set specific logger levels
|
||||||
|
logging.getLogger("uvicorn").setLevel(logging.INFO)
|
||||||
|
logging.getLogger("sqlalchemy").setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
logger = logging.getLogger(f'{__name__}.setup_logging')
|
||||||
|
logger.info("Logging configuration completed")
|
||||||
0
app/engine/__init__.py
Normal file
0
app/engine/__init__.py
Normal file
65
app/main.py
Normal file
65
app/main.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
"""
|
||||||
|
FastAPI application setup for Paper Dynasty web app.
|
||||||
|
Implements Model/Service Architecture with dependency injection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
|
from app.routers import auth, games, api, pages
|
||||||
|
from app.config.logging_config import setup_logging
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(f'{__name__}.main')
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
"""Application lifespan manager."""
|
||||||
|
# Startup
|
||||||
|
setup_logging()
|
||||||
|
logger.info("FastAPI application starting up")
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
# Shutdown
|
||||||
|
logger.info("FastAPI application shutting down")
|
||||||
|
|
||||||
|
|
||||||
|
def create_app() -> FastAPI:
|
||||||
|
"""Create and configure the FastAPI application."""
|
||||||
|
app = FastAPI(
|
||||||
|
title="Paper Dynasty",
|
||||||
|
description="Baseball simulation web game with Model/Service Architecture",
|
||||||
|
version="2.0.0",
|
||||||
|
lifespan=lifespan
|
||||||
|
)
|
||||||
|
|
||||||
|
# Include routers
|
||||||
|
app.include_router(auth.router, prefix="/auth", tags=["authentication"])
|
||||||
|
app.include_router(games.router, prefix="/games", tags=["games"])
|
||||||
|
app.include_router(api.router, prefix="/api", tags=["api"])
|
||||||
|
app.include_router(pages.router, tags=["pages"])
|
||||||
|
|
||||||
|
# Static files and templates
|
||||||
|
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
# Create the application instance
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
"""Health check endpoint."""
|
||||||
|
return {"status": "healthy", "service": "paper-dynasty-web"}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run("app.main:app", host="0.0.0.0", port=8001, reload=True)
|
||||||
0
app/models/__init__.py
Normal file
0
app/models/__init__.py
Normal file
0
app/repositories/__init__.py
Normal file
0
app/repositories/__init__.py
Normal file
138
app/routers/README.md
Normal file
138
app/routers/README.md
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
# Routers Directory
|
||||||
|
|
||||||
|
This directory contains **FastAPI route handlers** following the Model/Service Architecture pattern. Routes are thin controllers that handle HTTP concerns and delegate business logic to services.
|
||||||
|
|
||||||
|
## Architecture Pattern
|
||||||
|
|
||||||
|
### Route Responsibilities
|
||||||
|
- **HTTP Handling**: Request/response processing, status codes
|
||||||
|
- **Input Validation**: Request data validation and parsing
|
||||||
|
- **Service Delegation**: Delegate business logic to services
|
||||||
|
- **Response Formatting**: Transform service results for HTTP responses
|
||||||
|
|
||||||
|
### Design Principles
|
||||||
|
- **Thin Controllers**: Minimal logic, delegate to services
|
||||||
|
- **Dependency Injection**: Use FastAPI `Depends()` for services
|
||||||
|
- **Error Handling**: Transform service exceptions to HTTP errors
|
||||||
|
- **Documentation**: FastAPI auto-generates OpenAPI documentation
|
||||||
|
|
||||||
|
## Current Files
|
||||||
|
|
||||||
|
### `auth.py`
|
||||||
|
Authentication routes:
|
||||||
|
- **Discord OAuth**: Login redirect and callback handling
|
||||||
|
- **Session Management**: Login/logout functionality
|
||||||
|
- **Uses**: `AuthService` for business logic
|
||||||
|
|
||||||
|
### `games.py`
|
||||||
|
Game-related endpoints:
|
||||||
|
- **Game Creation**: Start new games
|
||||||
|
- **Game Retrieval**: Get game details and listings
|
||||||
|
- **Uses**: `GameService` for business logic
|
||||||
|
|
||||||
|
### `api.py`
|
||||||
|
JSON API endpoints for HTMX:
|
||||||
|
- **Live Updates**: Scoreboard and game state
|
||||||
|
- **Player Actions**: Execute gameplay actions
|
||||||
|
- **Uses**: `GameplayService` for real-time updates
|
||||||
|
|
||||||
|
### `pages.py`
|
||||||
|
HTML page routes with Jinja2 templates:
|
||||||
|
- **Home Page**: Main application entry point
|
||||||
|
- **Game Interface**: Live game viewing
|
||||||
|
- **Template Rendering**: Server-side HTML generation
|
||||||
|
|
||||||
|
## Route Pattern Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from app.services.service_container import GameServiceDep
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.post("/games/start")
|
||||||
|
async def start_game(
|
||||||
|
request: StartGameRequest,
|
||||||
|
game_service: GameServiceDep
|
||||||
|
):
|
||||||
|
"""Start a new game - delegates to GameService."""
|
||||||
|
try:
|
||||||
|
game = await game_service.create_game(
|
||||||
|
away_team_id=request.away_team_id,
|
||||||
|
home_team_id=request.home_team_id
|
||||||
|
)
|
||||||
|
return {"game_id": game.id, "status": "created"}
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service Integration
|
||||||
|
|
||||||
|
Routes use dependency injection to access services:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Service dependencies from service_container.py
|
||||||
|
GameServiceDep = Annotated[GameService, Depends(get_game_service)]
|
||||||
|
UserServiceDep = Annotated[UserService, Depends(get_user_service)]
|
||||||
|
AuthServiceDep = Annotated[AuthService, Depends(get_auth_service)]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Route Organization
|
||||||
|
|
||||||
|
### `/auth` - Authentication
|
||||||
|
- `GET /auth/login` - Discord OAuth redirect
|
||||||
|
- `GET /auth/callback` - OAuth callback handler
|
||||||
|
- `POST /auth/logout` - Logout and session cleanup
|
||||||
|
|
||||||
|
### `/games` - Game Management
|
||||||
|
- `POST /games/start` - Create new game
|
||||||
|
- `GET /games/{game_id}` - Get game details
|
||||||
|
- `GET /games/` - List active games
|
||||||
|
|
||||||
|
### `/api` - HTMX JSON API
|
||||||
|
- `GET /api/scoreboard/{game_id}` - Live scoreboard data
|
||||||
|
- `POST /api/play` - Execute gameplay action
|
||||||
|
|
||||||
|
### `/` - HTML Pages
|
||||||
|
- `GET /` - Home page
|
||||||
|
- `GET /game/{game_id}` - Game interface page
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
Routes should transform service exceptions to appropriate HTTP responses:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
result = await service.do_something()
|
||||||
|
return result
|
||||||
|
except NotFoundException as e:
|
||||||
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
|
except ValidationError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Internal server error")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Routes
|
||||||
|
|
||||||
|
Routes should be tested with service dependencies mocked:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
def test_start_game(mock_game_service):
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
# Mock service response
|
||||||
|
mock_game_service.create_game.return_value = Game(id=1)
|
||||||
|
|
||||||
|
response = client.post("/games/start", json={
|
||||||
|
"away_team_id": 1,
|
||||||
|
"home_team_id": 2
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["game_id"] == 1
|
||||||
|
```
|
||||||
0
app/routers/__init__.py
Normal file
0
app/routers/__init__.py
Normal file
25
app/routers/api.py
Normal file
25
app/routers/api.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
"""
|
||||||
|
JSON API routes for HTMX endpoints.
|
||||||
|
Uses various services following Model/Service Architecture.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(f'{__name__}.api_router')
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/scoreboard/{game_id}")
|
||||||
|
async def get_scoreboard(game_id: int):
|
||||||
|
"""Get live scoreboard data for HTMX updates."""
|
||||||
|
# TODO: Implement scoreboard API using GameplayService
|
||||||
|
raise HTTPException(status_code=501, detail="Scoreboard API not yet implemented")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/play")
|
||||||
|
async def execute_play():
|
||||||
|
"""Execute a gameplay action."""
|
||||||
|
# TODO: Implement play execution using GameplayService
|
||||||
|
raise HTTPException(status_code=501, detail="Play execution not yet implemented")
|
||||||
33
app/routers/auth.py
Normal file
33
app/routers/auth.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
"""
|
||||||
|
Authentication routes for Discord OAuth and session management.
|
||||||
|
Uses AuthService following Model/Service Architecture.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
|
from app.services.service_container import AuthServiceDep
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(f'{__name__}.auth_router')
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/login")
|
||||||
|
async def login_redirect():
|
||||||
|
"""Redirect to Discord OAuth login."""
|
||||||
|
# TODO: Implement Discord OAuth redirect
|
||||||
|
raise HTTPException(status_code=501, detail="Discord OAuth not yet implemented")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/callback")
|
||||||
|
async def auth_callback(code: str, auth_service: AuthServiceDep):
|
||||||
|
"""Handle Discord OAuth callback."""
|
||||||
|
# TODO: Implement OAuth callback handling using AuthService
|
||||||
|
raise HTTPException(status_code=501, detail="OAuth callback not yet implemented")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/logout")
|
||||||
|
async def logout():
|
||||||
|
"""Logout user and invalidate session."""
|
||||||
|
# TODO: Implement logout using AuthService
|
||||||
|
raise HTTPException(status_code=501, detail="Logout not yet implemented")
|
||||||
33
app/routers/games.py
Normal file
33
app/routers/games.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
"""
|
||||||
|
Game-related routes using GameService.
|
||||||
|
Follows Model/Service Architecture with thin controllers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
|
from app.services.service_container import GameServiceDep
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(f'{__name__}.games_router')
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/start")
|
||||||
|
async def start_game(game_service: GameServiceDep):
|
||||||
|
"""Start a new game using GameService."""
|
||||||
|
# TODO: Implement game creation using GameService
|
||||||
|
raise HTTPException(status_code=501, detail="Game creation not yet implemented")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{game_id}")
|
||||||
|
async def get_game(game_id: int, game_service: GameServiceDep):
|
||||||
|
"""Get game details by ID."""
|
||||||
|
# TODO: Implement game retrieval using GameService
|
||||||
|
raise HTTPException(status_code=501, detail="Game retrieval not yet implemented")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/")
|
||||||
|
async def list_games(game_service: GameServiceDep):
|
||||||
|
"""List active games."""
|
||||||
|
# TODO: Implement game listing using GameService
|
||||||
|
raise HTTPException(status_code=501, detail="Game listing not yet implemented")
|
||||||
30
app/routers/pages.py
Normal file
30
app/routers/pages.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"""
|
||||||
|
HTML page routes using Jinja2 templates.
|
||||||
|
Uses services following Model/Service Architecture.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from fastapi import APIRouter, Request, Depends
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(f'{__name__}.pages_router')
|
||||||
|
router = APIRouter()
|
||||||
|
templates = Jinja2Templates(directory="app/templates")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_class=HTMLResponse)
|
||||||
|
async def home_page(request: Request):
|
||||||
|
"""Home page."""
|
||||||
|
return templates.TemplateResponse("home.html", {"request": request})
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/game/{game_id}", response_class=HTMLResponse)
|
||||||
|
async def game_page(request: Request, game_id: int):
|
||||||
|
"""Game interface page."""
|
||||||
|
# TODO: Get game data using GameService
|
||||||
|
return templates.TemplateResponse("game.html", {
|
||||||
|
"request": request,
|
||||||
|
"game_id": game_id
|
||||||
|
})
|
||||||
109
app/services/README.md
Normal file
109
app/services/README.md
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
# Services Directory
|
||||||
|
|
||||||
|
This directory contains the **business logic layer** of the Model/Service Architecture. Services are the core of the application architecture, containing all business rules and logic separated from HTTP concerns and database details.
|
||||||
|
|
||||||
|
## Architecture Pattern
|
||||||
|
|
||||||
|
### Service Responsibilities
|
||||||
|
- **Business Logic**: Core application logic and rules
|
||||||
|
- **Data Validation**: Input validation and business rule enforcement
|
||||||
|
- **Service Orchestration**: Coordination between multiple services
|
||||||
|
- **Error Handling**: Business-appropriate error handling and logging
|
||||||
|
|
||||||
|
### Design Principles
|
||||||
|
- **Single Responsibility**: Each service has one clear purpose
|
||||||
|
- **Dependency Injection**: Services receive dependencies via constructor
|
||||||
|
- **Stateless**: Services don't maintain instance state between calls
|
||||||
|
- **Testable**: Services can be unit tested with mocked dependencies
|
||||||
|
- **Pure Business Logic**: No HTTP, database, or UI concerns
|
||||||
|
|
||||||
|
## Current Files
|
||||||
|
|
||||||
|
### `base_service.py`
|
||||||
|
Abstract base class providing common functionality:
|
||||||
|
- **Standardized logging**: `self._log_operation()` and `self._log_error()`
|
||||||
|
- **Session management**: SQLModel database session handling
|
||||||
|
- **Validation helpers**: Common validation patterns
|
||||||
|
|
||||||
|
### `service_container.py`
|
||||||
|
Dependency injection container:
|
||||||
|
- **Database sessions**: Session lifecycle management
|
||||||
|
- **Service dependencies**: FastAPI `Depends()` integration
|
||||||
|
- **Type annotations**: Service dependency type aliases
|
||||||
|
|
||||||
|
## Planned Services
|
||||||
|
|
||||||
|
### Core Game Services
|
||||||
|
- **GameService**: Game creation, management, queries
|
||||||
|
- **GameplayService**: Core gameplay simulation & flow
|
||||||
|
- **AIService**: AI decision making and automation
|
||||||
|
|
||||||
|
### Supporting Services
|
||||||
|
- **UserService**: User sessions, preferences
|
||||||
|
- **AuthService**: Discord OAuth, session management
|
||||||
|
- **NotificationService**: Web notifications and real-time updates
|
||||||
|
|
||||||
|
## Service Pattern Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
from app.services.base_service import BaseService
|
||||||
|
from app.services.service_container import SessionDep
|
||||||
|
|
||||||
|
class GameService(BaseService):
|
||||||
|
def __init__(self, session: Session):
|
||||||
|
super().__init__(session)
|
||||||
|
|
||||||
|
async def create_game(self, away_team_id: int, home_team_id: int) -> Game:
|
||||||
|
self._log_operation("create_game", f"Teams: {away_team_id} vs {home_team_id}")
|
||||||
|
|
||||||
|
# Business logic here
|
||||||
|
game = Game(away_team_id=away_team_id, home_team_id=home_team_id)
|
||||||
|
|
||||||
|
# Database persistence
|
||||||
|
self.session.add(game)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
return game
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency Injection
|
||||||
|
|
||||||
|
Services are injected into routes using FastAPI's `Depends()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from app.services.service_container import GameServiceDep
|
||||||
|
|
||||||
|
@router.post("/games/start")
|
||||||
|
async def start_game(game_service: GameServiceDep):
|
||||||
|
# Service contains business logic, route handles HTTP concerns
|
||||||
|
return await game_service.create_game(away_team_id=1, home_team_id=2)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Services
|
||||||
|
|
||||||
|
Services should be unit tested independently:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_session():
|
||||||
|
return Mock(spec=Session)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def game_service(mock_session):
|
||||||
|
return GameService(mock_session)
|
||||||
|
|
||||||
|
async def test_create_game(game_service):
|
||||||
|
# Test business logic without database
|
||||||
|
result = await game_service.create_game(1, 2)
|
||||||
|
assert result.away_team_id == 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration from Discord App
|
||||||
|
|
||||||
|
When migrating from `../discord-app/`:
|
||||||
|
|
||||||
|
1. **Extract business logic** from Discord command handlers
|
||||||
|
2. **Remove Discord dependencies** (discord.py, interaction objects)
|
||||||
|
3. **Create service methods** with pure business logic
|
||||||
|
4. **Add comprehensive unit tests** for service methods
|
||||||
|
5. **Create thin route handlers** that delegate to services
|
||||||
0
app/services/__init__.py
Normal file
0
app/services/__init__.py
Normal file
66
app/services/base_service.py
Normal file
66
app/services/base_service.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
"""
|
||||||
|
Base service class for the Model/Service Architecture.
|
||||||
|
Provides common functionality and patterns for all services.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from abc import ABC
|
||||||
|
from typing import Any, Optional
|
||||||
|
from sqlmodel import Session
|
||||||
|
|
||||||
|
|
||||||
|
class BaseService(ABC):
|
||||||
|
"""
|
||||||
|
Base class for all services in the application.
|
||||||
|
Provides common functionality like logging and database session management.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, session: Session):
|
||||||
|
"""
|
||||||
|
Initialize the base service.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: SQLModel database session
|
||||||
|
"""
|
||||||
|
self.session = session
|
||||||
|
self.logger = logging.getLogger(f'{__name__}.{self.__class__.__name__}')
|
||||||
|
|
||||||
|
def _log_operation(self, operation: str, details: Optional[str] = None) -> None:
|
||||||
|
"""
|
||||||
|
Log a service operation with standardized format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
operation: The operation being performed
|
||||||
|
details: Optional additional details
|
||||||
|
"""
|
||||||
|
message = f"{self.__class__.__name__}.{operation}"
|
||||||
|
if details:
|
||||||
|
message += f" - {details}"
|
||||||
|
self.logger.info(message)
|
||||||
|
|
||||||
|
def _log_error(self, operation: str, error: Exception) -> None:
|
||||||
|
"""
|
||||||
|
Log a service error with standardized format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
operation: The operation that failed
|
||||||
|
error: The exception that occurred
|
||||||
|
"""
|
||||||
|
self.logger.error(
|
||||||
|
f"{self.__class__.__name__}.{operation} - Failed: {str(error)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _validate_required_fields(self, data: dict, required_fields: list[str]) -> None:
|
||||||
|
"""
|
||||||
|
Validate that required fields are present in data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Dictionary to validate
|
||||||
|
required_fields: List of required field names
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If any required field is missing
|
||||||
|
"""
|
||||||
|
missing_fields = [field for field in required_fields if field not in data or data[field] is None]
|
||||||
|
if missing_fields:
|
||||||
|
raise ValueError(f"Missing required fields: {', '.join(missing_fields)}")
|
||||||
90
app/services/service_container.py
Normal file
90
app/services/service_container.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
"""
|
||||||
|
Dependency injection container for services.
|
||||||
|
Manages service lifecycles and dependencies using FastAPI's dependency injection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from functools import lru_cache
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import Depends
|
||||||
|
from sqlmodel import Session, SQLModel, create_engine
|
||||||
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
|
from app.config.constants import settings
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(f'{__name__}.service_container')
|
||||||
|
|
||||||
|
|
||||||
|
# Database setup
|
||||||
|
engine = create_engine(
|
||||||
|
settings.DATABASE_URL,
|
||||||
|
echo=settings.DEBUG,
|
||||||
|
poolclass=StaticPool if "sqlite" in settings.DATABASE_URL else None,
|
||||||
|
connect_args={"check_same_thread": False} if "sqlite" in settings.DATABASE_URL else {}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_db_and_tables():
|
||||||
|
"""Create database tables."""
|
||||||
|
SQLModel.metadata.create_all(engine)
|
||||||
|
|
||||||
|
|
||||||
|
def get_session() -> Session:
|
||||||
|
"""
|
||||||
|
Dependency to get database session.
|
||||||
|
Each request gets its own session that's automatically closed.
|
||||||
|
"""
|
||||||
|
with Session(engine) as session:
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Database session error: {str(e)}")
|
||||||
|
session.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
|
# Type aliases for dependency injection
|
||||||
|
SessionDep = Annotated[Session, Depends(get_session)]
|
||||||
|
|
||||||
|
|
||||||
|
# Service dependencies - these will be implemented as services are created
|
||||||
|
def get_game_service(session: SessionDep):
|
||||||
|
"""Get GameService instance."""
|
||||||
|
from app.services.game_service import GameService
|
||||||
|
return GameService(session)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_service(session: SessionDep):
|
||||||
|
"""Get UserService instance."""
|
||||||
|
from app.services.user_service import UserService
|
||||||
|
return UserService(session)
|
||||||
|
|
||||||
|
|
||||||
|
def get_auth_service(session: SessionDep):
|
||||||
|
"""Get AuthService instance."""
|
||||||
|
from app.services.auth_service import AuthService
|
||||||
|
return AuthService(session)
|
||||||
|
|
||||||
|
|
||||||
|
def get_gameplay_service(session: SessionDep):
|
||||||
|
"""Get GameplayService instance."""
|
||||||
|
from app.services.gameplay_service import GameplayService
|
||||||
|
return GameplayService(session)
|
||||||
|
|
||||||
|
|
||||||
|
def get_ai_service(session: SessionDep):
|
||||||
|
"""Get AIService instance."""
|
||||||
|
from app.services.ai_service import AIService
|
||||||
|
return AIService(session)
|
||||||
|
|
||||||
|
|
||||||
|
# Type aliases for service dependencies
|
||||||
|
GameServiceDep = Annotated[object, Depends(get_game_service)]
|
||||||
|
UserServiceDep = Annotated[object, Depends(get_user_service)]
|
||||||
|
AuthServiceDep = Annotated[object, Depends(get_auth_service)]
|
||||||
|
GameplayServiceDep = Annotated[object, Depends(get_gameplay_service)]
|
||||||
|
AIServiceDep = Annotated[object, Depends(get_ai_service)]
|
||||||
13
app/templates/home.html
Normal file
13
app/templates/home.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Paper Dynasty - Baseball Simulation</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Paper Dynasty</h1>
|
||||||
|
<p>Baseball simulation web app - Coming Soon!</p>
|
||||||
|
<p>Model/Service Architecture implementation</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15
|
||||||
|
container_name: pddev-postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: paper_dynasty_user
|
||||||
|
POSTGRES_PASSWORD: paper_dynasty_dev_password
|
||||||
|
POSTGRES_DB: paper_dynasty
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U paper_dynasty_user -d paper_dynasty"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
26
requirements.txt
Normal file
26
requirements.txt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# FastAPI and web framework dependencies
|
||||||
|
fastapi==0.104.1
|
||||||
|
uvicorn[standard]==0.24.0
|
||||||
|
jinja2==3.1.2
|
||||||
|
python-multipart==0.0.6
|
||||||
|
|
||||||
|
# Database and ORM
|
||||||
|
sqlmodel==0.0.14
|
||||||
|
psycopg[binary]==3.2.3
|
||||||
|
alembic==1.13.1
|
||||||
|
|
||||||
|
# Authentication and security
|
||||||
|
python-jose[cryptography]==3.3.0
|
||||||
|
passlib[bcrypt]==1.7.4
|
||||||
|
httpx==0.25.2
|
||||||
|
|
||||||
|
# Development and testing
|
||||||
|
pytest==7.4.3
|
||||||
|
pytest-asyncio==0.21.1
|
||||||
|
pytest-cov==4.1.0
|
||||||
|
|
||||||
|
# Logging and monitoring
|
||||||
|
structlog==23.2.0
|
||||||
|
|
||||||
|
# Static files and templates
|
||||||
|
aiofiles==23.2.1
|
||||||
46
scripts/README.md
Normal file
46
scripts/README.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Scripts Directory
|
||||||
|
|
||||||
|
This directory contains database initialization scripts and other utility scripts for the Paper Dynasty web app.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
### `init-db.sql`
|
||||||
|
PostgreSQL initialization script that runs when the Docker container is first created:
|
||||||
|
|
||||||
|
- **Creates test database**: `paper_dynasty_test` for integration testing
|
||||||
|
- **Grants permissions**: Ensures `paper_dynasty_user` has access to both databases
|
||||||
|
- **Runs automatically**: Executed by Docker Compose on first container startup
|
||||||
|
|
||||||
|
## Database Setup
|
||||||
|
|
||||||
|
The `init-db.sql` script is automatically executed when the PostgreSQL container starts for the first time via Docker's `docker-entrypoint-initdb.d` mechanism.
|
||||||
|
|
||||||
|
### What it creates:
|
||||||
|
```sql
|
||||||
|
-- Main development database (created by POSTGRES_DB env var)
|
||||||
|
-- paper_dynasty
|
||||||
|
|
||||||
|
-- Test database for integration tests
|
||||||
|
CREATE DATABASE paper_dynasty_test;
|
||||||
|
|
||||||
|
-- User permissions
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE paper_dynasty TO paper_dynasty_user;
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE paper_dynasty_test TO paper_dynasty_user;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container Integration
|
||||||
|
The script is mounted into the PostgreSQL container via `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Scripts
|
||||||
|
|
||||||
|
This directory can be extended with additional utility scripts:
|
||||||
|
|
||||||
|
- **Migration scripts**: Database schema migrations
|
||||||
|
- **Data seeding**: Sample data for development
|
||||||
|
- **Backup scripts**: Database backup utilities
|
||||||
|
- **Development helpers**: Setup and maintenance scripts
|
||||||
9
scripts/init-db.sql
Normal file
9
scripts/init-db.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
-- Initial database setup for Paper Dynasty
|
||||||
|
-- This script runs when the PostgreSQL container is first created
|
||||||
|
|
||||||
|
-- Create any additional databases needed for testing
|
||||||
|
CREATE DATABASE paper_dynasty_test;
|
||||||
|
|
||||||
|
-- Grant permissions
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE paper_dynasty TO paper_dynasty_user;
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE paper_dynasty_test TO paper_dynasty_user;
|
||||||
206
tests/README.md
Normal file
206
tests/README.md
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
# Tests Directory
|
||||||
|
|
||||||
|
This directory contains the comprehensive test suite for the Paper Dynasty web app, organized by testing scope and purpose following the Model/Service Architecture.
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Test Pyramid Structure
|
||||||
|
- **Unit Tests (80%+ coverage)**: Fast, isolated tests for services and engine
|
||||||
|
- **Integration Tests (70%+ coverage)**: Service + database interactions
|
||||||
|
- **End-to-End Tests (Happy path)**: Complete user workflows
|
||||||
|
|
||||||
|
### Testing Priorities
|
||||||
|
1. **Services**: Core business logic with mocked dependencies
|
||||||
|
2. **Engine**: Stateless game simulation functions
|
||||||
|
3. **Integration**: Service interactions with real database
|
||||||
|
4. **Routes**: HTTP handling with mocked services
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── unit/ # Fast, isolated unit tests
|
||||||
|
│ ├── services/ # Service unit tests (MOST IMPORTANT)
|
||||||
|
│ ├── engine/ # Engine unit tests
|
||||||
|
│ └── models/ # Model validation tests
|
||||||
|
├── integration/ # Service + database integration
|
||||||
|
└── e2e/ # Full application tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Unit Tests (`unit/`)
|
||||||
|
|
||||||
|
### Services (`unit/services/`)
|
||||||
|
**Critical for Model/Service Architecture**
|
||||||
|
|
||||||
|
Tests business logic independently of database and web framework:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# test_game_service.py
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_session():
|
||||||
|
return Mock(spec=Session)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def game_service(mock_session):
|
||||||
|
return GameService(mock_session)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_game_validates_teams(game_service):
|
||||||
|
# Test business logic without database
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
await game_service.create_game(999, 1000) # Invalid teams
|
||||||
|
```
|
||||||
|
|
||||||
|
### Engine (`unit/engine/`)
|
||||||
|
Tests stateless game simulation functions:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# test_dice.py
|
||||||
|
def test_dice_probability_distribution():
|
||||||
|
# Test dice rolling mechanics
|
||||||
|
results = [roll_dice() for _ in range(1000)]
|
||||||
|
assert 1 <= min(results) <= max(results) <= 6
|
||||||
|
|
||||||
|
# test_simulation.py
|
||||||
|
def test_pitcher_vs_batter_mechanics():
|
||||||
|
# Test core game simulation
|
||||||
|
result = simulate_at_bat(pitcher_stats, batter_stats)
|
||||||
|
assert result.outcome in ['hit', 'out', 'walk', 'strikeout']
|
||||||
|
```
|
||||||
|
|
||||||
|
### Models (`unit/models/`)
|
||||||
|
Tests data validation and relationships:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# test_models.py
|
||||||
|
def test_game_model_validation():
|
||||||
|
# Test SQLModel validation
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
Game(away_team_id=None) # Required field
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Tests (`integration/`)
|
||||||
|
|
||||||
|
Tests service interactions with real database (isolated transactions):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# test_game_flow.py
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_complete_game_creation_flow(db_session):
|
||||||
|
game_service = GameService(db_session)
|
||||||
|
|
||||||
|
# Create test data
|
||||||
|
team1 = await create_test_team(db_session)
|
||||||
|
team2 = await create_test_team(db_session)
|
||||||
|
|
||||||
|
# Test full flow with real database
|
||||||
|
game = await game_service.create_game(team1.id, team2.id)
|
||||||
|
|
||||||
|
assert game.id is not None
|
||||||
|
assert game.away_team_id == team1.id
|
||||||
|
```
|
||||||
|
|
||||||
|
## End-to-End Tests (`e2e/`)
|
||||||
|
|
||||||
|
Tests complete user journeys through web interface:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# test_game_creation.py
|
||||||
|
def test_user_can_create_game(client):
|
||||||
|
# Test complete user workflow
|
||||||
|
response = client.post("/auth/login") # Login
|
||||||
|
response = client.post("/games/start", json={...}) # Create game
|
||||||
|
response = client.get(f"/game/{game_id}") # View game
|
||||||
|
|
||||||
|
assert "Game created successfully" in response.text
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
### All Tests
|
||||||
|
```bash
|
||||||
|
pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Specific Test Types
|
||||||
|
```bash
|
||||||
|
# Unit tests only (fast)
|
||||||
|
pytest tests/unit/
|
||||||
|
|
||||||
|
# Integration tests only
|
||||||
|
pytest tests/integration/
|
||||||
|
|
||||||
|
# End-to-end tests only
|
||||||
|
pytest tests/e2e/
|
||||||
|
|
||||||
|
# Specific service tests
|
||||||
|
pytest tests/unit/services/test_game_service.py
|
||||||
|
|
||||||
|
# Single test function
|
||||||
|
pytest tests/unit/services/test_game_service.py::test_create_game_success
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Coverage
|
||||||
|
```bash
|
||||||
|
# Coverage report
|
||||||
|
pytest --cov=app
|
||||||
|
|
||||||
|
# Coverage with HTML report
|
||||||
|
pytest --cov=app --cov-report=html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Configuration
|
||||||
|
|
||||||
|
### Fixtures
|
||||||
|
Common test fixtures for database, services, and test data:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# conftest.py
|
||||||
|
@pytest.fixture
|
||||||
|
def db_session():
|
||||||
|
# Isolated database session for integration tests
|
||||||
|
pass
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_game_service():
|
||||||
|
# Mocked service for route testing
|
||||||
|
pass
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_game_data():
|
||||||
|
# Sample game data for tests
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Database
|
||||||
|
Integration tests use a separate test database:
|
||||||
|
- `DATABASE_TEST_URL` environment variable
|
||||||
|
- Isolated transactions (rollback after each test)
|
||||||
|
- Clean state for each test
|
||||||
|
|
||||||
|
## Testing Best Practices
|
||||||
|
|
||||||
|
### Service Testing
|
||||||
|
- **Mock database sessions** for unit tests
|
||||||
|
- **Test business logic** independently of framework
|
||||||
|
- **Validate error handling** and edge cases
|
||||||
|
- **Test service interactions** with integration tests
|
||||||
|
|
||||||
|
### Test Data Management
|
||||||
|
- **Use factories** for creating test data
|
||||||
|
- **Isolate test state** (no shared mutable state)
|
||||||
|
- **Clean up after tests** (database rollback)
|
||||||
|
|
||||||
|
### Coverage Goals
|
||||||
|
- **Services**: 90%+ coverage (core business logic)
|
||||||
|
- **Engine**: 95%+ coverage (critical game mechanics)
|
||||||
|
- **Routes**: 80%+ coverage (HTTP handling)
|
||||||
|
- **Models**: 85%+ coverage (data validation)
|
||||||
|
|
||||||
|
## Migration Testing
|
||||||
|
|
||||||
|
When migrating from Discord app:
|
||||||
|
1. **Extract and test business logic** in service unit tests
|
||||||
|
2. **Validate game mechanics** with engine tests
|
||||||
|
3. **Test service integration** with database
|
||||||
|
4. **Ensure web interface** works with e2e tests
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
0
tests/e2e/__init__.py
Normal file
0
tests/e2e/__init__.py
Normal file
0
tests/integration/__init__.py
Normal file
0
tests/integration/__init__.py
Normal file
0
tests/unit/__init__.py
Normal file
0
tests/unit/__init__.py
Normal file
0
tests/unit/engine/__init__.py
Normal file
0
tests/unit/engine/__init__.py
Normal file
0
tests/unit/models/__init__.py
Normal file
0
tests/unit/models/__init__.py
Normal file
0
tests/unit/services/__init__.py
Normal file
0
tests/unit/services/__init__.py
Normal file
Loading…
Reference in New Issue
Block a user