"""Game state management for MCP server."""
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Set, Any
@dataclass
class PublicSuspect:
"""Public information about a suspect (no secrets!)."""
name: str
role: str
personality: str
alibi: str # What they CLAIM
voice_id: Optional[str] = None
@dataclass
class SuspectState:
"""Emotional and conversation state for a suspect."""
trust: int = 50
nervousness: int = 30
conversations: List[Dict[str, str]] = field(default_factory=list)
contradictions_caught: int = 0
secret_revealed: bool = False
@dataclass
class TimelineEvent:
"""An event in the investigation timeline."""
time_slot: str
event_type: str # alibi_claim, witness_sighting, clue_found, contradiction
description: str
suspect_name: Optional[str] = None
source: str = ""
is_verified: bool = False
class GameSession:
"""Manages the state of a game session.
This is the PUBLIC view - the Oracle holds the truth.
"""
def __init__(self, session_id: str):
self.session_id = session_id
self.turn = 0
# Mystery info (public only)
self.setting: str = ""
self.victim_name: str = ""
self.victim_background: str = ""
self.public_suspects: List[PublicSuspect] = []
# Investigation progress
self.suspects_talked_to: Set[str] = set()
self.discovered_clues: List[str] = []
self.searched_locations: Set[str] = set()
self.available_locations: List[str] = []
# Suspect emotional states
self._suspect_states: Dict[str, SuspectState] = {}
# Timeline of discovered events
self.timeline: List[TimelineEvent] = []
# Game state
self.wrong_accusations: int = 0
self.game_over: bool = False
self.won: bool = False
# Services
from .oracle import MysteryOracle
from .memory import GameMemory
self.oracle = MysteryOracle()
self.memory = GameMemory()
self._initialized = False
@property
def is_initialized(self) -> bool:
return self._initialized
def initialize_from_mystery(self, mystery: Any):
"""Initialize session from a generated mystery.
This extracts only PUBLIC information - secrets stay in Oracle.
"""
self.setting = mystery.setting
self.victim_name = mystery.victim.name if mystery.victim else "Unknown"
self.victim_background = mystery.victim.background if mystery.victim else ""
# Create public suspects (no secrets!)
self.public_suspects = [
PublicSuspect(
name=s.name,
role=s.role,
personality=s.personality,
alibi=s.alibi,
voice_id=getattr(s, "voice_id", None)
)
for s in mystery.suspects
]
# Initialize suspect states
self._suspect_states = {
s.name: SuspectState() for s in mystery.suspects
}
# Initialize memory
self.memory.initialize()
self._initialized = True
def find_suspect(self, name: str) -> Optional[PublicSuspect]:
"""Find a suspect by name (fuzzy match)."""
name_lower = name.lower()
for s in self.public_suspects:
if s.name.lower() == name_lower or name_lower in s.name.lower():
return s
return None
def find_location(self, location: str) -> Optional[str]:
"""Find a location by name (fuzzy match)."""
location_lower = location.lower()
for loc in self.available_locations:
if loc.lower() == location_lower or location_lower in loc.lower():
return loc
return None
def get_suspect_state(self, name: str) -> SuspectState:
"""Get or create suspect state."""
if name not in self._suspect_states:
self._suspect_states[name] = SuspectState()
return self._suspect_states[name]
def get_suspect_trust(self, name: str) -> int:
return self.get_suspect_state(name).trust
def get_suspect_nervousness(self, name: str) -> int:
return self.get_suspect_state(name).nervousness
def get_conversation_history(self, name: str) -> List[Dict[str, str]]:
return self.get_suspect_state(name).conversations
def record_conversation(self, suspect_name: str, question: str, answer: str):
"""Record a conversation exchange."""
state = self.get_suspect_state(suspect_name)
state.conversations.append({
"question": question,
"answer": answer,
"turn": self.turn
})
self.suspects_talked_to.add(suspect_name)
self.turn += 1
def update_emotional_state(
self,
suspect_name: str,
trust_delta: int = 0,
nervousness_delta: int = 0
):
"""Update a suspect's emotional state."""
state = self.get_suspect_state(suspect_name)
state.trust = max(0, min(100, state.trust + trust_delta))
state.nervousness = max(0, min(100, state.nervousness + nervousness_delta))
def unlock_location(self, location: str):
"""Unlock a new location for searching."""
if location and location not in self.available_locations:
self.available_locations.append(location)
def mark_location_searched(self, location: str):
"""Mark a location as searched."""
self.searched_locations.add(location)
def add_discovered_clue(self, clue: str):
"""Add a discovered clue."""
if clue not in self.discovered_clues:
self.discovered_clues.append(clue)
def mark_secret_revealed(self, suspect_name: str):
"""Mark that a suspect revealed their secret."""
state = self.get_suspect_state(suspect_name)
state.secret_revealed = True
def add_timeline_event(
self,
time_slot: str,
event_type: str,
description: str,
suspect_name: Optional[str] = None,
source: str = "",
is_verified: bool = False
):
"""Add an event to the investigation timeline."""
self.timeline.append(TimelineEvent(
time_slot=time_slot,
event_type=event_type,
description=description,
suspect_name=suspect_name,
source=source,
is_verified=is_verified
))
def get_timeline(self) -> List[Dict]:
"""Get timeline as list of dicts."""
return [
{
"time_slot": e.time_slot,
"event_type": e.event_type,
"description": e.description,
"suspect_name": e.suspect_name,
"source": e.source,
"is_verified": e.is_verified
}
for e in self.timeline
]