# Multiplayer System - API Backend **Status:** Planned **Phase:** 6 (Multiplayer Sessions) **Timeline:** Week 12-13 (14 days) **Last Updated:** November 18, 2025 --- ## Overview The Multiplayer System backend handles all business logic for time-limited co-op sessions, including session management, invite system, AI campaign generation, combat orchestration, realtime synchronization, and reward distribution. **Backend Responsibilities:** - Session lifecycle management (create, join, start, end, expire) - Invite code generation and validation - AI-generated campaign creation - Turn-based combat validation and state management - Realtime event broadcasting via Appwrite - Reward calculation and distribution - Character snapshot management - Session expiration enforcement (2-hour limit) --- ## Multiplayer vs Solo Gameplay | Feature | Solo Gameplay | Multiplayer Gameplay | |---------|---------------|----------------------| | **Tier Requirement** | All tiers (Free, Basic, Premium, Elite) | Premium/Elite only | | **Session Type** | Ongoing, persistent | Time-limited (2 hours) | | **Story Progression** | ✅ Full button-based actions | ❌ Limited (combat-focused) | | **Quests** | ✅ Context-aware quest offering | ❌ No quest system | | **Combat** | ✅ Full combat system | ✅ Cooperative combat | | **AI Narration** | ✅ Rich narrative responses | ✅ Campaign narration | | **Character Use** | Uses character location/state | Temporary instance (doesn't affect solo character) | | **Session Duration** | Unlimited | 2 hours max | | **Access Method** | Create anytime | Invite link required | **Design Philosophy:** - **Solo**: Deep, persistent RPG experience with quests, exploration, character progression - **Multiplayer**: Drop-in co-op sessions for quick adventures with friends --- ## Session Architecture ### Session Types The system supports two distinct session types: #### Solo Session - Created via `POST /api/v1/sessions` with `session_type: "solo"` - Single player character - Persistent across logins - Full story progression and quest systems - No time limit #### Multiplayer Session - Created via `POST /api/v1/sessions/multiplayer` with invite link - 2-4 player characters (configurable) - Time-limited (2 hour duration) - Combat and short campaign focus - Realtime synchronization required ### Multiplayer Session Lifecycle ``` ┌─────────────────────────────────────────────────────┐ │ Host Creates Session │ │ - Premium/Elite tier required │ │ - Select difficulty and party size (2-4 players) │ │ - Generate invite link │ └────────────────┬────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ Invite Link Shared │ │ - Host shares link with friends │ │ - Link valid for 24 hours or until session starts │ └────────────────┬────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ Players Join Session │ │ - Click invite link │ │ - Select character to use (from their characters) │ │ - Wait in lobby for all players │ └────────────────┬────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ Host Starts Session │ │ - All players ready in lobby │ │ - AI generates custom campaign │ │ - 2-hour timer begins │ └────────────────┬────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ Gameplay Loop │ │ - Turn-based cooperative combat │ │ - AI narration and encounters │ │ - Shared loot and rewards │ │ - Realtime updates for all players │ └────────────────┬────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ Session Ends │ │ - 2 hour timer expires OR │ │ - Party completes campaign OR │ │ - Party wipes in combat OR │ │ - Host manually ends session │ └────────────────┬────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ Rewards Distributed │ │ - XP, gold, items granted to characters │ │ - Characters saved back to player accounts │ │ - Session archived (read-only history) │ └─────────────────────────────────────────────────────┘ ``` --- ## Data Models ### MultiplayerSession Extends GameSession with multiplayer-specific fields. ```python @dataclass class MultiplayerSession: """Represents a time-limited multiplayer co-op session.""" session_id: str # Unique identifier host_user_id: str # User who created the session invite_code: str # Shareable invite link code session_type: str = "multiplayer" # Always "multiplayer" status: str = "lobby" # "lobby", "active", "completed", "expired" # Party configuration max_players: int = 4 # 2-4 players allowed party_members: List[PartyMember] = field(default_factory=list) # Time limits created_at: str = "" # ISO timestamp started_at: Optional[str] = None # When session started expires_at: Optional[str] = None # 2 hours after started_at time_remaining_seconds: int = 7200 # 2 hours = 7200 seconds # Campaign campaign: MultiplayerCampaign = None # AI-generated campaign current_encounter_index: int = 0 # Progress through campaign # Combat state combat_encounter: Optional[CombatEncounter] = None turn_order: List[str] = field(default_factory=list) # Character IDs current_turn_index: int = 0 # Conversation/narration conversation_history: List[ConversationEntry] = field(default_factory=list) # Tier requirements tier_required: str = "premium" # "premium" or "elite" def is_expired(self) -> bool: """Check if session has exceeded time limit.""" if not self.expires_at: return False return datetime.utcnow() > datetime.fromisoformat(self.expires_at) def get_time_remaining(self) -> int: """Get remaining time in seconds.""" if not self.expires_at: return 7200 # Default 2 hours remaining = datetime.fromisoformat(self.expires_at) - datetime.utcnow() return max(0, int(remaining.total_seconds())) def can_join(self, user: UserData) -> bool: """Check if user can join this session.""" # Check tier requirement if user.tier not in ["premium", "elite"]: return False # Check party size limit if len(self.party_members) >= self.max_players: return False # Check session status if self.status not in ["lobby", "active"]: return False return True ``` ### PartyMember ```python @dataclass class PartyMember: """Represents a player in a multiplayer session.""" user_id: str # User account ID username: str # Display name character_id: str # Character being used character_snapshot: Character # Snapshot at session start (immutable) is_host: bool = False # Is this player the host? is_ready: bool = False # Ready to start (lobby only) is_connected: bool = True # Currently connected via Realtime joined_at: str = "" # ISO timestamp ``` ### MultiplayerCampaign ```python @dataclass class MultiplayerCampaign: """AI-generated short campaign for multiplayer session.""" campaign_id: str title: str # "The Goblin Raid", "Dragon's Hoard" description: str # Campaign overview difficulty: str # "easy", "medium", "hard", "deadly" estimated_duration_minutes: int # 60-120 minutes encounters: List[CampaignEncounter] = field(default_factory=list) rewards: CampaignRewards = None def is_complete(self) -> bool: """Check if all encounters are completed.""" return all(enc.completed for enc in self.encounters) ``` ### CampaignEncounter ```python @dataclass class CampaignEncounter: """A single encounter within a multiplayer campaign.""" encounter_id: str encounter_type: str # "combat", "puzzle", "boss" title: str # "Goblin Ambush", "The Dragon Awakens" narrative_intro: str # AI-generated intro text narrative_completion: str # AI-generated completion text # Combat details (if encounter_type == "combat") enemies: List[Dict] = field(default_factory=list) # Enemy definitions combat_modifiers: Dict = field(default_factory=dict) # Terrain, weather, etc. completed: bool = False completion_time: Optional[str] = None ``` ### CampaignRewards ```python @dataclass class CampaignRewards: """Rewards for completing the multiplayer campaign.""" gold_per_player: int = 0 experience_per_player: int = 0 shared_items: List[str] = field(default_factory=list) # Items to distribute completion_bonus: int = 0 # Bonus for finishing under time limit ``` --- ## Invite System ### Invite Link Generation When a Premium/Elite player creates a multiplayer session: 1. **Generate unique invite code**: 8-character alphanumeric (e.g., `A7K9X2M4`) 2. **Create shareable link**: `https://codeofconquest.com/join/{invite_code}` 3. **Set link expiration**: Valid for 24 hours or until session starts 4. **Store invite metadata**: Host info, tier requirement, party size **Endpoint:** `POST /api/v1/sessions/multiplayer/create` **Request:** ```json { "max_players": 4, "difficulty": "medium", "tier_required": "premium" } ``` **Response:** ```json { "session_id": "mp_xyz789", "invite_code": "A7K9X2M4", "invite_link": "https://codeofconquest.com/join/A7K9X2M4", "expires_at": "2025-11-17T12:00:00Z", "max_players": 4, "status": "lobby" } ``` ### Joining via Invite Link Players click the invite link and are prompted to select a character: **Endpoint:** `GET /api/v1/sessions/multiplayer/join/{invite_code}` **Response:** ```json { "session_id": "mp_xyz789", "host_username": "PlayerOne", "max_players": 4, "current_players": 2, "difficulty": "medium", "tier_required": "premium", "can_join": true, "user_characters": [ { "character_id": "char_abc123", "name": "Thorin", "class": "Vanguard", "level": 5 } ] } ``` **Join Endpoint:** `POST /api/v1/sessions/multiplayer/join/{invite_code}` **Request:** ```json { "character_id": "char_abc123" } ``` **Response:** ```json { "success": true, "session_id": "mp_xyz789", "party_member_id": "pm_def456", "lobby_status": { "players_joined": 3, "max_players": 4, "all_ready": false } } ``` --- ## Session Time Limit ### 2-Hour Duration All multiplayer sessions have a hard 2-hour time limit: - **Timer starts**: When host clicks "Start Session" (all players ready) - **Expiration**: Session automatically ends, rewards distributed - **Warnings**: Backend sends notifications at 10min, 5min, 1min remaining ### Session Expiration Handling ```python def check_session_expiration(session: MultiplayerSession) -> bool: """Check if session has expired and handle cleanup.""" if not session.is_expired(): return False # Session has expired logger.info(f"Session {session.session_id} expired after 2 hours") # If in combat, end combat (players flee) if session.combat_encounter: end_combat(session, status="fled") # Calculate partial rewards (progress-based) partial_rewards = calculate_partial_rewards(session) distribute_rewards(session, partial_rewards) # Mark session as expired session.status = "expired" save_session(session) # Notify all players notify_all_players(session, "Session time limit reached. Rewards distributed.") return True ``` ### Completion Before Time Limit If the party completes the campaign before 2 hours: - **Completion bonus**: Extra gold/XP for finishing quickly - **Session ends**: No need to wait for timer - **Full rewards**: All campaign rewards distributed --- ## AI Campaign Generation ### Campaign Generation at Session Start When the host starts the session (all players ready), the AI generates a custom campaign: **Campaign Generation Function:** ```python def generate_multiplayer_campaign( party_members: List[PartyMember], difficulty: str, duration_minutes: int = 120 ) -> MultiplayerCampaign: """Use AI to generate a custom campaign for the party.""" # Build context context = { "party_size": len(party_members), "party_composition": [ { "name": pm.character_snapshot.name, "class": pm.character_snapshot.player_class.name, "level": pm.character_snapshot.level } for pm in party_members ], "average_level": sum(pm.character_snapshot.level for pm in party_members) / len(party_members), "difficulty": difficulty, "duration_minutes": duration_minutes } # AI prompt prompt = f""" Generate a {difficulty} difficulty D&D-style campaign for a party of {context['party_size']} adventurers. Average party level: {context['average_level']:.1f} Duration: {duration_minutes} minutes Party composition: {context['party_composition']} Create a cohesive short campaign with: - Engaging title and description - 3-5 combat encounters (scaled to party level and difficulty) - Narrative connecting the encounters - Appropriate rewards (gold, XP, magic items) - A climactic final boss encounter Return JSON format: {{ "title": "Campaign title", "description": "Campaign overview", "encounters": [ {{ "title": "Encounter name", "narrative_intro": "Intro text", "enemies": [/* enemy definitions */], "narrative_completion": "Completion text" }} ], "rewards": {{ "gold_per_player": 500, "experience_per_player": 1000, "shared_items": ["magic_sword", "healing_potion"] }} }} """ ai_response = call_ai_api(prompt, model="claude-sonnet") campaign_data = parse_json_response(ai_response) return MultiplayerCampaign.from_dict(campaign_data) ``` ### Example Generated Campaign **Title:** "The Goblin Raid on Millstone Village" **Description:** A band of goblins has been terrorizing the nearby village of Millstone. The village elder has hired your party to track down the goblin war band and put an end to their raids. **Encounters:** 1. **Goblin Scouts** - Encounter 2 goblin scouts on the road 2. **Ambush in the Woods** - 5 goblins ambush the party 3. **The Goblin Camp** - Assault the main goblin camp (8 goblins + 1 hobgoblin) 4. **The Goblin Chieftain** - Final boss fight (Goblin Chieftain + 2 elite guards) **Rewards:** - 300 gold per player - 800 XP per player - Shared loot: Goblin Warblade, Ring of Minor Protection, 3x Healing Potions --- ## Turn Management ### Turn-Based Cooperative Combat In multiplayer combat, all party members take turns along with enemies: #### Initialize Multiplayer Combat ```python def initialize_multiplayer_combat( session: MultiplayerSession, encounter: CampaignEncounter ) -> None: """Start combat for multiplayer session.""" # Create combatants for all party members party_combatants = [] for member in session.party_members: combatant = create_combatant_from_character(member.character_snapshot) combatant.player_id = member.user_id # Track which player controls this party_combatants.append(combatant) # Create enemy combatants enemy_combatants = [] for enemy_def in encounter.enemies: enemy = create_enemy_combatant(enemy_def) enemy_combatants.append(enemy) # Create combat encounter combat = CombatEncounter( encounter_id=f"combat_{session.session_id}_{encounter.encounter_id}", combatants=party_combatants + enemy_combatants, turn_order=[], current_turn_index=0, round_number=1, status="active" ) # Roll initiative for all combatants combat.initialize_combat() # Set combat encounter on session session.combat_encounter = combat save_session(session) # Notify all players combat has started notify_all_players(session, "combat_started", { "encounter_title": encounter.title, "turn_order": combat.turn_order }) ``` #### Validate Combat Actions Only the player whose character's turn it is can take actions: **Endpoint:** `POST /api/v1/sessions/multiplayer/{session_id}/combat/action` **Request:** ```json { "action_type": "attack", "ability_id": "basic_attack", "target_id": "enemy_goblin_1" } ``` **Validation:** ```python def validate_combat_action( session: MultiplayerSession, user_id: str, action: CombatAction ) -> bool: """Ensure it's this player's turn.""" combat = session.combat_encounter current_combatant = combat.get_current_combatant() # Check if current combatant belongs to this user if hasattr(current_combatant, 'player_id'): if current_combatant.player_id != user_id: raise NotYourTurnError("It's not your turn") return True ``` --- ## Realtime Synchronization ### Backend Event Broadcasting Backend broadcasts events to all players via Appwrite Realtime: **Events Broadcast to All Players:** - Player joined lobby - Player ready status changed - Session started - Combat started - Turn advanced - Action taken (attack, spell, item use) - Damage dealt - Character defeated - Combat ended - Encounter completed - Session time warnings (10min, 5min, 1min) - Session expired ### Player Disconnection Handling If a player disconnects during active session: ```python def handle_player_disconnect(session: MultiplayerSession, user_id: str) -> None: """Handle player disconnection gracefully.""" # Mark player as disconnected for member in session.party_members: if member.user_id == user_id: member.is_connected = False break # If in combat, set their character to auto-defend if session.combat_encounter: set_character_auto_mode(session.combat_encounter, user_id, mode="defend") # Notify other players notify_other_players(session, user_id, "player_disconnected", { "username": member.username, "message": f"{member.username} has disconnected" }) # If host disconnects, promote new host if member.is_host: promote_new_host(session) ``` **Auto-Defend Mode:** When a player is disconnected, their character automatically: - Takes "Defend" action on their turn (reduces incoming damage) - Skips any decision-making - Continues until player reconnects or session ends --- ## Rewards and Character Updates ### Reward Distribution At session end (completion or expiration), rewards are distributed: ```python def distribute_rewards(session: MultiplayerSession, rewards: CampaignRewards) -> None: """Distribute rewards to all party members.""" for member in session.party_members: character = get_character(member.character_id) # Grant gold character.gold += rewards.gold_per_player # Grant XP (check for level up) character.experience += rewards.experience_per_player leveled_up = check_level_up(character) # Grant shared items (distribute evenly) distributed_items = distribute_shared_items(rewards.shared_items, len(session.party_members)) for item_id in distributed_items: item = load_item(item_id) character.inventory.append(item) # Save character updates update_character(character) # Notify player notify_player(member.user_id, "rewards_received", { "gold": rewards.gold_per_player, "experience": rewards.experience_per_player, "items": distributed_items, "leveled_up": leveled_up }) ``` ### Character Snapshot vs Live Character **Important Design Decision:** When a player joins a multiplayer session, a **snapshot** of their character is taken: ```python @dataclass class PartyMember: character_id: str # Original character ID character_snapshot: Character # Immutable copy at session start ``` **Why?** - Multiplayer sessions don't affect character location/state in solo campaigns - Character in solo game can continue progressing independently - Only rewards (gold, XP, items) are transferred back at session end **Example:** - Player has "Thorin" at level 5 in Thornfield Plains - Joins multiplayer session (snapshot taken) - Multiplayer session takes place in different location - After 2 hours, session ends - Thorin gains 800 XP, 300 gold, and levels up to 6 - **In solo campaign**: Thorin is still in Thornfield Plains, now level 6, with new gold/items --- ## API Endpoints ### Session Management | Endpoint | Method | Description | |----------|--------|-------------| | `/api/v1/sessions/multiplayer/create` | POST | Create new multiplayer session (Premium/Elite only) | | `/api/v1/sessions/multiplayer/join/{invite_code}` | GET | Get session info by invite code | | `/api/v1/sessions/multiplayer/join/{invite_code}` | POST | Join session with character | | `/api/v1/sessions/multiplayer/{session_id}` | GET | Get current session state | | `/api/v1/sessions/multiplayer/{session_id}/ready` | POST | Toggle ready status (lobby) | | `/api/v1/sessions/multiplayer/{session_id}/start` | POST | Start session (host only, all ready) | | `/api/v1/sessions/multiplayer/{session_id}/leave` | POST | Leave session | | `/api/v1/sessions/multiplayer/{session_id}/end` | POST | End session (host only) | ### Combat Actions | Endpoint | Method | Description | |----------|--------|-------------| | `/api/v1/sessions/multiplayer/{session_id}/combat/action` | POST | Take combat action (attack, spell, item, defend) | | `/api/v1/sessions/multiplayer/{session_id}/combat/state` | GET | Get current combat state | ### Campaign Progress | Endpoint | Method | Description | |----------|--------|-------------| | `/api/v1/sessions/multiplayer/{session_id}/campaign` | GET | Get campaign overview and progress | | `/api/v1/sessions/multiplayer/{session_id}/rewards` | GET | Get final rewards (after completion) | --- ## Implementation Timeline ### Week 12: Core Multiplayer Infrastructure (Days 1-7) | Task | Priority | Status | Notes | |------|----------|--------|-------| | Create MultiplayerSession dataclass | High | ⬜ | Extends GameSession with time limits, invite codes | | Create PartyMember dataclass | High | ⬜ | Player info, character snapshot | | Create MultiplayerCampaign models | High | ⬜ | Campaign, CampaignEncounter, CampaignRewards | | Implement invite code generation | High | ⬜ | 8-char alphanumeric, unique, 24hr expiration | | Implement session creation API | High | ⬜ | POST /sessions/multiplayer/create (Premium/Elite only) | | Implement join via invite API | High | ⬜ | GET/POST /join/{invite_code} | | Implement lobby system | High | ⬜ | Ready status, player list, host controls | | Implement 2-hour timer logic | High | ⬜ | Session expiration, warnings, auto-end | | Set up Appwrite Realtime | High | ⬜ | WebSocket subscriptions for live updates | | Write unit tests | Medium | ⬜ | Invite generation, join validation, timer logic | ### Week 13: Campaign Generation & Combat (Days 8-14) | Task | Priority | Status | Notes | |------|----------|--------|-------| | Implement AI campaign generator | High | ⬜ | Generate 3-5 encounters based on party composition | | Create campaign templates | Medium | ⬜ | Pre-built campaign structures for AI to fill | | Implement turn management | High | ⬜ | Initiative, turn order, action validation | | Implement multiplayer combat flow | High | ⬜ | Reuse Phase 5 combat, add multi-player support | | Implement disconnect handling | High | ⬜ | Auto-defend mode, host promotion | | Implement reward distribution | High | ⬜ | Calculate and grant rewards at session end | | Write integration tests | High | ⬜ | Full session flow: create → join → play → complete | | Test session expiration | Medium | ⬜ | Force expiration, verify cleanup | --- ## Cost Considerations ### AI Campaign Generation Cost | Tier | Model | Campaign Gen Cost | Per Session | |------|-------|-------------------|-------------| | Premium | Claude Sonnet | ~3000 tokens | ~$0.09 | | Elite | Claude Opus | ~3000 tokens | ~$0.45 | **Mitigation:** - Campaign generation happens once per session - Can cache campaign templates - Cost is acceptable for paid-tier feature ### Realtime Connection Cost Appwrite Realtime connections are included in Appwrite Cloud pricing. No additional cost per connection. --- ## Related Documentation - **[STORY_PROGRESSION.md](STORY_PROGRESSION.md)** - Solo story gameplay (comparison) - **[GAME_SYSTEMS.md](GAME_SYSTEMS.md)** - Combat system (reused in multiplayer) - **[DATA_MODELS.md](DATA_MODELS.md)** - GameSession, CombatEncounter models - **[API_REFERENCE.md](API_REFERENCE.md)** - Full API endpoint documentation - **[/public_web/docs/MULTIPLAYER.md](../../public_web/docs/MULTIPLAYER.md)** - Web frontend UI implementation - **[/godot_client/docs/MULTIPLAYER.md](../../godot_client/docs/MULTIPLAYER.md)** - Godot client implementation --- **Document Version:** 2.0 (Microservices Split) **Created:** November 16, 2025 **Last Updated:** November 18, 2025