fix: #831
This commit is contained in:
233
evennia/timmy_world/game.py
Normal file
233
evennia/timmy_world/game.py
Normal 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
52
tests/test_world_dir.py
Normal 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()
|
||||
Reference in New Issue
Block a user