architecture plan — designing for programmability ==================================================== where we are and where we're going. we have a working telnet MUD with procedural terrain, movement, viewport rendering, effects. we've built the command system, game loop, mode stack, content loading, entity model, combat, persistence, and editor mode. in-world programming remains ahead. this doc captured what we needed to get right in the foundation — most of it is done. the core lesson from studying the ecosystem: the things that lasted (LPC mudlibs, MOO worlds, evennia typeclasses) all share one trait — game content is data that can be inspected and modified at runtime, not hardcoded logic. the things that didn't last hardcoded everything. the engine/content boundary ============================ the single most important architectural decision. everything downstream depends on this. what the engine provides (python code, changes require restart): - telnet server, protocol handling, I/O - the game loop (tick processing) - terrain generation and world loading - rendering pipeline (viewport, ANSI, effects) - the command dispatcher (not the commands themselves) - the mode stack mechanism - the DSL interpreter/runtime - persistence layer (sqlite) - player session management what content provides (data files + DSL, hot-reloadable): - individual commands (move, look, attack, build...) - combat moves, timing windows, damage formulas - NPC behaviors and dialogue - room/area descriptions and triggers - IF games and puzzles - item definitions and interactions - channel definitions - help text the boundary: the engine loads and executes content. content never imports engine internals directly — it uses a stable API surface. this is the LPC model adapted for python. why this matters now: our current commands are python functions that import player.py and world/terrain.py directly. that's fine for bootstrapping but it means every command is hardcoded. we need to decide: do commands stay as python modules (evennia model) or become data-defined (MOO/LPC model)? recommendation: HYBRID. core commands (movement, look, system admin) stay as python. game commands (combat moves, social commands, builder tools, IF verbs) are defined in a DSL or data format. the command registry already exists — we just need it to accept both python handlers AND data-defined handlers. the content definition format ============================== commands, combat moves, NPC behaviors, room triggers — they all need a definition format. options: YAML/TOML (pure data): works for: item stats, terrain configs, channel definitions doesn't work for: anything with logic (combat timing, triggers, conditionals) python modules (current approach): works for: anything doesn't work for: in-MUD editing, hot-reload without restart, non-programmer builders a DSL (domain-specific language): works for: logic + data in a safe sandbox this is what the dreambook calls for the DSL doesn't need to exist yet. but the content format should be designed so that when the DSL arrives, it can express the same things that data files express now. that means: - commands defined as declarations (name, aliases, help text) + a body (what happens) - combat moves as declarations (name, stamina cost, timing window, telegraph message) + resolution logic - triggers as event + condition + action triples - descriptions as templates with conditional sections for now: use YAML for declarations, python callables for bodies. the YAML says WHAT something is, python says WHAT IT DOES. when the DSL arrives, the python callables get replaced with DSL scripts. the YAML stays. example of what a command definition might look like: name: shout aliases: [yell] help: shout a message to nearby players mode: normal body: python:commands.social.shout # later: body: dsl:shout.mud example combat move: name: roundhouse type: attack stamina_cost: 15 telegraph: "{attacker} shifts their weight to one leg" timing_window_ms: 800 damage_pct: 12 countered_by: [duck, jump] body: python:combat.moves.roundhouse # later: body: dsl:combat/roundhouse.mud command registry evolution =========================== current: dict mapping strings to async python functions. simple, works. next step: registry accepts CommandDefinition objects that carry metadata (help text, required mode, permissions) alongside the handler. the handler can be a python callable OR (later) a DSL script reference. this is NOT a rewrite. it's wrapping the existing register() call: instead of: register("shout", cmd_shout, aliases=["yell"]) become: register(CommandDefinition(name="shout", aliases=["yell"], mode="normal", handler=cmd_shout)) the dispatcher checks the current session mode before executing. commands with mode="combat" only work in combat mode. this gives us the mode stack for free. loading: at startup, scan a commands/ directory for YAML definitions. each one creates a CommandDefinition. python-backed commands register themselves as they do now. both end up in the same registry. the mode stack ============== from the dreambook: normal, combat, editor, IF modes. modes filter what events reach the player and what commands are available. implementation: each player session has a stack of modes. the top mode determines: - which commands are available (by filtering the registry) - which world events reach the player (chat, movement, combat, ambient) - how output is formatted/routed pushing a mode: entering combat pushes "combat" mode. commands tagged mode="combat" become available. ambient chat gets buffered. popping a mode: leaving combat pops back to normal. buffered messages get summarized. this maps directly to evennia's cmdset merging, but simpler. we don't need set-theoretic operations — just a stack with mode tags on commands. the editor mode is special: it captures ALL input (no command dispatch) and routes it to an editor buffer. the editor is its own subsystem that sits on top of the mode stack. jared's friend is building one with syntax highlighting for telnetlib3 — we'll study that implementation when it's available. what lives in files vs what lives in the database ================================================== world definitions (terrain, rooms, area layouts): files (YAML/TOML, version controlled) command definitions: files (YAML + python/DSL, version controlled) combat move definitions: files (YAML + python/DSL, version controlled) NPC templates: files item templates: files player accounts and state: sqlite player-created content (IF games, custom rooms): sqlite (but exportable to files) runtime state (positions, combat encounters, mob instances): memory only the key insight: files are the source of truth for world content. sqlite is the source of truth for player data. player-created content starts in sqlite but can be exported to files (and potentially committed to a repo by admins). working from the repo: edit YAML files, restart server (or hot-reload). standard dev workflow. working from in-MUD: edit via the editor mode, changes go to sqlite. admin command to "publish" player content to files. both workflows produce the same content format. the engine doesn't care where a definition came from. the entity model ================ current: Player is a dataclass. no mobs, no items, no NPCs yet. the ecosystem shows two paths: - deep inheritance (evennia: Object > Character > NPC, most traditional MUDs) - composition/ECS (graphicmud: entity + components) recommendation: start with simple classes, but design for composition. a character (player or NPC) is a bag of: - identity (name, description) - position (x, y, current map) - stats (PL, stamina, whatever the combat system needs) - inventory (list of item refs) - mode stack (for players) - behavior (for NPCs — patrol, aggro, dialogue) these could be separate objects composed together rather than a deep class hierarchy. we don't need a full ECS framework — just don't put everything in one god class. Player and Mob should share a common interface (something that has a position, can receive messages, can be targeted). whether that's a base class, a protocol, or duck typing — keep it flexible. the game loop ============= from the dreambook: tick-based, 10 ticks/second. current: no game loop yet. commands execute synchronously in the shell coroutine. the game loop is an async task that runs alongside the telnet server: every tick (100ms): 1. drain input queues (player commands waiting to execute) 2. process combat (check timing windows, resolve hits) 3. run NPC AI (behavior tree ticks) 4. process effects (expire old ones, add new ones) 5. flush output buffers (send pending text to players) player input goes into a queue, not directly to dispatch. this decouples input from execution and gives combat its timing windows. "you typed dodge but the timing window closed 50ms ago" — that's the tick checking the timestamp. this is a significant change from the current direct-dispatch model. it should happen before combat gets built, because combat depends on it. making combat programmable ========================== the dreambook describes DBZ-inspired timing combat. the ecosystem research shows most MUDs hardcode combat. we want combat moves to be content, not engine code. a combat move is: - metadata (name, type, stamina cost, damage formula) - telegraph (what the opponent sees) - timing window (how long the opponent has to react) - valid counters (which defensive moves work) - resolution (what happens on hit/miss/counter) all of this can be data. the resolution logic is the only part that might need scripting (DSL), and even that can start as a formula: damage = attacker_pl * damage_pct * (1 - defense_modifier) the combat ENGINE processes the state machine (idle > telegraph > window > resolve). the combat CONTENT defines what moves exist and how they interact. builders can create new moves by writing YAML (and later DSL scripts in the editor). timing windows, stamina costs, damage curves — all tunable from data files. you can balance combat without touching python. what to do now (priority order) ================================ 1. CommandDefinition object wrap the existing registry with metadata. this is small and unlocks mode filtering, help text, permissions. do it before adding more commands. 2. game loop as async task input queues + tick processing. do it before building combat. effects system already exists — move its expiry into the tick. 3. mode stack on player sessions simple stack of mode strings. command dispatcher checks mode. do it before building editor or combat modes. 4. content loading from YAML scan directories for definition files, create CommandDefinition objects. python handlers still work alongside. do it before there are too many hardcoded commands to migrate. 5. entity model shared interface for Player and Mob. do it before building NPCs. 6. combat as content moves defined in YAML, combat engine as a state machine. depends on game loop and mode stack. 7. persistence sqlite for player data. depends on entity model. 8. editor mode depends on mode stack. study the telnetlib3 editor implementation. 9. DSL replace python callables in content definitions. depends on everything above being stable. items 1-4 are foundation work. they're small individually but they shape everything that follows. getting them right means combat, editor, persistence, and eventually the DSL all slot in cleanly. getting them wrong means rewriting the command system when we realize commands need metadata, rewriting the game loop when we realize combat needs ticks, rewriting session handling when we realize modes need a stack. what we'll wish we did differently (predictions) ================================================= things the ecosystem learned the hard way: "we should have separated engine from content earlier" every MUD that survived long-term has this separation. dikumud's zone files, LPC's mudlib, MOO's verb system. we should establish the boundary now. "we should have made the command registry richer" help text, permissions, mode requirements, argument parsing — these all get bolted on later if not designed in. CommandDefinition solves this. "we should have had a game loop from the start" direct command execution is simple but it doesn't compose with timing-based combat, NPC AI, or periodic world events. the tick loop is fundamental. note: when combat/NPC AI gets added to the game loop, add tick health monitoring — log warnings when tick processing exceeds TICK_INTERVAL. the skeleton loop handles overruns correctly (skips sleep, catches up) but sustained overruns mean the tick rate is too ambitious for the workload. "we should have designed entities for composition" inheritance hierarchies (Item > Weapon > Sword > MagicSword) get brittle. composition (entity with damage_component + magic_component) stays flexible. "global mutable state is fine until it isn't" our current global players dict and active_effects list work now. as the system grows, these should migrate into a World or GameState object that owns all runtime state. not urgent, but worth planning for. "the DSL is the hardest part" every MUD that tried in-world programming struggled with the language design. MOO's language is powerful but complex. LPC requires learning C-like syntax. MUSH softcode is cryptic. our DSL should be simple enough to learn from a help file, powerful enough to express puzzles and combat moves. that's the hardest design challenge in this project. defer it until we know exactly what it needs to express. closing ======= the theme: make things data before making them code. if combat moves are data, they can be edited from the MUD. if commands carry metadata, the mode stack works. if the engine/content boundary is clean, the DSL can replace python callables without touching the engine. the architecture should grow by adding content types, not by adding engine complexity. new combat moves, new commands, new NPC behaviors, new IF puzzles — these should all be content that loads from definitions, not python modules that get imported. code ---- this document docs/how/architecture-plan.txt dreambook DREAMBOOK.md ecosystem research docs/how/mud-ecosystem.txt landscape research docs/how/mudlib-landscape.txt