""" Dice mechanics module for Code of Conquest. This module provides core dice rolling functionality using a D20 + modifier vs DC system. All game chance mechanics (searches, skill checks, etc.) use these functions to determine outcomes before passing results to AI for narration. """ import random from dataclasses import dataclass from typing import Optional from enum import Enum class Difficulty(Enum): """Standard difficulty classes for skill checks.""" TRIVIAL = 5 EASY = 10 MEDIUM = 15 HARD = 20 VERY_HARD = 25 NEARLY_IMPOSSIBLE = 30 class SkillType(Enum): """ Skill types and their associated base stats. Each skill maps to a core stat for modifier calculation. """ # Wisdom-based PERCEPTION = "wisdom" INSIGHT = "wisdom" SURVIVAL = "wisdom" MEDICINE = "wisdom" # Dexterity-based STEALTH = "dexterity" ACROBATICS = "dexterity" SLEIGHT_OF_HAND = "dexterity" LOCKPICKING = "dexterity" # Charisma-based PERSUASION = "charisma" DECEPTION = "charisma" INTIMIDATION = "charisma" PERFORMANCE = "charisma" # Strength-based ATHLETICS = "strength" # Intelligence-based ARCANA = "intelligence" HISTORY = "intelligence" INVESTIGATION = "intelligence" NATURE = "intelligence" RELIGION = "intelligence" # Constitution-based ENDURANCE = "constitution" @dataclass class CheckResult: """ Result of a dice check. Contains all information needed for UI display (dice roll animation) and game logic (success/failure determination). Attributes: roll: The natural d20 roll (1-20) modifier: Total modifier from stats total: roll + modifier dc: Difficulty class that was checked against success: Whether the check succeeded margin: How much the check succeeded or failed by (total - dc) skill_type: The skill used for this check (if applicable) """ roll: int modifier: int total: int dc: int success: bool margin: int skill_type: Optional[str] = None @property def is_critical_success(self) -> bool: """Natural 20 - only relevant for combat.""" return self.roll == 20 @property def is_critical_failure(self) -> bool: """Natural 1 - only relevant for combat.""" return self.roll == 1 def to_dict(self) -> dict: """Serialize for API response.""" return { "roll": self.roll, "modifier": self.modifier, "total": self.total, "dc": self.dc, "success": self.success, "margin": self.margin, "skill_type": self.skill_type, } def roll_d20() -> int: """ Roll a standard 20-sided die. Returns: Integer from 1 to 20 (inclusive) """ return random.randint(1, 20) def calculate_modifier(stat_value: int) -> int: """ Calculate the D&D-style modifier from a stat value. Formula: (stat - 10) // 2 Examples: - Stat 10 = +0 modifier - Stat 14 = +2 modifier - Stat 18 = +4 modifier - Stat 8 = -1 modifier Args: stat_value: The raw stat value (typically 1-20) Returns: The modifier value (can be negative) """ return (stat_value - 10) // 2 def skill_check( stat_value: int, dc: int, skill_type: Optional[SkillType] = None, bonus: int = 0 ) -> CheckResult: """ Perform a skill check: d20 + modifier vs DC. Args: stat_value: The relevant stat value (e.g., character's wisdom for perception) dc: Difficulty class to beat skill_type: Optional skill type for logging/display bonus: Additional bonus (e.g., from equipment or proficiency) Returns: CheckResult with full details of the roll """ roll = roll_d20() modifier = calculate_modifier(stat_value) + bonus total = roll + modifier success = total >= dc margin = total - dc return CheckResult( roll=roll, modifier=modifier, total=total, dc=dc, success=success, margin=margin, skill_type=skill_type.name if skill_type else None ) def get_stat_for_skill(skill_type: SkillType) -> str: """ Get the base stat name for a skill type. Args: skill_type: The skill to look up Returns: The stat name (e.g., "wisdom", "dexterity") """ return skill_type.value def perception_check(wisdom: int, dc: int, bonus: int = 0) -> CheckResult: """ Convenience function for perception checks (searching, spotting). Args: wisdom: Character's wisdom stat dc: Difficulty class bonus: Additional bonus Returns: CheckResult """ return skill_check(wisdom, dc, SkillType.PERCEPTION, bonus) def stealth_check(dexterity: int, dc: int, bonus: int = 0) -> CheckResult: """ Convenience function for stealth checks (sneaking, hiding). Args: dexterity: Character's dexterity stat dc: Difficulty class bonus: Additional bonus Returns: CheckResult """ return skill_check(dexterity, dc, SkillType.STEALTH, bonus) def persuasion_check(charisma: int, dc: int, bonus: int = 0) -> CheckResult: """ Convenience function for persuasion checks (convincing, negotiating). Args: charisma: Character's charisma stat dc: Difficulty class bonus: Additional bonus Returns: CheckResult """ return skill_check(charisma, dc, SkillType.PERSUASION, bonus) def lockpicking_check(dexterity: int, dc: int, bonus: int = 0) -> CheckResult: """ Convenience function for lockpicking checks. Args: dexterity: Character's dexterity stat dc: Difficulty class bonus: Additional bonus Returns: CheckResult """ return skill_check(dexterity, dc, SkillType.LOCKPICKING, bonus)