first commit

This commit is contained in:
2025-11-24 23:10:55 -06:00
commit 8315fa51c9
279 changed files with 74600 additions and 0 deletions

View File

@@ -0,0 +1,351 @@
"""
Item validation service for AI-granted items.
This module validates and resolves items that the AI grants to players during
gameplay, ensuring they meet character requirements and game balance rules.
"""
import uuid
from pathlib import Path
from typing import Optional
import structlog
import yaml
from app.models.items import Item
from app.models.enums import ItemType
from app.models.character import Character
from app.ai.response_parser import ItemGrant
logger = structlog.get_logger(__name__)
class ItemValidationError(Exception):
"""
Exception raised when an item fails validation.
Attributes:
message: Human-readable error message
item_grant: The ItemGrant that failed validation
reason: Machine-readable reason code
"""
def __init__(self, message: str, item_grant: ItemGrant, reason: str):
super().__init__(message)
self.message = message
self.item_grant = item_grant
self.reason = reason
class ItemValidator:
"""
Validates and resolves items granted by the AI.
This service:
1. Resolves item references (by ID or creates generic items)
2. Validates items against character requirements
3. Logs validation failures for review
"""
# Map of generic item type strings to ItemType enums
TYPE_MAP = {
"weapon": ItemType.WEAPON,
"armor": ItemType.ARMOR,
"consumable": ItemType.CONSUMABLE,
"quest_item": ItemType.QUEST_ITEM,
}
def __init__(self, data_path: Optional[Path] = None):
"""
Initialize the item validator.
Args:
data_path: Path to game data directory. Defaults to app/data/
"""
if data_path is None:
# Default to api/app/data/
data_path = Path(__file__).parent.parent / "data"
self.data_path = data_path
self._item_registry: dict[str, dict] = {}
self._generic_templates: dict[str, dict] = {}
self._load_data()
logger.info(
"ItemValidator initialized",
items_loaded=len(self._item_registry),
generic_templates_loaded=len(self._generic_templates)
)
def _load_data(self) -> None:
"""Load item data from YAML files."""
# Load main item registry if it exists
items_file = self.data_path / "items.yaml"
if items_file.exists():
with open(items_file) as f:
data = yaml.safe_load(f) or {}
self._item_registry = data.get("items", {})
# Load generic item templates
generic_file = self.data_path / "generic_items.yaml"
if generic_file.exists():
with open(generic_file) as f:
data = yaml.safe_load(f) or {}
self._generic_templates = data.get("templates", {})
def resolve_item(self, item_grant: ItemGrant) -> Item:
"""
Resolve an ItemGrant to an actual Item instance.
For existing items (by item_id), looks up from item registry.
For generic items (by name/type), creates a new Item.
Args:
item_grant: The ItemGrant from AI response
Returns:
Resolved Item instance
Raises:
ItemValidationError: If item cannot be resolved
"""
if item_grant.is_existing_item():
return self._resolve_existing_item(item_grant)
elif item_grant.is_generic_item():
return self._create_generic_item(item_grant)
else:
raise ItemValidationError(
"ItemGrant has neither item_id nor name",
item_grant,
"INVALID_ITEM_GRANT"
)
def _resolve_existing_item(self, item_grant: ItemGrant) -> Item:
"""
Look up an existing item by ID.
Args:
item_grant: ItemGrant with item_id set
Returns:
Item instance from registry
Raises:
ItemValidationError: If item not found
"""
item_id = item_grant.item_id
if item_id not in self._item_registry:
logger.warning(
"Item not found in registry",
item_id=item_id
)
raise ItemValidationError(
f"Unknown item_id: {item_id}",
item_grant,
"ITEM_NOT_FOUND"
)
item_data = self._item_registry[item_id]
# Convert to Item instance
return Item.from_dict({
"item_id": item_id,
**item_data
})
def _create_generic_item(self, item_grant: ItemGrant) -> Item:
"""
Create a generic item from AI-provided details.
Generic items are simple items with no special stats,
suitable for mundane objects like torches, food, etc.
Args:
item_grant: ItemGrant with name, type, description
Returns:
New Item instance
Raises:
ItemValidationError: If item type is invalid
"""
# Validate item type
item_type_str = (item_grant.item_type or "consumable").lower()
if item_type_str not in self.TYPE_MAP:
logger.warning(
"Invalid item type from AI",
item_type=item_type_str,
item_name=item_grant.name
)
# Default to consumable for unknown types
item_type_str = "consumable"
item_type = self.TYPE_MAP[item_type_str]
# Generate unique ID for this item instance
item_id = f"generic_{uuid.uuid4().hex[:8]}"
# Check if we have a template for this item name
template = self._find_template(item_grant.name or "")
if template:
# Use template values as defaults
return Item(
item_id=item_id,
name=item_grant.name or template.get("name", "Unknown Item"),
item_type=item_type,
description=item_grant.description or template.get("description", ""),
value=item_grant.value or template.get("value", 0),
is_tradeable=template.get("is_tradeable", True),
required_level=template.get("required_level", 1),
)
else:
# Create with provided values only
return Item(
item_id=item_id,
name=item_grant.name or "Unknown Item",
item_type=item_type,
description=item_grant.description or "A simple item.",
value=item_grant.value,
is_tradeable=True,
required_level=1,
)
def _find_template(self, item_name: str) -> Optional[dict]:
"""
Find a generic item template by name.
Uses case-insensitive partial matching.
Args:
item_name: Name of the item to find
Returns:
Template dict or None if not found
"""
name_lower = item_name.lower()
# Exact match first
if name_lower in self._generic_templates:
return self._generic_templates[name_lower]
# Partial match
for template_name, template in self._generic_templates.items():
if template_name in name_lower or name_lower in template_name:
return template
return None
def validate_item_for_character(
self,
item: Item,
character: Character
) -> tuple[bool, Optional[str]]:
"""
Validate that a character can receive an item.
Checks:
- Level requirements
- Class restrictions
Args:
item: The Item to validate
character: The Character to receive the item
Returns:
Tuple of (is_valid, error_message)
"""
# Check level requirement
if item.required_level > character.level:
error_msg = (
f"Item '{item.name}' requires level {item.required_level}, "
f"but character is level {character.level}"
)
logger.warning(
"Item validation failed: level requirement",
item_name=item.name,
required_level=item.required_level,
character_level=character.level,
character_name=character.name
)
return False, error_msg
# Check class restriction
if item.required_class:
character_class = character.player_class.class_id
if item.required_class.lower() != character_class.lower():
error_msg = (
f"Item '{item.name}' requires class {item.required_class}, "
f"but character is {character_class}"
)
logger.warning(
"Item validation failed: class restriction",
item_name=item.name,
required_class=item.required_class,
character_class=character_class,
character_name=character.name
)
return False, error_msg
return True, None
def validate_and_resolve_item(
self,
item_grant: ItemGrant,
character: Character
) -> tuple[Optional[Item], Optional[str]]:
"""
Resolve an item grant and validate it for a character.
This is the main entry point for processing AI-granted items.
Args:
item_grant: The ItemGrant from AI response
character: The Character to receive the item
Returns:
Tuple of (Item if valid else None, error_message if invalid else None)
"""
try:
# Resolve the item
item = self.resolve_item(item_grant)
# Validate for character
is_valid, error_msg = self.validate_item_for_character(item, character)
if not is_valid:
return None, error_msg
logger.info(
"Item validated successfully",
item_name=item.name,
item_id=item.item_id,
character_name=character.name
)
return item, None
except ItemValidationError as e:
logger.warning(
"Item resolution failed",
error=e.message,
reason=e.reason
)
return None, e.message
# Global instance for convenience
_validator_instance: Optional[ItemValidator] = None
def get_item_validator() -> ItemValidator:
"""
Get or create the global ItemValidator instance.
Returns:
ItemValidator singleton instance
"""
global _validator_instance
if _validator_instance is None:
_validator_instance = ItemValidator()
return _validator_instance