Spaces:
Running
Running
Blu3Orange
feat: Introduce argument direction handling and enhance conviction mechanics for juror interactions
373ff24
| """Conviction score mechanics for juror persuasion.""" | |
| import random | |
| from typing import Literal | |
| from .models import JurorConfig, JurorMemory | |
| from .game_state import DeliberationTurn, ArgumentDirection | |
| # Archetype modifiers for different argument types | |
| ARCHETYPE_MODIFIERS: dict[str, dict[str, float]] = { | |
| "rationalist": { | |
| "logical": 1.5, | |
| "evidence": 1.3, | |
| "emotional": 0.4, | |
| "moral": 0.6, | |
| "narrative": 0.7, | |
| "question": 1.2, | |
| }, | |
| "empath": { | |
| "logical": 0.6, | |
| "evidence": 0.8, | |
| "emotional": 1.5, | |
| "moral": 1.3, | |
| "narrative": 1.2, | |
| "question": 0.9, | |
| }, | |
| "cynic": { | |
| "logical": 0.8, | |
| "evidence": 1.4, | |
| "emotional": 0.3, | |
| "moral": 0.5, | |
| "narrative": 0.6, | |
| "question": 0.7, | |
| }, | |
| "conformist": { | |
| "logical": 0.9, | |
| "evidence": 0.9, | |
| "emotional": 1.2, | |
| "moral": 1.0, | |
| "narrative": 1.1, | |
| "question": 0.8, | |
| }, | |
| "contrarian": { | |
| "logical": 1.2, | |
| "evidence": 1.0, | |
| "emotional": 0.7, | |
| "moral": 0.8, | |
| "narrative": 0.9, | |
| "question": 1.4, | |
| }, | |
| "impatient": { | |
| "logical": 0.7, | |
| "evidence": 0.8, | |
| "emotional": 1.0, | |
| "moral": 0.7, | |
| "narrative": 0.6, | |
| "question": 0.5, | |
| }, | |
| "detail_obsessed": { | |
| "logical": 1.3, | |
| "evidence": 1.5, | |
| "emotional": 0.5, | |
| "moral": 0.6, | |
| "narrative": 0.8, | |
| "question": 1.3, | |
| }, | |
| "moralist": { | |
| "logical": 0.7, | |
| "evidence": 0.8, | |
| "emotional": 1.2, | |
| "moral": 1.5, | |
| "narrative": 1.1, | |
| "question": 0.9, | |
| }, | |
| "pragmatist": { | |
| "logical": 1.2, | |
| "evidence": 1.1, | |
| "emotional": 0.6, | |
| "moral": 0.9, | |
| "narrative": 0.8, | |
| "question": 1.1, | |
| }, | |
| "storyteller": { | |
| "logical": 0.7, | |
| "evidence": 0.9, | |
| "emotional": 1.1, | |
| "moral": 1.0, | |
| "narrative": 1.5, | |
| "question": 1.0, | |
| }, | |
| "wildcard": { | |
| "logical": 0.9, | |
| "evidence": 0.9, | |
| "emotional": 1.0, | |
| "moral": 1.0, | |
| "narrative": 1.2, | |
| "question": 1.0, | |
| }, | |
| } | |
| def get_archetype_modifier(archetype: str, argument_type: str) -> float: | |
| """Get the modifier for an archetype's response to an argument type.""" | |
| archetype_mods = ARCHETYPE_MODIFIERS.get(archetype, {}) | |
| return archetype_mods.get(argument_type, 1.0) | |
| def calculate_direction_impact( | |
| direction: ArgumentDirection, | |
| base_strength: float = 0.1 | |
| ) -> float: | |
| """ | |
| Convert argument direction to signed impact. | |
| Args: | |
| direction: Which side the argument supports | |
| base_strength: Base argument strength (0.0 to 1.0) | |
| Returns: | |
| Signed impact: positive pushes toward guilty, negative toward not guilty | |
| """ | |
| if direction == ArgumentDirection.PROSECUTION: | |
| return base_strength # Positive: push toward guilty | |
| elif direction == ArgumentDirection.DEFENSE: | |
| return -base_strength # Negative: push toward not guilty | |
| return 0.0 # Neutral: no directional push | |
| def calculate_conviction_change( | |
| juror: JurorConfig, | |
| juror_memory: JurorMemory, | |
| argument: DeliberationTurn, | |
| base_strength: float = 0.1, | |
| ) -> float: | |
| """ | |
| Calculate how much an argument shifts a juror's conviction. | |
| Args: | |
| juror: The juror configuration | |
| juror_memory: The juror's current memory state | |
| argument: The argument being made (includes direction) | |
| base_strength: Base argument strength (0.0 to 1.0) | |
| Returns: | |
| delta to add to conviction score (-0.3 to +0.3 typically) | |
| """ | |
| # Calculate directional base impact from argument direction | |
| base_impact = calculate_direction_impact( | |
| argument.direction, | |
| base_strength | |
| ) | |
| # If no meaningful impact (neutral direction), skip noise and return 0 | |
| if abs(base_impact) < 0.001: | |
| return 0.0 | |
| # Personality modifiers | |
| archetype_modifier = get_archetype_modifier( | |
| juror.archetype, | |
| argument.argument_type | |
| ) | |
| # Stubbornness reduces all changes | |
| stubbornness_modifier = 1.0 - (juror.stubbornness * 0.7) | |
| # Relationship modifier - trust the speaker? | |
| trust = juror_memory.opinions_of_others.get(argument.speaker_id, 0.0) | |
| trust_modifier = 1.0 + (trust * 0.3) # -30% to +30% | |
| # Conviction resistance - quadratic falloff makes extremes much harder to move | |
| current = juror_memory.current_conviction | |
| distance = abs(current - 0.5) # 0 at center, 0.5 at extremes | |
| extreme_resistance = 1.0 - (distance ** 2) * 1.5 # Quadratic: 1.0 center, 0.625 extremes | |
| # Calculate base delta without noise | |
| delta = ( | |
| base_impact | |
| * archetype_modifier | |
| * stubbornness_modifier | |
| * trust_modifier | |
| * extreme_resistance | |
| ) | |
| # Only add volatility noise when there's meaningful impact | |
| if abs(delta) > 0.01: | |
| effective_volatility = juror.volatility * (1.0 - juror.stubbornness * 0.5) | |
| volatility_noise = random.gauss(0, effective_volatility * 0.1) | |
| delta += volatility_noise | |
| # Clamp to reasonable range | |
| return max(-0.3, min(0.3, delta)) | |
| def check_vote_flip(juror_memory: JurorMemory) -> bool: | |
| """ | |
| Check if conviction score warrants a vote change. | |
| Uses hysteresis - need to cross threshold by margin to flip. | |
| """ | |
| if not juror_memory.conviction_history: | |
| return False | |
| # Get previous conviction (before latest update) | |
| prev_conviction = juror_memory.conviction_history[-2] if len(juror_memory.conviction_history) > 1 else 0.5 | |
| current_vote_is_guilty = prev_conviction > 0.5 | |
| new_conviction = juror_memory.current_conviction | |
| # Hysteresis - need to cross threshold by margin to flip | |
| if current_vote_is_guilty and new_conviction < 0.4: | |
| return True # Flip to not guilty | |
| elif not current_vote_is_guilty and new_conviction > 0.6: | |
| return True # Flip to guilty | |
| return False | |
| def apply_conviction_update( | |
| juror_memory: JurorMemory, | |
| delta: float | |
| ) -> tuple[bool, Literal["guilty", "not_guilty"] | None]: | |
| """ | |
| Apply conviction update and check for vote flip using hysteresis. | |
| Args: | |
| juror_memory: Juror's memory state | |
| delta: Conviction change to apply | |
| Returns: | |
| Tuple of (vote_flipped, new_vote or None) | |
| """ | |
| old_vote = juror_memory.get_current_vote() | |
| # Apply the delta | |
| juror_memory.update_conviction(delta) | |
| # Check for vote flip using hysteresis | |
| if check_vote_flip(juror_memory): | |
| new_vote = juror_memory.get_current_vote() | |
| return True, new_vote | |
| return False, None | |
| def conviction_to_text(conviction: float) -> str: | |
| """Convert conviction score to human-readable text.""" | |
| if conviction < 0.2: | |
| return "Strongly believes NOT GUILTY" | |
| elif conviction < 0.4: | |
| return "Leaning NOT GUILTY" | |
| elif conviction < 0.6: | |
| return "Undecided" | |
| elif conviction < 0.8: | |
| return "Leaning GUILTY" | |
| else: | |
| return "Strongly believes GUILTY" | |