Files
timmy-home/evennia/timmy_world/world/emergence.py
Alexander Whitestone 5b419c9f22 feat: Emergence Engine — 1464 ticks of world history
- 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
2026-04-06 11:31:49 -04:00

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)