diff --git a/docs/how/architecture-plan.txt b/docs/how/architecture-plan.txt new file mode 100644 index 0000000..3e3d3dd --- /dev/null +++ b/docs/how/architecture-plan.txt @@ -0,0 +1,372 @@ +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. combat, persistence, +session modes, editor, in-world programming — all ahead of us. this doc +captures what we should get right in the foundation so those features don't +require rewrites. + +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. + +"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