UNWRITTEN: complete short-form RPG at games/unwritten/

A 30-45 minute turn-based RPG built overnight as an engine showcase:
six recruitable characters (party of three), four recurring NPCs,
choice-driven dialogue with visible-but-locked skill checks, shops,
loot, levels, two bosses with in-battle Talk options, the reverted
grey-town beat, and an epilogue assembled from the player's actual
choices. 33 dialogue scenes / 171 nodes. Terrain is ColorLayers only;
kenney_tinydungeon supplies characters, items, and props.

Creative direction, story, dialogue, maps, and systems design by
Fable; implementation by four Opus subagents (framework, battle,
overworld, integration). Verified by tests/playthrough.py: a scripted
headless run of all three acts, 20/20 beats passing, plus per-system
unit tests (script integrity, UI kit, 225-battle balance sim,
overworld).

Run: cd build && ./mcrogueface --exec ../games/unwritten/main.py

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014vxWJ6GY384SmZiMVXgrF4
This commit is contained in:
John McCardle 2026-07-03 10:11:12 -04:00
commit 80908f771c
31 changed files with 8847 additions and 0 deletions

View file

@ -0,0 +1,46 @@
"""UNWRITTEN - modal input dispatch. Owner: Agent A.
One InputStack per scene owns scene.on_key and dispatches to a stack of
handlers, top-most first. A handler is fn(key, state) -> bool; returning True
consumes the event (lower handlers do not see it). Overworld pushes its
movement handler; a DialogueBox / menu pushes itself while open and pops on
close, freezing everything beneath it.
"""
class InputStack:
def __init__(self, scene):
self.scene = scene
self.stack = [] # list of (name, handler)
scene.on_key = self._dispatch
def push(self, handler, name=""):
self.stack.append((name, handler))
def pop(self, name=""):
"""Pop the top handler, or the top-most handler matching name."""
if name:
for i in range(len(self.stack) - 1, -1, -1):
if self.stack[i][0] == name:
return self.stack.pop(i)
return None
if self.stack:
return self.stack.pop()
return None
def replace(self, handler, name=""):
"""Replace the top handler with a new one."""
self.pop()
self.push(handler, name)
def clear(self):
self.stack = []
def has(self, name):
return any(n == name for n, _ in self.stack)
def _dispatch(self, key, state):
for i in range(len(self.stack) - 1, -1, -1):
handler = self.stack[i][1]
if handler(key, state):
return