{ "meta": { "version": "1.0.0", "created": "2026-01-27", "lastUpdated": "2026-01-27", "planType": "phase", "phaseId": "PHASE_1", "phaseName": "Database + Infrastructure", "description": "PostgreSQL models, Redis caching, CardService, and development environment setup", "totalEstimatedHours": 40, "totalTasks": 18, "completedTasks": 18, "masterPlan": "../PROJECT_PLAN_MASTER.json" }, "goals": [ "Establish PostgreSQL as durable storage with async SQLAlchemy", "Set up Redis for game state caching and future matchmaking", "Implement GameStateManager with write-behind persistence", "Create CardService to load bundled JSON into CardDefinitions", "Configure environment-based settings (dev/staging/prod)", "Docker compose for local development" ], "architectureNotes": { "gameStatePersistence": { "strategy": "Write-behind cache", "primary": "Redis (fast reads/writes during gameplay)", "durable": "PostgreSQL (write at turn boundaries + game end)", "recovery": "On restart, load active games from Postgres → Redis" }, "asyncEverywhere": "All DB and Redis operations use async/await", "sessionManagement": "Scoped async sessions with proper cleanup" }, "directoryStructure": { "db": "backend/app/db/", "services": "backend/app/services/", "config": "backend/app/config.py", "docker": "backend/docker-compose.yml" }, "tasks": [ { "id": "DB-001", "name": "Create environment config module", "description": "Pydantic Settings class for environment-based configuration (database URLs, Redis URL, secrets)", "category": "critical", "priority": 1, "completed": true, "completedDate": "2026-01-27", "dependencies": [], "files": [ {"path": "app/config.py", "status": "done"} ], "details": [ "Use pydantic-settings for env var parsing", "Support .env files for local dev", "Separate configs: DATABASE_URL, REDIS_URL, SECRET_KEY, OAUTH_*", "Environment detection: dev/staging/prod" ], "estimatedHours": 2 }, { "id": "DB-002", "name": "Create Docker compose for local dev", "description": "Docker compose file with PostgreSQL and Redis services", "category": "critical", "priority": 2, "completed": true, "completedDate": "2026-01-27", "dependencies": ["DB-001"], "files": [ {"path": "docker-compose.yml", "status": "done"}, {"path": ".env.example", "status": "done"} ], "details": [ "PostgreSQL 15 with health check", "Redis 7 with persistence disabled (dev only)", "Volume mounts for data persistence", "Network for service communication", "Uses ports 5433 (Postgres) and 6380 (Redis) to avoid conflicts" ], "estimatedHours": 1 }, { "id": "DB-003", "name": "Set up SQLAlchemy async engine", "description": "Create async engine, session factory, and base model class", "category": "critical", "priority": 3, "completed": true, "completedDate": "2026-01-27", "dependencies": ["DB-001"], "files": [ {"path": "app/db/__init__.py", "status": "done"}, {"path": "app/db/session.py", "status": "done"}, {"path": "app/db/base.py", "status": "done"} ], "details": [ "AsyncEngine with connection pooling", "async_sessionmaker for scoped sessions", "Base declarative class with common columns (id, created_at, updated_at)", "get_session() async context manager" ], "estimatedHours": 2 }, { "id": "DB-004", "name": "Create User model", "description": "SQLAlchemy model for user accounts with OAuth support", "category": "high", "priority": 4, "completed": true, "completedDate": "2026-01-27", "dependencies": ["DB-003"], "files": [ {"path": "app/db/models/user.py", "status": "done"} ], "details": [ "Fields: id (UUID), email, display_name, avatar_url", "OAuth fields: oauth_provider, oauth_id (composite unique)", "Premium fields: is_premium, premium_until (nullable datetime)", "Timestamps: created_at, last_login", "Index on (oauth_provider, oauth_id)" ], "estimatedHours": 1.5 }, { "id": "DB-005", "name": "Create Collection model", "description": "SQLAlchemy model for player card collections", "category": "high", "priority": 5, "completed": true, "completedDate": "2026-01-27", "dependencies": ["DB-004"], "files": [ {"path": "app/db/models/collection.py", "status": "done"} ], "details": [ "Fields: id, user_id (FK), card_definition_id (str), quantity", "Source tracking: source (enum: starter, booster, reward, purchase)", "Timestamps: obtained_at", "Unique constraint on (user_id, card_definition_id)", "Index on user_id for fast collection lookups" ], "estimatedHours": 1.5 }, { "id": "DB-006", "name": "Create Deck model", "description": "SQLAlchemy model for player decks", "category": "high", "priority": 6, "completed": true, "completedDate": "2026-01-27", "dependencies": ["DB-004"], "files": [ {"path": "app/db/models/deck.py", "status": "done"} ], "details": [ "Fields: id, user_id (FK), name, cards (JSONB), energy_cards (JSONB)", "Validation: is_valid (bool), validation_errors (JSONB nullable)", "Metadata: is_starter (bool), starter_type (nullable)", "Timestamps: created_at, updated_at", "Index on user_id" ], "estimatedHours": 1.5 }, { "id": "DB-007", "name": "Create CampaignProgress model", "description": "SQLAlchemy model for single-player campaign state", "category": "high", "priority": 7, "completed": true, "completedDate": "2026-01-27", "dependencies": ["DB-004"], "files": [ {"path": "app/db/models/campaign.py", "status": "done"} ], "details": [ "Fields: id, user_id (FK unique), current_club, medals (JSONB)", "Progress: defeated_npcs (JSONB), total_wins, total_losses", "Economy: booster_packs (int), mantibucks (int)", "Timestamps: created_at, updated_at" ], "estimatedHours": 1.5 }, { "id": "DB-008", "name": "Create ActiveGame model", "description": "SQLAlchemy model for in-progress games (Postgres backup)", "category": "high", "priority": 8, "completed": true, "completedDate": "2026-01-27", "dependencies": ["DB-003"], "files": [ {"path": "app/db/models/game.py", "status": "done"} ], "details": [ "Fields: id (UUID), game_type (enum: campaign, freeplay, ranked)", "Players: player1_id (FK), player2_id (FK nullable), npc_id (nullable)", "State: rules_config (JSONB), game_state (JSONB), turn_number", "Timing: started_at, last_action_at, turn_deadline (nullable)", "Index on player IDs for reconnection lookup" ], "estimatedHours": 2 }, { "id": "DB-009", "name": "Create GameHistory model", "description": "SQLAlchemy model for completed games", "category": "medium", "priority": 9, "completed": true, "completedDate": "2026-01-27", "dependencies": ["DB-003"], "files": [ {"path": "app/db/models/game.py", "status": "done"} ], "details": [ "Fields: id, game_type, player1_id, player2_id, npc_id", "Result: winner_id, end_reason, turn_count, duration_seconds", "Replay: replay_data (JSONB for future replay feature)", "Timestamp: played_at", "Indexes for leaderboard queries" ], "estimatedHours": 1.5 }, { "id": "DB-010", "name": "Create models __init__ and export", "description": "Consolidate all models in db/models/__init__.py", "category": "medium", "priority": 10, "completed": true, "completedDate": "2026-01-27", "dependencies": ["DB-004", "DB-005", "DB-006", "DB-007", "DB-008", "DB-009"], "files": [ {"path": "app/db/models/__init__.py", "status": "done"} ], "details": [ "Import and re-export all models", "Import Base for Alembic" ], "estimatedHours": 0.5 }, { "id": "DB-011", "name": "Set up Alembic migrations", "description": "Initialize Alembic and create initial migration", "category": "high", "priority": 11, "completed": true, "completedDate": "2026-01-27", "dependencies": ["DB-010"], "files": [ {"path": "alembic.ini", "status": "done"}, {"path": "app/db/migrations/env.py", "status": "done"}, {"path": "app/db/migrations/versions/7ac994d6f89c_initial_schema.py", "status": "done"}, {"path": "app/db/migrations/versions/ab8a0039fe55_allow_null_player1_id.py", "status": "done"} ], "details": [ "Configure async Alembic", "Auto-generate from SQLAlchemy models", "Initial migration with all tables", "Add to pyproject.toml dependencies", "Black formatting enabled for migrations" ], "estimatedHours": 2 }, { "id": "DB-012", "name": "Create Redis connection utilities", "description": "Async Redis client with connection pooling", "category": "high", "priority": 12, "completed": true, "completedDate": "2026-01-27", "dependencies": ["DB-001"], "files": [ {"path": "app/db/redis.py", "status": "done"} ], "details": [ "Use redis.asyncio (redis-py)", "Connection pool configuration", "get_redis() async context manager", "Helper methods: get_json, set_json, delete" ], "estimatedHours": 1.5 }, { "id": "DB-013", "name": "Create GameStateManager", "description": "Redis-primary, Postgres-backup game state management", "category": "critical", "priority": 13, "completed": true, "completedDate": "2026-01-27", "dependencies": ["DB-012", "DB-008"], "files": [ {"path": "app/services/game_state_manager.py", "status": "done"} ], "details": [ "load_state(): Try Redis, fallback to Postgres", "save_to_cache(): Write to Redis only (fast path)", "persist_to_db(): Write to Postgres (turn boundaries)", "delete_game(): Clean up Redis + Postgres", "recover_active_games(): Load from Postgres → Redis on startup", "Key format: game:{game_id}" ], "estimatedHours": 3 }, { "id": "DB-014", "name": "Create CardService", "description": "Load card definitions from bundled JSON files", "category": "critical", "priority": 14, "completed": true, "completedDate": "2026-01-27", "dependencies": [], "files": [ {"path": "app/services/card_service.py", "status": "done"}, {"path": "scripts/scrape_pokemon_pocket.py", "status": "done"}, {"path": "scripts/convert_cards.py", "status": "done"}, {"path": "data/definitions/", "status": "done"}, {"path": "data/raw/", "status": "done"} ], "details": [ "Load from data/definitions/_index.json on startup", "Parse JSON → CardDefinition models", "get_card(card_id) → CardDefinition", "get_cards_by_ids(card_ids) → dict[str, CardDefinition]", "get_set_cards(set_code) → list[CardDefinition]", "search(filters) for deck building UI", "Includes scraper and converter scripts for card data pipeline", "382 cards total (372 scraped + 10 basic energy)" ], "estimatedHours": 3, "notes": "Includes complete card data pipeline with scraper that properly extracts energy types from HTML" }, { "id": "DB-015", "name": "Create database tests", "description": "Tests for models, sessions, and basic CRUD", "category": "high", "priority": 15, "completed": true, "completedDate": "2026-01-27", "dependencies": ["DB-011"], "files": [ {"path": "tests/db/__init__.py", "status": "done"}, {"path": "tests/db/conftest.py", "status": "done"}, {"path": "tests/db/test_models.py", "status": "done"}, {"path": "tests/db/test_relationships.py", "status": "done"} ], "details": [ "Uses real Postgres via testcontainers pattern (sync psycopg2 for fixtures)", "Test each model: create, read, update, delete", "Test relationships and constraints", "Test JSONB serialization", "Test cascade deletes and relationship integrity" ], "estimatedHours": 4 }, { "id": "DB-016", "name": "Create GameStateManager tests", "description": "Tests for Redis + Postgres state management", "category": "high", "priority": 16, "completed": true, "completedDate": "2026-01-27", "dependencies": ["DB-013", "DB-015"], "files": [ {"path": "tests/services/test_game_state_manager.py", "status": "done"}, {"path": "tests/services/conftest.py", "status": "done"} ], "details": [ "Uses fakeredis for Redis mocking", "Test cache hit (Redis has state)", "Test cache miss (fallback to Postgres)", "Test persist_to_db writes correctly", "Test recovery on startup", "Test cleanup on game end" ], "estimatedHours": 3 }, { "id": "DB-017", "name": "Create CardService tests", "description": "Tests for card loading and lookup", "category": "high", "priority": 17, "completed": true, "completedDate": "2026-01-27", "dependencies": ["DB-014"], "files": [ {"path": "tests/services/test_card_service.py", "status": "done"}, {"path": "tests/scripts/test_convert_cards.py", "status": "done"} ], "details": [ "Test loads all cards from JSON", "Test get_card returns correct definition", "Test get_cards_by_ids batch lookup", "Test search with filters (type, set, rarity, stage, variant)", "Test handles missing card gracefully", "Test converter script validation and evolution chains" ], "estimatedHours": 2 }, { "id": "DB-018", "name": "Update dependencies in pyproject.toml", "description": "Add all required packages for Phase 1", "category": "high", "priority": 18, "completed": true, "completedDate": "2026-01-27", "dependencies": [], "files": [ {"path": "pyproject.toml", "status": "done"} ], "details": [ "sqlalchemy[asyncio] >= 2.0", "asyncpg (Postgres async driver)", "alembic", "redis >= 5.0 (async support)", "pydantic-settings", "psycopg2-binary (for test fixtures)", "fakeredis[lua] (for Redis mocking in tests)" ], "estimatedHours": 0.5 } ], "testingStrategy": { "approach": "Real Postgres via Docker + fakeredis for Redis", "fixtures": "Async fixtures in conftest.py with sync psycopg2 for cleanup", "coverage": "974 tests passing, high coverage on new code" }, "weeklyRoadmap": { "week1": { "theme": "Foundation", "tasks": ["DB-001", "DB-002", "DB-003", "DB-018"], "goals": ["Dev environment running", "SQLAlchemy configured"], "status": "complete" }, "week2": { "theme": "Models + Migrations", "tasks": ["DB-004", "DB-005", "DB-006", "DB-007", "DB-008", "DB-009", "DB-010", "DB-011"], "goals": ["All models defined", "Migrations working"], "status": "complete" }, "week3": { "theme": "Services + Testing", "tasks": ["DB-012", "DB-013", "DB-014", "DB-015", "DB-016", "DB-017"], "goals": ["GameStateManager working", "CardService working", "Full test coverage"], "status": "complete" } }, "acceptanceCriteria": [ {"criterion": "docker-compose up starts Postgres + Redis locally", "met": true}, {"criterion": "Alembic migrations run successfully", "met": true}, {"criterion": "All models have CRUD tests passing", "met": true}, {"criterion": "GameStateManager persists at turn boundaries", "met": true}, {"criterion": "GameStateManager recovers from Postgres on restart", "met": true}, {"criterion": "CardService loads all 382 cards from JSON", "met": true, "notes": "372 scraped + 10 basic energy"}, {"criterion": "All tests pass with testcontainers", "met": true, "notes": "974 tests passing"} ], "completionSummary": { "status": "COMPLETE", "completedDate": "2026-01-27", "totalTests": 974, "keyDeliverables": [ "Full async SQLAlchemy infrastructure with 6 models", "GameStateManager with Redis cache + Postgres persistence", "CardService loading 382 card definitions", "Complete card data pipeline (scraper + converter)", "Comprehensive test suite with real database testing", "FastAPI app with lifespan hooks for startup/shutdown" ], "notableImplementationDetails": [ "Uses ports 5433/6380 to avoid conflicts with existing services", "Scraper properly extracts energy types from HTML spans", "pytest-asyncio fixtures use sync psycopg2 for reliable cleanup", "fakeredis used for Redis mocking in service tests", "FastAPI lifespan wires init_db, init_redis, CardService.load_all", "get_session_dependency exported for FastAPI dependency injection", "game_cache_ttl_seconds configurable via Settings", "datetime.utcnow() replaced with datetime.now(UTC)" ], "gapsFixedPostReview": [ "Added FastAPI lifespan hooks (startup/shutdown)", "Exported get_session_dependency from app/db/__init__.py", "Made game cache TTL configurable via Settings", "Fixed datetime.utcnow() deprecation (4 occurrences)", "Added /health/ready endpoint for readiness checks", "Added CORS middleware" ] }, "phase2Prerequisites": { "description": "Items identified during Phase 1 review that should be addressed in Phase 2", "items": [ { "name": "Auth infrastructure (JWT)", "description": "Add JWT encode/decode utilities using secret_key from Settings", "priority": "high" }, { "name": "python-socketio integration", "description": "Install python-socketio and create WebSocket handlers for real-time gameplay", "priority": "high" } ] } }