Spaces:
Sleeping
Sleeping
| """ | |
| Character data models for D&D 5e characters | |
| """ | |
| from datetime import datetime | |
| from typing import Optional, List, Dict, Any | |
| from enum import Enum | |
| from pydantic import BaseModel, Field, validator | |
| class DnDRace(str, Enum): | |
| """D&D 5e races""" | |
| HUMAN = "Human" | |
| ELF = "Elf" | |
| DWARF = "Dwarf" | |
| HALFLING = "Halfling" | |
| DRAGONBORN = "Dragonborn" | |
| GNOME = "Gnome" | |
| HALF_ELF = "Half-Elf" | |
| HALF_ORC = "Half-Orc" | |
| TIEFLING = "Tiefling" | |
| DROW = "Drow" | |
| class DnDClass(str, Enum): | |
| """D&D 5e classes""" | |
| BARBARIAN = "Barbarian" | |
| BARD = "Bard" | |
| CLERIC = "Cleric" | |
| DRUID = "Druid" | |
| FIGHTER = "Fighter" | |
| MONK = "Monk" | |
| PALADIN = "Paladin" | |
| RANGER = "Ranger" | |
| ROGUE = "Rogue" | |
| SORCERER = "Sorcerer" | |
| WARLOCK = "Warlock" | |
| WIZARD = "Wizard" | |
| class Alignment(str, Enum): | |
| """D&D alignments""" | |
| LAWFUL_GOOD = "Lawful Good" | |
| NEUTRAL_GOOD = "Neutral Good" | |
| CHAOTIC_GOOD = "Chaotic Good" | |
| LAWFUL_NEUTRAL = "Lawful Neutral" | |
| TRUE_NEUTRAL = "True Neutral" | |
| CHAOTIC_NEUTRAL = "Chaotic Neutral" | |
| LAWFUL_EVIL = "Lawful Evil" | |
| NEUTRAL_EVIL = "Neutral Evil" | |
| CHAOTIC_EVIL = "Chaotic Evil" | |
| # D&D 5e Hit Dice by Class (shared constant to avoid duplication) | |
| HIT_DICE_BY_CLASS = { | |
| DnDClass.BARBARIAN: 12, | |
| DnDClass.FIGHTER: 10, | |
| DnDClass.PALADIN: 10, | |
| DnDClass.RANGER: 10, | |
| DnDClass.BARD: 8, | |
| DnDClass.CLERIC: 8, | |
| DnDClass.DRUID: 8, | |
| DnDClass.MONK: 8, | |
| DnDClass.ROGUE: 8, | |
| DnDClass.WARLOCK: 8, | |
| DnDClass.SORCERER: 6, | |
| DnDClass.WIZARD: 6, | |
| } | |
| class CharacterStats(BaseModel): | |
| """D&D 5e ability scores and derived stats""" | |
| strength: int = Field(ge=1, le=20, default=10, description="Strength score (1-20 per D&D 5e standard rules)") | |
| dexterity: int = Field(ge=1, le=20, default=10, description="Dexterity score (1-20 per D&D 5e standard rules)") | |
| constitution: int = Field(ge=1, le=20, default=10, description="Constitution score (1-20 per D&D 5e standard rules)") | |
| intelligence: int = Field(ge=1, le=20, default=10, description="Intelligence score (1-20 per D&D 5e standard rules)") | |
| wisdom: int = Field(ge=1, le=20, default=10, description="Wisdom score (1-20 per D&D 5e standard rules)") | |
| charisma: int = Field(ge=1, le=20, default=10, description="Charisma score (1-20 per D&D 5e standard rules)") | |
| def strength_modifier(self) -> int: | |
| """Calculate strength modifier""" | |
| return (self.strength - 10) // 2 | |
| def dexterity_modifier(self) -> int: | |
| """Calculate dexterity modifier""" | |
| return (self.dexterity - 10) // 2 | |
| def constitution_modifier(self) -> int: | |
| """Calculate constitution modifier""" | |
| return (self.constitution - 10) // 2 | |
| def intelligence_modifier(self) -> int: | |
| """Calculate intelligence modifier""" | |
| return (self.intelligence - 10) // 2 | |
| def wisdom_modifier(self) -> int: | |
| """Calculate wisdom modifier""" | |
| return (self.wisdom - 10) // 2 | |
| def charisma_modifier(self) -> int: | |
| """Calculate charisma modifier""" | |
| return (self.charisma - 10) // 2 | |
| def get_all_modifiers(self) -> Dict[str, int]: | |
| """Get all ability modifiers""" | |
| return { | |
| "strength": self.strength_modifier, | |
| "dexterity": self.dexterity_modifier, | |
| "constitution": self.constitution_modifier, | |
| "intelligence": self.intelligence_modifier, | |
| "wisdom": self.wisdom_modifier, | |
| "charisma": self.charisma_modifier, | |
| } | |
| class CharacterBackground(BaseModel): | |
| """Character background and personality""" | |
| background_type: str = Field(default="Adventurer", description="Background archetype") | |
| personality_traits: List[str] = Field(default_factory=list, description="2-3 personality traits") | |
| ideals: str = Field(default="", description="Character's ideals") | |
| bonds: str = Field(default="", description="Character's bonds") | |
| flaws: str = Field(default="", description="Character's flaws") | |
| backstory: str = Field(default="", description="Full backstory") | |
| goals: List[str] = Field(default_factory=list, description="Character goals") | |
| class Character(BaseModel): | |
| """Complete D&D 5e character""" | |
| # Core identity | |
| id: Optional[str] = Field(default=None, description="Unique character ID") | |
| name: str = Field(min_length=1, max_length=100, description="Character name") | |
| race: DnDRace = Field(description="Character race") | |
| character_class: DnDClass = Field(description="Character class") | |
| level: int = Field(ge=1, le=20, default=1, description="Character level") | |
| alignment: Alignment = Field(default=Alignment.TRUE_NEUTRAL) | |
| gender: Optional[str] = Field(default=None, description="Character gender") | |
| skin_tone: Optional[str] = Field(default=None, description="Character skin tone/color") | |
| # Stats | |
| stats: CharacterStats = Field(default_factory=CharacterStats) | |
| max_hit_points: int = Field(ge=1, default=10) | |
| current_hit_points: int = Field(ge=0, default=10) | |
| armor_class: int = Field(ge=1, default=10) | |
| proficiency_bonus: int = Field(ge=2, default=2) | |
| # Background & personality | |
| background: CharacterBackground = Field(default_factory=CharacterBackground) | |
| # Equipment & abilities | |
| equipment: List[str] = Field(default_factory=list, description="Equipment list") | |
| spells: List[str] = Field(default_factory=list, description="Known spells") | |
| features: List[str] = Field(default_factory=list, description="Class features") | |
| proficiencies: List[str] = Field(default_factory=list, description="Skill proficiencies") | |
| # Portrait | |
| portrait_url: Optional[str] = Field(default=None, description="Character portrait URL") | |
| portrait_prompt: Optional[str] = Field(default=None, description="Prompt used for portrait") | |
| # Metadata | |
| campaign_id: Optional[str] = Field(default=None, description="Associated campaign") | |
| player_name: Optional[str] = Field(default=None, description="Player's name") | |
| created_at: datetime = Field(default_factory=datetime.now) | |
| updated_at: datetime = Field(default_factory=datetime.now) | |
| # Additional data | |
| notes: str = Field(default="", description="GM/player notes") | |
| experience_points: int = Field(ge=0, default=0) | |
| def calculate_proficiency_bonus(cls, v, values): | |
| """Calculate proficiency bonus based on level""" | |
| level = values.get("level", 1) | |
| return 2 + ((level - 1) // 4) | |
| def validate_hp(cls, v, values): | |
| """Ensure current HP doesn't exceed max HP""" | |
| max_hp = values.get("max_hit_points", 10) | |
| return min(v, max_hp) | |
| def calculate_max_hp(self) -> int: | |
| """Calculate max HP based on class and level (D&D 5e rules)""" | |
| hit_die = HIT_DICE_BY_CLASS.get(self.character_class, 8) | |
| con_mod = self.stats.constitution_modifier | |
| # First level: max hit die + con mod (minimum 1) | |
| first_level_hp = max(1, hit_die + con_mod) | |
| # Subsequent levels: average (rounded up) + con mod (minimum 1 per level per D&D 5e) | |
| hp_per_level = max(1, (hit_die // 2) + 1 + con_mod) | |
| subsequent_hp = hp_per_level * (self.level - 1) | |
| return first_level_hp + subsequent_hp | |
| def take_damage(self, damage: int) -> int: | |
| """Apply damage to character""" | |
| self.current_hit_points = max(0, self.current_hit_points - damage) | |
| self.updated_at = datetime.now() | |
| return self.current_hit_points | |
| def heal(self, healing: int) -> int: | |
| """Heal character""" | |
| self.current_hit_points = min(self.max_hit_points, self.current_hit_points + healing) | |
| self.updated_at = datetime.now() | |
| return self.current_hit_points | |
| def level_up(self): | |
| """Level up the character (D&D 5e rules)""" | |
| if self.level < 20: | |
| self.level += 1 | |
| # Recalculate proficiency bonus based on new level | |
| self.proficiency_bonus = 2 + ((self.level - 1) // 4) | |
| self.max_hit_points = self.calculate_max_hp() | |
| self.current_hit_points = self.max_hit_points | |
| self.updated_at = datetime.now() | |
| def to_dict(self) -> Dict[str, Any]: | |
| """Convert to dictionary""" | |
| return self.model_dump() | |
| def to_markdown(self) -> str: | |
| """Generate markdown character sheet""" | |
| return f"""# {self.name} | |
| **Level {self.level} {self.race.value} {self.character_class.value}** | |
| *{self.alignment.value}* | |
| ## Ability Scores | |
| - **STR:** {self.stats.strength} ({self.stats.strength_modifier:+d}) | |
| - **DEX:** {self.stats.dexterity} ({self.stats.dexterity_modifier:+d}) | |
| - **CON:** {self.stats.constitution} ({self.stats.constitution_modifier:+d}) | |
| - **INT:** {self.stats.intelligence} ({self.stats.intelligence_modifier:+d}) | |
| - **WIS:** {self.stats.wisdom} ({self.stats.wisdom_modifier:+d}) | |
| - **CHA:** {self.stats.charisma} ({self.stats.charisma_modifier:+d}) | |
| ## Combat Stats | |
| - **HP:** {self.current_hit_points}/{self.max_hit_points} | |
| - **AC:** {self.armor_class} | |
| - **Proficiency:** +{self.proficiency_bonus} | |
| ## Background | |
| **{self.background.background_type}** | |
| {self.background.backstory} | |
| ## Personality | |
| **Traits:** {', '.join(self.background.personality_traits)} | |
| **Ideals:** {self.background.ideals} | |
| **Bonds:** {self.background.bonds} | |
| **Flaws:** {self.background.flaws} | |
| ## Equipment | |
| {chr(10).join(f"- {item}" for item in self.equipment)} | |
| ## Notes | |
| {self.notes} | |
| """ | |
| class Config: | |
| json_schema_extra = { | |
| "example": { | |
| "name": "Thorin Ironforge", | |
| "race": "Dwarf", | |
| "character_class": "Fighter", | |
| "level": 3, | |
| "alignment": "Lawful Good", | |
| "stats": { | |
| "strength": 16, | |
| "dexterity": 12, | |
| "constitution": 15, | |
| "intelligence": 10, | |
| "wisdom": 13, | |
| "charisma": 8 | |
| }, | |
| "background": { | |
| "background_type": "Soldier", | |
| "backstory": "A veteran warrior seeking redemption." | |
| } | |
| } | |
| } | |