Compare commits
3 commits
544b7dfee1
...
51dc583818
| Author | SHA1 | Date | |
|---|---|---|---|
| 51dc583818 | |||
| d9e9d1b785 | |||
| 9671f3c286 |
5 changed files with 522 additions and 5 deletions
|
|
@ -70,5 +70,6 @@ lessons
|
|||
plans
|
||||
-----
|
||||
|
||||
- ``docs/plans/roadmap.rst`` — master plan: 12 phases from object model through DSL (start here)
|
||||
- ``docs/plans/if-terminal.txt`` — level 1 IF architecture design (predates implementation)
|
||||
- ``docs/plans/zmachine-game-compatibility.rst`` — game test plan for interpreter
|
||||
|
|
|
|||
400
docs/plans/roadmap.rst
Normal file
400
docs/plans/roadmap.rst
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
roadmap — order of operations
|
||||
=============================
|
||||
|
||||
the master plan. what to build and in what order.
|
||||
|
||||
current state: the engine works. command system, game loop, mode stack,
|
||||
combat (state machine + TOML moves), persistence (SQLite), editor mode,
|
||||
IF/z-machine (dfrotz + embedded interpreter), terrain generation, content
|
||||
loading — all built. what's unbuilt is the object model and everything it
|
||||
enables: zones, things, inventory, containers, portals, verbs, multi-zone
|
||||
worlds, rich NPCs, player building, and eventually the DSL.
|
||||
|
||||
the architecture-plan.txt priority list (items 1-8) is essentially done.
|
||||
this plan picks up from there.
|
||||
|
||||
each phase builds on the one before it. the MUD stays functional
|
||||
throughout — no phase leaves things broken.
|
||||
|
||||
|
||||
phase 1: object foundation
|
||||
--------------------------
|
||||
|
||||
the base class everything inherits from. pure additive — nothing breaks,
|
||||
existing tests keep passing.
|
||||
|
||||
build:
|
||||
|
||||
- Object base class: name, location (Object | None), x (int | None),
|
||||
y (int | None)
|
||||
- contents property — reverse lookup: everything whose location is self
|
||||
- can_accept(obj) method — default: False
|
||||
- Entity inherits from Object instead of being a standalone dataclass
|
||||
- Player, Mob unchanged externally — they still work exactly as now
|
||||
|
||||
the location field exists but isn't wired up yet. Entity still has x, y
|
||||
from before, they just come through Object now. no behavioral change.
|
||||
|
||||
depends on: nothing
|
||||
enables: everything below
|
||||
refs: object-model.rst (hierarchy, containment mechanics)
|
||||
|
||||
|
||||
phase 2: zones + overworld migration
|
||||
-------------------------------------
|
||||
|
||||
the big migration. the overworld becomes a Zone object. players live
|
||||
inside it. movement, rendering, persistence, mob spawning all update.
|
||||
|
||||
build:
|
||||
|
||||
- Zone class: width, height, toroidal flag, terrain data, generator
|
||||
- Zone.can_accept() returns True (zones accept everything)
|
||||
- Zone.contents_at(x, y) for spatial queries
|
||||
- convert current World terrain generator into Zone initialization — the
|
||||
procedural overworld becomes a Zone instance loaded at startup
|
||||
- Entity.location = zone (players and mobs live in the overworld zone)
|
||||
- movement system goes through zone: wrapping, passability checks come
|
||||
from player.location (the zone), not a global world reference
|
||||
- viewport rendering reads terrain from player.location
|
||||
- persistence adds zone name to save/load schema
|
||||
- mob spawning sets location=zone
|
||||
- remove world injection into command modules — commands access
|
||||
player.location instead of module-level globals
|
||||
- send_nearby_message uses zone.contents_at() instead of iterating the
|
||||
global players dict
|
||||
|
||||
the global ``_world`` singleton goes away. ``players`` dict can stay for
|
||||
session management but spatial queries go through zones.
|
||||
|
||||
migration strategy:
|
||||
|
||||
- keep World class temporarily as Zone's terrain generator
|
||||
- migrate one system at a time (movement → rendering → persistence →
|
||||
broadcasting) with tests green at every step
|
||||
- object-model.rst "how existing code maps in" section traces each path
|
||||
|
||||
depends on: phase 1
|
||||
enables: multi-zone, portals, interior spaces
|
||||
refs: object-model.rst (Zone section, migration paths),
|
||||
zones-and-building.rst (zone properties, wrapping, generators)
|
||||
|
||||
|
||||
phase 3: things + inventory
|
||||
---------------------------
|
||||
|
||||
items exist. you can carry them. the world has stuff in it.
|
||||
|
||||
build:
|
||||
|
||||
- Thing class: description, portable flag (from Object)
|
||||
- Entity.can_accept() allows portable Things (inventory)
|
||||
- pick up / drop commands
|
||||
- inventory command (list Things whose location is the player)
|
||||
- items on the ground: Thing at location=zone, x=5, y=10
|
||||
- look shows nearby items at your coordinates
|
||||
- thing templates in TOML (content/things/) loaded at startup
|
||||
- thing spawning: template → instance with location=zone
|
||||
- viewport shows ground items (new symbol or marker)
|
||||
- persistence: player inventory persists to SQLite; ground items respawn
|
||||
from templates on restart
|
||||
|
||||
depends on: phase 2 (things need zones to exist in)
|
||||
enables: equipment, crafting ingredients, quest items, loot
|
||||
refs: object-model.rst (Thing section), things-and-building.rst (decorator
|
||||
pattern, content format)
|
||||
|
||||
|
||||
phase 4: containers + portals
|
||||
-----------------------------
|
||||
|
||||
nesting and zone transitions. both depend on Thing existing.
|
||||
|
||||
containers:
|
||||
|
||||
- Container class: capacity, closed (bool), locked (bool)
|
||||
- Container.can_accept() checks capacity + open state
|
||||
- put / get commands (move Thing into/out of Container)
|
||||
- open / close commands
|
||||
- lock / unlock (requires key item — first real item interaction)
|
||||
- nested inventory display (backpack contains: ...)
|
||||
|
||||
portals:
|
||||
|
||||
- Portal class: target_zone, target_x, target_y, portable=False
|
||||
- zone transition: step on portal → location changes to target zone,
|
||||
position changes to target coords
|
||||
- enter / go command for explicit portal use
|
||||
- build at least one interior zone (tavern or test room) with hand-built
|
||||
tile data from TOML
|
||||
- two-way portals (portal in each zone pointing at the other)
|
||||
- look shows portal presence, portal descriptions
|
||||
|
||||
depends on: phase 3 (portals and containers are Things)
|
||||
enables: multi-zone world, interior spaces, locked doors, bags, keys
|
||||
refs: object-model.rst (Container, Portal sections),
|
||||
zones-and-building.rst (zone data format, hand-built zones)
|
||||
|
||||
|
||||
phase 5: verbs + interaction
|
||||
----------------------------
|
||||
|
||||
the layer that makes the world feel alive. drink from a fountain, pet a
|
||||
mob, read a sign. anything can have verbs.
|
||||
|
||||
build:
|
||||
|
||||
- @verb decorator on Object — any object in the world can have verbs
|
||||
- @thing decorator for spawnable templates with verbs pre-attached
|
||||
- verb resolution: "drink fountain" → find fountain at player's
|
||||
coordinates → call its drink verb handler
|
||||
- verb dispatch integrates with command system — verbs checked as
|
||||
fallback after global commands fail to match
|
||||
- content format: verbs defined in TOML alongside thing definitions,
|
||||
or in python via the decorator
|
||||
- default verbs: look at / examine (work on everything with a
|
||||
description)
|
||||
|
||||
depends on: phase 4 (need objects with verbs to interact with)
|
||||
enables: rich interaction, builder content, puzzle creation
|
||||
refs: things-and-building.rst (@thing/@verb decorator system, full
|
||||
PepsiCan example)
|
||||
|
||||
|
||||
phase 6: multi-zone content + building
|
||||
--------------------------------------
|
||||
|
||||
the world has places. builders can create more.
|
||||
|
||||
build:
|
||||
|
||||
- zone loading from TOML files (hand-built zones with tile grids)
|
||||
- hub / phone-switch zone with portals to different areas
|
||||
- tutorial zone for new players
|
||||
- tavern / social zone (interior, bounded, not toroidal)
|
||||
- paint mode: admin tool for editing zone terrain tile-by-tile
|
||||
- zone TOML export (runtime state → version-controlled files)
|
||||
- spawn points per zone (where you appear on entry)
|
||||
- zone-specific ambient messages
|
||||
- per-zone mob spawning rules (what mobs live here, density, respawn)
|
||||
|
||||
depends on: phase 4 (portals connect zones)
|
||||
enables: a world with actual geography, builder workflow
|
||||
refs: zones-and-building.rst (zone format, paint mode, building tools,
|
||||
portal network design)
|
||||
|
||||
|
||||
phase 7: GMCP + rich client support
|
||||
------------------------------------
|
||||
|
||||
structured data for graphical MUD clients (Mudlet, Blightmud, etc.).
|
||||
independent of the object model — can be done earlier if desired.
|
||||
|
||||
build:
|
||||
|
||||
- GMCP option negotiation in telnet handshake (telnetlib3 supports this)
|
||||
- Char.Vitals package: pl, stamina, max_stamina
|
||||
- Char.Status package: flying, resting, mode, combat state
|
||||
- Room.Info package: zone name, x, y, terrain type
|
||||
- MSDP for real-time variable updates (client-side HP bars, gauges)
|
||||
- map data for clients that support structured map rendering
|
||||
|
||||
telnetlib3 already handles the protocol layer. this phase is defining the
|
||||
data packages and wiring them to game state changes.
|
||||
|
||||
depends on: nothing (can start anytime)
|
||||
enables: rich MUD client experience alongside raw telnet
|
||||
refs: DREAMBOOK.md (things to explore: GMCP, MSDP)
|
||||
|
||||
|
||||
phase 8: NPC evolution
|
||||
----------------------
|
||||
|
||||
mobs become characters, not just combat targets.
|
||||
|
||||
build:
|
||||
|
||||
- dialogue trees: TOML-defined, triggered by "talk to" or "ask" commands
|
||||
- non-combat behaviors: patrol, wander, idle, shopkeeper, guard
|
||||
- behavior trees or state machines defined in TOML
|
||||
- mob reactions to verbs ("pet" the cat, "ask" the bartender about X)
|
||||
- spawn/respawn system using location=None as template state (die →
|
||||
location=None, timer → respawn at zone with fresh stats)
|
||||
- mob schedules (bartender appears in tavern during the day, gone at
|
||||
night)
|
||||
- shopkeeper mobs (buy/sell interface using inventory system)
|
||||
|
||||
depends on: phase 5 (verbs for interaction), phase 6 (zones for mobs
|
||||
to inhabit)
|
||||
enables: living world, quests, commerce
|
||||
refs: architecture-plan.txt (entity model section),
|
||||
object-model.rst (location=None for templates/despawned)
|
||||
|
||||
|
||||
phase 9: world systems
|
||||
----------------------
|
||||
|
||||
time, weather, seasons. the world breathes.
|
||||
|
||||
build:
|
||||
|
||||
- game time cycle: ticks → hours → days → seasons
|
||||
- time of day affects descriptions ("the tavern is dim in the evening
|
||||
light" vs "sunlight streams through the windows")
|
||||
- weather system: rain, wind, storms (per zone or global)
|
||||
- weather affects gameplay: rain slows movement, storms block flying,
|
||||
cold water is worse in winter
|
||||
- seasonal terrain variation: snow in winter, flowers in spring, color
|
||||
palette shifts
|
||||
- ambient messages tied to time/weather ("a cold wind picks up")
|
||||
|
||||
depends on: phase 6 (zones with descriptions to vary)
|
||||
enables: atmosphere, seasonal events, weather-dependent gameplay
|
||||
refs: DREAMBOOK.md (seasons, weather, time of day)
|
||||
|
||||
|
||||
phase 10: player creation + housing
|
||||
------------------------------------
|
||||
|
||||
players shape the world. the MOO dream.
|
||||
|
||||
build:
|
||||
|
||||
- player-owned zones (personal rooms, houses, pocket dimensions)
|
||||
- building commands: create zone, set dimensions, paint terrain, place
|
||||
things
|
||||
- in-world editing via editor mode (descriptions, verb scripts)
|
||||
- player-created content stored in SQLite
|
||||
- admin review + export to files (sqlite → TOML for version control)
|
||||
- permissions: who can build, who can edit what, visitor access controls
|
||||
- crafting system: combine things → new thing, recipes in TOML
|
||||
- IF creation tools: use the IF primitives to build small adventures
|
||||
from inside the MUD
|
||||
|
||||
depends on: phase 6 (zones), phase 5 (verbs), persistence
|
||||
enables: the MOO vision — player-built, player-programmed world
|
||||
refs: DREAMBOOK.md (user-created content, presence and community),
|
||||
architecture-plan.txt (player-created content in sqlite,
|
||||
engine/content boundary)
|
||||
|
||||
|
||||
phase 11: the DSL
|
||||
-----------------
|
||||
|
||||
replace python callables with a scripting language writable from inside
|
||||
the MUD. this is the hardest design challenge in the project.
|
||||
|
||||
build:
|
||||
|
||||
- language design: informed by what phases 1-10 needed to express
|
||||
- interpreter/runtime: sandboxed, no file/network access
|
||||
- content definitions gain ``body: dsl:script_name`` option alongside
|
||||
``body: python:module.function``
|
||||
- DSL scripts editable in-world via editor mode
|
||||
- help system teaches the DSL from inside the MUD
|
||||
- standard library: common verbs, triggers, conditions
|
||||
- migration path: existing python handlers get DSL equivalents over time
|
||||
|
||||
defer specifics until phases 1-10 reveal exactly what the DSL needs to
|
||||
express. the ecosystem research (dsls.md) catalogs prior art: MOO's
|
||||
language, LPC, MUSH softcode.
|
||||
|
||||
depends on: everything above being stable
|
||||
enables: non-programmers creating game content from a telnet session
|
||||
refs: architecture-plan.txt (DSL section, content definition format),
|
||||
docs/research/dsls.md (prior art)
|
||||
|
||||
|
||||
phase 12: horizons
|
||||
------------------
|
||||
|
||||
things from the dreambook's "explore later" list. order is flexible —
|
||||
tackle based on interest and what the world needs.
|
||||
|
||||
- web client: xterm.js terminal emulator pointed at telnet port. honor
|
||||
the protocol even in the browser
|
||||
- MCCP compression for bandwidth-heavy sessions
|
||||
- inter-MUD communication (IMC2, I3)
|
||||
- AI-driven NPCs: LLM-backed conversation, personality, memory
|
||||
- alternate screen buffer: split views (map panel + text panel)
|
||||
- sound via MSP or links
|
||||
- procedural dungeon generation: zones with random layout per visit
|
||||
- quest system: objectives, tracking, rewards, quest-giver NPCs
|
||||
|
||||
|
||||
dependency graph
|
||||
----------------
|
||||
|
||||
::
|
||||
|
||||
phase 1: object foundation
|
||||
│
|
||||
▼
|
||||
phase 2: zones + migration
|
||||
│
|
||||
▼
|
||||
phase 3: things + inventory
|
||||
│
|
||||
▼
|
||||
phase 4: containers + portals
|
||||
│
|
||||
▼
|
||||
phase 5: verbs + interaction
|
||||
│ phase 7: GMCP (independent)
|
||||
▼
|
||||
phase 6: multi-zone content
|
||||
│
|
||||
├────────────────┐
|
||||
▼ ▼
|
||||
phase 8: NPCs phase 9: world systems
|
||||
│ │
|
||||
└───────┬────────┘
|
||||
▼
|
||||
phase 10: player creation
|
||||
│
|
||||
▼
|
||||
phase 11: DSL
|
||||
│
|
||||
▼
|
||||
phase 12: horizons
|
||||
|
||||
|
||||
notes
|
||||
-----
|
||||
|
||||
**the phase 2 migration is the riskiest step.** it touches movement,
|
||||
rendering, persistence, mob spawning, and message broadcasting. the
|
||||
strategy is incremental: migrate one subsystem at a time, keep tests
|
||||
green, keep the MUD playable. the object-model.rst doc has a detailed
|
||||
"how existing code maps in" section for this.
|
||||
|
||||
**phase 7 (GMCP) floats.** it doesn't depend on the object model and
|
||||
can be done whenever energy or interest is there. it's slotted at 7
|
||||
because the data it reports (zone name, position, combat state) gets
|
||||
richer as the object model matures, but basic vitals work anytime.
|
||||
|
||||
**phases 8-10 have some flexibility.** world systems (weather, time)
|
||||
could come before NPC evolution. player creation could come before
|
||||
either. the dependency shown is the tightest path — if housing needs
|
||||
NPCs (shopkeepers, guards), then 8 before 10. if not, reorder freely.
|
||||
|
||||
**the DSL (phase 11) is intentionally last.** the architecture-plan.txt
|
||||
says "defer it until we know exactly what it needs to express." phases
|
||||
1-10 discover that organically.
|
||||
|
||||
|
||||
related docs
|
||||
-------------
|
||||
|
||||
- ``DREAMBOOK.md`` — vision, philosophy, wild ideas
|
||||
- ``docs/how/architecture-plan.txt`` — engine/content boundary, original
|
||||
priority list (items 1-8 now complete)
|
||||
- ``docs/how/object-model.rst`` — class hierarchy, containment, verbs,
|
||||
migration paths
|
||||
- ``docs/how/zones-and-building.rst`` — zone format, building tools,
|
||||
paint mode, portal network
|
||||
- ``docs/how/things-and-building.rst`` — @thing/@verb decorator system
|
||||
- ``docs/how/combat.rst`` — combat system (built, referenced by phase 8)
|
||||
- ``docs/how/commands.txt`` — command system (built, extended by new
|
||||
phases)
|
||||
- ``docs/research/dsls.md`` — scripting language catalog (phase 11)
|
||||
|
|
@ -2,14 +2,20 @@
|
|||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from mudlib.object import Object
|
||||
|
||||
|
||||
@dataclass
|
||||
class Entity:
|
||||
"""Base class for anything with position and identity in the world."""
|
||||
class Entity(Object):
|
||||
"""Base class for anything with position and identity in the world.
|
||||
|
||||
name: str
|
||||
x: int
|
||||
y: int
|
||||
Inherits name, location from Object. Narrows x, y to int (entities
|
||||
always have spatial coordinates).
|
||||
"""
|
||||
|
||||
# Entities always have spatial coordinates (override Object's int | None)
|
||||
x: int = 0
|
||||
y: int = 0
|
||||
# Combat stats
|
||||
pl: float = 100.0 # power level (health and damage multiplier)
|
||||
stamina: float = 100.0 # current stamina
|
||||
|
|
|
|||
37
src/mudlib/object.py
Normal file
37
src/mudlib/object.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
"""Base class for everything in the world."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class Object:
|
||||
"""Base class for everything in the world.
|
||||
|
||||
Every object has a location (another Object, or None for top-level things
|
||||
like zones). The location pointer is the entire containment system —
|
||||
contents is just the reverse lookup.
|
||||
"""
|
||||
|
||||
name: str
|
||||
location: Object | None = None
|
||||
x: int | None = None
|
||||
y: int | None = None
|
||||
|
||||
_contents: list[Object] = field(
|
||||
default_factory=list, init=False, repr=False, compare=False
|
||||
)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.location is not None:
|
||||
self.location._contents.append(self)
|
||||
|
||||
@property
|
||||
def contents(self) -> list[Object]:
|
||||
"""Everything whose location is this object."""
|
||||
return list(self._contents)
|
||||
|
||||
def can_accept(self, obj: Object) -> bool:
|
||||
"""Whether this object accepts obj as contents. Default: no."""
|
||||
return False
|
||||
73
tests/test_object.py
Normal file
73
tests/test_object.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
"""Tests for the Object base class."""
|
||||
|
||||
from mudlib.object import Object
|
||||
|
||||
|
||||
def test_object_creation_minimal():
|
||||
"""Object can be created with just a name."""
|
||||
obj = Object(name="rock")
|
||||
assert obj.name == "rock"
|
||||
assert obj.location is None
|
||||
assert obj.x is None
|
||||
assert obj.y is None
|
||||
|
||||
|
||||
def test_object_creation_with_position():
|
||||
"""Object can have spatial coordinates."""
|
||||
obj = Object(name="tree", x=5, y=10)
|
||||
assert obj.x == 5
|
||||
assert obj.y == 10
|
||||
|
||||
|
||||
def test_object_creation_with_location():
|
||||
"""Object can be placed inside another object."""
|
||||
zone = Object(name="overworld")
|
||||
tree = Object(name="oak", location=zone, x=3, y=7)
|
||||
assert tree.location is zone
|
||||
|
||||
|
||||
def test_contents_empty_by_default():
|
||||
"""An object with nothing inside has empty contents."""
|
||||
obj = Object(name="empty")
|
||||
assert obj.contents == []
|
||||
|
||||
|
||||
def test_contents_tracks_children():
|
||||
"""Contents lists objects whose location is self."""
|
||||
zone = Object(name="overworld")
|
||||
tree = Object(name="oak", location=zone, x=3, y=7)
|
||||
rock = Object(name="rock", location=zone, x=1, y=1)
|
||||
assert tree in zone.contents
|
||||
assert rock in zone.contents
|
||||
assert len(zone.contents) == 2
|
||||
|
||||
|
||||
def test_contents_nested():
|
||||
"""Containment can nest: zone > player > backpack > item."""
|
||||
zone = Object(name="overworld")
|
||||
player = Object(name="jared", location=zone, x=5, y=5)
|
||||
backpack = Object(name="backpack", location=player)
|
||||
gem = Object(name="gem", location=backpack)
|
||||
|
||||
assert player in zone.contents
|
||||
assert backpack in player.contents
|
||||
assert gem in backpack.contents
|
||||
# gem is NOT directly in zone or player
|
||||
assert gem not in zone.contents
|
||||
assert gem not in player.contents
|
||||
|
||||
|
||||
def test_can_accept_default_false():
|
||||
"""Object.can_accept() returns False by default."""
|
||||
obj = Object(name="rock")
|
||||
other = Object(name="sword")
|
||||
assert obj.can_accept(other) is False
|
||||
|
||||
|
||||
def test_contents_returns_copy():
|
||||
"""Contents returns a copy, not the internal list."""
|
||||
zone = Object(name="zone")
|
||||
Object(name="thing", location=zone)
|
||||
contents = zone.contents
|
||||
contents.append(Object(name="intruder"))
|
||||
assert len(zone.contents) == 1 # internal list unchanged
|
||||
Loading…
Reference in a new issue