DigitalPal / digipal /core /attribute_engine.py
BladeSzaSza's picture
🥚 Initial DigiPal deployment to HuggingFace Spaces🤖 Generated with [Claude Code](https://claude.ai/code)Co-Authored-By: Claude <noreply@anthropic.com>
4399e64
"""
AttributeEngine for DigiPal - Implements Digimon World 1 inspired attribute calculations.
"""
from typing import Dict, List, Optional, Tuple
import random
from datetime import datetime, timedelta
from .models import DigiPal, CareAction, AttributeModifier, Interaction
from .enums import AttributeType, CareActionType, LifeStage, InteractionResult
class AttributeEngine:
"""
Core engine for managing DigiPal attributes and care mechanics.
Implements Digimon World 1 inspired attribute calculations with bounds checking.
"""
# Attribute bounds (min, max)
ATTRIBUTE_BOUNDS = {
AttributeType.HP: (1, 999),
AttributeType.MP: (0, 999),
AttributeType.OFFENSE: (0, 999),
AttributeType.DEFENSE: (0, 999),
AttributeType.SPEED: (0, 999),
AttributeType.BRAINS: (0, 999),
AttributeType.DISCIPLINE: (0, 100),
AttributeType.HAPPINESS: (0, 100),
AttributeType.WEIGHT: (1, 99),
AttributeType.CARE_MISTAKES: (0, 999),
AttributeType.ENERGY: (0, 100)
}
# Energy decay rate per hour
ENERGY_DECAY_RATE = 2.0
# Happiness decay rate per hour
HAPPINESS_DECAY_RATE = 1.0
def __init__(self):
"""Initialize the AttributeEngine."""
self.care_actions = self._initialize_care_actions()
def _initialize_care_actions(self) -> Dict[str, CareAction]:
"""Initialize all available care actions with their effects."""
actions = {}
# Training actions
actions["strength_training"] = CareAction(
name="Strength Training",
action_type=CareActionType.TRAIN,
energy_cost=15,
happiness_change=-5,
attribute_modifiers=[
AttributeModifier(AttributeType.OFFENSE, 3),
AttributeModifier(AttributeType.HP, 2),
AttributeModifier(AttributeType.WEIGHT, -1)
],
success_conditions=["energy >= 15"],
failure_effects=[
AttributeModifier(AttributeType.CARE_MISTAKES, 1)
]
)
actions["defense_training"] = CareAction(
name="Defense Training",
action_type=CareActionType.TRAIN,
energy_cost=15,
happiness_change=-5,
attribute_modifiers=[
AttributeModifier(AttributeType.DEFENSE, 3),
AttributeModifier(AttributeType.HP, 2),
AttributeModifier(AttributeType.WEIGHT, -1)
],
success_conditions=["energy >= 15"],
failure_effects=[
AttributeModifier(AttributeType.CARE_MISTAKES, 1)
]
)
actions["speed_training"] = CareAction(
name="Speed Training",
action_type=CareActionType.TRAIN,
energy_cost=12,
happiness_change=-3,
attribute_modifiers=[
AttributeModifier(AttributeType.SPEED, 3),
AttributeModifier(AttributeType.WEIGHT, -2)
],
success_conditions=["energy >= 12"],
failure_effects=[
AttributeModifier(AttributeType.CARE_MISTAKES, 1)
]
)
actions["brain_training"] = CareAction(
name="Brain Training",
action_type=CareActionType.TRAIN,
energy_cost=10,
happiness_change=-2,
attribute_modifiers=[
AttributeModifier(AttributeType.BRAINS, 3),
AttributeModifier(AttributeType.MP, 2)
],
success_conditions=["energy >= 10"],
failure_effects=[
AttributeModifier(AttributeType.CARE_MISTAKES, 1)
]
)
# Advanced training actions
actions["endurance_training"] = CareAction(
name="Endurance Training",
action_type=CareActionType.TRAIN,
energy_cost=20,
happiness_change=-8,
attribute_modifiers=[
AttributeModifier(AttributeType.HP, 4),
AttributeModifier(AttributeType.DEFENSE, 2),
AttributeModifier(AttributeType.WEIGHT, -2)
],
success_conditions=["energy >= 20"],
failure_effects=[
AttributeModifier(AttributeType.CARE_MISTAKES, 1)
]
)
actions["agility_training"] = CareAction(
name="Agility Training",
action_type=CareActionType.TRAIN,
energy_cost=18,
happiness_change=-6,
attribute_modifiers=[
AttributeModifier(AttributeType.SPEED, 4),
AttributeModifier(AttributeType.OFFENSE, 1),
AttributeModifier(AttributeType.WEIGHT, -3)
],
success_conditions=["energy >= 18"],
failure_effects=[
AttributeModifier(AttributeType.CARE_MISTAKES, 1)
]
)
# Feeding actions
actions["meat"] = CareAction(
name="Feed Meat",
action_type=CareActionType.FEED,
energy_cost=0,
happiness_change=5,
attribute_modifiers=[
AttributeModifier(AttributeType.WEIGHT, 2),
AttributeModifier(AttributeType.HP, 1),
AttributeModifier(AttributeType.OFFENSE, 1)
],
success_conditions=[],
failure_effects=[]
)
actions["fish"] = CareAction(
name="Feed Fish",
action_type=CareActionType.FEED,
energy_cost=0,
happiness_change=3,
attribute_modifiers=[
AttributeModifier(AttributeType.WEIGHT, 1),
AttributeModifier(AttributeType.BRAINS, 1),
AttributeModifier(AttributeType.MP, 1)
],
success_conditions=[],
failure_effects=[]
)
actions["vegetables"] = CareAction(
name="Feed Vegetables",
action_type=CareActionType.FEED,
energy_cost=0,
happiness_change=2,
attribute_modifiers=[
AttributeModifier(AttributeType.WEIGHT, 1),
AttributeModifier(AttributeType.DEFENSE, 1)
],
success_conditions=[],
failure_effects=[]
)
# Additional food types for variety
actions["protein_shake"] = CareAction(
name="Feed Protein Shake",
action_type=CareActionType.FEED,
energy_cost=0,
happiness_change=1,
attribute_modifiers=[
AttributeModifier(AttributeType.WEIGHT, 1),
AttributeModifier(AttributeType.OFFENSE, 2),
AttributeModifier(AttributeType.HP, 1)
],
success_conditions=[],
failure_effects=[]
)
actions["energy_drink"] = CareAction(
name="Feed Energy Drink",
action_type=CareActionType.FEED,
energy_cost=-5, # Restores some energy
happiness_change=3,
attribute_modifiers=[
AttributeModifier(AttributeType.SPEED, 1),
AttributeModifier(AttributeType.MP, 1)
],
success_conditions=[],
failure_effects=[]
)
# Care actions
actions["praise"] = CareAction(
name="Praise",
action_type=CareActionType.PRAISE,
energy_cost=0,
happiness_change=10,
attribute_modifiers=[
AttributeModifier(AttributeType.DISCIPLINE, -2)
],
success_conditions=[],
failure_effects=[]
)
actions["scold"] = CareAction(
name="Scold",
action_type=CareActionType.SCOLD,
energy_cost=0,
happiness_change=-8,
attribute_modifiers=[
AttributeModifier(AttributeType.DISCIPLINE, 5)
],
success_conditions=[],
failure_effects=[]
)
actions["rest"] = CareAction(
name="Rest",
action_type=CareActionType.REST,
energy_cost=-30, # Negative cost means it restores energy
happiness_change=3,
attribute_modifiers=[],
success_conditions=[],
failure_effects=[]
)
actions["play"] = CareAction(
name="Play",
action_type=CareActionType.PLAY,
energy_cost=8,
happiness_change=8,
attribute_modifiers=[
AttributeModifier(AttributeType.WEIGHT, -1)
],
success_conditions=["energy >= 8"],
failure_effects=[
AttributeModifier(AttributeType.CARE_MISTAKES, 1)
]
)
return actions
def apply_care_action(self, pet: DigiPal, action_name: str) -> Tuple[bool, Interaction]:
"""
Apply a care action to the DigiPal and return the result.
Args:
pet: The DigiPal to apply the action to
action_name: Name of the care action to apply
Returns:
Tuple of (success, interaction_record)
"""
if action_name not in self.care_actions:
interaction = Interaction(
user_input=action_name,
interpreted_command=action_name,
pet_response=f"Unknown care action: {action_name}",
success=False,
result=InteractionResult.INVALID_COMMAND
)
return False, interaction
action = self.care_actions[action_name]
# Check success conditions
success = self._check_action_conditions(pet, action)
attribute_changes = {}
if success:
# Apply energy cost
if action.energy_cost > 0:
pet.modify_attribute(AttributeType.ENERGY, -action.energy_cost)
attribute_changes["energy"] = -action.energy_cost
elif action.energy_cost < 0: # Rest action restores energy
pet.modify_attribute(AttributeType.ENERGY, -action.energy_cost)
attribute_changes["energy"] = -action.energy_cost
# Apply happiness change
pet.modify_attribute(AttributeType.HAPPINESS, action.happiness_change)
attribute_changes["happiness"] = action.happiness_change
# Apply attribute modifiers
for modifier in action.attribute_modifiers:
if self._check_modifier_conditions(pet, modifier):
pet.modify_attribute(modifier.attribute, modifier.change)
attribute_changes[modifier.attribute.value] = modifier.change
response = f"Successfully performed {action.name}!"
result = InteractionResult.SUCCESS
else:
# Apply failure effects
for modifier in action.failure_effects:
pet.modify_attribute(modifier.attribute, modifier.change)
attribute_changes[modifier.attribute.value] = modifier.change
response = f"Failed to perform {action.name} - insufficient energy or conditions not met"
result = InteractionResult.INSUFFICIENT_ENERGY
# Update last interaction time
pet.last_interaction = datetime.now()
interaction = Interaction(
user_input=action_name,
interpreted_command=action_name,
pet_response=response,
attribute_changes=attribute_changes,
success=success,
result=result
)
# Add to conversation history
pet.conversation_history.append(interaction)
return success, interaction
def _check_action_conditions(self, pet: DigiPal, action: CareAction) -> bool:
"""Check if all conditions for an action are met."""
for condition in action.success_conditions:
if not self._evaluate_condition(pet, condition):
return False
return True
def _check_modifier_conditions(self, pet: DigiPal, modifier: AttributeModifier) -> bool:
"""Check if all conditions for an attribute modifier are met."""
for condition in modifier.conditions:
if not self._evaluate_condition(pet, condition):
return False
return True
def _evaluate_condition(self, pet: DigiPal, condition: str) -> bool:
"""Evaluate a condition string against the pet's current state."""
# Simple condition parser for basic comparisons
if ">=" in condition:
attr_name, value = condition.split(">=")
attr_name = attr_name.strip()
value = int(value.strip())
if attr_name == "energy":
return pet.energy >= value
elif attr_name == "happiness":
return pet.happiness >= value
elif attr_name == "discipline":
return pet.discipline >= value
# Add more conditions as needed
return True # Default to true for unknown conditions
def apply_time_decay(self, pet: DigiPal, hours_passed: float) -> Dict[str, int]:
"""
Apply time-based attribute decay to the DigiPal.
Args:
pet: The DigiPal to apply decay to
hours_passed: Number of hours that have passed
Returns:
Dictionary of attribute changes applied
"""
changes = {}
# Energy decay
energy_decay = int(hours_passed * self.ENERGY_DECAY_RATE)
if energy_decay > 0:
old_energy = pet.energy
pet.modify_attribute(AttributeType.ENERGY, -energy_decay)
changes["energy"] = pet.energy - old_energy
# Happiness decay
happiness_decay = int(hours_passed * self.HAPPINESS_DECAY_RATE)
if happiness_decay > 0:
old_happiness = pet.happiness
pet.modify_attribute(AttributeType.HAPPINESS, -happiness_decay)
changes["happiness"] = pet.happiness - old_happiness
# Weight changes based on energy levels
if pet.energy < 20: # Very low energy causes weight loss
weight_change = -max(1, int(hours_passed * 0.5))
old_weight = pet.weight
pet.modify_attribute(AttributeType.WEIGHT, weight_change)
changes["weight"] = pet.weight - old_weight
return changes
def calculate_care_mistake(self, pet: DigiPal, action_type: CareActionType) -> bool:
"""
Calculate if a care mistake should be recorded based on pet state and action.
Args:
pet: The DigiPal being cared for
action_type: Type of care action being performed
Returns:
True if a care mistake should be recorded
"""
mistake_probability = 0.0
# Higher chance of mistakes when pet is in poor condition
if pet.energy < 20:
mistake_probability += 0.3
if pet.happiness < 20:
mistake_probability += 0.2
if pet.weight < 10 or pet.weight > 80:
mistake_probability += 0.2
# Training when tired is more likely to cause mistakes
if action_type == CareActionType.TRAIN and pet.energy < 30:
mistake_probability += 0.4
# Overfeeding increases mistake chance
if action_type == CareActionType.FEED and pet.weight > 70:
mistake_probability += 0.3
# Excessive scolding increases mistake chance
if action_type == CareActionType.SCOLD and pet.discipline > 80:
mistake_probability += 0.25
# Random chance
return random.random() < mistake_probability
def get_care_quality_assessment(self, pet: DigiPal) -> Dict[str, str]:
"""
Assess the overall care quality based on pet's current state.
Args:
pet: The DigiPal to assess
Returns:
Dictionary with care quality metrics
"""
assessment = {}
# Energy assessment
if pet.energy >= 80:
assessment["energy"] = "excellent"
elif pet.energy >= 60:
assessment["energy"] = "good"
elif pet.energy >= 40:
assessment["energy"] = "fair"
elif pet.energy >= 20:
assessment["energy"] = "poor"
else:
assessment["energy"] = "critical"
# Happiness assessment
if pet.happiness >= 80:
assessment["happiness"] = "very_happy"
elif pet.happiness >= 60:
assessment["happiness"] = "happy"
elif pet.happiness >= 40:
assessment["happiness"] = "neutral"
elif pet.happiness >= 20:
assessment["happiness"] = "sad"
else:
assessment["happiness"] = "very_sad"
# Weight assessment
if 15 <= pet.weight <= 35:
assessment["weight"] = "healthy"
elif 10 <= pet.weight < 15 or 35 < pet.weight <= 50:
assessment["weight"] = "slightly_off"
elif 5 <= pet.weight < 10 or 50 < pet.weight <= 70:
assessment["weight"] = "concerning"
else:
assessment["weight"] = "unhealthy"
# Discipline assessment
if 40 <= pet.discipline <= 70:
assessment["discipline"] = "balanced"
elif pet.discipline < 40:
assessment["discipline"] = "undisciplined"
else:
assessment["discipline"] = "over_disciplined"
# Care mistakes assessment
if pet.care_mistakes == 0:
assessment["care_quality"] = "perfect"
elif pet.care_mistakes <= 3:
assessment["care_quality"] = "excellent"
elif pet.care_mistakes <= 7:
assessment["care_quality"] = "good"
elif pet.care_mistakes <= 15:
assessment["care_quality"] = "fair"
else:
assessment["care_quality"] = "poor"
return assessment
def get_attribute_bounds(self, attribute: AttributeType) -> Tuple[int, int]:
"""Get the min and max bounds for an attribute."""
return self.ATTRIBUTE_BOUNDS.get(attribute, (0, 999))
def validate_attribute_value(self, attribute: AttributeType, value: int) -> int:
"""Validate and clamp an attribute value to its bounds."""
min_val, max_val = self.get_attribute_bounds(attribute)
return max(min_val, min(max_val, value))
def get_available_actions(self, pet: DigiPal) -> List[str]:
"""Get list of care actions available for the current pet state."""
available = []
for action_name, action in self.care_actions.items():
# Check if pet has enough energy for the action
if action.energy_cost > 0 and pet.energy < action.energy_cost:
continue
# Check life stage appropriateness
if self._is_action_appropriate_for_stage(action, pet.life_stage):
available.append(action_name)
return available
def _is_action_appropriate_for_stage(self, action: CareAction, stage: LifeStage) -> bool:
"""Check if an action is appropriate for the current life stage."""
# All stages can do basic care
basic_actions = {CareActionType.FEED, CareActionType.PRAISE, CareActionType.SCOLD, CareActionType.REST}
if action.action_type in basic_actions:
return True
# Training and play require child stage or higher
if action.action_type in {CareActionType.TRAIN, CareActionType.PLAY}:
return stage in {LifeStage.CHILD, LifeStage.TEEN, LifeStage.YOUNG_ADULT, LifeStage.ADULT, LifeStage.ELDERLY}
return True
def get_care_action(self, action_name: str) -> Optional[CareAction]:
"""Get a care action by name."""
return self.care_actions.get(action_name)
def get_all_care_actions(self) -> Dict[str, CareAction]:
"""Get all available care actions."""
return self.care_actions.copy()