Files
Code_of_Conquest/api/docs/MULTIPLAYER.md
2025-11-24 23:10:55 -06:00

28 KiB

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.

@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

@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

@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

@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

@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

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:

{
  "max_players": 4,
  "difficulty": "medium",
  "tier_required": "premium"
}

Response:

{
  "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"
}

Players click the invite link and are prompted to select a character:

Endpoint: GET /api/v1/sessions/multiplayer/join/{invite_code}

Response:

{
  "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:

{
  "character_id": "char_abc123"
}

Response:

{
  "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

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:

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

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:

{
  "action_type": "attack",
  "ability_id": "basic_attack",
  "target_id": "enemy_goblin_1"
}

Validation:

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:

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:

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:

@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.



Document Version: 2.0 (Microservices Split) Created: November 16, 2025 Last Updated: November 18, 2025