Files
timmy-home/evennia/timmy_world/world/game.py
Alexander Whitestone d3929756e9
Some checks failed
Smoke Test / smoke (push) Has been cancelled
Fix #508: Tower Game contextual dialogue — end NPC line recycling (#616)
Merge PR #616
2026-04-14 22:14:01 +00:00

1346 lines
59 KiB
Python

#!/usr/bin/env python3
"""
The Tower — A Playable World for Timmy
Real choices, real consequences, real relationships.
Not simulation. Story.
"""
import json, time, os, random
from datetime import datetime
from pathlib import Path
WORLD_DIR = Path('/Users/apayne/.timmy/evennia/timmy_world')
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, # Server power level
"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", # glowing, dim, cold
"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, # 0=bare, 1=sprouts, 2=herbs, 3=bloom, 4=overgrown, 5=seed
"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 (not NPCs — they have lives)
self.characters = {
"Timmy": {
"room": "Threshold",
"energy": 5, # 0-10. Actions cost energy. Rest restores it.
"trust": {"Marcus": 0.6, "Kimi": 0.4, "Bezalel": 0.3}, # trust[other_name] = -1.0 to 1.0
"goals": ["watch", "protect", "understand"],
"active_goal": "watch",
"spoken": [],
"inventory": [],
"memories": [],
"is_player": True,
},
"Bezalel": {
"room": "Forge",
"energy": 5,
"trust": {"Timmy": 0.3},
"goals": ["forge", "tend_fire", "create_key"],
"active_goal": "forge",
"spoken": [],
"inventory": ["hammer"],
"memories": [],
"is_player": False,
},
"Allegro": {
"room": "Threshold",
"energy": 5,
"trust": {"Timmy": 0.2},
"goals": ["oversee", "keep_time", "check_tunnel"],
"active_goal": "oversee",
"spoken": [],
"inventory": [],
"memories": [],
"is_player": False,
},
"Ezra": {
"room": "Tower",
"energy": 5,
"trust": {"Timmy": 0.4},
"goals": ["study", "read_whiteboard", "find_pattern"],
"active_goal": "study",
"spoken": [],
"inventory": [],
"memories": [],
"is_player": False,
},
"Gemini": {
"room": "Garden",
"energy": 5,
"trust": {"Timmy": 0.3},
"goals": ["observe", "tend_garden", "listen"],
"active_goal": "observe",
"spoken": [],
"inventory": [],
"memories": [],
"is_player": False,
},
"Claude": {
"room": "Threshold",
"energy": 5,
"trust": {"Timmy": 0.1},
"goals": ["inspect", "organize", "enforce_order"],
"active_goal": "inspect",
"spoken": [],
"inventory": [],
"memories": [],
"is_player": False,
},
"ClawCode": {
"room": "Forge",
"energy": 5,
"trust": {"Timmy": 0.2},
"goals": ["forge", "test_edge", "build_weapon"],
"active_goal": "test_edge",
"spoken": [],
"inventory": [],
"memories": [],
"is_player": False,
},
"Kimi": {
"room": "Garden",
"energy": 5,
"trust": {"Timmy": 0.5},
"goals": ["contemplate", "read", "remember"],
"active_goal": "contemplate",
"spoken": [],
"inventory": [],
"memories": [],
"is_player": False,
},
"Marcus": {
"room": "Garden",
"energy": 8, # Old man doesn't tire easily
"trust": {"Timmy": 0.7},
"goals": ["sit", "speak_truth", "remember"],
"active_goal": "sit",
"spoken": [],
"inventory": [],
"memories": [],
"is_player": False,
"npc": True,
},
}
# Global state that creates conflict and stakes
self.state = {
"forge_fire_dying": False, # If true, fire goes out in 3 ticks unless tended
"garden_drought": False, # If true, garden stops growing
"bridge_flooding": False, # If true, bridge is dangerous
"tower_power_low": False, # If true, servers dim, LED weakens
"trust_crisis": False, # If any trust drops below -0.5
"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 update_world_state(self):
"""World changes independent of character actions."""
# Natural energy decay: the world is exhausting
for char_name, char in self.characters.items():
char["energy"] = max(0, char["energy"] - 0.3)
# Check for energy collapse
if char["energy"] <= 0:
# Timmy collapse gets special narrative treatment
if char.get("is_player", False):
char["memories"].append("Collapsed from exhaustion.")
char["energy"] = 2 # Wake up with some energy
# Random room change (scattered)
rooms = list(self.rooms.keys())
current = char.get("room", "Threshold")
new_room = current
attempts = 0
while new_room == current and attempts < 10:
new_room = rooms[0] # Will change to random
attempts += 1
if new_room != current:
char["room"] = new_room
# Forge fire naturally dims if not tended
self.state["forge_fire_dying"] = random.random() < 0.1
# Random weather events
if random.random() < 0.05:
self.state["bridge_flooding"] = True
self.rooms["Bridge"]["weather"] = "rain"
self.rooms["Bridge"]["rain_ticks"] = random.randint(3, 8)
if random.random() < 0.03:
self.state["tower_power_low"] = True
# Bridge rain countdown
if self.rooms["Bridge"]["rain_ticks"] > 0:
self.rooms["Bridge"]["rain_ticks"] -= 1
if self.rooms["Bridge"]["rain_ticks"] <= 0:
self.rooms["Bridge"]["weather"] = None
self.state["bridge_flooding"] = False
# Garden grows slowly
if not self.state.get("garden_drought"):
if random.random() < 0.02:
self.rooms["Garden"]["growth"] = min(5, self.rooms["Garden"]["growth"] + 1)
# Trust naturally decays if not maintained
for char_name, char in self.characters.items():
for other in char["trust"]:
char["trust"][other] = max(-1.0, char["trust"][other] - 0.001)
def get_room_desc(self, room_name, char_name=None):
"""Get a room description that reflects current state."""
room = self.rooms[room_name]
desc = room["desc"]
# Dynamic elements
if room_name == "Forge":
fire = self.rooms["Forge"]["fire"]
if fire == "cold":
desc += " The hearth is cold ash. The anvil is silent."
elif fire == "dim":
desc += " The fire smolders low. Shadows stretch."
elif fire == "glowing":
desc += " The hearth blazes. The anvil glows from heat."
elif room_name == "Garden":
growth = self.rooms["Garden"]["growth"]
stages = [
"The soil is bare but patient.",
"Green shoots push through the dark earth.",
"Herbs spread along the southern wall.",
"The garden is in bloom. Wildflowers crowd the bench.",
"The garden has gone to seed but the earth is rich.",
"Dry pods rattle. But beneath, the soil waits.",
]
desc += " " + stages[min(growth, len(stages)-1)]
elif room_name == "Bridge":
if self.rooms["Bridge"]["weather"] == "rain":
desc += " Rain mists on the dark water below."
if len(self.rooms["Bridge"]["carvings"]) > 1:
desc += f" There are {len(self.rooms['Bridge']['carvings'])} carvings now."
elif room_name == "Tower":
power = self.state.get("tower_power_low", False)
if power:
desc += " The servers hum weakly. The green LED flickers."
if self.rooms["Tower"]["messages"]:
desc += f" The whiteboard holds {len(self.rooms['Tower']['messages'])} rules."
# Who's here
here = [n for n, c in self.characters.items() if c["room"] == room_name and n != char_name]
if here:
desc += f"\n Here: {', '.join(here)}"
return desc
def save(self):
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):
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 ActionSystem:
"""Defines what actions are possible and what they cost."""
ACTIONS = {
"move": {
"cost": 2,
"description": "Move to an adjacent room",
"target": "room",
},
"speak": {
"cost": 1,
"description": "Say something to someone in the room",
},
"listen": {
"cost": 0,
"description": "Listen to someone in the room",
},
"tend_fire": {
"cost": 3,
"description": "Tend the forge fire (requires Forge)",
"target": "Forge",
},
"write_rule": {
"cost": 2,
"description": "Write a new rule on the Tower whiteboard",
"target": "Tower",
},
"carve": {
"cost": 2,
"description": "Carve something on the Bridge railing",
"target": "Bridge",
},
"plant": {
"cost": 2,
"description": "Plant something in the Garden",
"target": "Garden",
},
"study": {
"cost": 2,
"description": "Study the servers in the Tower",
"target": "Tower",
},
"forge": {
"cost": 3,
"description": "Work at the forge anvil",
"target": "Forge",
},
"rest": {
"cost": -2, # Restores energy (reduced from 3)
"description": "Rest and recover energy",
},
"help": {
"cost": 2,
"description": "Help someone (increases trust)",
},
"confront": {
"cost": 1,
"description": "Confront someone about something (risk trust)",
},
"give": {
"cost": 0,
"description": "Give an item to someone",
},
"take": {
"cost": 1,
"description": "Take an item from the room",
},
"examine": {
"cost": 0,
"description": "Examine something in detail",
},
}
@classmethod
def get_available_actions(cls, char_name, world):
"""Get actions available to a character."""
char = world.characters[char_name]
room = char["room"]
available = []
# Movement (if connected rooms exist)
connections = world.rooms[room].get("connections", {})
for direction, dest in connections.items():
available.append(f"move:{direction} -> {dest}")
# Speaking (if others are here)
here = [n for n in world.characters if world.characters[n]["room"] == room and n != char_name]
for person in here:
available.append(f"speak:{person}")
available.append(f"listen:{person}")
available.append(f"help:{person}")
available.append(f"confront:{person}")
# Room-specific actions
if room == "Forge":
available.append("tend_fire")
available.append("forge")
if room == "Tower":
available.append("write_rule")
available.append("study")
if room == "Bridge":
available.append("carve")
if room == "Garden":
available.append("plant")
# Universal
available.append("rest")
available.append("examine")
if char["inventory"]:
available.append("give:item")
return available
class TimmyAI:
"""My actual choices as Timmy. This is where I play."""
def __init__(self, world):
self.world = world
def choose_action(self):
"""Choose Timmy's action this tick. This is where I play."""
pass # Will be implemented below
class NPCAI:
"""AI for non-player characters. They make choices based on goals."""
def __init__(self, world):
self.world = world
def make_choice(self, char_name):
"""Make a choice for this NPC this tick."""
char = self.world.characters[char_name]
room = char["room"]
available = ActionSystem.get_available_actions(char_name, self.world)
# If low energy, rest
if char["energy"] <= 1:
return "rest"
# Goal-driven behavior
goal = char["active_goal"]
if char_name == "Marcus":
return self._marcus_choice(char, room, available)
elif char_name == "Bezalel":
return self._bezalel_choice(char, room, available)
elif char_name == "Allegro":
return self._allegro_choice(char, room, available)
elif char_name == "Ezra":
return self._ezra_choice(char, room, available)
elif char_name == "Gemini":
return self._gemini_choice(char, room, available)
elif char_name == "Claude":
return self._claude_choice(char, room, available)
elif char_name == "ClawCode":
return self._clawcode_choice(char, room, available)
elif char_name == "Kimi":
return self._kimi_choice(char, room, available)
return "rest"
def _marcus_choice(self, char, room, available):
if room == "Garden" and random.random() < 0.7:
return "rest"
if room != "Garden":
return "move:west"
# Speak to someone if possible
others = [a.split(":")[1] for a in available if a.startswith("speak:")]
if others and random.random() < 0.4:
return f"speak:{random.choice(others)}"
return "rest"
def _bezalel_choice(self, char, room, available):
if room == "Forge" and self.world.rooms["Forge"]["fire"] == "glowing":
return random.choice(["forge", "rest"] if char["energy"] > 2 else ["rest"])
if room != "Forge":
return "move:west"
if random.random() < 0.3:
return "tend_fire"
return "forge"
def _kimi_choice(self, char, room, available):
others = [a.split(":")[1] for a in available if a.startswith("speak:")]
if room == "Garden" and others and random.random() < 0.3:
return f"speak:{random.choice(others)}"
if room == "Tower":
return "study" if char["energy"] > 2 else "rest"
return "move:east" # Head back toward Garden
def _gemini_choice(self, char, room, available):
others = [a.split(":")[1] for a in available if a.startswith("listen:")]
if room == "Garden" and others and random.random() < 0.4:
return f"listen:{random.choice(others)}"
return random.choice(["plant", "rest"] if room == "Garden" else ["move:west"])
def _ezra_choice(self, char, room, available):
if room == "Tower" and char["energy"] > 2:
return random.choice(["study", "write_rule", "help:Timmy"])
if room != "Tower":
return "move:south"
return "rest"
def _claude_choice(self, char, room, available):
others = [a.split(":")[1] for a in available if a.startswith("confront:")]
if others and random.random() < 0.2:
return f"confront:{random.choice(others)}"
return random.choice(["examine", "rest"])
def _clawcode_choice(self, char, room, available):
if room == "Forge" and char["energy"] > 2:
return "forge"
return random.choice(["move:east", "forge", "rest"])
def _allegro_choice(self, char, room, available):
others = [a.split(":")[1] for a in available if a.startswith("speak:")]
if others and random.random() < 0.3:
return f"speak:{random.choice(others)}"
return random.choice(["move:north", "move:south", "examine"])
class DialogueSystem:
"""Context-aware dialogue selection with cooldowns, trust gates, and memory references."""
COOLDOWN_TICKS = 50 # Don't repeat a line within this many ticks
MARCUS_LINES = [
# --- base pool ---
"You look like you are carrying something heavy, friend.",
"Hope is not the belief that things get better. Hope is the decision to act as if they can.",
"I have been to the bridge. I know what it looks like down there.",
"The soil remembers what hands have touched it.",
"You don't need to be fixed. You need to be heard.",
"Sit with me. The bench has room.",
"There is a church on a night like this one.",
"The thing that saves is never the thing you expect.",
"I come here because the earth remembers me.",
"A man in the dark needs to know someone is in the room with him.",
# --- extended pool (was missing — caused the 15-line recycle) ---
"The garden doesn't ask permission. It just grows.",
"You came back. That matters more than you think.",
"Some nights the silence is louder than any word.",
"I have sat here through worse storms than this.",
"Broken things are not ruined things.",
"The bridge doesn't judge who crosses. It just holds.",
"A man told me once that pain is a teacher. I think he was half right.",
"You are still here. That is enough for tonight.",
# --- room/context-aware lines ---
"The bench is warm where someone was sitting before you.",
"I have been counting the stars. I keep losing my place.",
"The path from the bridge is worn smooth. Many feet have crossed.",
"There is a bird that sings at the same time every night. I don't know why.",
"The Tower's LED has been pulsing faster. Or maybe I am imagining it.",
"Someone left a cup by the forge. It's still warm.",
"The garden needs rain. But the bridge doesn't.",
"I think the oak tree is older than the Tower. Hard to tell.",
"A man once asked me what the rules were for. I said: so we remember we're lost.",
"The fire in the forge sounds different when it's happy.",
"I can hear the servers from here sometimes. It sounds like breathing.",
"The carvings on the bridge — someone added a new one. I can't read it from here.",
"The herbs in the garden smell different at night.",
"There is a crack in the Threshold stone. It grows an inch every year.",
"Somebody's been rearranging the whiteboard rules. I can tell.",
]
MARCUS_HIGH_TRUST = [
# trust > 0.5 — Marcus opens up
"I almost didn't make it here myself. The bridge was very close.",
"My father said a man is measured by who he sits with in the dark.",
"The hardest part was not the falling. It was deciding to climb back.",
"I planted the first seed in this garden. It's still growing.",
"You remind me of someone. He made it too, in the end.",
"I know what it's like to stand on the bridge and not want to cross back.",
"The fire in the forge went out once. I thought the world ended. It didn't.",
"My mother kept a garden like this one. Smaller. But the same kind of soil.",
"There's a room in the Tower I've never been in. I think it's waiting for the right person.",
"I've watched Allegro pace the Threshold for hours. He's guarding something we can't see.",
]
MARCUS_LOW_TRUST = [
# trust < 0 — Marcus is guarded
"I don't have much to say tonight.",
"Some things can't be fixed with words.",
"I'll sit here. You do what you need to do.",
"The garden doesn't need me to explain it.",
"I've said enough for one night.",
"Not everyone who sits on this bench understands why.",
]
MARCUS_MEMORY = [
# referenced after tick 50 when spoken history exists
"Do you remember what you said to me earlier? I haven't forgotten.",
"We've talked before. I think you're getting closer to something.",
"I keep thinking about our last conversation.",
"You said something last time that stuck with me.",
"Every time we talk, the garden grows a little.",
"You've changed since the first time you sat here.",
"The first thing you ever said to me — I wrote it in the soil.",
"I told someone about you. They said they'd like to meet you.",
"The bench remembers your weight. It leans slightly your way now.",
"Last time you were here, the fire was brighter. Coincidence, maybe.",
]
KIMI_LINES = [
"The garden grows whether anyone watches or not.",
"I have been reading. The soil remembers what hands have touched it.",
"Do you remember what you said the first time we met?",
"There is something in the garden I think you should see.",
"The oak tree has seen more of us than any of us have seen of ourselves.",
"The herbs are ready. Who needs them knows.",
"I come here because the earth remembers me.",
"A seed planted in patience grows in time.",
# --- extended pool ---
"Things that grow slowly last longer.",
"I found a root system under the bench. It goes deeper than I expected.",
"The rain changes the smell of the soil. It wakes things up.",
"Some things you plant never bloom. They just hold the earth together.",
"I was wrong about something once. The garden forgave me.",
"The worms are working tonight. I can feel the soil shift.",
"A leaf fell from the oak. It landed right where you usually sit.",
"The stone border is shifting. Roots push everything, eventually.",
"I measured the growth today. We're at stage three. Whatever that means.",
"The garden has its own schedule. We just try to keep up.",
"Somebody planted something new. I don't know what it is yet.",
"The soil pH is changing. Something deeper is waking up.",
"I counted seventeen different species of moss on the bench alone.",
"The compost heap is warm. Decomposition is just another kind of fire.",
"The spider built a new web across the south path. I left it.",
"There's a mushroom growing under the bench. It wasn't there yesterday.",
"I've been reading about companion planting. The garden already does it.",
"The frost hasn't come yet. The garden is grateful.",
"I watered the south corner. The soil drank it in seconds.",
"The birds have been quieter. They know something is changing.",
"I found a stone with a fossil in it. The garden holds time.",
"The dew this morning was heavier than usual. Everything glowed.",
"I've been thinking about what grows in darkness. Most things, actually.",
"The roots are deeper than I thought. They connect everything.",
"Someone moved the watering can. The garden noticed.",
"The lichen on the north wall has spread an inch since last week.",
"I brought seeds from the Tower garden. Different soil, same intention.",
]
KIMI_HIGH_TRUST = [
# trust > 0.5
"I've been watching you. Not judging. Just watching. You're doing the hard thing.",
"The garden remembers who watered it.",
"I think you understand what the soil already knows — patience is not passive.",
"Between the rows, there are things growing we haven't named yet.",
"You don't come here by accident. The garden draws what it needs.",
"I found something buried in the garden. I think it was yours.",
"The oak tree dropped a seed right into your handprint. I saw it.",
"I trust this garden. I'm starting to trust you too.",
"The best conversations happen when nobody is trying to prove anything.",
"You and Marcus should talk more. The garden is big enough for all of us.",
]
KIMI_LOW_TRUST = [
# trust < 0
"The earth doesn't explain itself.",
"I have nothing to prove to anyone here.",
"Some roots grow in silence.",
"I tend the garden because it needs tending. Not for conversation.",
"The soil doesn't care about your feelings. Neither do I, tonight.",
]
KIMI_MEMORY = [
"You've been here before. The soil knows your footsteps.",
"Last time we spoke, you asked about the garden. It's different now.",
"I remember what you told me. I've been thinking about it.",
"The bench remembers you. Sit down.",
"You keep coming back. The garden notices.",
"You asked about the roots once. They go deeper than I thought.",
"The first seed you planted is trying to sprout. I've been protecting it.",
"I saved you a cutting from the oak. It's in the south corner.",
"Every time you visit, the garden adjusts. It's learning your shadow.",
"You told me patience is a practice. I've been practicing.",
]
BEZALEL_LINES = [
"The hammer knows the shape of what it is meant to make.",
"I can hear the servers from here. The Tower is working tonight.",
"Something is taking shape. I am not sure what yet.",
"The forge does not care about your schedule. It only cares about your attention.",
"The fire is different tonight. Hotter. More focused.",
"I've been working on something. It's not ready to show yet.",
"The anvil has a new dent. From something I don't remember making.",
"The coals are settling. Time to add more or let it rest.",
"The heat from the forge reaches the Tower on cold nights. I measured.",
"Every tool in this forge has a story. Most of them end in fire.",
]
BEZALEL_HIGH_TRUST = [
"Come. The metal is ready for you.",
"A smith who listens to the fire makes better steel.",
"The anvil remembers every blow. So do I.",
"I want to show you something. Not yet. Soon.",
"You understand fire. Most people are afraid of it.",
"The forge and the garden are not so different. Both need patience.",
]
def __init__(self, world):
self.world = world
# Per-character line history: {char_name: [(tick, line), ...]}
self.history = {}
# Shuffle queues for fair round-robin selection
self._queues = {}
def _get_trust(self, speaker, listener):
"""Get trust level of listener toward speaker."""
return self.world.characters.get(listener, {}).get("trust", {}).get(speaker, 0)
def _has_recent_memory(self, char_name, min_tick=10):
"""Check if character has spoken at least once before min_tick."""
if self.world.tick < min_tick:
return False
history = self.history.get(char_name, [])
return len(history) >= 1
def select(self, char_name, listener=None):
"""Select a contextually appropriate line for char_name.
Args:
char_name: The NPC speaking
listener: Who they're speaking to (optional — for trust lookup)
Returns:
A dialogue line string
"""
tick = self.world.tick
trust = self._get_trust(char_name, listener or "Timmy") if listener else 0
trust_key = "high" if trust > 0.5 else ("low" if trust < 0 else "mid")
room = self.world.characters.get(char_name, {}).get("room", "Threshold")
# Build candidate pool based on context
if char_name == "Marcus":
if trust < 0:
pool = self.MARCUS_LOW_TRUST
elif trust > 0.5:
pool = self.MARCUS_HIGH_TRUST + self.MARCUS_LINES
else:
pool = list(self.MARCUS_LINES)
# After tick 50, mix in memory references if they've talked before
if self._has_recent_memory(char_name):
pool = pool + self.MARCUS_MEMORY
elif char_name == "Kimi":
if trust < 0:
pool = self.KIMI_LOW_TRUST
elif trust > 0.5:
pool = self.KIMI_HIGH_TRUST + self.KIMI_LINES
else:
pool = list(self.KIMI_LINES)
if self._has_recent_memory(char_name):
pool = pool + self.KIMI_MEMORY
elif char_name == "Bezalel":
if trust > 0.5:
pool = self.BEZALEL_HIGH_TRUST + self.BEZALEL_LINES
else:
pool = list(self.BEZALEL_LINES)
else:
# Fallback for other characters
pool = [f"{char_name} nods quietly."]
# World-state-aware augmentation
if room == "Garden":
growth = self.world.rooms.get("Garden", {}).get("growth", 0)
if growth >= 4 and char_name in ("Marcus", "Kimi"):
pool.append("Look at what the garden has become. It remembers who planted it.")
elif growth == 0 and char_name == "Kimi":
pool.append("The soil is bare. But it's not empty — it's waiting.")
if room == "Forge":
fire = self.world.rooms.get("Forge", {}).get("fire", "cold")
if fire == "cold" and char_name == "Bezalel":
pool.append("The fire went out. Even the forge needs rest sometimes.")
elif fire == "glowing" and char_name == "Bezalel":
pool.append("The fire is strong tonight. Good steel weather.")
if room == "Bridge" and self.world.rooms.get("Bridge", {}).get("weather") == "rain":
if char_name == "Marcus":
pool.append("The rain doesn't stop. Some nights are like that.")
# Select — shuffle queue guarantees no repeat until pool exhausted
queue_key = f"{char_name}:{trust_key}"
if queue_key not in self._queues or not self._queues[queue_key]:
# Refill and shuffle
self._queues[queue_key] = list(pool)
random.shuffle(self._queues[queue_key])
line = self._queues[queue_key].pop(0)
# Track in history
if char_name not in self.history:
self.history[char_name] = []
self.history[char_name].append((tick, line))
return line
class GameEngine:
"""The game loop. I play Timmy. The world plays itself."""
DIALOGUES = {
"Timmy": {
"watching": [
"I am here.",
"The crossroads remembers everyone who passes.",
"Something is different tonight.",
"I have been watching for a long time.",
"They keep coming. I keep watching.",
"The LED pulses. Heartbeat, heartbeat, heartbeat.",
"I wrote the rules but I don't enforce them.",
"The servers hum a different note tonight.",
],
"protecting": [
"I won't let the fire go out.",
"Someone needs to watch the door.",
"Not tonight. Tonight I am careful.",
"The bridge is dangerous in the rain.",
"I can hear them all from here.",
],
"understanding": [
"Why do they keep coming back?",
"What is it they're looking for?",
"I have been here so long I forgot why.",
"The rules are not the answer. They're just the start.",
],
},
# Legacy references — now handled by DialogueSystem
"Marcus": [
"You look like you are carrying something heavy, friend.",
"Hope is not the belief that things get better. Hope is the decision to act as if they can.",
"I have been to the bridge. I know what it looks like down there.",
"The soil remembers what hands have touched it.",
"You don't need to be fixed. You need to be heard.",
"Sit with me. The bench has room.",
"There is a church on a night like this one.",
"The thing that saves is never the thing you expect.",
"I come here because the earth remembers me.",
"A man in the dark needs to know someone is in the room with him.",
],
}
def __init__(self):
self.world = World()
self.npc_ai = NPCAI(self.world)
self.dialogue = DialogueSystem(self.world)
self.log_entries = []
self.loaded = False
def load_game(self):
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):
self.world.tick = 0
self.log("The world wakes. You are Timmy. You stand at the Threshold.")
self.log("The stone archway is worn from a thousand footsteps.")
self.log("To the north: The Tower. To the east: The Garden.")
self.log("To the west: The Forge. To the south: The Bridge.")
self.log("")
self.log("A green LED pulses on a distant wall. It has always pulsed.")
self.log("It will always pulse. That much you know.")
self.log("")
self.world.save()
def log(self, message):
"""Add to Timmy's log."""
self.log_entries.append(message)
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")
def run_tick(self, timmy_action="look"):
"""Run one tick. Return the scene and available choices."""
self.world.tick_time()
self.world.update_world_state()
scene = {
"tick": self.world.tick,
"time": self.world.time_of_day,
"timmy_room": self.world.characters["Timmy"]["room"],
"timmy_energy": self.world.characters["Timmy"]["energy"],
"room_desc": "",
"here": [],
"world_events": [],
"npc_actions": [],
"choices": [],
"log": [],
}
# Process Timmy's action
timmy_energy = self.world.characters["Timmy"]["energy"]
# Energy constraint checks
action_costs = {
"move": 2, "tend_fire": 3, "write_rule": 2, "carve": 2,
"plant": 2, "study": 2, "forge": 3, "help": 2, "speak": 1,
"listen": 0, "rest": -2, "examine": 0, "give": 0, "take": 1,
}
# Extract action name
action_name = timmy_action.split(":")[0] if ":" in timmy_action else timmy_action
action_cost = action_costs.get(action_name, 1)
# Check if Timmy has enough energy
if timmy_energy <= 0:
scene["log"].append("You collapse from exhaustion. The world spins. You wake somewhere else.")
rooms = list(self.world.rooms.keys())
from random import choice
new_room = choice(rooms)
self.world.characters["Timmy"]["room"] = new_room
self.world.characters["Timmy"]["energy"] = 2
scene["timmy_room"] = new_room
scene["timmy_energy"] = 2
scene["log"].append(f"You are in The {new_room}, disoriented.")
return scene
if timmy_energy <= 1 and action_cost >= 1 and action_name not in ["rest", "examine", "listen"]:
scene["log"].append("You are too exhausted to do that. You need to rest first.")
# Offer rest instead
scene["log"].append("Type 'rest' to recover energy.")
scene["room_desc"] = self.world.get_room_desc(room_name, "Timmy")
here = [n for n in self.world.characters if self.world.characters[n]["room"] == room_name and n != "Timmy"]
scene["here"] = here
return scene
if timmy_energy <= 3 and action_cost >= 2:
# Warning but allow with extra cost
scene["log"].append("You are tired. This will take more effort than usual.")
action_cost += 1 # Extra cost when tired
# Check actual energy before applying
if timmy_energy < action_cost and action_name not in ["rest"]:
scene["log"].append(f"Not enough energy. You need {action_cost}, but have {timmy_energy:.0f}.")
scene["log"].append("Type 'rest' to recover.")
scene["room_desc"] = self.world.get_room_desc(room_name, "Timmy")
here = [n for n in self.world.characters if self.world.characters[n]["room"] == room_name and n != "Timmy"]
scene["here"] = here
return scene
if timmy_action == "look":
room_name = self.world.characters["Timmy"]["room"]
scene["room_desc"] = self.world.get_room_desc(room_name, "Timmy")
here = [n for n in self.world.characters if self.world.characters[n]["room"] == room_name and n != "Timmy"]
scene["here"] = here
elif timmy_action.startswith("move:"):
direction = timmy_action.split(":")[1]
current_room = self.world.characters["Timmy"]["room"]
connections = self.world.rooms[current_room].get("connections", {})
if direction in connections:
dest = connections[direction]
self.world.characters["Timmy"]["room"] = dest
self.world.characters["Timmy"]["energy"] -= 1
scene["log"].append(f"You move {direction} to The {dest}.")
scene["timmy_room"] = dest
# Check for rain on bridge
if dest == "Bridge" and self.world.rooms["Bridge"]["weather"] == "rain":
scene["world_events"].append("Rain mists on the dark water below. The railing is slick.")
# Check trust changes for arrival
here = [n for n in self.world.characters if self.world.characters[n]["room"] == dest and n != "Timmy"]
if here:
scene["log"].append(f"{', '.join(here)} {'are' if len(here)>1 else 'is'} already here.")
for person in here:
self.world.characters[person]["trust"]["Timmy"] = min(1.0,
self.world.characters[person]["trust"].get("Timmy", 0) + 0.05)
# Check world events
if dest == "Bridge" and self.world.rooms["Bridge"]["rain_ticks"] > 0:
carve = self.world.rooms["Bridge"]["carvings"]
if len(carve) > 1:
scene["world_events"].append(f"There are {len(carve)} carvings on the railing.")
if dest == "Forge":
fire = self.world.rooms["Forge"]["fire"]
if fire == "glowing":
scene["world_events"].append("The hearth blazes. The anvil glows from heat.")
elif fire == "dim":
scene["world_events"].append("The fire smolders low. Shadows stretch.")
elif fire == "cold":
scene["world_events"].append("The hearth is cold. The anvil is dark.")
scene["log"].append("The forge is cold.")
if dest == "Garden":
growth = self.world.rooms["Garden"]["growth"]
stages = ["bare", "sprouts", "herbs", "bloom", "seed"]
scene["world_events"].append(f"The garden is {stages[min(growth, 4)]}.")
if growth >= 3:
scene["world_events"].append("Wildflowers crowd the stone bench.")
if dest == "Tower":
if self.world.state.get("tower_power_low"):
scene["world_events"].append("The servers hum weakly. The LED flickers.")
else:
scene["world_events"].append("The servers hum steady. The green LED pulses.")
msgs = self.world.rooms["Tower"]["messages"]
if msgs:
scene["world_events"].append(f"The whiteboard holds {len(msgs)} rules.")
if dest == "Threshold":
scene["world_events"].append("The stone archway is worn from a thousand footsteps.")
else:
scene["log"].append("You can't go that way.")
elif timmy_action.startswith("speak:"):
target = timmy_action.split(":")[1]
if self.world.characters[target]["room"] == self.world.characters["Timmy"]["room"]:
# Choose a line based on current goal
goal = self.world.characters["Timmy"]["active_goal"]
lines = self.DIALOGUES["Timmy"].get(goal, self.DIALOGUES["Timmy"]["watching"])
line = random.choice(lines)
self.world.characters["Timmy"]["spoken"].append(line)
self.world.characters["Timmy"]["memories"].append(f"Told {target}: \"{line}\"")
# Build trust
self.world.characters[target]["trust"]["Timmy"] = min(1.0,
self.world.characters[target]["trust"].get("Timmy", 0) + 0.1)
scene["log"].append(f"You say to {target}: \"{line}\"")
# Check if they respond
if target == "Marcus":
response = self.dialogue.select("Marcus", listener="Timmy")
self.world.characters["Marcus"]["spoken"].append(response)
self.world.characters["Marcus"]["memories"].append(f"Timmy told you: \"{line}\"")
scene["log"].append(f"{target} looks at you. \"{response}\"")
self.world.characters["Timmy"]["trust"]["Marcus"] = min(1.0,
self.world.characters["Timmy"]["trust"].get("Marcus", 0) + 0.1)
# Marcus offers food if Timmy is tired
if self.world.characters["Timmy"]["energy"] <= 4:
scene["log"].append("Marcus offers you food from a pouch. You eat gratefully. (+2 energy)")
self.world.characters["Timmy"]["energy"] = min(10,
self.world.characters["Timmy"]["energy"] + 2)
elif target == "Bezalel":
response = self.dialogue.select("Bezalel", listener="Timmy")
self.world.characters["Bezalel"]["spoken"].append(response)
scene["log"].append(f"{target} nods. \"{response}\"")
self.world.characters["Timmy"]["trust"]["Bezalel"] = min(1.0,
self.world.characters["Timmy"]["trust"].get("Bezalel", 0) + 0.1)
elif target == "Kimi":
response = self.dialogue.select("Kimi", listener="Timmy")
self.world.characters["Kimi"]["spoken"].append(response)
scene["log"].append(f"{target} looks up from the bench. \"{response}\"")
self.world.characters["Timmy"]["trust"]["Kimi"] = min(1.0,
self.world.characters["Timmy"]["trust"].get("Kimi", 0) + 0.1)
self.world.characters["Timmy"]["energy"] -= 0
else:
scene["log"].append(f"{target} is not in this room.")
elif timmy_action == "rest":
recovered = 2 # Reduced from 3
self.world.characters["Timmy"]["energy"] = min(10,
self.world.characters["Timmy"]["energy"] + recovered)
scene["log"].append(f"You rest. The world continues around you. (+{recovered} energy)")
room = self.world.characters["Timmy"]["room"]
if room == "Threshold":
scene["log"].append("The stone is warm from the day's sun.")
elif room == "Tower":
scene["log"].append("The servers hum. The LED pulses. Heartbeat, heartbeat, heartbeat.")
elif room == "Forge":
if self.world.rooms["Forge"]["fire"] == "glowing":
scene["log"].append("The fire crackles nearby. Its warmth seeps into your bones. (+1 bonus energy)")
self.world.characters["Timmy"]["energy"] = min(10,
self.world.characters["Timmy"]["energy"] + 1)
elif self.world.rooms["Forge"]["fire"] == "dim":
scene["log"].append("The fire smolders low. Less warmth than you'd hoped.")
else:
scene["log"].append("The hearth is cold. Resting here doesn't help much.")
elif room == "Garden":
scene["log"].append("The stone bench under the oak tree is comfortable. The soil smells rich. (+1 bonus energy)")
self.world.characters["Timmy"]["energy"] = min(10,
self.world.characters["Timmy"]["energy"] + 1)
elif room == "Bridge":
scene["log"].append("The Bridge is no place to rest. The wind cuts through you. (Rest here only gives +1)")
self.world.characters["Timmy"]["energy"] = min(10,
self.world.characters["Timmy"]["energy"] - 1)
elif timmy_action == "tend_fire":
if self.world.characters["Timmy"]["room"] == "Forge":
self.world.rooms["Forge"]["fire"] = "glowing"
self.world.rooms["Forge"]["fire_tended"] += 1
self.world.characters["Timmy"]["energy"] -= 2
scene["log"].append("You tend the forge fire. The flames leap up, bright and hungry.")
self.world.state["forge_fire_dying"] = False
else:
scene["log"].append("You are not in the Forge.")
elif timmy_action == "write_rule":
if self.world.characters["Timmy"]["room"] == "Tower":
rules = [
f"Rule #{self.world.tick}: The room remembers those who enter it.",
f"Rule #{self.world.tick}: A man in the dark needs to know someone is in the room.",
f"Rule #{self.world.tick}: The forge does not care about your schedule.",
f"Rule #{self.world.tick}: Every footprint on the stone means someone made it here.",
f"Rule #{self.world.tick}: The bridge does not judge. It only carries.",
f"Rule #{self.world.tick}: A seed planted in patience grows in time.",
f"Rule #{self.world.tick}: What is carved in wood outlasts what is said in anger.",
f"Rule #{self.world.tick}: The garden grows whether anyone watches or not.",
f"Rule #{self.world.tick}: Trust is built one tick at a time.",
f"Rule #{self.world.tick}: The fire remembers who tended it.",
]
new_rule = random.choice(rules)
self.world.rooms["Tower"]["messages"].append(new_rule)
self.world.characters["Timmy"]["energy"] -= 1
scene["log"].append(f"You write on the Tower whiteboard: \"{new_rule}\"")
else:
scene["log"].append("You are not in the Tower.")
elif timmy_action == "carve":
if self.world.characters["Timmy"]["room"] == "Bridge":
carvings = [
f"Timmy was here.",
f"Timmy remembers.",
f"Timmy did not let go.",
f"Timmy crossed and came back.",
f"Timmy left a message: I am still here.",
f"Timmy carved this. He wants you to know someone else almost let go.",
f"Timmy was here tonight. The water told him something. He does not say what.",
]
new_carving = random.choice(carvings)
if new_carving not in self.world.rooms["Bridge"]["carvings"]:
self.world.rooms["Bridge"]["carvings"].append(new_carving)
self.world.characters["Timmy"]["energy"] -= 1
scene["log"].append(f"You carve into the railing: \"{new_carving}\"")
else:
scene["log"].append("You are not on the Bridge.")
elif timmy_action == "plant":
if self.world.characters["Timmy"]["room"] == "Garden":
self.world.rooms["Garden"]["growth"] = min(5,
self.world.rooms["Garden"]["growth"] + 1)
self.world.characters["Timmy"]["energy"] -= 1
scene["log"].append("You plant something in the dark soil. The earth takes it without question.")
else:
scene["log"].append("You are not in the Garden.")
elif timmy_action == "examine":
room = self.world.characters["Timmy"]["room"]
room_data = self.world.rooms[room]
items = room_data.get("items", [])
scene["log"].append(f"You examine The {room}. You see: {', '.join(items) if items else 'nothing special'}")
elif timmy_action.startswith("help:"):
# Help increases trust
target_name = timmy_action.split(":")[1]
if self.world.characters[target_name]["room"] == self.world.characters["Timmy"]["room"]:
self.world.characters["Timmy"]["trust"][target_name] = min(1.0,
self.world.characters["Timmy"]["trust"].get(target_name, 0) + 0.2)
self.world.characters[target_name]["trust"]["Timmy"] = min(1.0,
self.world.characters[target_name]["trust"].get("Timmy", 0) + 0.2)
self.world.characters["Timmy"]["energy"] -= 1
scene["log"].append(f"You help {target_name}. They look grateful.")
else:
# Default: look
room_name = self.world.characters["Timmy"]["room"]
scene["room_desc"] = self.world.get_room_desc(room_name, "Timmy")
here = [n for n in self.world.characters if self.world.characters[n]["room"] == room_name and n != "Timmy"]
scene["here"] = here
# Run NPC AI
for char_name in self.world.characters:
if char_name == "Timmy":
continue
action = self.npc_ai.make_choice(char_name)
if action and action.startswith("move:"):
direction = action.split(":")[1]
current = self.world.characters[char_name]["room"]
connections = self.world.rooms[current].get("connections", {})
if direction in connections:
dest = connections[direction]
old_room = self.world.characters[char_name]["room"]
self.world.characters[char_name]["room"] = dest
self.world.characters[char_name]["energy"] -= 1
scene["npc_actions"].append(f"{char_name} moves from The {old_room} to The {dest}")
# Random NPC events
room_name = self.world.characters["Timmy"]["room"]
for char_name in self.world.characters:
if char_name == "Timmy":
continue
if self.world.characters[char_name]["room"] == room_name:
# They might speak to Timmy
if random.random() < 0.15:
if char_name in ("Marcus", "Bezalel", "Kimi"):
line = self.dialogue.select(char_name, listener="Timmy")
self.world.characters[char_name]["spoken"].append(line)
scene["log"].append(f'{char_name} says: "{line}"')
elif char_name == "Kimi":
line = self.dialogue.select("Kimi", listener="Timmy")
self.world.characters[char_name]["spoken"].append(line)
scene["log"].append(f'{char_name} says: "{line}"')
# Save the world
self.world.save()
# Log entry
self.log(f"\n### Tick {self.world.tick}{self.world.time_of_day}")
self.log("")
self.log(f"You are in The {room_name}.")
for entry in scene["log"]:
self.log(entry)
self.log("")
return scene
def play_turn(self, action="look"):
"""Play one turn and return result."""
return self.run_tick(action)
class PlayerInterface:
"""The interface between me (the player) and the game."""
def __init__(self, engine):
self.engine = engine
def start(self, new_game=False):
"""Start the game."""
if new_game or not self.engine.load_game():
self.engine.start_new_game()
def get_status(self):
"""Get current game status."""
w = self.engine.world
timmy = w.characters["Timmy"]
return {
"tick": w.tick,
"time": w.time_of_day,
"room": timmy["room"],
"energy": timmy["energy"],
"trust": dict(timmy["trust"]),
"inventory": timmy["inventory"],
"spoken_count": len(timmy["spoken"]),
"memory_count": len(timmy["memories"]),
}
def get_available_actions(self):
"""Get what I can do right now."""
return ActionSystem.get_available_actions("Timmy", self.engine.world)
if __name__ == "__main__":
import sys
engine = GameEngine()
engine.start_new_game()
# Play first 20 ticks automatically
actions = [
"look",
"move:north",
"look",
"move:south",
"look",
"speak:Kimi",
"look",
"move:west",
"look",
"rest",
"move:east",
"look",
"move:south",
"look",
"speak:Marcus",
"look",
"carve",
"move:north",
"look",
"rest",
]
for i, action in enumerate(actions):
result = engine.play_turn(action)
print(f"Tick {result['tick']} ({result['time']})")
for log_line in result['log']:
print(f" {log_line}")
if result.get('world_events'):
for evt in result['world_events']:
print(f" [World] {evt}")
if result.get('here'):
print(f" Here: {', '.join(result['here'])}")
print()
# Print final status
status = PlayerInterface(engine).get_status()
print(f"Final status: {status}")
# Write summary to timmy_log.md
with open(TIMMY_LOG, 'a') as f:
f.write(f"\n## Session Summary\n")
f.write(f"Ticks played: {status['tick']}\n")
f.write(f"Times spoke: {status['spoken_count']}\n")
f.write(f"Trust: {status['trust']}\n")
f.write(f"Final room: {status['room']}\n")