12-Angry-Agent / core /game_state.py
Blu3Orange
feat: Introduce argument direction handling and enhance conviction mechanics for juror interactions
373ff24
"""Game state models for 12 Angry Agents."""
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Literal
from uuid import uuid4
class GamePhase(str, Enum):
"""Phases of the game."""
SETUP = "setup"
PRESENTATION = "presentation"
SIDE_SELECTION = "side_selection"
INITIAL_VOTE = "initial_vote"
DELIBERATION = "deliberation"
FINAL_VOTE = "final_vote"
VERDICT = "verdict"
class ArgumentDirection(str, Enum):
"""Direction of an argument's stance."""
PROSECUTION = "prosecution" # Argues for guilty verdict
DEFENSE = "defense" # Argues for not guilty verdict
NEUTRAL = "neutral" # Procedural, questions, undecided
@dataclass
class DeliberationTurn:
"""A single turn in deliberation."""
round_number: int
speaker_id: str
speaker_name: str
argument_type: str # "evidence", "emotional", "logical", "question", etc.
direction: ArgumentDirection # prosecution, defense, or neutral
content: str
target_id: str | None = None # Who they're addressing
impact: dict[str, float] = field(default_factory=dict) # conviction changes
timestamp: datetime = field(default_factory=datetime.now)
def to_dict(self) -> dict:
"""Convert to dictionary for serialization."""
return {
"round_number": self.round_number,
"speaker_id": self.speaker_id,
"speaker_name": self.speaker_name,
"argument_type": self.argument_type,
"direction": self.direction.value,
"content": self.content,
"target_id": self.target_id,
"impact": self.impact,
"timestamp": self.timestamp.isoformat(),
}
@dataclass
class GameState:
"""Central game state - managed by Orchestrator."""
# Session
session_id: str = field(default_factory=lambda: str(uuid4()))
case_id: str = ""
phase: GamePhase = GamePhase.SETUP
# Rounds
round_number: int = 0
max_rounds: int = 12 # Safety limit (was 20, reduced for better pacing)
stability_threshold: int = 3 # Base threshold, can be dynamic
rounds_without_change: int = 0
# Votes
votes: dict[str, Literal["guilty", "not_guilty"]] = field(default_factory=dict)
vote_history: list[dict[str, str]] = field(default_factory=list)
# Conviction scores (0.0 = certain not guilty, 1.0 = certain guilty)
conviction_scores: dict[str, float] = field(default_factory=dict)
# Deliberation
speaking_queue: list[str] = field(default_factory=list)
deliberation_log: list[DeliberationTurn] = field(default_factory=list)
# Player
player_side: Literal["defend", "prosecute"] | None = None
player_seat: int = 7 # Which seat is the player
def get_vote_tally(self) -> tuple[int, int]:
"""Get current vote tally (guilty, not_guilty)."""
guilty = sum(1 for v in self.votes.values() if v == "guilty")
not_guilty = sum(1 for v in self.votes.values() if v == "not_guilty")
return guilty, not_guilty
def is_unanimous(self) -> bool:
"""Check if vote is unanimous."""
if not self.votes:
return False
votes = set(self.votes.values())
return len(votes) == 1
def get_dynamic_stability_threshold(self) -> int:
"""Stability threshold based on vote split - close votes need more debate."""
guilty, not_guilty = self.get_vote_tally()
minority = min(guilty, not_guilty)
if minority <= 2: # 10-2 or more lopsided
return 2 # Ends quickly
elif minority <= 4: # 8-4 to 9-3
return 3 # Normal
else: # 6-6 to 7-5 (close)
return 4 # Needs more debate
def should_end_deliberation(self) -> bool:
"""Check if deliberation should end."""
# Unanimous verdict
if self.is_unanimous():
return True
# Votes stabilized - use dynamic threshold based on vote split
dynamic_threshold = self.get_dynamic_stability_threshold()
if self.rounds_without_change >= dynamic_threshold:
return True
# Max rounds reached
if self.round_number >= self.max_rounds:
return True
return False
def record_vote_snapshot(self) -> None:
"""Record current votes to history."""
self.vote_history.append(dict(self.votes))
def to_dict(self) -> dict:
"""Convert to dictionary for serialization."""
return {
"session_id": self.session_id,
"case_id": self.case_id,
"phase": self.phase.value,
"round_number": self.round_number,
"votes": self.votes,
"conviction_scores": self.conviction_scores,
"player_side": self.player_side,
"vote_tally": self.get_vote_tally(),
}