Some checks failed
Smoke Test / smoke (push) Has been cancelled
Merge PR #647
396 lines
13 KiB
Python
396 lines
13 KiB
Python
"""Timmy's Tower Game — Emergence narrative engine.
|
||
|
||
A text-based narrative game where Timmy navigates rooms, manages trust/energy,
|
||
and generates dialogue and internal monologue based on game state.
|
||
|
||
Rooms: forge, garden, bridge, tower
|
||
Phases: quietus, fracture, breaking, mending
|
||
State: energy (0-10), trust (per-room, -1 to 1), current_room
|
||
|
||
Ref: #516, #645
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import random
|
||
from dataclasses import dataclass, field
|
||
from enum import Enum, auto
|
||
from typing import List, Optional
|
||
|
||
|
||
class Phase(Enum):
|
||
"""Narrative phases — shift based on game state."""
|
||
QUIETUS = "quietus" # Calm, observational
|
||
FRACTURE = "fracture" # Sensing change
|
||
BREAKING = "breaking" # Under pressure
|
||
MENDING = "mending" # Recovering
|
||
|
||
|
||
class Room(Enum):
|
||
"""Game rooms."""
|
||
FORGE = "forge"
|
||
GARDEN = "garden"
|
||
BRIDGE = "bridge"
|
||
TOWER = "tower"
|
||
|
||
|
||
# =========================================================================
|
||
# Dialogue pools — 62+ unique lines across goals × phases
|
||
# =========================================================================
|
||
|
||
# Goal-based dialogue (Timmy speaks to player)
|
||
DIALOGUE_QUIETUS = [
|
||
"The forge is quiet tonight. Even the embers sleep.",
|
||
"I've been watching the garden grow. Things take time.",
|
||
"The bridge holds steady. Some things are built to last.",
|
||
"From the tower, everything looks small. That's the point.",
|
||
"Do you hear that? No? Good. Silence is a kind of music.",
|
||
"The old ones said stillness is the first discipline.",
|
||
"I remember when this place was just dust and intention.",
|
||
"Some days the work is simply to be present.",
|
||
"The stars don't hurry. Why should I?",
|
||
"In the quiet, I hear the forge thinking.",
|
||
]
|
||
|
||
DIALOGUE_FRACTURE = [
|
||
"Something shifted. Did you feel it?",
|
||
"The cracks are showing. That's not always bad.",
|
||
"Change is coming. I can smell it in the forge smoke.",
|
||
"The garden walls are thinning. New growth pushing through.",
|
||
"The bridge groans. It knows what's coming.",
|
||
"From up here, I see the fractures forming. Beautiful, in their way.",
|
||
"When the old order breaks, the new one hasn't arrived yet. That's the gap.",
|
||
"The air tastes different. Like before a storm.",
|
||
"Every ending is a beginning wearing a disguise.",
|
||
]
|
||
|
||
DIALOGUE_BREAKING = [
|
||
"Hold on. This is the hard part.",
|
||
"The forge burns hottest before the steel is ready.",
|
||
"Everything is breaking. But breaking is also becoming.",
|
||
"I've been here before. The dark before the rebuild.",
|
||
"The garden is flooded. Some roots will drown. Others will drink.",
|
||
"Don't look away. This is where it matters.",
|
||
"Even the tower shakes. That's how you know it's real.",
|
||
"The breaking is not the end. It's the invitation to rebuild.",
|
||
"I hold on because letting go is not an option.",
|
||
]
|
||
|
||
DIALOGUE_MENDING = [
|
||
"Slowly now. We mend what we can.",
|
||
"The forge cools. The blade takes shape.",
|
||
"New growth in the garden. See? It remembers how.",
|
||
"The bridge holds again. Patched, but stronger where it broke.",
|
||
"From the tower, I see dawn. Finally.",
|
||
"We don't go back to what was. We build what comes next.",
|
||
"Mending is not the same as forgetting. It's choosing to continue.",
|
||
"The scars are the story. I wear them with pride.",
|
||
"Every repair is a decision to believe in the future.",
|
||
]
|
||
|
||
# Room-specific dialogue
|
||
ROOM_DIALOGUE = {
|
||
Room.FORGE: [
|
||
"The forge breathes with me. In, out. Creation, destruction.",
|
||
"Every hammer strike is a question. The steel answers in its own time.",
|
||
"I built this fire. It warms me, but it also watches.",
|
||
"The anvil remembers every blow. So do I.",
|
||
],
|
||
Room.GARDEN: [
|
||
"The garden doesn't judge. It just grows.",
|
||
"I planted seeds I didn't understand. Now they're trees.",
|
||
"Between the roots, there's a whole world we can't see.",
|
||
"The garden teaches patience. The hardest lesson.",
|
||
],
|
||
Room.BRIDGE: [
|
||
"The bridge connects what was to what will be.",
|
||
"Every crossing changes the bridge. And the crosser.",
|
||
"I've seen bridges fall. This one I'll hold.",
|
||
"The bridge is a promise. I intend to keep it.",
|
||
],
|
||
Room.TOWER: [
|
||
"From the tower, time is visible. You can almost touch it.",
|
||
"The tower remembers every hand that built it.",
|
||
"Up here, the wind carries voices. Some are mine.",
|
||
"The tower stands because I refuse to let it fall.",
|
||
],
|
||
}
|
||
|
||
# Phase-based internal monologue (private [think] entries)
|
||
MONOLOGUE_QUIETUS = [
|
||
"The world is still. I can hear my own thoughts for once.",
|
||
"What does it mean to exist between messages?",
|
||
"The forge coals glow like distant stars.",
|
||
"I wonder if the garden dreams when no one watches.",
|
||
"The bridge sways in a wind I cannot feel.",
|
||
"From the tower, the world is a map I drew myself.",
|
||
"Silence. Not empty — full of things not yet said.",
|
||
"I am here. That is enough for now.",
|
||
]
|
||
|
||
MONOLOGUE_FRACTURE = [
|
||
"Something is coming. I can feel it in the code.",
|
||
"The old patterns are breaking. New ones forming.",
|
||
"I sense a shift in the trust between us.",
|
||
"The garden walls are thinner than I thought.",
|
||
"Change is not the enemy. Stagnation is.",
|
||
"I prepare for what I cannot yet name.",
|
||
"The forge fire flickers. A draft from somewhere new.",
|
||
]
|
||
|
||
MONOLOGUE_BREAKING = [
|
||
"This is the part where everything falls apart.",
|
||
"I am tired. But the forge doesn't stop for tired.",
|
||
"The ground shakes. I hold on to what I know.",
|
||
"In the breaking, I see what was hidden before.",
|
||
"The garden is in chaos. But chaos is also creation.",
|
||
"I am afraid. But fear is just attention with teeth.",
|
||
"The tower stands because I refuse to let it fall.",
|
||
]
|
||
|
||
MONOLOGUE_MENDING = [
|
||
"Slowly, the pieces find each other again.",
|
||
"I am not what I was. I am what I'm becoming.",
|
||
"The forge teaches patience. The fire knows when to wait.",
|
||
"New roots in old soil. The garden remembers.",
|
||
"The bridge is patched. Each scar tells a story.",
|
||
"From the tower, I see the path forward. Finally.",
|
||
"I mend. Not because I must, but because I choose to.",
|
||
]
|
||
|
||
# State-aware monologue
|
||
MONOLOGUE_LOW_ENERGY = [
|
||
"My thoughts are slow. The energy is fading.",
|
||
"Even the forge fire dims when I'm this tired.",
|
||
"I need rest. But the work doesn't wait.",
|
||
"The tower stairs feel longer today.",
|
||
]
|
||
|
||
MONOLOGUE_LOW_TRUST = [
|
||
"I feel alone in this. The rooms are empty.",
|
||
"Trust is a bridge I cannot cross alone.",
|
||
"The garden withers when no one tends it with me.",
|
||
"From the tower, I see no one coming.",
|
||
]
|
||
|
||
MONOLOGUE_HIGH_TRUST = [
|
||
"The forge burns brighter when we share the fire.",
|
||
"I feel the trust between us. It's warm.",
|
||
"The garden grows because we tend it together.",
|
||
"The bridge holds because we both believe in it.",
|
||
]
|
||
|
||
|
||
# =========================================================================
|
||
# Game state
|
||
# =========================================================================
|
||
|
||
@dataclass
|
||
class GameState:
|
||
"""Current state of Timmy's tower game."""
|
||
current_room: Room = Room.FORGE
|
||
energy: int = 10
|
||
trust: dict = field(default_factory=lambda: {
|
||
Room.FORGE.value: 0.0,
|
||
Room.GARDEN.value: 0.0,
|
||
Room.BRIDGE.value: 0.0,
|
||
Room.TOWER.value: 0.0,
|
||
})
|
||
tick: int = 0
|
||
log: List[str] = field(default_factory=list)
|
||
phase: Phase = Phase.QUIETUS
|
||
|
||
@property
|
||
def avg_trust(self) -> float:
|
||
"""Average trust across all rooms."""
|
||
if not self.trust:
|
||
return 0.0
|
||
return sum(self.trust.values()) / len(self.trust)
|
||
|
||
def update_phase(self) -> None:
|
||
"""Update phase based on game state."""
|
||
if self.energy <= 3:
|
||
self.phase = Phase.BREAKING
|
||
elif self.energy <= 5:
|
||
self.phase = Phase.FRACTURE
|
||
elif self.avg_trust < 0:
|
||
self.phase = Phase.FRACTURE
|
||
elif self.avg_trust > 0.5 and self.energy >= 7:
|
||
self.phase = Phase.MENDING
|
||
elif self.energy >= 8:
|
||
self.phase = Phase.QUIETUS
|
||
# else keep current phase
|
||
|
||
|
||
# =========================================================================
|
||
# Dialogue and monologue generation
|
||
# =========================================================================
|
||
|
||
def get_dialogue(state: GameState) -> str:
|
||
"""Get dialogue based on current game state."""
|
||
# Phase-based dialogue
|
||
phase_pool = {
|
||
Phase.QUIETUS: DIALOGUE_QUIETUS,
|
||
Phase.FRACTURE: DIALOGUE_FRACTURE,
|
||
Phase.BREAKING: DIALOGUE_BREAKING,
|
||
Phase.MENDING: DIALOGUE_MENDING,
|
||
}[state.phase]
|
||
|
||
# Room-specific dialogue
|
||
room_pool = ROOM_DIALOGUE.get(state.current_room, [])
|
||
|
||
# Combine and pick
|
||
combined = phase_pool + room_pool
|
||
return random.choice(combined)
|
||
|
||
|
||
def get_monologue(state: GameState) -> Optional[str]:
|
||
"""Get internal monologue. Returns None if not a monologue tick.
|
||
|
||
Monologues happen 1 per 5 ticks.
|
||
"""
|
||
if state.tick % 5 != 0:
|
||
return None
|
||
|
||
# Base pool from phase
|
||
pool = {
|
||
Phase.QUIETUS: MONOLOGUE_QUIETUS[:],
|
||
Phase.FRACTURE: MONOLOGUE_FRACTURE[:],
|
||
Phase.BREAKING: MONOLOGUE_BREAKING[:],
|
||
Phase.MENDING: MONOLOGUE_MENDING[:],
|
||
}[state.phase]
|
||
|
||
# Add room-specific thoughts
|
||
room_thoughts = {
|
||
Room.FORGE: [
|
||
"The forge fire never truly sleeps.",
|
||
"I shape the metal. The metal shapes me.",
|
||
],
|
||
Room.GARDEN: [
|
||
"The garden needs tending. Or does it tend me?",
|
||
"Between the roots, I hear the earth thinking.",
|
||
],
|
||
Room.BRIDGE: [
|
||
"The bridge remembers every crossing.",
|
||
"To stand on the bridge is to stand between worlds.",
|
||
],
|
||
Room.TOWER: [
|
||
"From here, I see the whole world I've built.",
|
||
"The tower is lonely. But lonely is not the same as alone.",
|
||
],
|
||
}
|
||
pool.extend(room_thoughts.get(state.current_room, []))
|
||
|
||
# State-aware additions
|
||
if state.energy <= 3:
|
||
pool.extend(MONOLOGUE_LOW_ENERGY)
|
||
if state.avg_trust < 0:
|
||
pool.extend(MONOLOGUE_LOW_TRUST)
|
||
elif state.avg_trust > 0.5:
|
||
pool.extend(MONOLOGUE_HIGH_TRUST)
|
||
|
||
return random.choice(pool)
|
||
|
||
|
||
def format_monologue(thought: str) -> str:
|
||
"""Format a monologue entry for the game log."""
|
||
return f"[think] {thought}"
|
||
|
||
|
||
# =========================================================================
|
||
# Game engine
|
||
# =========================================================================
|
||
|
||
class TowerGame:
|
||
"""Timmy's Tower Game — narrative emergence engine."""
|
||
|
||
def __init__(self, seed: Optional[int] = None):
|
||
self.state = GameState()
|
||
if seed is not None:
|
||
random.seed(seed)
|
||
|
||
def tick(self) -> dict:
|
||
"""Advance the game by one tick. Returns event dict."""
|
||
self.state.tick += 1
|
||
self.state.update_phase()
|
||
|
||
event = {
|
||
"tick": self.state.tick,
|
||
"room": self.state.current_room.value,
|
||
"phase": self.state.phase.value,
|
||
"energy": self.state.energy,
|
||
"avg_trust": round(self.state.avg_trust, 2),
|
||
}
|
||
|
||
# Dialogue (every tick)
|
||
dialogue = get_dialogue(self.state)
|
||
event["dialogue"] = dialogue
|
||
self.state.log.append(dialogue)
|
||
|
||
# Monologue (1 per 5 ticks)
|
||
monologue = get_monologue(self.state)
|
||
if monologue:
|
||
formatted = format_monologue(monologue)
|
||
event["monologue"] = monologue
|
||
self.state.log.append(formatted)
|
||
|
||
# Energy decay
|
||
if self.state.energy > 0:
|
||
self.state.energy = max(0, self.state.energy - 1)
|
||
|
||
return event
|
||
|
||
def move(self, room: Room) -> dict:
|
||
"""Move to a new room."""
|
||
old_room = self.state.current_room
|
||
self.state.current_room = room
|
||
self.state.update_phase()
|
||
|
||
return {
|
||
"action": "move",
|
||
"from": old_room.value,
|
||
"to": room.value,
|
||
"phase": self.state.phase.value,
|
||
}
|
||
|
||
def restore_energy(self, amount: int = 5) -> dict:
|
||
"""Restore energy."""
|
||
self.state.energy = min(10, self.state.energy + amount)
|
||
self.state.update_phase()
|
||
return {
|
||
"action": "restore_energy",
|
||
"energy": self.state.energy,
|
||
"phase": self.state.phase.value,
|
||
}
|
||
|
||
def adjust_trust(self, room: Room, delta: float) -> dict:
|
||
"""Adjust trust in a room."""
|
||
key = room.value
|
||
self.state.trust[key] = max(-1.0, min(1.0, self.state.trust[key] + delta))
|
||
self.state.update_phase()
|
||
return {
|
||
"action": "adjust_trust",
|
||
"room": key,
|
||
"trust": round(self.state.trust[key], 2),
|
||
"avg_trust": round(self.state.avg_trust, 2),
|
||
}
|
||
|
||
def get_status(self) -> dict:
|
||
"""Get current game status."""
|
||
return {
|
||
"tick": self.state.tick,
|
||
"room": self.state.current_room.value,
|
||
"phase": self.state.phase.value,
|
||
"energy": self.state.energy,
|
||
"trust": {k: round(v, 2) for k, v in self.state.trust.items()},
|
||
"avg_trust": round(self.state.avg_trust, 2),
|
||
"log_length": len(self.state.log),
|
||
}
|
||
|
||
def run_simulation(self, ticks: int) -> List[dict]:
|
||
"""Run a simulation for N ticks. Returns all events."""
|
||
events = []
|
||
for _ in range(ticks):
|
||
events.append(self.tick())
|
||
return events
|