248 lines
5.9 KiB
Python
248 lines
5.9 KiB
Python
"""
|
|
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)
|