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

808 lines
28 KiB
Markdown

# 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