Files
taletorrent/taletorrent/models.py
fedir 0b37f329c7 refactor: major overhaul of game engine architecture
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.
2026-01-27 10:12:28 +01:00

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()