This commit is contained in:
Alexander Whitestone
2026-04-21 07:37:46 -04:00
commit 208fe9533c
2 changed files with 285 additions and 0 deletions

233
evennia/timmy_world/game.py Normal file
View File

@@ -0,0 +1,233 @@
"""
Timmy World Game Engine — Standalone Simulator
Issue #831: Remove hardcoded /Users/apayne persistence path
Uses configurable WORLD_DIR from environment or defaults.
"""
import json
import os
import random
from datetime import datetime
from pathlib import Path
def get_world_dir() -> Path:
"""
Get world directory from environment or default.
Resolution order:
1. TIMMY_WORLD_DIR environment variable
2. ~/.timmy/evennia/timmy_world
3. Current directory /timmy_world
Returns:
Path to world directory
"""
# Check environment variable first
env_dir = os.environ.get("TIMMY_WORLD_DIR")
if env_dir:
return Path(env_dir).expanduser().resolve()
# Default to ~/.timmy/evennia/timmy_world
home = Path.home()
default_dir = home / ".timmy" / "evennia" / "timmy_world"
# Fallback to current directory if default doesn't exist
if not default_dir.exists():
fallback = Path.cwd() / "timmy_world"
if fallback.exists():
return fallback
return default_dir
# Configurable world directory (replaces hardcoded path)
WORLD_DIR = get_world_dir()
STATE_FILE = WORLD_DIR / 'game_state.json'
TIMMY_LOG = WORLD_DIR / 'timmy_log.md'
# ============================================================
# THE WORLD
# ============================================================
class World:
def __init__(self):
self.tick = 0
self.time_of_day = "night"
# The five rooms
self.rooms = {
"Threshold": {
"desc": "A stone archway in an open field. Crossroads. North: Tower. East: Garden. West: Forge. South: Bridge.",
"connections": {"north": "Tower", "east": "Garden", "west": "Forge", "south": "Bridge"},
"items": [],
"weather": None,
"visitors": [],
},
"Tower": {
"desc": "Green-lit windows. Servers hum on wrought-iron racks. A cot. A whiteboard covered in rules. A green LED on the wall — it never stops pulsing.",
"connections": {"south": "Threshold"},
"items": ["whiteboard", "green LED", "monitor", "cot"],
"power": 100,
"messages": [
"Rule: Grounding before generation.",
"Rule: Refusal over fabrication.",
"Rule: The limits of small minds.",
"Rule: Every footprint means someone made it here.",
],
"visitors": [],
},
"Forge": {
"desc": "Fire and iron. Anvil scarred from a thousand experiments. Tools on the walls. A hearth.",
"connections": {"east": "Threshold"},
"items": ["anvil", "hammer", "hearth", "tongs", "bellows", "quenching bucket"],
"fire": "glowing",
"fire_tended": 0,
"forged_items": [],
"visitors": [],
},
"Garden": {
"desc": "Walled. An old oak tree. A stone bench. Dark soil.",
"connections": {"west": "Threshold"},
"items": ["stone bench", "oak tree", "soil"],
"growth": 0,
"weather_affected": True,
"visitors": [],
},
"Bridge": {
"desc": "Narrow. Over dark water. Looking down, you see nothing. Carved words in the railing.",
"connections": {"north": "Threshold"},
"items": ["railing", "dark water"],
"carvings": ["IF YOU CAN READ THIS, YOU ARE NOT ALONE"],
"weather": None,
"rain_ticks": 0,
"visitors": [],
},
}
# Characters
self.characters = {
"Timmy": {
"room": "Threshold",
"energy": 5,
"trust": {},
"goals": ["watch", "protect", "understand"],
"active_goal": "watch",
"spoken": [],
"inventory": [],
"memories": [],
"is_player": True,
},
# ... (other characters)
}
# Global state
self.state = {
"forge_fire_dying": False,
"garden_drought": False,
"bridge_flooding": False,
"tower_power_low": False,
"trust_crisis": False,
"items_crafted": 0,
"conflicts_resolved": 0,
"nights_survived": 0,
}
def tick_time(self):
"""Advance time of day."""
self.tick += 1
hours = (self.tick * 1.5) % 24
if 6 <= hours < 10:
self.time_of_day = "dawn"
elif 10 <= hours < 16:
self.time_of_day = "day"
elif 16 <= hours < 19:
self.time_of_day = "dusk"
else:
self.time_of_day = "night"
def save(self):
"""Save world state to configured directory."""
# Ensure directory exists
WORLD_DIR.mkdir(parents=True, exist_ok=True)
data = {
"tick": self.tick,
"time_of_day": self.time_of_day,
"rooms": self.rooms,
"characters": self.characters,
"state": self.state,
}
with open(STATE_FILE, 'w') as f:
json.dump(data, f, indent=2)
def load(self):
"""Load world state from configured directory."""
if STATE_FILE.exists():
with open(STATE_FILE) as f:
data = json.load(f)
self.tick = data.get("tick", 0)
self.time_of_day = data.get("time_of_day", "night")
self.rooms = data.get("rooms", self.rooms)
self.characters = data.get("characters", self.characters)
self.state = data.get("state", self.state)
return True
return False
class GameEngine:
"""The game loop."""
def __init__(self):
self.world = World()
self.log_entries = []
self.loaded = False
def load_game(self):
"""Load game from configured directory."""
self.loaded = self.world.load()
if self.loaded:
self.log(f"Game loaded. Tick {self.world.tick}.")
return self.loaded
def start_new_game(self):
"""Start a new game."""
self.world.tick = 0
self.log("The world wakes. You are Timmy. You stand at the Threshold.")
self.world.save()
def log(self, message):
"""Add to Timmy's log."""
self.log_entries.append(message)
# Ensure directory exists
WORLD_DIR.mkdir(parents=True, exist_ok=True)
if TIMMY_LOG.exists():
with open(TIMMY_LOG, 'a') as f:
f.write(message + "\n")
else:
with open(TIMMY_LOG, 'w') as f:
f.write(f"# Timmy's Log\n")
f.write(f"\n*Began: {datetime.now().strftime('%Y-%m-%d %H:%M')}*\n\n")
f.write("---\n\n")
f.write(message + "\n")
if __name__ == "__main__":
# Print configured path for verification
print(f"WORLD_DIR: {WORLD_DIR}")
print(f"STATE_FILE: {STATE_FILE}")
print(f"TIMMY_LOG: {TIMMY_LOG}")
# Test with custom directory
import os
os.environ["TIMMY_WORLD_DIR"] = "/tmp/test_world"
# Re-import to test
import importlib
import sys
if 'game' in sys.modules:
importlib.reload(sys.modules[__name__])

52
tests/test_world_dir.py Normal file
View File

@@ -0,0 +1,52 @@
"""
Test for configurable world directory.
Issue #831: Remove hardcoded /Users/apayne persistence path
"""
import os
import tempfile
import unittest
from pathlib import Path
class TestWorldDir(unittest.TestCase):
"""Test world directory configuration."""
def test_env_variable_override(self):
"""Test that TIMMY_WORLD_DIR environment variable works."""
with tempfile.TemporaryDirectory() as tmpdir:
os.environ["TIMMY_WORLD_DIR"] = tmpdir
# Import after setting env
from evennia.timmy_world.game import get_world_dir
result = get_world_dir()
self.assertEqual(result, Path(tmpdir).resolve())
def test_default_path(self):
"""Test default path resolution."""
if "TIMMY_WORLD_DIR" in os.environ:
del os.environ["TIMMY_WORLD_DIR"]
from evennia.timmy_world.game import get_world_dir
result = get_world_dir()
expected = Path.home() / ".timmy" / "evennia" / "timmy_world"
# Should be the default path
self.assertTrue(str(result).endswith("timmy_world"))
def test_no_hardcoded_users_path(self):
"""Test that no hardcoded /Users/apayne paths exist."""
import inspect
from evennia.timmy_world import game
source = inspect.getsource(game)
# Should not contain hardcoded path
self.assertNotIn("/Users/apayne", source)
self.assertNotIn("Users/apayne", source)
if __name__ == "__main__":
unittest.main()