Files
Code_of_Conquest/api/docs/SESSION_MANAGEMENT.md
Phillip Tarrant 61a42d3a77 feat(api,web): tier-based session limits and daily turn usage display
Backend Changes:
- Add tier-based max_sessions config (free: 1, basic: 2, premium: 3, elite: 5)
- Add DELETE /api/v1/sessions/{id} endpoint for hard session deletion
- Cascade delete chat messages when session is deleted
- Add GET /api/v1/usage endpoint for daily turn limit info
- Replace hardcoded TIER_LIMITS with config-based ai_calls_per_day
- Handle unlimited (-1) tier in rate limiter service

Frontend Changes:
- Add inline session delete buttons with HTMX on character list
- Add usage_display.html component showing remaining daily turns
- Display usage indicator on character list and game play pages
- Page refresh after session deletion to update UI state

Documentation:
- Update API_REFERENCE.md with new endpoints and tier limits
- Update API_TESTING.md with session endpoint examples
- Update SESSION_MANAGEMENT.md with tier-based limits

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 10:00:45 -06:00

9.9 KiB

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

class SessionType(Enum):
    SOLO = "solo"               # Single-player session
    MULTIPLAYER = "multiplayer" # Multi-player party session

LocationType Enum

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:

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

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

@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

from app.services.session_service import get_session_service

service = get_session_service()

Creating Sessions

Solo Session

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 their tier's session limit

Session Limits by Tier:

Tier Max Sessions
FREE 1
BASIC 2
PREMIUM 3
ELITE 5

Returns: GameSession instance

Raises:

  • CharacterNotFound - Character doesn't exist or user doesn't own it
  • SessionLimitExceeded - User has reached their tier's session limit

Retrieving Sessions

Get Single Session

session = service.get_session(
    session_id="sess_789",
    user_id="user_123"      # Optional, validates ownership
)

Raises: SessionNotFound

Get User's Sessions

sessions = service.get_user_sessions(
    user_id="user_123",
    active_only=True,       # Default: True
    limit=25                # Default: 25
)

Count Sessions

count = service.count_user_sessions(
    user_id="user_123",
    active_only=True
)

Updating Sessions

Direct Update

session.turn_number += 1
session = service.update_session(session)

End Session

session = service.end_session(
    session_id="sess_789",
    user_id="user_123"
)
# Sets status to COMPLETED

Conversation History

Adding Entries

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

# 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

# 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

# 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

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 Tier-based (1-5) See tier limits below
Active quests per session 2 Complete or abandon to accept new
Conversation history Unlimited Consider archiving for very long sessions

Session Limits by Tier:

Tier Max Sessions
FREE 1
BASIC 2
PREMIUM 3
ELITE 5

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

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

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

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 reached their tier's session limit
    pass

try:
    service.add_active_quest(session_id, "quest_3")
except SessionValidationError:
    # Already have 2 active quests
    pass

Testing

Run session tests:

# 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