first commit
This commit is contained in:
435
api/docs/SESSION_MANAGEMENT.md
Normal file
435
api/docs/SESSION_MANAGEMENT.md
Normal file
@@ -0,0 +1,435 @@
|
||||
# Session Management
|
||||
|
||||
This document describes the game session system for Code of Conquest, covering both solo and multiplayer sessions.
|
||||
|
||||
**Last Updated:** November 22, 2025
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Game sessions track the state of gameplay including:
|
||||
- Current location and discovered locations
|
||||
- Conversation history between player and AI DM
|
||||
- Active quests (max 2)
|
||||
- World events
|
||||
- Combat encounters
|
||||
|
||||
Sessions support both **solo play** (single character) and **multiplayer** (party-based).
|
||||
|
||||
---
|
||||
|
||||
## Data Models
|
||||
|
||||
### SessionType Enum
|
||||
|
||||
```python
|
||||
class SessionType(Enum):
|
||||
SOLO = "solo" # Single-player session
|
||||
MULTIPLAYER = "multiplayer" # Multi-player party session
|
||||
```
|
||||
|
||||
### LocationType Enum
|
||||
|
||||
```python
|
||||
class LocationType(Enum):
|
||||
TOWN = "town" # Town or city
|
||||
TAVERN = "tavern" # Tavern or inn
|
||||
WILDERNESS = "wilderness" # Outdoor wilderness areas
|
||||
DUNGEON = "dungeon" # Underground dungeons/caves
|
||||
RUINS = "ruins" # Ancient ruins
|
||||
LIBRARY = "library" # Library or archive
|
||||
SAFE_AREA = "safe_area" # Safe rest areas
|
||||
```
|
||||
|
||||
### GameState
|
||||
|
||||
Tracks current world state for a session:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class GameState:
|
||||
current_location: str = "Crossroads Village"
|
||||
location_type: LocationType = LocationType.TOWN
|
||||
discovered_locations: List[str] = []
|
||||
active_quests: List[str] = [] # Max 2
|
||||
world_events: List[Dict] = []
|
||||
```
|
||||
|
||||
### ConversationEntry
|
||||
|
||||
Single turn in the conversation history:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ConversationEntry:
|
||||
turn: int # Turn number (1-indexed)
|
||||
character_id: str # Acting character
|
||||
character_name: str # Character display name
|
||||
action: str # Player's action text
|
||||
dm_response: str # AI DM's response
|
||||
timestamp: str # ISO timestamp (auto-generated)
|
||||
combat_log: List[Dict] = [] # Combat actions if any
|
||||
quest_offered: Optional[Dict] = None # Quest offering info
|
||||
```
|
||||
|
||||
### GameSession
|
||||
|
||||
Main session object:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class GameSession:
|
||||
session_id: str
|
||||
session_type: SessionType = SessionType.SOLO
|
||||
solo_character_id: Optional[str] = None # For solo sessions
|
||||
user_id: str = ""
|
||||
party_member_ids: List[str] = [] # For multiplayer
|
||||
config: SessionConfig
|
||||
combat_encounter: Optional[CombatEncounter] = None
|
||||
conversation_history: List[ConversationEntry] = []
|
||||
game_state: GameState
|
||||
turn_order: List[str] = []
|
||||
current_turn: int = 0
|
||||
turn_number: int = 0
|
||||
created_at: str # ISO timestamp
|
||||
last_activity: str # ISO timestamp
|
||||
status: SessionStatus = SessionStatus.ACTIVE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SessionService
|
||||
|
||||
The `SessionService` class (`app/services/session_service.py`) provides all session operations.
|
||||
|
||||
### Initialization
|
||||
|
||||
```python
|
||||
from app.services.session_service import get_session_service
|
||||
|
||||
service = get_session_service()
|
||||
```
|
||||
|
||||
### Creating Sessions
|
||||
|
||||
#### Solo Session
|
||||
|
||||
```python
|
||||
session = service.create_solo_session(
|
||||
user_id="user_123",
|
||||
character_id="char_456",
|
||||
starting_location="Crossroads Village", # Optional
|
||||
starting_location_type=LocationType.TOWN # Optional
|
||||
)
|
||||
```
|
||||
|
||||
**Validations:**
|
||||
- User must own the character
|
||||
- User cannot exceed 5 active sessions
|
||||
|
||||
**Returns:** `GameSession` instance
|
||||
|
||||
**Raises:**
|
||||
- `CharacterNotFound` - Character doesn't exist or user doesn't own it
|
||||
- `SessionLimitExceeded` - User has 5+ active sessions
|
||||
|
||||
### Retrieving Sessions
|
||||
|
||||
#### Get Single Session
|
||||
|
||||
```python
|
||||
session = service.get_session(
|
||||
session_id="sess_789",
|
||||
user_id="user_123" # Optional, validates ownership
|
||||
)
|
||||
```
|
||||
|
||||
**Raises:** `SessionNotFound`
|
||||
|
||||
#### Get User's Sessions
|
||||
|
||||
```python
|
||||
sessions = service.get_user_sessions(
|
||||
user_id="user_123",
|
||||
active_only=True, # Default: True
|
||||
limit=25 # Default: 25
|
||||
)
|
||||
```
|
||||
|
||||
#### Count Sessions
|
||||
|
||||
```python
|
||||
count = service.count_user_sessions(
|
||||
user_id="user_123",
|
||||
active_only=True
|
||||
)
|
||||
```
|
||||
|
||||
### Updating Sessions
|
||||
|
||||
#### Direct Update
|
||||
|
||||
```python
|
||||
session.turn_number += 1
|
||||
session = service.update_session(session)
|
||||
```
|
||||
|
||||
#### End Session
|
||||
|
||||
```python
|
||||
session = service.end_session(
|
||||
session_id="sess_789",
|
||||
user_id="user_123"
|
||||
)
|
||||
# Sets status to COMPLETED
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conversation History
|
||||
|
||||
### Adding Entries
|
||||
|
||||
```python
|
||||
session = service.add_conversation_entry(
|
||||
session_id="sess_789",
|
||||
character_id="char_456",
|
||||
character_name="Brave Hero",
|
||||
action="I explore the tavern",
|
||||
dm_response="You enter a smoky tavern filled with patrons...",
|
||||
combat_log=[], # Optional
|
||||
quest_offered={"quest_id": "q1", "name": "..."} # Optional
|
||||
)
|
||||
```
|
||||
|
||||
**Automatic behaviors:**
|
||||
- Increments `turn_number`
|
||||
- Adds timestamp
|
||||
- Updates `last_activity`
|
||||
|
||||
### Retrieving History
|
||||
|
||||
```python
|
||||
# Get all history (with pagination)
|
||||
history = service.get_conversation_history(
|
||||
session_id="sess_789",
|
||||
limit=20, # Optional
|
||||
offset=0 # Optional, from end
|
||||
)
|
||||
|
||||
# Get recent entries for AI context
|
||||
recent = service.get_recent_history(
|
||||
session_id="sess_789",
|
||||
num_turns=3 # Default: 3
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Game State Tracking
|
||||
|
||||
### Location Management
|
||||
|
||||
```python
|
||||
# Update current location
|
||||
session = service.update_location(
|
||||
session_id="sess_789",
|
||||
new_location="Dark Forest",
|
||||
location_type=LocationType.WILDERNESS
|
||||
)
|
||||
# Also adds to discovered_locations if new
|
||||
|
||||
# Add discovered location without traveling
|
||||
session = service.add_discovered_location(
|
||||
session_id="sess_789",
|
||||
location="Ancient Ruins"
|
||||
)
|
||||
```
|
||||
|
||||
### Quest Management
|
||||
|
||||
```python
|
||||
# Add active quest (max 2)
|
||||
session = service.add_active_quest(
|
||||
session_id="sess_789",
|
||||
quest_id="quest_goblin_cave"
|
||||
)
|
||||
|
||||
# Remove quest (on completion or abandonment)
|
||||
session = service.remove_active_quest(
|
||||
session_id="sess_789",
|
||||
quest_id="quest_goblin_cave"
|
||||
)
|
||||
```
|
||||
|
||||
**Raises:** `SessionValidationError` if adding 3rd quest
|
||||
|
||||
### World Events
|
||||
|
||||
```python
|
||||
session = service.add_world_event(
|
||||
session_id="sess_789",
|
||||
event={
|
||||
"type": "festival",
|
||||
"description": "A harvest festival begins in town"
|
||||
}
|
||||
)
|
||||
# Timestamp auto-added
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Session Limits
|
||||
|
||||
| Limit | Value | Notes |
|
||||
|-------|-------|-------|
|
||||
| Active sessions per user | 5 | End existing sessions to create new |
|
||||
| Active quests per session | 2 | Complete or abandon to accept new |
|
||||
| Conversation history | Unlimited | Consider archiving for very long sessions |
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
Sessions are stored in Appwrite `game_sessions` collection:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `$id` | string | Session ID (document ID) |
|
||||
| `userId` | string | Owner's user ID |
|
||||
| `sessionData` | string | JSON serialized GameSession |
|
||||
| `status` | string | "active", "completed", "timeout" |
|
||||
| `sessionType` | string | "solo" or "multiplayer" |
|
||||
|
||||
### Indexes
|
||||
|
||||
- `userId` - For user session queries
|
||||
- `status` - For active session filtering
|
||||
- `userId + status` - Composite for active user sessions
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Complete Solo Gameplay Flow
|
||||
|
||||
```python
|
||||
from app.services.session_service import get_session_service
|
||||
from app.models.enums import LocationType
|
||||
|
||||
service = get_session_service()
|
||||
|
||||
# 1. Create session
|
||||
session = service.create_solo_session(
|
||||
user_id="user_123",
|
||||
character_id="char_456"
|
||||
)
|
||||
|
||||
# 2. Player takes action, AI responds
|
||||
session = service.add_conversation_entry(
|
||||
session_id=session.session_id,
|
||||
character_id="char_456",
|
||||
character_name="Hero",
|
||||
action="I look around the village square",
|
||||
dm_response="The village square bustles with activity..."
|
||||
)
|
||||
|
||||
# 3. Player travels
|
||||
session = service.update_location(
|
||||
session_id=session.session_id,
|
||||
new_location="The Rusty Anchor Tavern",
|
||||
location_type=LocationType.TAVERN
|
||||
)
|
||||
|
||||
# 4. Quest offered and accepted
|
||||
session = service.add_active_quest(
|
||||
session_id=session.session_id,
|
||||
quest_id="quest_goblin_cave"
|
||||
)
|
||||
|
||||
# 5. End session
|
||||
session = service.end_session(
|
||||
session_id=session.session_id,
|
||||
user_id="user_123"
|
||||
)
|
||||
```
|
||||
|
||||
### Checking Session State
|
||||
|
||||
```python
|
||||
session = service.get_session("sess_789")
|
||||
|
||||
# Check session type
|
||||
if session.is_solo():
|
||||
char_id = session.solo_character_id
|
||||
else:
|
||||
char_id = session.get_current_character_id()
|
||||
|
||||
# Check current location
|
||||
location = session.game_state.current_location
|
||||
location_type = session.game_state.location_type
|
||||
|
||||
# Check active quests
|
||||
quests = session.game_state.active_quests
|
||||
can_accept_quest = len(quests) < 2
|
||||
|
||||
# Get recent context for AI
|
||||
recent = service.get_recent_history(session.session_id, num_turns=3)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Exception Classes
|
||||
|
||||
```python
|
||||
from app.services.session_service import (
|
||||
SessionNotFound,
|
||||
SessionLimitExceeded,
|
||||
SessionValidationError,
|
||||
)
|
||||
|
||||
try:
|
||||
session = service.get_session("invalid_id", "user_123")
|
||||
except SessionNotFound:
|
||||
# Session doesn't exist or user doesn't own it
|
||||
pass
|
||||
|
||||
try:
|
||||
service.create_solo_session(user_id, char_id)
|
||||
except SessionLimitExceeded:
|
||||
# User has 5+ active sessions
|
||||
pass
|
||||
|
||||
try:
|
||||
service.add_active_quest(session_id, "quest_3")
|
||||
except SessionValidationError:
|
||||
# Already have 2 active quests
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
Run session tests:
|
||||
|
||||
```bash
|
||||
# Unit tests
|
||||
pytest tests/test_session_model.py -v
|
||||
pytest tests/test_session_service.py -v
|
||||
|
||||
# Verification script (requires TEST_USER_ID and TEST_CHARACTER_ID in .env)
|
||||
python scripts/verify_session_persistence.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [DATA_MODELS.md](DATA_MODELS.md) - Character and item models
|
||||
- [STORY_PROGRESSION.md](STORY_PROGRESSION.md) - Story turn system
|
||||
- [QUEST_SYSTEM.md](QUEST_SYSTEM.md) - Quest mechanics
|
||||
- [API_REFERENCE.md](API_REFERENCE.md) - Session API endpoints
|
||||
Reference in New Issue
Block a user