Some checks failed
Smoke Test / smoke (push) Has been cancelled
Merge PR #616
1346 lines
59 KiB
Python
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")
|