Spaces:
Running
Running
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 | |
| 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(), | |
| } | |
| 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(), | |
| } | |