Add things and building design doc
This commit is contained in:
parent
d3df09f4de
commit
0395aaadad
1 changed files with 384 additions and 0 deletions
384
docs/how/things-and-building.txt
Normal file
384
docs/how/things-and-building.txt
Normal file
|
|
@ -0,0 +1,384 @@
|
||||||
|
things and building
|
||||||
|
===================
|
||||||
|
|
||||||
|
overview
|
||||||
|
--------
|
||||||
|
|
||||||
|
this doc sketches what it looks like to create interactive objects in the MUD.
|
||||||
|
from the builder's perspective (what commands do i type) and the code
|
||||||
|
perspective (what does the python look like).
|
||||||
|
|
||||||
|
not a spec yet. a sketch to react against.
|
||||||
|
|
||||||
|
|
||||||
|
the decorator pattern
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
the core idea: things are python classes with decorators. no custom DSL.
|
||||||
|
python IS the language.
|
||||||
|
|
||||||
|
here's a full example of a pepsi can:
|
||||||
|
|
||||||
|
from mudlib.things import thing, verb
|
||||||
|
|
||||||
|
@thing
|
||||||
|
class PepsiCan:
|
||||||
|
name = "pepsi can"
|
||||||
|
aliases = ["can", "pepsi"]
|
||||||
|
description = "a cold can of pepsi. unopened."
|
||||||
|
|
||||||
|
opened = False
|
||||||
|
empty = False
|
||||||
|
|
||||||
|
@verb("open")
|
||||||
|
def open(self, player):
|
||||||
|
if self.opened:
|
||||||
|
player.tell("it's already open.")
|
||||||
|
return
|
||||||
|
self.opened = True
|
||||||
|
self.description = "an open can of pepsi."
|
||||||
|
player.tell("you crack open the pepsi. psssht.")
|
||||||
|
player.room.emit(f"{player.name} opens a pepsi.", exclude=player)
|
||||||
|
|
||||||
|
@verb("drink")
|
||||||
|
def drink(self, player):
|
||||||
|
if not self.opened:
|
||||||
|
player.tell("you'd need to open it first.")
|
||||||
|
elif self.empty:
|
||||||
|
player.tell("it's empty.")
|
||||||
|
else:
|
||||||
|
self.empty = True
|
||||||
|
self.description = "an empty pepsi can."
|
||||||
|
player.tell("you chug the pepsi. refreshing.")
|
||||||
|
|
||||||
|
@verb("crumple")
|
||||||
|
def crumple(self, player):
|
||||||
|
if not self.empty:
|
||||||
|
player.tell("it's still got pepsi in it.")
|
||||||
|
return
|
||||||
|
player.tell("you crush the can in your fist.")
|
||||||
|
player.room.emit(f"{player.name} crushes a pepsi can.", exclude=player)
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
what's happening here:
|
||||||
|
|
||||||
|
- @thing registers the class as a spawnable template, looked up by name
|
||||||
|
- @verb makes methods into in-game commands that work on this object
|
||||||
|
- base behaviors are free: every @thing can be dropped, picked up, given,
|
||||||
|
looked at
|
||||||
|
- the class is the template. instances are spawned from it. 10 cans = 10
|
||||||
|
instances, one class.
|
||||||
|
- state (opened, empty) lives on the instance, not the class
|
||||||
|
- self.destroy() removes the instance from the world
|
||||||
|
- no yaml needed for behavioral things. the python file IS the definition.
|
||||||
|
|
||||||
|
for static items (no behavior, just a description), we could support TOML-only
|
||||||
|
for simple cases:
|
||||||
|
|
||||||
|
[thing]
|
||||||
|
name = "rock"
|
||||||
|
aliases = ["stone"]
|
||||||
|
description = "a smooth grey rock."
|
||||||
|
weight = 2.0
|
||||||
|
|
||||||
|
|
||||||
|
builder commands
|
||||||
|
----------------
|
||||||
|
|
||||||
|
the @ prefix for builder/admin commands. not combat, not normal play. building.
|
||||||
|
|
||||||
|
@create item <name> opens editor with a template python file
|
||||||
|
@create room <name> opens editor for a new room definition
|
||||||
|
@edit <name> opens editor for an existing thing/room
|
||||||
|
@copy <name> <newname> opens editor with a copy, requires new name
|
||||||
|
@spawn <name> create an instance of a thing in current location
|
||||||
|
@destroy remove a thing instance you're looking at / holding
|
||||||
|
@inspect <name> view the definition (source code or toml)
|
||||||
|
@dig <direction> create a new room connected to current location
|
||||||
|
|
||||||
|
the flow for @create item pepsi can:
|
||||||
|
|
||||||
|
1. command parsed, "pepsi can" becomes the name
|
||||||
|
2. engine generates a template python file with the name filled in
|
||||||
|
3. editor mode is pushed onto mode stack
|
||||||
|
4. player edits the template (they see python code)
|
||||||
|
5. on save: file written to items/pepsi_can.py, hot-reloaded
|
||||||
|
6. editor mode pops, back to normal
|
||||||
|
7. the thing is now in the registry, spawnable
|
||||||
|
|
||||||
|
the flow for @copy pepsi can cola can:
|
||||||
|
|
||||||
|
1. loads pepsi can's source
|
||||||
|
2. replaces name with "cola can"
|
||||||
|
3. opens editor with the modified copy
|
||||||
|
4. on save: new file, new registration
|
||||||
|
5. cola can is its own independent thing now
|
||||||
|
|
||||||
|
|
||||||
|
permissions & ownership
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
things have an owner - whoever created them.
|
||||||
|
|
||||||
|
permission levels (rough sketch):
|
||||||
|
|
||||||
|
player - can interact with things (use verbs, pick up, drop)
|
||||||
|
builder - can @create, @edit own things, @spawn, @dig
|
||||||
|
admin - can @edit anyone's things, manage permissions
|
||||||
|
|
||||||
|
owning a name: there's only one "pepsi can" template. the owner controls it.
|
||||||
|
anyone can spawn instances, but only the owner (or admin) can edit the
|
||||||
|
definition.
|
||||||
|
|
||||||
|
instances vs templates:
|
||||||
|
|
||||||
|
- template: the class/definition. owned by creator. one per name.
|
||||||
|
- instance: a spawned copy in the world. can be picked up, dropped,
|
||||||
|
destroyed.
|
||||||
|
- instances don't have individual owners in the filesystem sense.
|
||||||
|
- dropped instances will probably get cleaned up eventually but that's a
|
||||||
|
runtime concern, not a design blocker.
|
||||||
|
|
||||||
|
open questions:
|
||||||
|
|
||||||
|
- do we store permissions in the file? in sqlite? in a separate registry?
|
||||||
|
- how granular? per-thing? per-directory? role-based?
|
||||||
|
- can a builder grant co-ownership?
|
||||||
|
|
||||||
|
|
||||||
|
map editing
|
||||||
|
-----------
|
||||||
|
|
||||||
|
the map format (TOML) is already writable:
|
||||||
|
|
||||||
|
[map]
|
||||||
|
width = 20
|
||||||
|
height = 20
|
||||||
|
data = """
|
||||||
|
.........===.....
|
||||||
|
...etc...
|
||||||
|
"""
|
||||||
|
|
||||||
|
[palette]
|
||||||
|
entries = """
|
||||||
|
o|tile|#26a269
|
||||||
|
.|gravel|#9a9996
|
||||||
|
=|brick|#a51d2d
|
||||||
|
"""
|
||||||
|
|
||||||
|
an in-game map editor could:
|
||||||
|
|
||||||
|
- show the current map in the viewport
|
||||||
|
- let you paint tiles with a cursor
|
||||||
|
- write changes back to the TOML file on save
|
||||||
|
- this is essentially the editor mode but for spatial data instead of text
|
||||||
|
|
||||||
|
open questions:
|
||||||
|
|
||||||
|
- locking: what if two builders edit the same map?
|
||||||
|
- versioning: do edits go through git? just overwrite?
|
||||||
|
- for now: single builder, no locking. solve concurrency if it matters.
|
||||||
|
|
||||||
|
|
||||||
|
rooms and interiors
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
the overworld is terrain tiles. but buildings, caves, dungeons are interiors.
|
||||||
|
interiors are rooms (classic MUD style) or small grids.
|
||||||
|
|
||||||
|
entering: a tile on the overworld has an "enter" action that transitions you
|
||||||
|
to an interior. the interior has an "exit" that puts you back on the overworld.
|
||||||
|
|
||||||
|
a room definition might look like:
|
||||||
|
|
||||||
|
[room]
|
||||||
|
name = "tavern"
|
||||||
|
description = "a warm tavern. fire crackling. smells like stew."
|
||||||
|
exits = { out = "overworld:12,45" }
|
||||||
|
|
||||||
|
or as python if it has behavior:
|
||||||
|
|
||||||
|
@room
|
||||||
|
class Tavern:
|
||||||
|
name = "tavern"
|
||||||
|
description = "..."
|
||||||
|
exits = {"out": "overworld:12,45"}
|
||||||
|
|
||||||
|
@on("enter")
|
||||||
|
def on_enter(self, player):
|
||||||
|
player.tell("the warmth hits you as you step inside.")
|
||||||
|
|
||||||
|
@dig north from inside a room creates a new connected room and opens editor.
|
||||||
|
|
||||||
|
|
||||||
|
roadmap
|
||||||
|
-------
|
||||||
|
|
||||||
|
this is the important part. mix of implementation and design work. each phase
|
||||||
|
builds on the last. design decisions that block implementation are called out.
|
||||||
|
|
||||||
|
|
||||||
|
phase 1: things exist
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
status: not started
|
||||||
|
depends on: entity model (done), command system (done)
|
||||||
|
|
||||||
|
DESIGN: finalize @thing/@verb decorator API
|
||||||
|
- what methods does the base thing provide?
|
||||||
|
- how does verb dispatch work with the command system?
|
||||||
|
- does @thing register globally or per-module?
|
||||||
|
> this blocks everything below
|
||||||
|
|
||||||
|
BUILD: thing registry
|
||||||
|
- like command registry but for spawnable templates
|
||||||
|
- lookup by name, list all, filter by type
|
||||||
|
|
||||||
|
BUILD: base behaviors
|
||||||
|
- drop, get/take, give, inventory, look at
|
||||||
|
- these are commands that operate on thing instances
|
||||||
|
- "get can" finds a PepsiCan instance in the room, moves to inventory
|
||||||
|
|
||||||
|
BUILD: spawn/destroy lifecycle
|
||||||
|
- create instance from template
|
||||||
|
- place in room or inventory
|
||||||
|
- destroy removes from world
|
||||||
|
|
||||||
|
BUILD: thing persistence
|
||||||
|
- which instances persist? (dropped items, placed furniture)
|
||||||
|
- which are ephemeral? (consumables, quest items)
|
||||||
|
> DESIGN DECISION: do we persist instance state to sqlite?
|
||||||
|
> or just persist "there is a pepsi can at 12,45 with opened=true"?
|
||||||
|
> this can be deferred - start with ephemeral, add persistence later
|
||||||
|
|
||||||
|
|
||||||
|
phase 2: builder commands
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
status: not started
|
||||||
|
depends on: phase 1, editor mode (done)
|
||||||
|
|
||||||
|
BUILD: @create command
|
||||||
|
- parses args (type + name)
|
||||||
|
- generates template file
|
||||||
|
- pushes editor mode
|
||||||
|
|
||||||
|
BUILD: @edit command
|
||||||
|
- finds thing by name
|
||||||
|
- loads source file
|
||||||
|
- pushes editor mode
|
||||||
|
|
||||||
|
BUILD: @copy command
|
||||||
|
- loads source, substitutes name
|
||||||
|
- pushes editor mode
|
||||||
|
|
||||||
|
BUILD: @spawn / @destroy commands
|
||||||
|
- spawn: create instance from registry
|
||||||
|
- destroy: remove instance from world
|
||||||
|
|
||||||
|
BUILD: @inspect command
|
||||||
|
- show definition source (read-only)
|
||||||
|
|
||||||
|
DESIGN: hot-reload mechanism
|
||||||
|
- when a file is saved from the editor, how does the registry update?
|
||||||
|
- reimport the module? invalidate instances of the old version?
|
||||||
|
> this blocks @edit (creating works without reload, editing needs it)
|
||||||
|
|
||||||
|
|
||||||
|
phase 3: world building
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
status: not started
|
||||||
|
depends on: phase 2
|
||||||
|
|
||||||
|
DESIGN: how interiors connect to overworld
|
||||||
|
- which tiles are "enterable"?
|
||||||
|
- how is the transition represented in data?
|
||||||
|
> this blocks @dig and room creation
|
||||||
|
|
||||||
|
BUILD: room system
|
||||||
|
- room definitions (TOML or python)
|
||||||
|
- enter/exit transitions
|
||||||
|
- room-local commands and things
|
||||||
|
|
||||||
|
BUILD: @dig command
|
||||||
|
- creates room, connects via exit
|
||||||
|
- opens editor for new room
|
||||||
|
|
||||||
|
BUILD: map editing from in-game
|
||||||
|
- cursor-based tile painting
|
||||||
|
- write back to TOML
|
||||||
|
- start simple: single builder, no locking
|
||||||
|
|
||||||
|
|
||||||
|
phase 4: permissions
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
status: not started
|
||||||
|
depends on: phase 2 (at minimum)
|
||||||
|
|
||||||
|
DESIGN: permission model
|
||||||
|
- role-based? (player/builder/admin)
|
||||||
|
- where are permissions stored?
|
||||||
|
- how granular?
|
||||||
|
> can be deferred if you're the only builder for now
|
||||||
|
> becomes critical when others build
|
||||||
|
|
||||||
|
BUILD: permission checks on @ commands
|
||||||
|
BUILD: ownership tracking on templates
|
||||||
|
|
||||||
|
|
||||||
|
phase 5: npcs and AI
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
status: not started
|
||||||
|
depends on: phase 1, combat system (done)
|
||||||
|
|
||||||
|
DESIGN: mob behavior composition
|
||||||
|
- patrol routes, aggro radius, dialogue trees
|
||||||
|
- how do these compose? decorators? data files?
|
||||||
|
> can design alongside phase 1-2 since Mob class exists
|
||||||
|
|
||||||
|
BUILD: mob spawning from templates
|
||||||
|
BUILD: basic AI loop (runs in game tick)
|
||||||
|
BUILD: mob persistence (respawn rules, loot tables)
|
||||||
|
|
||||||
|
|
||||||
|
phase 6: the full loop
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
depends on: phases 1-4
|
||||||
|
|
||||||
|
this is "a builder sits down and creates a thing from scratch, tests it,
|
||||||
|
publishes it, other players use it." the MOO dream but with python.
|
||||||
|
|
||||||
|
BUILD: thing catalog / library browser
|
||||||
|
BUILD: testing mode (try your thing before publishing)
|
||||||
|
BUILD: sharing / discovery
|
||||||
|
|
||||||
|
no design blockers here - if phases 1-4 work, this is just polish.
|
||||||
|
|
||||||
|
|
||||||
|
open questions (parking lot)
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
- sandboxing: if someone writes python in the editor, what can they import?
|
||||||
|
what modules are available? can they access the filesystem? the network?
|
||||||
|
this is the MOO permissions problem. not urgent if builders are trusted.
|
||||||
|
becomes critical if/when players can create.
|
||||||
|
|
||||||
|
- hot reload details: reimport module? use importlib.reload? what about
|
||||||
|
instances that reference the old class? do they get upgraded or stay old?
|
||||||
|
|
||||||
|
- naming conflicts: what if two builders both want "sword"? namespacing?
|
||||||
|
builder/sword vs items/sword? or first-come-first-served?
|
||||||
|
|
||||||
|
- static vs behavioral things: is the TOML path worth maintaining? or just
|
||||||
|
make everything python, even simple things? simpler to have one path.
|
||||||
|
|
||||||
|
- composition: the architecture plan warns against deep inheritance
|
||||||
|
(Item > Weapon > MagicSword). how do decorators encourage composition?
|
||||||
|
maybe @trait decorators that add capabilities?
|
||||||
|
|
||||||
|
- the editor experience: telnet editors are rough. can we make it not awful?
|
||||||
|
study how MOO's @program worked. study the existing editor mode we have.
|
||||||
Loading…
Reference in a new issue