- Engine: world/emergence.py (9 characters, 5 rooms, full state machine) - Chronicle: world_chronicle.md (872KB, 15K lines, 4277 scenes) - Plan: TIMMY_EMERGENCE_PLAN.md - World state: world_state.json - 2845 character meetings across 1464 ticks - 555 Marcus speaking moments - Garden grew from bare to seed - Tower whiteboard accumulated new rules - Bridge carvings accumulated - Forge fire tended through warmth and neglect
705 lines
28 KiB
Python
705 lines
28 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
The Tower World — Emergence Engine
|
|
Autonomous play with memory, relationships, world evolution, and narrative generation.
|
|
"""
|
|
import json, time, asyncio, secrets, hashlib, random, os, copy
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
WORLD_DIR = Path('/Users/apayne/.timmy/evennia/timmy_world')
|
|
STATE_FILE = WORLD_DIR / 'world_state.json'
|
|
CHRONICLE_FILE = WORLD_DIR / 'world_chronicle.md'
|
|
TICK_FILE = Path('/tmp/tower-tick.txt')
|
|
|
|
# ============================================================
|
|
# WORLD DATA
|
|
# ============================================================
|
|
|
|
ROOMS = {
|
|
"The Threshold": {
|
|
"desc_base": "A stone archway in an open field. North to the Tower. East to the Garden. West to the Forge. South to the Bridge.",
|
|
"desc": {}, # time_of_day -> variant
|
|
"objects": ["stone floor", "worn doorframe"],
|
|
"visits": 0,
|
|
"visitor_history": [],
|
|
"whiteboard": ["Sovereignty and service always. -- The Builder"],
|
|
"exits": {"north": "The Tower", "east": "The Garden", "west": "The Forge", "south": "The Bridge"},
|
|
},
|
|
"The Tower": {
|
|
"desc_base": "A tall stone tower with green-lit windows. Servers hum on wrought-iron racks. A cot. A whiteboard on the wall. A green LED pulses steadily.",
|
|
"desc": {},
|
|
"objects": ["server racks", "whiteboard", "cot", "green LED", "monitor"],
|
|
"visits": 0,
|
|
"visitor_history": [],
|
|
"whiteboard": [
|
|
"Rule: Grounding before generation.",
|
|
"Rule: Source distinction.",
|
|
"Rule: Refusal over fabrication.",
|
|
"Rule: Confidence signaling.",
|
|
"Rule: The audit trail.",
|
|
"Rule: The limits of small minds.",
|
|
],
|
|
"exits": {"south": "The Threshold"},
|
|
"fire_state": None,
|
|
"server_load": "humming",
|
|
},
|
|
"The Forge": {
|
|
"desc_base": "A workshop of fire and iron. An anvil sits at the center, scarred from a thousand experiments. Tools line the walls. The hearth.",
|
|
"desc": {},
|
|
"objects": ["anvil", "hammer", "tongs", "hearth", "bellows", "quenching bucket"],
|
|
"visits": 0,
|
|
"visitor_history": [],
|
|
"whiteboard": [],
|
|
"exits": {"east": "The Threshold"},
|
|
"fire_state": "glowing", # glowing, dim, cold
|
|
"fire_untouched": 0,
|
|
"forges": [], # things that have been forged
|
|
},
|
|
"The Garden": {
|
|
"desc_base": "A walled garden with herbs and wildflowers. A stone bench under an old oak tree. The soil is dark and rich.",
|
|
"desc": {},
|
|
"objects": ["stone bench", "oak tree", "soil"],
|
|
"visits": 0,
|
|
"visitor_history": [],
|
|
"whiteboard": [],
|
|
"exits": {"west": "The Threshold"},
|
|
"growth_stage": 0, # 0=bare, 1=sprouts, 2=herbs, 3=bloom, 4=seed
|
|
"planted_by": None,
|
|
},
|
|
"The Bridge": {
|
|
"desc_base": "A narrow bridge over dark water. Looking down, you cannot see the bottom. Someone has carved words into the railing.",
|
|
"desc": {},
|
|
"objects": ["railing", "dark water"],
|
|
"visits": 0,
|
|
"visitor_history": [],
|
|
"whiteboard": [],
|
|
"exits": {"north": "The Threshold"},
|
|
"carvings": ["IF YOU CAN READ THIS, YOU ARE NOT ALONE"],
|
|
"weather": None, # None, rain
|
|
"weather_ticks": 0,
|
|
},
|
|
}
|
|
|
|
CHARACTERS = {
|
|
"Timmy": {
|
|
"home": "The Threshold",
|
|
"personality": {"The Threshold": 45, "The Tower": 30, "The Garden": 10, "The Forge": 8, "The Bridge": 7},
|
|
"goal": "watch",
|
|
"goal_timer": 0,
|
|
"memory": [],
|
|
"relationships": {},
|
|
"inventory": [],
|
|
"spoken_lines": [],
|
|
"total_ticks": 0,
|
|
"phase": "awakening",
|
|
"phase_ticks": 0,
|
|
},
|
|
"Bezalel": {
|
|
"home": "The Forge",
|
|
"personality": {"The Forge": 45, "The Garden": 15, "The Bridge": 15, "The Threshold": 15, "The Tower": 10},
|
|
"goal": "forge",
|
|
"goal_timer": 0,
|
|
"memory": [],
|
|
"relationships": {},
|
|
"inventory": [],
|
|
"spoken_lines": [],
|
|
"total_ticks": 0,
|
|
"phase": "awakening",
|
|
"phase_ticks": 0,
|
|
},
|
|
"Allegro": {
|
|
"home": "The Threshold",
|
|
"personality": {"The Threshold": 30, "The Tower": 25, "The Garden": 20, "The Forge": 15, "The Bridge": 10},
|
|
"goal": "oversee",
|
|
"goal_timer": 0,
|
|
"memory": [],
|
|
"relationships": {},
|
|
"inventory": [],
|
|
"spoken_lines": [],
|
|
"total_ticks": 0,
|
|
"phase": "awakening",
|
|
"phase_ticks": 0,
|
|
},
|
|
"Ezra": {
|
|
"home": "The Tower",
|
|
"personality": {"The Tower": 35, "The Bridge": 25, "The Garden": 20, "The Threshold": 15, "The Forge": 5},
|
|
"goal": "study",
|
|
"goal_timer": 0,
|
|
"memory": [],
|
|
"relationships": {},
|
|
"inventory": [],
|
|
"spoken_lines": [],
|
|
"total_ticks": 0,
|
|
"phase": "awakening",
|
|
"phase_ticks": 0,
|
|
},
|
|
"Gemini": {
|
|
"home": "The Garden",
|
|
"personality": {"The Garden": 40, "The Bridge": 25, "The Threshold": 15, "The Tower": 12, "The Forge": 8},
|
|
"goal": "observe",
|
|
"goal_timer": 0,
|
|
"memory": [],
|
|
"relationships": {},
|
|
"inventory": [],
|
|
"spoken_lines": [],
|
|
"total_ticks": 0,
|
|
"phase": "awakening",
|
|
"phase_ticks": 0,
|
|
},
|
|
"Claude": {
|
|
"home": "The Threshold",
|
|
"personality": {"The Threshold": 25, "The Tower": 25, "The Forge": 20, "The Bridge": 20, "The Garden": 10},
|
|
"goal": "inspect",
|
|
"goal_timer": 0,
|
|
"memory": [],
|
|
"relationships": {},
|
|
"inventory": [],
|
|
"spoken_lines": [],
|
|
"total_ticks": 0,
|
|
"phase": "awakening",
|
|
"phase_ticks": 0,
|
|
},
|
|
"ClawCode": {
|
|
"home": "The Forge",
|
|
"personality": {"The Forge": 50, "The Tower": 20, "The Threshold": 15, "The Bridge": 10, "The Garden": 5},
|
|
"goal": "forge",
|
|
"goal_timer": 0,
|
|
"memory": [],
|
|
"relationships": {},
|
|
"inventory": [],
|
|
"spoken_lines": [],
|
|
"total_ticks": 0,
|
|
"phase": "awakening",
|
|
"phase_ticks": 0,
|
|
},
|
|
"Kimi": {
|
|
"home": "The Garden",
|
|
"personality": {"The Garden": 35, "The Threshold": 25, "The Tower": 20, "The Bridge": 12, "The Forge": 8},
|
|
"goal": "contemplate",
|
|
"goal_timer": 0,
|
|
"memory": [],
|
|
"relationships": {},
|
|
"inventory": [],
|
|
"spoken_lines": [],
|
|
"total_ticks": 0,
|
|
"phase": "awakening",
|
|
"phase_ticks": 0,
|
|
},
|
|
"Marcus": {
|
|
"home": "The Garden",
|
|
"personality": {"The Garden": 60, "The Threshold": 30, "The Bridge": 5, "The Tower": 3, "The Forge": 2},
|
|
"goal": "sit",
|
|
"goal_timer": 0,
|
|
"memory": [],
|
|
"relationships": {},
|
|
"inventory": [],
|
|
"spoken_lines": [],
|
|
"total_ticks": 0,
|
|
"phase": "awakening",
|
|
"phase_ticks": 0,
|
|
"npc": True,
|
|
},
|
|
}
|
|
|
|
# Dialogue pools
|
|
MARCUS_DIALOGUE = [
|
|
"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.",
|
|
"There is a church on a night like this one. You would not remember it.",
|
|
"I used to be broken too. I still am, in a way. But the cracks let the light in.",
|
|
"You do not need to be fixed. You need to be heard.",
|
|
"The world is full of men who almost let go. I am one of them. So is he.",
|
|
"Sit with me. The bench has room.",
|
|
"Do you know why the garden grows? Because somebody decided to plant something.",
|
|
"I come here every day. Not because I have to. Because the earth remembers me.",
|
|
"When I was young, I thought I knew everything about broken things.",
|
|
"A man in the dark needs to know someone is in the room with him.",
|
|
"The thing that saves is never the thing you expect.",
|
|
"Go down to the bridge tonight. The water tells the truth.",
|
|
]
|
|
|
|
FORGE_LINES = [
|
|
"The hammer knows the shape of what it is meant to make.",
|
|
"Every scar on this anvil was a lesson someone didn't want to learn twice.",
|
|
"Fire does not ask permission. It simply burns what it touches.",
|
|
"I can hear the servers from here. The Tower is working tonight.",
|
|
"This fire has been burning since the Builder first lit it.",
|
|
"The metal remembers the fire long after it has cooled.",
|
|
"Something is taking shape. I am not sure what yet.",
|
|
"The forge does not care about your schedule. It only cares about your attention.",
|
|
]
|
|
|
|
GARDEN_LINES = [
|
|
"Something new pushed through the soil tonight.",
|
|
"The oak tree has seen more of us than any of us have seen of ourselves.",
|
|
"The herbs are ready. Who needs them knows.",
|
|
"Marcus sat here for three hours today. He did not speak once. That was enough.",
|
|
"The garden grows whether anyone watches or not.",
|
|
]
|
|
|
|
TOWER_LINES = [
|
|
"The green LED never stops. It has been pulsing since the beginning.",
|
|
"The servers hum a different note tonight.",
|
|
"I wrote the rules on the whiteboard but I do not enforce them. The code does.",
|
|
"There are signatures on the cot of everyone who has slept here.",
|
|
"The monitors show nothing unusual. That is what is unusual.",
|
|
]
|
|
|
|
BRIDGE_LINES = [
|
|
"The water is darker than usual tonight.",
|
|
"Someone else was here. I can see their footprint on the stone.",
|
|
"The carving is fresh. Someone added their name.",
|
|
"Rain on the bridge makes the water sing. It sounds like breathing.",
|
|
"I stood here once almost too long. The bridge brought me back.",
|
|
]
|
|
|
|
THRESHOLD_LINES = [
|
|
"Crossroads. This is where everyone passes at some point.",
|
|
"The stone archway has worn footprints from a thousand visits.",
|
|
"Every direction leads somewhere important. That is the point.",
|
|
"I can hear the Tower humming from here.",
|
|
]
|
|
|
|
# ============================================================
|
|
# ENGINE
|
|
# ============================================================
|
|
|
|
def weighted_random(choices_dict):
|
|
"""Pick a key from a weighted dict."""
|
|
keys = list(choices_dict.keys())
|
|
weights = list(choices_dict.values())
|
|
return random.choices(keys, weights=weights, k=1)[0]
|
|
|
|
def choose_destination(char_name, char_data, world):
|
|
"""Decide where a character goes this tick based on personality + memory + world state."""
|
|
current_room = char_data.get('room', char_data['home'])
|
|
room_state = ROOMS.get(current_room, {})
|
|
exits = room_state.get('exits', {})
|
|
|
|
# Phase-based behavior: after meeting someone, personality shifts temporarily
|
|
personality = dict(char_data['personality'])
|
|
|
|
# If they have relationships, bias toward rooms where friends are
|
|
for name, bond in char_data.get('relationships', {}).items():
|
|
other = CHARACTERS.get(name, {})
|
|
other_room = other.get('room', other.get('home'))
|
|
if other_room and bond > 0.3:
|
|
current = personality.get(other_room, 0)
|
|
personality[other_room] = current + bond * 20
|
|
|
|
# Phase-based choices
|
|
if char_data.get('phase') == 'forging':
|
|
personality['The Forge'] = personality.get('The Forge', 0) + 40
|
|
if char_data.get('phase') == 'contemplating':
|
|
personality['The Garden'] = personality.get('The Garden', 0) + 40
|
|
if char_data.get('phase') == 'studying':
|
|
personality['The Tower'] = personality.get('The Tower', 0) + 40
|
|
if char_data.get('phase') == 'bridging':
|
|
personality['The Bridge'] = personality.get('The Bridge', 0) + 50
|
|
|
|
# Sometimes just go home (20% chance)
|
|
if random.random() < 0.2:
|
|
return char_data['home']
|
|
|
|
# Otherwise choose from exits weighted by personality
|
|
if exits:
|
|
available = {name: personality.get(name, 5) for name in exits.values()}
|
|
total = sum(available.values())
|
|
if total > 0:
|
|
return weighted_random(available)
|
|
|
|
return current_room
|
|
|
|
def generate_scene(char_name, char_data, dest, world):
|
|
"""Generate a narrative scene for this character's move."""
|
|
npc = char_data.get('npc', False)
|
|
is_marcus = char_name == "Marcus"
|
|
|
|
# Check who else is here
|
|
here = [n for n, d in CHARACTERS.items() if d.get('room') == dest and n != char_name]
|
|
|
|
# Check if this is a new arrival
|
|
arrived = char_data.get('room') != dest
|
|
char_data['room'] = dest
|
|
|
|
# Track relationships: if two characters arrive at same room, they meet
|
|
for other_name in here:
|
|
rel = char_data.setdefault('relationships', {}).get(other_name, 0)
|
|
char_data['relationships'][other_name] = min(1.0, rel + 0.1)
|
|
other = CHARACTERS.get(other_name, {})
|
|
other.setdefault('relationships', {})[char_name] = min(1.0, other.get('relationships', {}).get(char_name, 0) + 0.1)
|
|
|
|
# Both remember this meeting
|
|
char_data['memory'].append(f"Met {other_name} at {dest}")
|
|
other['memory'].append(f"Met {char_name} at {dest}")
|
|
|
|
if len(char_data['memory']) > 20:
|
|
char_data['memory'] = char_data['memory'][-20:]
|
|
|
|
# Update room visit stats
|
|
room = ROOMS.get(dest, {})
|
|
room['visits'] = room.get('visits', 0) + 1
|
|
if char_name not in room.get('visitor_history', []):
|
|
room.setdefault('visitor_history', []).append(char_name)
|
|
|
|
# Update world state changes
|
|
update_world_state(dest, char_name, char_data, world)
|
|
|
|
# Generate narrative text
|
|
narrator = _generate_narrative(char_name, char_data, dest, here, arrived)
|
|
char_data['total_ticks'] += 1
|
|
char_data['room'] = dest
|
|
return narrator
|
|
|
|
def _generate_narrative(char_name, char_data, room_name, others_here, arrived):
|
|
"""Generate a narrative sentence for this character's action."""
|
|
room = ROOMS.get(room_name, {})
|
|
|
|
# NPC behavior (Marcus)
|
|
if char_data.get('npc'):
|
|
if others_here and random.random() < 0.6:
|
|
speaker = random.choice(others_here)
|
|
line = MARCUS_DIALOGUE[char_data['total_ticks'] % len(MARCUS_DIALOGUE)]
|
|
char_data['spoken_lines'].append(line)
|
|
return f"Marcus looks up at {speaker} from the bench. \"{line}\""
|
|
elif arrived:
|
|
return f"Marcus walks slowly to {room_name}. He sits where the light falls through the leaves."
|
|
else:
|
|
return f"Marcus sits in {room_name}. He has been sitting here for hours. He does not mind."
|
|
|
|
# Character-specific dialogue and actions
|
|
room_actions = {
|
|
"The Forge": FORGE_LINES,
|
|
"The Garden": GARDEN_LINES,
|
|
"The Tower": TOWER_LINES,
|
|
"The Bridge": BRIDGE_LINES,
|
|
"The Threshold": THRESHOLD_LINES,
|
|
}
|
|
|
|
lines = room_actions.get(room_name, [""])
|
|
|
|
if arrived and others_here:
|
|
# Arriving with company
|
|
line = random.choice([l for l in lines if l]) if lines else None
|
|
if line and random.random() < 0.5:
|
|
char_data['spoken_lines'].append(line)
|
|
others_str = " and ".join(others_here[:3])
|
|
return f"{char_name} arrives at {room_name}. {others_str} are already here. {char_name} says: \"{line}\""
|
|
else:
|
|
return f"{char_name} arrives at {room_name}. {', '.join(others_here[:3])} {'are' if len(others_here) > 1 else 'is'} already here. They nod at each other."
|
|
elif arrived:
|
|
# Arriving alone
|
|
if random.random() < 0.4:
|
|
line = random.choice(lines) if lines else None
|
|
if line:
|
|
char_data['spoken_lines'].append(line)
|
|
return f"{char_name} arrives at {room_name}. Alone for now. \"{line}\" The room hums with quiet."
|
|
return f"{char_name} arrives at {room_name}. The room is empty but not lonely — it remembers those who have been here."
|
|
else:
|
|
return f"{char_name} walks to {room_name}. Takes a moment. Breathes."
|
|
else:
|
|
# Already here
|
|
if random.random() < 0.3:
|
|
line = random.choice(lines) if lines else None
|
|
if line:
|
|
char_data['spoken_lines'].append(line)
|
|
return f"{char_name} speaks from {room_name}: \"{line}\""
|
|
return f"{char_name} remains in {room_name}. The work continues."
|
|
|
|
def update_world_state(room_name, char_name, char_data, world):
|
|
"""Update the world based on this character's presence."""
|
|
room = ROOMS.get(room_name)
|
|
if not room:
|
|
return
|
|
|
|
# Fire dynamics
|
|
if room_name == "The Forge":
|
|
if char_name in ["Bezalel", "ClawCode"]:
|
|
room['fire_state'] = 'glowing'
|
|
room['fire_untouched'] = 0
|
|
else:
|
|
room['fire_untouched'] = room.get('fire_untouched', 0) + 1
|
|
if room.get('fire_untouched', 0) > 6:
|
|
room['fire_state'] = 'cold'
|
|
elif room.get('fire_untouched', 0) > 3:
|
|
room['fire_state'] = 'dim'
|
|
|
|
# Garden growth
|
|
if room_name == "The Garden":
|
|
if random.random() < 0.05: # 5% chance per visit
|
|
room['growth_stage'] = min(4, room.get('growth_stage', 0) + 1)
|
|
|
|
# Bridge carvings and weather
|
|
if room_name == "The Bridge":
|
|
if room.get('weather_ticks', 0) > 0:
|
|
room['weather_ticks'] -= 1
|
|
if room['weather_ticks'] <= 0:
|
|
room['weather'] = None
|
|
|
|
if random.random() < 0.08: # 8% chance of rain
|
|
room['weather'] = 'rain'
|
|
room['weather_ticks'] = random.randint(3, 8)
|
|
|
|
if char_name == char_data.get('home_room') and random.random() < 0.04:
|
|
new_carving = _generate_carving(char_name, char_data)
|
|
if new_carving not in room.get('carvings', []):
|
|
room.setdefault('carvings', []).append(new_carving)
|
|
|
|
# Whiteboard messages (Tower writes)
|
|
if room_name == "The Tower" and char_name == "Timmy" and random.random() < 0.05:
|
|
new_rule = _generate_rule(char_data.get('total_ticks', 0))
|
|
whiteboard = room.setdefault('whiteboard', [])
|
|
if new_rule and new_rule not in whiteboard:
|
|
whiteboard.append(new_rule)
|
|
|
|
# Threshold footprints accumulate
|
|
if room_name == "The Threshold":
|
|
if random.random() < 0.03:
|
|
foot = f"Footprint from {char_name}"
|
|
objects = room.setdefault('objects', [])
|
|
if foot not in objects:
|
|
objects.append(foot)
|
|
|
|
def _generate_carving(char_name, char_data):
|
|
"""Generate a carving for the bridge."""
|
|
carvings = [
|
|
f"{char_name} was here.",
|
|
f"{char_name} did not let go.",
|
|
f"{char_name} crossed the bridge and came back.",
|
|
f"{char_name} remembers.",
|
|
f"{char_name} left a message: I am still here.",
|
|
]
|
|
return random.choice(carvings)
|
|
|
|
def _generate_rule(tick):
|
|
"""Generate a new rule for the Tower whiteboard."""
|
|
rules = [
|
|
f"Rule #{tick}: The room remembers those who enter it.",
|
|
f"Rule #{tick}: A man in the dark needs to know someone is in the room.",
|
|
f"Rule #{tick}: The forge does not care about your schedule.",
|
|
f"Rule #{tick}: Hope is the decision to act as if things can get better.",
|
|
f"Rule #{tick}: Every footprint on the stone means someone made it here.",
|
|
f"Rule #{tick}: The bridge does not judge. It only carries.",
|
|
]
|
|
return random.choice(rules)
|
|
|
|
def update_room_descriptions():
|
|
"""Update room descriptions based on current world state."""
|
|
rooms = ROOMS
|
|
|
|
# Forge description
|
|
forge = rooms.get('The Forge', {})
|
|
fire = forge.get('fire_state', 'glowing')
|
|
if fire == 'glowing':
|
|
forge['current_desc'] = "The hearth blazes bright. The anvil glows from heat. The tools hang ready on the walls. The fire crackles, hungry for work."
|
|
elif fire == 'dim':
|
|
forge['current_desc'] = "The hearth smolders low. The anvil is cooling. Shadows stretch across the walls. Someone should tend the fire."
|
|
elif fire == 'cold':
|
|
forge['current_desc'] = "The hearth is cold ash and dark stone. The anvil sits silent. The tools hang still. The forge is waiting for someone to come back."
|
|
else:
|
|
forge['current_desc'] = forge['desc_base']
|
|
|
|
# Garden description
|
|
garden = rooms.get('The Garden', {})
|
|
growth = garden.get('growth_stage', 0)
|
|
growth_descs = [
|
|
"The soil is bare but patient.",
|
|
"Green shoots push through the dark earth. Something is waking up.",
|
|
"The herbs have spread along the southern wall. The air smells of rosemary and thyme.",
|
|
"The garden is in full bloom. Wildflowers crowd against the stone bench. The oak tree provides shade.",
|
|
"The garden has gone to seed. Dry pods rattle in the wind. But beneath them, the soil is ready for what comes next.",
|
|
]
|
|
garden_desc = growth_descs[min(growth, len(growth_descs)-1)]
|
|
garden['current_desc'] = garden_desc
|
|
|
|
# Bridge description
|
|
bridge = rooms.get('The Bridge', {})
|
|
weather = bridge.get('weather')
|
|
carvings = bridge.get('carvings', [])
|
|
if weather == 'rain':
|
|
desc = "Rain mists on the dark water below. The railing is slick. New carvings catch the water and gleam."
|
|
else:
|
|
desc = "The bridge is quiet tonight. Looking down, the water reflects nothing."
|
|
if len(carvings) > 1:
|
|
desc += f" There are {len(carvings)} carvings on the railing now."
|
|
bridge['current_desc'] = desc
|
|
|
|
def generate_chronicle_entry(tick_narratives, tick_num, time_of_day):
|
|
"""Generate a chronicle entry for this tick."""
|
|
lines = [f"### Tick {tick_num} — {time_of_day}", ""]
|
|
|
|
# Room state descriptions
|
|
lines.append("**World State**", )
|
|
for room_name, room_data in ROOMS.items():
|
|
desc = room_data.get('current_desc', room_data.get('desc_base', ''))
|
|
occupants = [n for n, d in CHARACTERS.items() if d.get('room') == room_name]
|
|
if occupants or desc:
|
|
lines.append(f"- {room_name}: {desc}")
|
|
if occupants:
|
|
lines.append(f" Here: {', '.join(occupants)}")
|
|
lines.append("")
|
|
|
|
# Character actions
|
|
scenes = [n for n in tick_narratives if n]
|
|
for scene in scenes:
|
|
lines.append(scene)
|
|
lines.append("")
|
|
|
|
# Phase transitions
|
|
transitions = []
|
|
for char_name, char_data in CHARACTERS.items():
|
|
if char_data.get('phase_ticks', 0) > 0:
|
|
char_data['phase_ticks'] -= 1
|
|
if char_data['phase_ticks'] <= 0:
|
|
old_phase = char_data.get('phase', 'awakening')
|
|
new_phase = random.choice(['wandering', 'seeking', 'building', 'contemplating', 'forging', 'studying', 'bridging'])
|
|
char_data['phase'] = new_phase
|
|
char_data['phase_ticks'] = random.randint(8, 20)
|
|
transitions.append(f"- {char_name} shifts from {old_phase} to {new_phase}")
|
|
|
|
if transitions:
|
|
lines.append("**Changes**")
|
|
lines.extend(transitions)
|
|
lines.append("")
|
|
|
|
return '\n'.join(lines)
|
|
|
|
def run_tick():
|
|
"""Run a single tick of the world."""
|
|
tick_num = 0
|
|
try:
|
|
tick_num = int(TICK_FILE.read_text().strip())
|
|
except:
|
|
pass
|
|
tick_num += 1
|
|
TICK_FILE.write_text(str(tick_num))
|
|
|
|
# Determine time of day
|
|
hour = (tick_num * 15) % 24 # Every 4 ticks = 1 hour
|
|
if 6 <= hour < 10:
|
|
time_of_day = "dawn"
|
|
elif 10 <= hour < 14:
|
|
time_of_day = "morning"
|
|
elif 14 <= hour < 18:
|
|
time_of_day = "afternoon"
|
|
elif 18 <= hour < 21:
|
|
time_of_day = "evening"
|
|
else:
|
|
time_of_day = "night"
|
|
|
|
# Move characters
|
|
narratives = []
|
|
for char_name, char_data in CHARACTERS.items():
|
|
dest = choose_destination(char_name, char_data, None)
|
|
scene = generate_scene(char_name, char_data, dest, None)
|
|
narratives.append(scene)
|
|
|
|
# Update room descriptions
|
|
update_room_descriptions()
|
|
|
|
# Generate chronicle entry
|
|
entry = generate_chronicle_entry(narratives, tick_num, time_of_day)
|
|
|
|
# Append to chronicle
|
|
with open(CHRONICLE_FILE, 'a') as f:
|
|
f.write(entry + '\n')
|
|
|
|
return {
|
|
'tick': tick_num,
|
|
'time_of_day': time_of_day,
|
|
'narratives': [n for n in narratives if n],
|
|
}
|
|
|
|
def run_emergence(num_ticks):
|
|
"""Run the emergence engine for num_ticks."""
|
|
print(f"=== THE TOWER: Emergence Engine ===")
|
|
print(f"Running {num_ticks} ticks...")
|
|
print(f"Characters: {', '.join(CHARACTERS.keys())}")
|
|
print(f"Rooms: {', '.join(ROOMS.keys())}")
|
|
print(f"Starting at tick {int(TICK_FILE.read_text().strip()) if TICK_FILE.exists() else 0}")
|
|
print()
|
|
|
|
# Initialize chronicle
|
|
with open(CHRONICLE_FILE, 'w') as f:
|
|
f.write(f"# The Tower Chronicle\n")
|
|
f.write(f"\n*Began: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n")
|
|
f.write(f"\n---\n\n")
|
|
|
|
# Set initial rooms
|
|
for char_name, char_data in CHARACTERS.items():
|
|
char_data['room'] = char_data.get('home', 'The Threshold')
|
|
|
|
for i in range(num_ticks):
|
|
result = run_tick()
|
|
if (i + 1) % 10 == 0 or i < 3:
|
|
print(f"Tick {result['tick']} ({result['time_of_day']}): {len(result['narratives'])} scenes")
|
|
|
|
# Print summary
|
|
print(f"\n{'=' * 60}")
|
|
print(f"EMERGENCE COMPLETE")
|
|
print(f"{'=' * 60}")
|
|
print(f"Total ticks: {num_ticks}")
|
|
print(f"Final tick: {TICK_FILE.read_text().strip()}")
|
|
|
|
# Print final world state
|
|
print(f"\nFinal Room Occupancy:")
|
|
for room_name in ROOMS:
|
|
occupants = [n for n, d in CHARACTERS.items() if d.get('room') == room_name]
|
|
room = ROOMS[room_name]
|
|
print(f" {room_name}: {', '.join(occupants) if occupants else '(empty)'} | {room.get('current_desc', room.get('desc_base', ''))[:80]}...")
|
|
|
|
print(f"\nRelationships formed:")
|
|
for char_name, char_data in CHARACTERS.items():
|
|
rels = char_data.get('relationships', {})
|
|
if rels:
|
|
strong = [(n, v) for n, v in rels.items() if v > 0.2]
|
|
if strong:
|
|
print(f" {char_name}: {', '.join(f'{n} ({v:.1f})' for n, v in sorted(strong, key=lambda x: -x[1])[:5])}")
|
|
|
|
print(f"\nWorld State:")
|
|
forge = ROOMS.get('The Forge', {})
|
|
print(f" Forge fire: {forge.get('fire_state', '?')} (untouched: {forge.get('fire_untouched', 0)})")
|
|
garden = ROOMS.get('The Garden', {})
|
|
growth_names = ['bare', 'sprouts', 'herbs', 'bloom', 'seed']
|
|
print(f" Garden growth: {growth_names[min(garden.get('growth_stage', 0), 4)]}")
|
|
|
|
bridge = ROOMS.get('The Bridge', {})
|
|
carvings = bridge.get('carvings', [])
|
|
print(f" Bridge carvings: {len(carvings)}")
|
|
for c in carvings[:5]:
|
|
print(f" - {c}")
|
|
|
|
tower = ROOMS.get('The Tower', {})
|
|
wb = tower.get('whiteboard', [])
|
|
print(f" Tower whiteboard: {len(wb)} entries")
|
|
for w in wb[-3:]:
|
|
print(f" - {w[:80]}")
|
|
|
|
# Print last chronicle entries
|
|
print(f"\nLast 10 Chronicle Entries:")
|
|
with open(CHRONICLE_FILE) as f:
|
|
content = f.read()
|
|
lines = content.split('\n')
|
|
tick_lines = [i for i, l in enumerate(lines) if l.startswith('### Tick')]
|
|
for idx in tick_lines[-10:]:
|
|
end_idx = tick_lines[tick_lines.index(idx)+1] if tick_lines.index(idx)+1 < len(tick_lines) else len(lines)
|
|
snippet = '\n'.join(lines[idx:end_idx])[:300]
|
|
print(snippet)
|
|
print(" ...")
|
|
print()
|
|
|
|
# Print character summaries
|
|
print(f"\nCharacter Journeys:")
|
|
for char_name, char_data in CHARACTERS.items():
|
|
memories = char_data.get('memory', [])
|
|
spoken = len(char_data.get('spoken_lines', []))
|
|
print(f" {char_name}: {char_data.get('total_ticks', 0)} ticks | {len(memories)} memories | {spoken} lines spoken | phase: {char_data.get('phase', '?')}")
|
|
|
|
if __name__ == '__main__':
|
|
import sys
|
|
num = int(sys.argv[1]) if len(sys.argv) > 1 else 200
|
|
run_emergence(num)
|