Some checks failed
Smoke Test / smoke (push) Has been cancelled
Merge PR #614
1542 lines
67 KiB
Python
1542 lines
67 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'
|
|
|
|
# ============================================================
|
|
# NARRATIVE ARC — 4 phases that transform the world
|
|
# ============================================================
|
|
|
|
NARRATIVE_PHASES = {
|
|
"quietus": {
|
|
"ticks": (1, 50),
|
|
"name": "Quietus",
|
|
"subtitle": "The world is quiet. The characters are still.",
|
|
"trust_decay": 0.001, # Very slow decay
|
|
"crisis_chance": 0.02, # Rare events
|
|
"tone": "contemplative",
|
|
},
|
|
"fracture": {
|
|
"ticks": (51, 100),
|
|
"name": "Fracture",
|
|
"subtitle": "Something is wrong. The air feels different.",
|
|
"trust_decay": 0.003, # Faster decay
|
|
"crisis_chance": 0.06, # More frequent
|
|
"tone": "uneasy",
|
|
},
|
|
"breaking": {
|
|
"ticks": (101, 150),
|
|
"name": "Breaking",
|
|
"subtitle": "The tower shakes. Nothing is safe.",
|
|
"trust_decay": 0.008, # Rapid decay
|
|
"crisis_chance": 0.12, # Constant crisis
|
|
"tone": "desperate",
|
|
},
|
|
"mending": {
|
|
"ticks": (151, 200),
|
|
"name": "Mending",
|
|
"subtitle": "What was broken can be made whole again.",
|
|
"trust_decay": 0.002, # Slowing
|
|
"crisis_chance": 0.04, # Calming
|
|
"tone": "hopeful",
|
|
},
|
|
}
|
|
|
|
|
|
def get_narrative_phase(tick):
|
|
"""Return the narrative phase dict for this tick."""
|
|
for phase_key, phase in NARRATIVE_PHASES.items():
|
|
start, end = phase["ticks"]
|
|
if start <= tick <= end:
|
|
return phase_key, phase
|
|
# After tick 200, stay in mending
|
|
return "mending", NARRATIVE_PHASES["mending"]
|
|
|
|
|
|
def get_phase_transition_event(old_phase, new_phase):
|
|
"""Return a narrative event string for a phase transition."""
|
|
transitions = {
|
|
("quietus", "fracture"): (
|
|
"The air changes. Something shifts in the stone. "
|
|
"The green LED flickers for the first time. "
|
|
"No one speaks of it, but everyone feels it."
|
|
),
|
|
("fracture", "breaking"): (
|
|
"The tower groans. Dust falls from the ceiling. "
|
|
"The forge fire gutters. The garden soil cracks. "
|
|
"This is not weather. This is something deeper."
|
|
),
|
|
("breaking", "mending"): (
|
|
"Silence. Then — a sound. Not breaking. Building. "
|
|
"Someone is picking up the pieces. "
|
|
"The green LED steadies. It pulses again. Heartbeat, heartbeat, heartbeat."
|
|
),
|
|
}
|
|
return transitions.get((old_phase, new_phase), f"The world shifts. Phase: {new_phase}.")
|
|
|
|
|
|
# ============================================================
|
|
# THE WORLD
|
|
# ============================================================
|
|
|
|
class World:
|
|
def __init__(self):
|
|
self.tick = 0
|
|
self.time_of_day = "night"
|
|
self.narrative_phase = "quietus" # Current narrative phase
|
|
|
|
# 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": {}, # 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. Phase-aware."""
|
|
# --- Narrative phase detection ---
|
|
old_phase = self.narrative_phase
|
|
new_phase_key, new_phase = get_narrative_phase(self.tick)
|
|
if new_phase_key != old_phase:
|
|
self.narrative_phase = new_phase_key
|
|
self.state["phase_transition_event"] = get_phase_transition_event(old_phase, new_phase_key)
|
|
else:
|
|
self.state.pop("phase_transition_event", None)
|
|
|
|
# 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:
|
|
import random as _r
|
|
new_room = _r.choice(rooms)
|
|
attempts += 1
|
|
if new_room != current:
|
|
char["room"] = new_room
|
|
|
|
# Forge fire naturally dims if not tended
|
|
# Phase-aware: Breaking phase has higher fire-death chance
|
|
crisis_chance = NARRATIVE_PHASES.get(self.narrative_phase, NARRATIVE_PHASES["quietus"])["crisis_chance"]
|
|
self.state["forge_fire_dying"] = random.random() < (0.05 + crisis_chance)
|
|
|
|
# Random weather events — more frequent in Fracture/Breaking
|
|
weather_chance = 0.03 + (crisis_chance * 0.5)
|
|
if random.random() < weather_chance:
|
|
self.state["bridge_flooding"] = True
|
|
self.rooms["Bridge"]["weather"] = "rain"
|
|
self.rooms["Bridge"]["rain_ticks"] = random.randint(3, 8)
|
|
|
|
# Tower power fluctuates more in crisis phases
|
|
if random.random() < (0.01 + crisis_chance * 0.3):
|
|
self.state["tower_power_low"] = True
|
|
elif random.random() < 0.05:
|
|
self.state["tower_power_low"] = False # Recovers
|
|
|
|
# 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 — but in Breaking phase, growth can reverse
|
|
if not self.state.get("garden_drought"):
|
|
if self.narrative_phase == "breaking" and random.random() < crisis_chance * 0.5:
|
|
# Breaking: garden withers
|
|
self.rooms["Garden"]["growth"] = max(0, self.rooms["Garden"]["growth"] - 1)
|
|
elif random.random() < 0.02:
|
|
self.rooms["Garden"]["growth"] = min(5, self.rooms["Garden"]["growth"] + 1)
|
|
|
|
# Trust naturally decays — PHASE-AWARE rate
|
|
trust_decay = NARRATIVE_PHASES.get(self.narrative_phase, NARRATIVE_PHASES["quietus"])["trust_decay"]
|
|
for char_name, char in self.characters.items():
|
|
for other in char["trust"]:
|
|
char["trust"][other] = max(-1.0, char["trust"][other] - trust_decay)
|
|
|
|
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,
|
|
"narrative_phase": self.narrative_phase,
|
|
"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.narrative_phase = data.get("narrative_phase", "quietus")
|
|
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. Phase-aware behavior."""
|
|
char = self.world.characters[char_name]
|
|
room = char["room"]
|
|
available = ActionSystem.get_available_actions(char_name, self.world)
|
|
phase = self.world.narrative_phase
|
|
|
|
# If low energy, rest
|
|
if char["energy"] <= 1:
|
|
return "rest"
|
|
|
|
# Goal-driven behavior — phase modifies behavior
|
|
if char_name == "Marcus":
|
|
return self._marcus_choice(char, room, available, phase)
|
|
elif char_name == "Bezalel":
|
|
return self._bezalel_choice(char, room, available, phase)
|
|
elif char_name == "Allegro":
|
|
return self._allegro_choice(char, room, available, phase)
|
|
elif char_name == "Ezra":
|
|
return self._ezra_choice(char, room, available, phase)
|
|
elif char_name == "Gemini":
|
|
return self._gemini_choice(char, room, available, phase)
|
|
elif char_name == "Claude":
|
|
return self._claude_choice(char, room, available, phase)
|
|
elif char_name == "ClawCode":
|
|
return self._clawcode_choice(char, room, available, phase)
|
|
elif char_name == "Kimi":
|
|
return self._kimi_choice(char, room, available, phase)
|
|
|
|
return "rest"
|
|
|
|
def _marcus_choice(self, char, room, available, phase):
|
|
# Quietus: calm, stays in garden
|
|
# Fracture: more active, moves between rooms
|
|
# Breaking: stays near people, speaks urgently
|
|
# Mending: returns to garden, reflective
|
|
if phase == "breaking":
|
|
# During breaking, Marcus seeks people out
|
|
others = [a.split(":")[1] for a in available if a.startswith("speak:")]
|
|
if others and random.random() < 0.6:
|
|
return f"speak:{random.choice(others)}"
|
|
if room != "Threshold":
|
|
return "move:west" # Go to crossroads to find people
|
|
return "examine"
|
|
elif phase == "fracture":
|
|
# Fracture: restless, moves more
|
|
if random.random() < 0.4:
|
|
return random.choice(["move:west", "move:east", "move:south"])
|
|
if room == "Garden":
|
|
return "rest"
|
|
return "move:east" # Head to garden
|
|
elif phase == "mending":
|
|
# Mending: returns to garden, speaks less but with weight
|
|
if room == "Garden" and random.random() < 0.5:
|
|
return "rest"
|
|
if room != "Garden":
|
|
return "move:east"
|
|
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 "rest"
|
|
else: # quietus
|
|
if room == "Garden" and random.random() < 0.7:
|
|
return "rest"
|
|
if room != "Garden":
|
|
return "move:east"
|
|
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, phase):
|
|
if phase == "breaking":
|
|
# Breaking: frantically tends fire
|
|
if room == "Forge":
|
|
if self.world.rooms["Forge"]["fire"] != "glowing":
|
|
return "tend_fire"
|
|
return "forge"
|
|
return "move:west" # Rush to forge
|
|
elif phase == "fracture":
|
|
# Fracture: more vigilant about fire
|
|
if room == "Forge":
|
|
if random.random() < 0.5:
|
|
return "tend_fire"
|
|
return "forge"
|
|
return "move:west"
|
|
else: # quietus, mending
|
|
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, phase):
|
|
others = [a.split(":")[1] for a in available if a.startswith("speak:")]
|
|
if phase == "breaking":
|
|
# Breaking: stays in garden protecting it
|
|
if room == "Garden":
|
|
if others and random.random() < 0.4:
|
|
return f"speak:{random.choice(others)}"
|
|
return "plant" # Tries to hold the garden
|
|
return "move:east" # Rush to garden
|
|
elif phase == "fracture":
|
|
# Fracture: anxious, checks garden then tower
|
|
if room == "Garden":
|
|
return random.choice(["plant", "examine", "rest"])
|
|
if room == "Tower":
|
|
return "study"
|
|
return "move:east"
|
|
elif phase == "mending":
|
|
# Mending: plants with purpose
|
|
if room == "Garden":
|
|
if random.random() < 0.5:
|
|
return "plant"
|
|
if others and random.random() < 0.3:
|
|
return f"speak:{random.choice(others)}"
|
|
return "rest"
|
|
return "move:east"
|
|
else: # quietus
|
|
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"
|
|
|
|
def _gemini_choice(self, char, room, available, phase):
|
|
others = [a.split(":")[1] for a in available if a.startswith("listen:")]
|
|
if phase == "breaking":
|
|
# Breaking: observes more intensely
|
|
if others and random.random() < 0.6:
|
|
return f"listen:{random.choice(others)}"
|
|
return "examine"
|
|
elif phase == "mending":
|
|
if room == "Garden":
|
|
if others and random.random() < 0.5:
|
|
return f"listen:{random.choice(others)}"
|
|
return "plant"
|
|
return "move:west"
|
|
else:
|
|
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, phase):
|
|
if phase == "breaking":
|
|
# Breaking: desperately writes rules, studies
|
|
if room == "Tower" and char["energy"] > 1:
|
|
return random.choice(["write_rule", "write_rule", "study"])
|
|
if room != "Tower":
|
|
return "move:south"
|
|
return "rest"
|
|
elif phase == "mending":
|
|
if room == "Tower":
|
|
return random.choice(["study", "write_rule", "rest"])
|
|
return "move:south"
|
|
else:
|
|
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, phase):
|
|
others = [a.split(":")[1] for a in available if a.startswith("confront:")]
|
|
if phase == "breaking":
|
|
# Breaking: confronts more, judgmental
|
|
if others and random.random() < 0.5:
|
|
return f"confront:{random.choice(others)}"
|
|
return "examine"
|
|
elif phase == "fracture":
|
|
if others and random.random() < 0.3:
|
|
return f"confront:{random.choice(others)}"
|
|
return random.choice(["examine", "rest"])
|
|
else:
|
|
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, phase):
|
|
if phase == "breaking":
|
|
# Breaking: works forge frantically
|
|
if room == "Forge":
|
|
return "forge"
|
|
return "move:west"
|
|
elif phase == "mending":
|
|
if room == "Forge" and char["energy"] > 2:
|
|
return random.choice(["forge", "rest"])
|
|
return "move:west"
|
|
else:
|
|
if room == "Forge" and char["energy"] > 2:
|
|
return "forge"
|
|
return random.choice(["move:east", "forge", "rest"])
|
|
|
|
def _allegro_choice(self, char, room, available, phase):
|
|
others = [a.split(":")[1] for a in available if a.startswith("speak:")]
|
|
if phase == "breaking":
|
|
# Breaking: stays at threshold, guards
|
|
if room == "Threshold":
|
|
if others and random.random() < 0.5:
|
|
return f"speak:{random.choice(others)}"
|
|
return "examine"
|
|
return "move:south" # Return to threshold
|
|
elif phase == "mending":
|
|
if others and random.random() < 0.3:
|
|
return f"speak:{random.choice(others)}"
|
|
return random.choice(["examine", "rest"])
|
|
else:
|
|
if others and random.random() < 0.3:
|
|
return f"speak:{random.choice(others)}"
|
|
return random.choice(["move:north", "move:south", "examine"])
|
|
|
|
|
|
class GameEngine:
|
|
"""The game loop. I play Timmy. The world plays itself."""
|
|
|
|
# ============================================================
|
|
# DIALOGUES — phase-specific pools prevent repetition
|
|
# ============================================================
|
|
|
|
DIALOGUES = {
|
|
"Timmy": {
|
|
"watching": {
|
|
"quietus": [
|
|
"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.",
|
|
],
|
|
"fracture": [
|
|
"Something changed. Did you feel it?",
|
|
"The rules used to be enough. I'm not sure anymore.",
|
|
"I keep watching, but the edges are fraying.",
|
|
"Have you noticed the LED flicker?",
|
|
"I wrote these rules when the world was simpler.",
|
|
"The crossroads still stands. But the paths are shifting.",
|
|
],
|
|
"breaking": [
|
|
"The watching isn't enough. It was never enough.",
|
|
"I see everything breaking and I can't stop it.",
|
|
"The LED is going dark. I can feel it.",
|
|
"Every rule I wrote is being tested right now.",
|
|
"I should have done more when things were quiet.",
|
|
"This is what I was built for. Not watching. Holding.",
|
|
],
|
|
"mending": [
|
|
"I am still here. I did not leave.",
|
|
"The LED is steady again. But I am different now.",
|
|
"I watched the breaking. I will remember the mending.",
|
|
"The rules survived. Maybe that means something.",
|
|
"I am learning what watching really means.",
|
|
"The crossroads remembers. So do I.",
|
|
],
|
|
},
|
|
"protecting": {
|
|
"quietus": [
|
|
"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.",
|
|
],
|
|
"fracture": [
|
|
"I need to watch more carefully now.",
|
|
"Something is threatening the garden.",
|
|
"I can't protect them from what I can't see.",
|
|
"The fire is more important than ever.",
|
|
"I won't let anyone fall.",
|
|
],
|
|
"breaking": [
|
|
"I will hold the line. Whatever it takes.",
|
|
"They need me now. Not later. Now.",
|
|
"The forge fire cannot die. Not today.",
|
|
"I can feel the tower straining. I hold it.",
|
|
"Protection means sacrifice. I understand that now.",
|
|
],
|
|
"mending": [
|
|
"I protected what I could. It was enough.",
|
|
"The fire survived because someone tended it.",
|
|
"I held the door. The door held me.",
|
|
"Protecting isn't about strength. It's about staying.",
|
|
"We made it through. Together.",
|
|
],
|
|
},
|
|
"understanding": {
|
|
"quietus": [
|
|
"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.",
|
|
],
|
|
"fracture": [
|
|
"I'm starting to understand what the fracture means.",
|
|
"Understanding takes longer when everything is shaking.",
|
|
"Maybe I was asking the wrong questions.",
|
|
"The cracks show what was underneath all along.",
|
|
],
|
|
"breaking": [
|
|
"Now I understand. The breaking is the teaching.",
|
|
"You can't understand something until it breaks.",
|
|
"I understand Marcus now. I understand the bridge.",
|
|
"Understanding isn't comfort. It's clarity in the storm.",
|
|
],
|
|
"mending": [
|
|
"I understand now. Broken things can be mended.",
|
|
"The mending is not the same as the original. It's better.",
|
|
"Understanding came through the breaking. That's the way.",
|
|
"I see what they were all trying to tell me.",
|
|
],
|
|
},
|
|
},
|
|
"Marcus": {
|
|
"quietus": [
|
|
"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.",
|
|
],
|
|
"fracture": [
|
|
"Something is wrong tonight. Can you feel it?",
|
|
"The garden is nervous. The soil is pulling back.",
|
|
"I've seen this before. In other places. Before the bridge.",
|
|
"Don't pretend everything is fine. It isn't.",
|
|
"When the ground shakes, you find out who stays.",
|
|
"I stayed at the bridge when everyone else left.",
|
|
"The bench feels different tonight. Colder.",
|
|
"We need to talk about what's coming.",
|
|
],
|
|
"breaking": [
|
|
"THIS is what I was talking about. This is the moment.",
|
|
"Hold on. Don't let go. Whatever you do, don't let go.",
|
|
"I have been to the edge. I came back. You can too.",
|
|
"The breaking isn't the end. It's the test.",
|
|
"When everything falls, the foundation is all that's left.",
|
|
"I am not leaving this bench. I am not leaving you.",
|
|
"The garden will come back. But only if someone stays.",
|
|
"A man at the bottom needs a hand, not a lecture.",
|
|
"This is the night that changes everything. Remember it.",
|
|
],
|
|
"mending": [
|
|
"We made it. We're still here.",
|
|
"The garden is growing back. See? The soil remembers.",
|
|
"Mending takes longer than breaking. That's the hard part.",
|
|
"I told you the bench had room. It still does.",
|
|
"What was broken is different now. Stronger in some places.",
|
|
"The bridge held. That's what bridges do.",
|
|
"Sit with me. Let's watch what grows next.",
|
|
"A man who survived the breaking knows what matters.",
|
|
"Hope is not a feeling. It's a decision. I decided.",
|
|
],
|
|
},
|
|
"Bezalel": {
|
|
"quietus": [
|
|
"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.",
|
|
],
|
|
"fracture": [
|
|
"The fire is harder to keep. It knows something.",
|
|
"Metal under stress shows its true grain.",
|
|
"I've been forging all night. Something is wrong with the iron.",
|
|
"The anvil rings different when the world is shifting.",
|
|
],
|
|
"breaking": [
|
|
"THE FIRE IS DYING. HELP ME.",
|
|
"I can't hold this alone. The forge needs tending.",
|
|
"Everything is breaking but the anvil. The anvil endures.",
|
|
"If the fire goes out, we lose everything. TEND IT.",
|
|
"This is what forging is. Breaking and reshaping.",
|
|
],
|
|
"mending": [
|
|
"The fire survived. We survived.",
|
|
"New metal. New shape. The forge remembers.",
|
|
"What I forged during the breaking will last forever.",
|
|
"The hammer learned something. So did I.",
|
|
"The forge is warm again. Come. Make something.",
|
|
],
|
|
},
|
|
"Kimi": {
|
|
"quietus": [
|
|
"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.",
|
|
],
|
|
"fracture": [
|
|
"The soil is restless. The roots are pulling.",
|
|
"I saw the garden shudder. Something underground.",
|
|
"The oak tree is dropping leaves. It's not autumn.",
|
|
"Don't disturb the garden tonight. It's listening.",
|
|
"I planted something and the earth rejected it.",
|
|
"The herbs are wilting. I don't know why.",
|
|
],
|
|
"breaking": [
|
|
"THE GARDEN IS CRACKING. THE SOIL IS SPLITTING.",
|
|
"The oak tree is leaning. It might fall.",
|
|
"I'm trying to hold the roots together.",
|
|
"Everything growing is dying. Everything dying is growing.",
|
|
"The garden will survive. It has to. I need it to.",
|
|
"I am not leaving this garden. Not for anything.",
|
|
],
|
|
"mending": [
|
|
"Green. I see green. The garden is coming back.",
|
|
"The soil is soft again. Something is growing.",
|
|
"The oak tree held. Its roots went deeper than we knew.",
|
|
"New seeds. New patience. New growth.",
|
|
"The garden remembers the breaking. It grows differently now.",
|
|
"I learned something from the soil: mending takes time.",
|
|
],
|
|
},
|
|
}
|
|
|
|
# NPC response lines for random speech events (phase-aware)
|
|
NPC_RANDOM_SPEECH = {
|
|
"Bezalel": {
|
|
"quietus": [
|
|
"The hammer knows the shape of what it is meant to make.",
|
|
"I can hear the servers from here.",
|
|
],
|
|
"fracture": [
|
|
"Something is wrong with the fire tonight.",
|
|
"The metal feels different. Stress in the alloy.",
|
|
"Can you hear that? The anvil is humming.",
|
|
],
|
|
"breaking": [
|
|
"THE FIRE. SOMEONE HELP WITH THE FIRE.",
|
|
"I've never seen the forge this cold.",
|
|
"Keep working. Don't stop. The forge needs us.",
|
|
],
|
|
"mending": [
|
|
"The fire is warm again. Good.",
|
|
"New metal. Let's build something that lasts.",
|
|
"The forge survived. So did we.",
|
|
],
|
|
},
|
|
"Kimi": {
|
|
"quietus": [
|
|
"The garden grows whether anyone watches or not.",
|
|
"I have been reading. The soil remembers what hands have touched it.",
|
|
"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.",
|
|
"Do you remember what you said the first time we met?",
|
|
"The herbs are ready. Who needs them knows.",
|
|
"I come here because the earth remembers me.",
|
|
"A seed planted in patience grows in time.",
|
|
],
|
|
"fracture": [
|
|
"The soil feels different tonight.",
|
|
"Something is wrong underground.",
|
|
"The garden is listening. Be careful what you say.",
|
|
"The oak tree dropped a branch. That never happens.",
|
|
],
|
|
"breaking": [
|
|
"Hold the roots. HOLD THE ROOTS.",
|
|
"The garden is breaking. I can feel it.",
|
|
"Don't step on the soil. It's fragile right now.",
|
|
"I won't leave the garden. I won't.",
|
|
],
|
|
"mending": [
|
|
"Green shoots. I see them.",
|
|
"The garden is healing. Slowly.",
|
|
"The soil remembers. It always remembers.",
|
|
"Something new is growing. Something different.",
|
|
],
|
|
},
|
|
}
|
|
|
|
def __init__(self):
|
|
self.world = World()
|
|
self.npc_ai = NPCAI(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,
|
|
"phase": self.world.narrative_phase,
|
|
"phase_name": NARRATIVE_PHASES.get(self.world.narrative_phase, NARRATIVE_PHASES["quietus"])["name"],
|
|
"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 AND phase
|
|
goal = self.world.characters["Timmy"]["active_goal"]
|
|
phase = self.world.narrative_phase
|
|
timmy_goal_pool = self.DIALOGUES["Timmy"].get(goal, self.DIALOGUES["Timmy"]["watching"])
|
|
# Phase-specific pool, fall back to quietus
|
|
lines = timmy_goal_pool.get(phase, timmy_goal_pool.get("quietus", ["I am here."]))
|
|
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 — phase-aware dialogue
|
|
if target == "Marcus":
|
|
marcus_pool = self.DIALOGUES["Marcus"].get(phase, self.DIALOGUES["Marcus"]["quietus"])
|
|
response = random.choice(marcus_pool)
|
|
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":
|
|
bezalel_pool = self.DIALOGUES["Bezalel"].get(phase, self.DIALOGUES["Bezalel"]["quietus"])
|
|
response = random.choice(bezalel_pool)
|
|
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":
|
|
kimi_pool = self.DIALOGUES["Kimi"].get(phase, self.DIALOGUES["Kimi"]["quietus"])
|
|
response = random.choice(kimi_pool)
|
|
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 — phase-aware speech
|
|
room_name = self.world.characters["Timmy"]["room"]
|
|
phase = self.world.narrative_phase
|
|
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
|
|
speech_chance = 0.15
|
|
# Breaking phase: NPCs speak more urgently
|
|
if phase == "breaking":
|
|
speech_chance = 0.25
|
|
elif phase == "fracture":
|
|
speech_chance = 0.20
|
|
|
|
if random.random() < speech_chance:
|
|
if char_name == "Marcus":
|
|
marcus_pool = self.DIALOGUES["Marcus"].get(phase, self.DIALOGUES["Marcus"]["quietus"])
|
|
line = random.choice(marcus_pool)
|
|
self.world.characters[char_name]["spoken"].append(line)
|
|
scene["log"].append(f"{char_name} says: \"{line}\"")
|
|
elif char_name in self.NPC_RANDOM_SPEECH:
|
|
pool = self.NPC_RANDOM_SPEECH[char_name].get(phase,
|
|
self.NPC_RANDOM_SPEECH[char_name].get("quietus", []))
|
|
if pool:
|
|
line = random.choice(pool)
|
|
self.world.characters[char_name]["spoken"].append(line)
|
|
scene["log"].append(f"{char_name} says: \"{line}\"")
|
|
|
|
# Save the world
|
|
self.world.save()
|
|
|
|
# Log entry — phase-marked chronicle
|
|
phase_key = self.world.narrative_phase
|
|
phase_info = NARRATIVE_PHASES.get(phase_key, NARRATIVE_PHASES["quietus"])
|
|
self.log(f"\n### Tick {self.world.tick} — {self.world.time_of_day} — [{phase_info['name']}]")
|
|
self.log("")
|
|
self.log(f"You are in The {room_name}.")
|
|
|
|
# Phase transition event (one-time narrative beat)
|
|
transition = self.world.state.get("phase_transition_event")
|
|
if transition:
|
|
self.log("")
|
|
self.log(f">>> {transition}")
|
|
self.log("")
|
|
scene["world_events"].append(transition)
|
|
|
|
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")
|