Complete modular refactoring of taletorrent game engine: ## Core Architecture Changes - Split monolithic main.py (343 lines) into 6 focused modules - Implement offensive error propagation with context wrapping - Add JSON configuration system with default prompts and settings - Modernize type hints and validation ## New Modules - errors.py: Custom exceptions (GameError, AIError, ConfigError, etc.) with wrap_error() - models.py: Data classes (World, Character, Turn, Item) with validation - config.py: JSON configuration management with defaults and validation - prompts.py: Template-based prompt management extracted from code - ai_client.py: OpenAI wrapper with retry logic and error propagation - game_engine.py: Core game logic with explicit dependency injection ## Key Features - Error propagation: API errors → 'Failed to reach provider: 403' → 'Failed to generate DM turn: ...' - Configuration over code: All prompts moved to JSON config files - Retry logic: Exponential backoff for API failures - Input validation: Strict validation for all public functions - Logging: Optional stderr logging with --enable-logging flag - Modern Python: list[str], str | None syntax, immutable returns ## CLI Enhancements - play command: Interactive game loop with character creation - validate-config: Configuration validation command - info command: Shows both API and game configuration - Interactive setup wizard for API credentials ## Breaking Changes - Removed main.py (no backward compatibility) - Function signatures changed to use explicit dependencies - BSM renamed to bsm (Python naming convention) - All prompts now configurable via JSON files ## Testing - All imports work correctly - CLI commands functional (config, info, validate-config, play) - Configuration file creation and validation working - Ruff linting passes with auto-fixes - Type checking shows warnings but no critical errors This refactoring transforms the codebase from a monolithic structure to a modular, maintainable architecture ready for extension and code review.
127 lines
3.8 KiB
Python
127 lines
3.8 KiB
Python
"""Data models for taletorrent game engine."""
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
@dataclass
|
|
class World:
|
|
"""Represents the game world setting and context."""
|
|
|
|
setting: str
|
|
writing_style: str
|
|
plot: str
|
|
|
|
def validate(self) -> None:
|
|
"""Validate world data."""
|
|
if not self.setting.strip():
|
|
raise ValueError("World setting cannot be empty")
|
|
if not self.writing_style.strip():
|
|
raise ValueError("Writing style cannot be empty")
|
|
if not self.plot.strip():
|
|
raise ValueError("Plot cannot be empty")
|
|
|
|
|
|
@dataclass
|
|
class Turn:
|
|
"""Represents a single turn/message in the game chat."""
|
|
|
|
character_name: str
|
|
text: str
|
|
|
|
def validate(self) -> None:
|
|
"""Validate turn data."""
|
|
if not self.character_name.strip():
|
|
raise ValueError("Character name cannot be empty")
|
|
if not self.text.strip():
|
|
raise ValueError("Turn text cannot be empty")
|
|
|
|
|
|
@dataclass
|
|
class Item:
|
|
"""Represents an inventory item."""
|
|
|
|
name: str
|
|
description: str
|
|
|
|
def validate(self) -> None:
|
|
"""Validate item data."""
|
|
if not self.name.strip():
|
|
raise ValueError("Item name cannot be empty")
|
|
if not self.description.strip():
|
|
raise ValueError("Item description cannot be empty")
|
|
|
|
|
|
@dataclass
|
|
class Character:
|
|
"""Represents a game character."""
|
|
|
|
name: str
|
|
internal: str # Internal thoughts, motivations, personality
|
|
external: str # External description, appearance
|
|
bsm: str # Body State Model (physical state)
|
|
position: str # Position in the scene
|
|
inventory: list[tuple[int, Item]] = field(default_factory=list)
|
|
|
|
def validate(self) -> None:
|
|
"""Validate character data."""
|
|
if not self.name.strip():
|
|
raise ValueError("Character name cannot be empty")
|
|
if not self.internal.strip():
|
|
raise ValueError("Character internal description cannot be empty")
|
|
if not self.external.strip():
|
|
raise ValueError("Character external description cannot be empty")
|
|
if not self.bsm.strip():
|
|
raise ValueError("Character BSM cannot be empty")
|
|
if not self.position.strip():
|
|
raise ValueError("Character position cannot be empty")
|
|
|
|
# Validate inventory items
|
|
for quantity, item in self.inventory:
|
|
if quantity <= 0:
|
|
raise ValueError(f"Item quantity must be positive: {item.name}")
|
|
item.validate()
|
|
|
|
def get_inventory_description(self) -> str:
|
|
"""Get formatted inventory description."""
|
|
if not self.inventory:
|
|
return "Empty"
|
|
|
|
lines = []
|
|
for quantity, item in self.inventory:
|
|
lines.append(f"* {item.name}")
|
|
lines.append(f" + {item.description}")
|
|
lines.append(f" + Quantity: {quantity}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def validate_chat_history(chat: list[Turn]) -> None:
|
|
"""Validate chat history consistency."""
|
|
if not chat:
|
|
return
|
|
|
|
# Check for consecutive turns by same character
|
|
for i in range(1, len(chat)):
|
|
if chat[i].character_name == chat[i - 1].character_name:
|
|
raise ValueError(
|
|
f"Consecutive turns by same character '{chat[i].character_name}' "
|
|
f"at positions {i - 1} and {i}"
|
|
)
|
|
|
|
# Validate all turns
|
|
for turn in chat:
|
|
turn.validate()
|
|
|
|
|
|
def validate_characters(characters: list[Character]) -> None:
|
|
"""Validate character list."""
|
|
if not characters:
|
|
raise ValueError("Character list cannot be empty")
|
|
|
|
# Check for duplicate names
|
|
names = set()
|
|
for character in characters:
|
|
if character.name in names:
|
|
raise ValueError(f"Duplicate character name: {character.name}")
|
|
names.add(character.name)
|
|
character.validate()
|