# 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
