From 1a21d3d2d4fb18b877a58d7425b01af15f7f6e3e Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sun, 1 Feb 2026 20:52:20 -0600 Subject: [PATCH] Add Phase F4 live gameplay project plan - Document Phase F4 implementation tasks - Track progress on live gameplay features - Define component structure and requirements Co-Authored-By: Claude Sonnet 4.5 --- .../project_plans/PHASE_F4_live_gameplay.json | 822 ++++++++++++++++++ 1 file changed, 822 insertions(+) create mode 100644 frontend/project_plans/PHASE_F4_live_gameplay.json diff --git a/frontend/project_plans/PHASE_F4_live_gameplay.json b/frontend/project_plans/PHASE_F4_live_gameplay.json new file mode 100644 index 0000000..b22f877 --- /dev/null +++ b/frontend/project_plans/PHASE_F4_live_gameplay.json @@ -0,0 +1,822 @@ +{ + "meta": { + "phaseId": "PHASE_F4", + "name": "Live Gameplay", + "version": "1.0.0", + "created": "2026-01-31", + "lastUpdated": "2026-02-01", + "totalTasks": 14, + "completedTasks": 14, + "status": "complete", + "description": "WebSocket integration, game state sync, action handling, complete game flow. This phase connects the Phaser rendering layer to the backend game engine via Socket.IO, enabling real-time multiplayer gameplay." + }, + "stylingPrinciples": [ + "Game UI overlays use Vue components positioned over Phaser canvas", + "Action menus are context-sensitive and appear near relevant game elements", + "Turn phase indicator always visible but non-intrusive", + "Loading states during action execution (optimistic UI optional)", + "Error feedback via toast notifications, not modal interrupts", + "Mobile touch-friendly: large buttons, swipe gestures where appropriate", + "Consistent visual language between Vue overlays and Phaser objects" + ], + "dependencies": { + "phases": [ + "PHASE_F0", + "PHASE_F1", + "PHASE_F2", + "PHASE_F3" + ], + "backend": [ + "POST /api/games - Create new game", + "GET /api/games/{id} - Get game info", + "GET /api/games/me/active - List active games", + "WebSocket namespace /game - All real-time events", + "game:join -> game:state - Join game and receive state", + "game:action -> game:action_result - Execute actions", + "game:state broadcasts on state changes", + "game:game_over on game completion" + ], + "external": [ + "Socket.IO client (already installed from F0)", + "Game types from src/types/game.ts (from F3)" + ] + }, + "architectureNotes": { + "stateFlow": { + "principle": "Pinia game store is single source of truth. WebSocket updates store, store updates Phaser.", + "flow": "Socket.IO -> game store -> Phaser (via bridge events)", + "storeResponsibilities": [ + "Hold current VisibleGameState", + "Track connection status", + "Provide computed helpers (isMyTurn, myHand, etc.)", + "Queue pending actions during reconnection" + ] + }, + "actionFlow": { + "principle": "Actions are intentions sent to server. Server is authoritative.", + "flow": "User interaction -> composable -> Socket.IO emit -> server validates -> broadcasts new state", + "optimisticUI": "Optional - can show pending state while awaiting confirmation", + "errorHandling": "game:action_result with success=false shows toast, reverts any optimistic changes" + }, + "componentStructure": { + "GamePage.vue": "Main page, manages socket connection lifecycle", + "PhaserGame.vue": "Phaser canvas (from F3)", + "GameOverlay.vue": "Container for all Vue overlays on top of Phaser", + "TurnIndicator.vue": "Shows current phase and whose turn", + "AttackMenu.vue": "Attack selection when in attack phase", + "HandPanel.vue": "Alternative hand display for complex interactions", + "ForcedActionModal.vue": "Modal for required actions (prize selection, etc.)" + }, + "socketEvents": { + "clientToServer": [ + { + "event": "join_game", + "payload": "JoinGameMessage", + "when": "On entering game page" + }, + { + "event": "action", + "payload": "ActionMessage", + "when": "On any game action" + }, + { + "event": "resign", + "payload": "ResignMessage", + "when": "On resign confirmation" + }, + { + "event": "heartbeat", + "payload": "HeartbeatMessage", + "when": "Every 30s while connected" + } + ], + "serverToClient": [ + { + "event": "game_state", + "payload": "GameStateMessage", + "when": "On join, after actions" + }, + { + "event": "action_result", + "payload": "ActionResultMessage", + "when": "After action processed" + }, + { + "event": "error", + "payload": "ErrorMessage", + "when": "On validation failures" + }, + { + "event": "turn_start", + "payload": "TurnStartMessage", + "when": "Turn begins" + }, + { + "event": "turn_timeout", + "payload": "TurnTimeoutMessage", + "when": "Timer warning/expiry" + }, + { + "event": "game_over", + "payload": "GameOverMessage", + "when": "Game ends" + }, + { + "event": "opponent_status", + "payload": "OpponentStatusMessage", + "when": "Opponent connects/disconnects" + } + ] + } + }, + "tasks": [ + { + "id": "F4-001", + "name": "Create WebSocket game types", + "description": "TypeScript types for all WebSocket messages matching backend schemas", + "category": "api", + "priority": 1, + "completed": true, + "tested": true, + "dependencies": [], + "files": [ + { + "path": "src/types/ws.ts", + "status": "create" + }, + { + "path": "src/types/index.ts", + "status": "modify" + } + ], + "details": [ + "Define ClientMessage types: JoinGameMessage, ActionMessage, ResignMessage, HeartbeatMessage", + "Define ServerMessage types: GameStateMessage, ActionResultMessage, ErrorMessage, TurnStartMessage, TurnTimeoutMessage, GameOverMessage, OpponentStatusMessage, HeartbeatAckMessage", + "Define WSErrorCode enum matching backend WSErrorCode", + "Define ConnectionStatus enum: connected, disconnected, reconnecting", + "Create type guards: isGameStateMessage, isErrorMessage, etc.", + "Export all from types/index.ts", + "Reference backend/app/schemas/ws_messages.py for exact field names" + ], + "acceptance": [ + "All WebSocket message types defined", + "Types match backend schemas exactly", + "Type guards work correctly", + "Exported from types/index.ts" + ], + "estimatedHours": 2, + "notes": "Can be done in parallel with other tasks as foundation work" + }, + { + "id": "F4-002", + "name": "Enhance game store for real-time state", + "description": "Extend the Pinia game store to handle WebSocket state and actions", + "category": "stores", + "priority": 2, + "completed": true, + "tested": true, + "dependencies": [ + "F4-001" + ], + "files": [ + { + "path": "src/stores/game.ts", + "status": "modify" + }, + { + "path": "src/stores/game.spec.ts", + "status": "create" + } + ], + "details": [ + "Add state: currentGameId, connectionStatus, lastEventId, pendingActions", + "Add computed: isMyTurn, myPlayerState, opponentPlayerState, currentPhase, isGameOver", + "Add computed: myHand, myActive, myBench, myDeckCount, myDiscardCount", + "Add computed: opponentActive, opponentBench, opponentHandCount, opponentDeckCount", + "Add action: setGameState(state: VisibleGameState) - updates from server", + "Add action: setConnectionStatus(status: ConnectionStatus)", + "Add action: queueAction(action: Action) - for offline queueing", + "Add action: clearGame() - reset all state on exit", + "Use computed helpers from types/game.ts (getMyPlayerState, getOpponentState)", + "Persist lastEventId for reconnection support" + ], + "acceptance": [ + "Store holds complete game state", + "Computed properties derive correct values", + "Connection status tracked", + "Tests verify all computed derivations", + "Store resets cleanly on game exit" + ], + "estimatedHours": 3 + }, + { + "id": "F4-003", + "name": "Create game socket composable", + "description": "Composable for managing WebSocket connection to game namespace", + "category": "composables", + "priority": 3, + "completed": true, + "tested": true, + "dependencies": [ + "F4-001", + "F4-002" + ], + "files": [ + { + "path": "src/composables/useGameSocket.ts", + "status": "create" + }, + { + "path": "src/composables/useGameSocket.spec.ts", + "status": "create" + } + ], + "details": [ + "Create singleton Socket.IO connection to /game namespace", + "Include auth token in connection handshake (from auth store)", + "Provide connect(gameId: string) - joins game room", + "Provide disconnect() - leaves game room and cleans up", + "Provide sendAction(action: Action) - emits action message", + "Provide sendResign() - emits resign message", + "Set up heartbeat interval (every 30s)", + "Handle all server events and update game store", + "Handle connection errors with toast notifications", + "Implement automatic reconnection with exponential backoff", + "Track lastEventId for reconnection replay", + "Emit 'game:join' with last_event_id on reconnect" + ], + "acceptance": [ + "Connection establishes with auth", + "All server events handled correctly", + "Actions sent and results processed", + "Heartbeat keeps connection alive", + "Reconnection works with event replay", + "Tests verify event handling (mocked socket)" + ], + "estimatedHours": 5 + }, + { + "id": "F4-004", + "name": "Create game actions composable", + "description": "Composable providing typed action dispatch functions", + "category": "composables", + "priority": 4, + "completed": true, + "tested": true, + "dependencies": [ + "F4-003" + ], + "files": [ + { + "path": "src/composables/useGameActions.ts", + "status": "create" + }, + { + "path": "src/composables/useGameActions.spec.ts", + "status": "create" + } + ], + "details": [ + "Wraps useGameSocket for type-safe action dispatch", + "playCard(instanceId: string, targetZone?: ZoneType, targetSlot?: number)", + "attachEnergy(energyInstanceId: string, targetPokemonId: string)", + "evolve(handCardId: string, targetPokemonId: string)", + "attack(attackIndex: number, targetPokemonId?: string)", + "retreat(newActiveId: string)", + "useAbility(pokemonId: string, abilityIndex: number, targets?: string[])", + "endTurn()", + "selectPrize(prizeIndex: number)", + "selectNewActive(pokemonId: string)", + "Each function validates basic preconditions before sending", + "Return Promise that resolves/rejects based on action_result", + "Track pending action state for UI feedback" + ], + "acceptance": [ + "All action types have typed functions", + "Precondition validation prevents invalid sends", + "Promises resolve/reject correctly", + "Pending state trackable", + "Tests verify each action type" + ], + "estimatedHours": 4 + }, + { + "id": "F4-005", + "name": "Create game lobby page", + "description": "Page for creating new games and viewing active games", + "category": "pages", + "priority": 5, + "completed": true, + "tested": true, + "dependencies": [ + "F4-002" + ], + "files": [ + { + "path": "src/pages/PlayPage.vue", + "status": "create" + }, + { + "path": "src/pages/PlayPage.spec.ts", + "status": "create" + }, + { + "path": "src/router/index.ts", + "status": "modify" + } + ], + "details": [ + "Route: /play (already defined in router)", + "Section: Active Games - list from GET /api/games/me/active", + "Each active game shows: opponent name, turn indicator, resume button", + "Section: Start New Game", + "Deck selector dropdown (from user's valid decks)", + "Create Game button -> POST /api/games with selected deck", + "On game creation, navigate to /game/:id", + "Show loading states during API calls", + "Handle errors with toast notifications", + "Future: invite link sharing (display game ID for now)" + ], + "styling": [ + "Card-based layout for active games", + "Visual distinction between 'your turn' and 'waiting'", + "Prominent 'Create Game' CTA button", + "Deck selector shows deck name and validation status", + "Mobile: stack sections vertically" + ], + "acceptance": [ + "Active games list displays correctly", + "Can resume existing games", + "Can create new game with deck selection", + "Navigation to game page works", + "Loading and error states handled" + ], + "estimatedHours": 4 + }, + { + "id": "F4-006", + "name": "Enhance GamePage with socket lifecycle", + "description": "Update GamePage to manage WebSocket connection and game state", + "category": "pages", + "priority": 6, + "completed": true, + "tested": true, + "dependencies": [ + "F4-003", + "F4-004" + ], + "files": [ + { + "path": "src/pages/GamePage.vue", + "status": "modify" + }, + { + "path": "src/pages/GamePage.spec.ts", + "status": "modify" + } + ], + "details": [ + "On mount: connect to game socket with game ID from route param", + "Wait for game:state before showing game content", + "Show loading overlay while connecting/loading state", + "Watch game store state -> emit to Phaser bridge (game:state_updated)", + "Handle connection errors with retry UI", + "Handle game:game_over -> show GameOverModal", + "On unmount: disconnect socket, clear game store", + "Add exit button with confirmation dialog", + "Exit navigates back to /play" + ], + "acceptance": [ + "Socket connects on mount with correct game ID", + "Game state flows to Phaser via bridge", + "Loading state shown until ready", + "Exit works with confirmation", + "Clean disconnect on unmount" + ], + "estimatedHours": 3 + }, + { + "id": "F4-007", + "name": "Create game overlay container", + "description": "Vue component container for all UI overlays on the game canvas", + "category": "components", + "priority": 7, + "completed": true, + "tested": true, + "dependencies": [ + "F4-006" + ], + "files": [ + { + "path": "src/components/game/GameOverlay.vue", + "status": "create" + } + ], + "details": [ + "Positioned absolutely over Phaser canvas (pointer-events: none on container)", + "Contains slots for: turn-indicator, phase-actions, attack-menu, forced-action", + "Each child overlay has pointer-events: auto to be interactive", + "Responsive positioning for overlays", + "Z-index above Phaser canvas", + "Pass down game state via provide/inject or props" + ], + "acceptance": [ + "Overlay container renders over Phaser", + "Child components are interactive", + "Phaser canvas still receives input in non-overlay areas", + "Responsive layout works" + ], + "estimatedHours": 2 + }, + { + "id": "F4-008", + "name": "Create turn indicator component", + "description": "Display current turn phase and active player", + "category": "components", + "priority": 8, + "completed": true, + "tested": true, + "dependencies": [ + "F4-007" + ], + "files": [ + { + "path": "src/components/game/TurnIndicator.vue", + "status": "create" + }, + { + "path": "src/components/game/TurnIndicator.spec.ts", + "status": "create" + } + ], + "details": [ + "Show current phase: DRAW, MAIN, ATTACK, END", + "Show whose turn it is: 'Your Turn' or 'Opponent's Turn'", + "Turn number display (optional)", + "Visual distinction for your turn vs waiting", + "Phase icons or colored indicators", + "Positioned top-center of game area", + "Animate phase transitions" + ], + "styling": [ + "Semi-transparent background", + "Your turn: accent color highlight", + "Opponent's turn: muted colors", + "Phase displayed as badge/pill", + "Compact on mobile, expanded on desktop" + ], + "acceptance": [ + "Correctly displays current phase", + "Correctly indicates whose turn", + "Visual styles match design system", + "Tests verify display logic" + ], + "estimatedHours": 2 + }, + { + "id": "F4-009", + "name": "Create phase actions component", + "description": "Action buttons available during each phase", + "category": "components", + "priority": 9, + "completed": true, + "tested": true, + "dependencies": [ + "F4-004", + "F4-008" + ], + "files": [ + { + "path": "src/components/game/PhaseActions.vue", + "status": "create" + }, + { + "path": "src/components/game/PhaseActions.spec.ts", + "status": "create" + } + ], + "details": [ + "Show context-appropriate actions for current phase", + "MAIN phase: End Turn button, Retreat button (if can retreat)", + "ATTACK phase: shows after selecting 'Attack' action from card", + "END phase: auto-advance (server handles)", + "Disable buttons when not your turn", + "Loading state on buttons during action execution", + "Position: bottom-right of game area", + "Use useGameActions composable for dispatch" + ], + "acceptance": [ + "Correct buttons shown per phase", + "Buttons disabled when not your turn", + "Actions dispatch correctly", + "Loading states work", + "Tests verify button visibility logic" + ], + "estimatedHours": 3 + }, + { + "id": "F4-010", + "name": "Create attack menu component", + "description": "UI for selecting which attack to use", + "category": "components", + "priority": 10, + "completed": true, + "tested": true, + "dependencies": [ + "F4-004", + "F4-007" + ], + "files": [ + { + "path": "src/components/game/AttackMenu.vue", + "status": "create" + }, + { + "path": "src/components/game/AttackMenu.spec.ts", + "status": "create" + } + ], + "details": [ + "Shows when user taps 'Attack' or taps active Pokemon during attack phase", + "List all attacks for current active Pokemon", + "Display: attack name, energy cost, damage, effect text", + "Disable attacks that don't have enough energy attached", + "Disable if Pokemon has status preventing attack (paralyzed)", + "On select: check if target selection needed, then dispatch attack action", + "Cancel button to close menu", + "Position: centered modal or bottom sheet on mobile" + ], + "styling": [ + "Card-like attack entries", + "Energy cost icons in correct colors", + "Disabled attacks grayed out with reason tooltip", + "Selected attack highlighted", + "Smooth open/close animation" + ], + "acceptance": [ + "Lists all attacks correctly", + "Energy requirements checked", + "Status conditions prevent attacks", + "Attack selection dispatches action", + "Tests verify enable/disable logic" + ], + "estimatedHours": 4 + }, + { + "id": "F4-011", + "name": "Create forced action modal", + "description": "Modal for handling required player choices", + "category": "components", + "priority": 11, + "completed": true, + "tested": true, + "dependencies": [ + "F4-004", + "F4-007" + ], + "files": [ + { + "path": "src/components/game/ForcedActionModal.vue", + "status": "create" + }, + { + "path": "src/components/game/ForcedActionModal.spec.ts", + "status": "create" + } + ], + "details": [ + "Shows when game state has forced_action_type set", + "Types: prize_selection, new_active_selection, discard_selection", + "Prize selection: show 6 prize card positions, select one to claim", + "New active selection: show bench Pokemon, select one to promote", + "Discard selection: show hand/bench cards, select required number to discard", + "Cannot close without completing the action", + "Display forced_action_reason as instruction text", + "Dispatch appropriate action on selection" + ], + "styling": [ + "Modal overlay blocks interaction with game", + "Clear instruction header", + "Selectable cards with visual feedback", + "Confirm button after selection", + "No close/cancel button (forced action)" + ], + "acceptance": [ + "Shows for all forced action types", + "Cannot be dismissed without action", + "Selection dispatches correct action", + "Modal closes after successful action", + "Tests verify each action type" + ], + "estimatedHours": 4 + }, + { + "id": "F4-012", + "name": "Create game over modal", + "description": "Display game results when game ends", + "category": "components", + "priority": 12, + "completed": true, + "tested": true, + "dependencies": [ + "F4-007" + ], + "files": [ + { + "path": "src/components/game/GameOverModal.vue", + "status": "create" + }, + { + "path": "src/components/game/GameOverModal.spec.ts", + "status": "create" + } + ], + "details": [ + "Shows when game store has winner_id or end_reason set", + "Display: Victory or Defeat (or Draw)", + "Show end reason: 'All prizes claimed', 'Opponent has no Pokemon', 'Deck empty', 'Resignation', 'Timeout'", + "Show basic stats: turn count, prizes taken", + "Return to Lobby button -> navigate to /play", + "Future: Rematch button (not in v1)" + ], + "styling": [ + "Celebratory for victory (subtle animation)", + "Muted for defeat", + "Large clear result text", + "Stats in readable format", + "Single prominent CTA button" + ], + "acceptance": [ + "Correct victory/defeat display", + "End reason shown", + "Return button navigates correctly", + "Tests verify display logic" + ], + "estimatedHours": 2 + }, + { + "id": "F4-013", + "name": "Implement Phaser hand interactions", + "description": "Handle card interactions in hand zone via Phaser", + "category": "game", + "priority": 13, + "completed": true, + "tested": true, + "dependencies": [ + "F4-004", + "F4-006" + ], + "files": [ + { + "path": "src/game/interactions/HandManager.ts", + "status": "create" + }, + { + "path": "src/game/scenes/MatchScene.ts", + "status": "modify" + } + ], + "details": [ + "HandManager class handles all hand card interactions", + "Tap/click card to select -> emit card_clicked to bridge", + "Vue handles selected card state and shows valid play options", + "Drag card to bench zone -> emit play_card intention to bridge", + "Drag energy card to Pokemon -> emit attach_energy intention", + "Validate drop zones based on card type (Pokemon to bench, energy to Pokemon)", + "Visual feedback during drag: valid zones highlight", + "Cancel drag by releasing outside valid zone", + "Bridge events trigger Vue action dispatch via useGameActions", + "Integrate with existing Card and Zone objects from F3" + ], + "acceptance": [ + "Cards in hand are interactive", + "Tap selects card and shows options", + "Drag to valid zone triggers action", + "Invalid drops are cancelled", + "Visual feedback during drag" + ], + "estimatedHours": 6 + }, + { + "id": "F4-014", + "name": "Implement Phaser board interactions", + "description": "Handle interactions with cards on the board", + "category": "game", + "priority": 14, + "completed": true, + "tested": true, + "dependencies": [ + "F4-007" + ], + "files": [ + { + "path": "src/game/interactions/BoardManager.ts", + "status": "create" + }, + { + "path": "src/game/scenes/MatchScene.ts", + "status": "modify" + } + ], + "details": [ + "BoardManager handles active/bench Pokemon interactions", + "Tap active Pokemon: show attack menu (if attack phase and your turn)", + "Tap bench Pokemon: option to retreat (if your turn and can retreat)", + "Tap opponent's Pokemon: for targeting (if attack/ability requires target)", + "Target selection mode: highlight valid targets, emit on selection", + "Long press/right-click: show card detail overlay", + "Integrate targeting with AttackMenu and ability usage", + "Zone clicks emit zone_clicked for general targeting", + "All interactions respect current phase and turn" + ], + "acceptance": [ + "Board cards are interactive", + "Actions respect phase/turn rules", + "Target selection works for attacks", + "Card details accessible", + "Opponent cards selectable as targets" + ], + "estimatedHours": 5 + } + ], + "testingApproach": { + "unitTests": [ + "Game store computed derivations", + "Action composable precondition checks", + "WebSocket message type guards", + "Component display logic (mocked store)" + ], + "componentTests": [ + "TurnIndicator renders correctly per state", + "AttackMenu enables/disables attacks correctly", + "ForcedActionModal handles each action type", + "GameOverModal shows correct result" + ], + "integrationTests": [ + "PlayPage creates game and navigates", + "GamePage connects and receives state", + "Full action flow: play card -> state update -> render" + ], + "manualTests": [ + "Complete game flow from lobby to game over", + "Reconnection after disconnect", + "All action types execute correctly", + "Mobile touch interactions", + "Multiple browser tabs (spectator mode future)" + ], + "note": "Socket.IO tests should mock the socket, not connect to real server" + }, + "backendIntegrationNotes": { + "apiEndpoints": [ + { + "endpoint": "POST /api/games", + "request": "GameCreateRequest", + "response": "GameResponse" + }, + { + "endpoint": "GET /api/games/{id}", + "response": "GameResponse" + }, + { + "endpoint": "GET /api/games/me/active", + "response": "List[GameResponse]" + } + ], + "socketNamespace": "/game", + "authInHandshake": "Access token sent in auth object during connection", + "messageIdempotency": "All client messages have message_id for tracking and duplicate detection", + "eventIdReplay": "On reconnect, send last_event_id to receive missed events" + }, + "riskMitigation": [ + { + "risk": "Socket.IO connection reliability", + "mitigation": "Implement robust reconnection with exponential backoff. Queue actions during disconnect. Show clear connection status to user." + }, + { + "risk": "State desync between client and server", + "mitigation": "Server is authoritative. On any mismatch or error, request full state refresh. Never trust client-computed state." + }, + { + "risk": "Mobile performance with overlays", + "mitigation": "Keep Vue overlays simple. Use CSS transforms for animations. Consider reducing overlay complexity on mobile." + }, + { + "risk": "Complex hand drag interactions on touch", + "mitigation": "Provide tap-to-select alternative. Show clear valid drop zone indicators. Large touch targets." + }, + { + "risk": "Action timing conflicts", + "mitigation": "Disable UI during pending actions. Show loading states. Handle race conditions gracefully." + } + ], + "notes": [ + "F4-001 (types) can be done first as foundation", + "F4-002 through F4-004 establish the core state/action pipeline", + "F4-005 and F4-006 create the page structure", + "F4-007 through F4-012 add the Vue overlay components", + "F4-013 and F4-014 complete the Phaser interaction layer", + "Consider implementing a simple AI opponent for testing (backend task)", + "Spectator mode is deferred to a future phase", + "Animations are minimal in this phase - focus on functionality. Polish comes in F5." + ] +}