Building a Persistent World from Scratch
Most readers do not need this tutorial. A from-scratch world means writing your own root class hierarchy, your own
look/take/drop/go/sayequivalents, and your own connection callbacks. None of the helpers indefault/verbs/apply — every command vocabulary is something you define. Expect this to take days, not hours.If what you actually want is to add rooms, items, or themed areas to the existing MOO, extend the
defaultdataset by adding numbered scripts undermoo/bootstrap/default/. The reasonable use cases for this tutorial are narrow: implementing a different game engine on top of the MOO substrate, or a non-MOO theme that should not share any state with the standard player commands.A working precedent for the latter ships with the companion moo-agent project: its
zork1dataset is a complete from-scratch bootstrap that defines its own root classes (Zork Room,Zork Thing,Zork Container,Zork Actor), its own verbs (take,drop,examine, etc., none of them inherited fromdefault), and its own runtime helpers via a$zork_sdksystem object. The package shape — and the namespace-package contribution model that lets it live in a separate repo — is exactly what this tutorial walks you through.
This tutorial walks through building a standalone bootstrap dataset as a
Python package. After you’re done, somebody can install your package and
run moo_init --bootstrap mygame to bring up your world from an empty
database.
Prerequisites
Before you start:
Familiarity with Your First MOO Verb and Writing Tests for Your Verbs
A working development environment (see Development)
Comfort reading existing django-moo source. You will want
moo/bootstrap/default/open in another window for reference.
What you’re building
A new top-level package — call it mygame — that contains:
An orchestrator
__init__.pythat initializes the dataset and runs numbered sub-scripts in order.Numbered sub-scripts that create your root classes, rooms, and other objects.
A sibling
mygame/verbs/package containing one verb file per command, organized by root class.A minimal
pyproject.tomlso the package can be installed.
Layout:
mygame/
├── __init__.py orchestrator
├── 010_classes.py root classes (your equivalents of Room/Thing/Player)
├── 020_rooms.py starting rooms
├── 030_exits.py exits between rooms
└── 999_finalize.py permission grants and verb loading
mygame/verbs/
├── room/
│ └── look.py
├── thing/
└── player/
pyproject.toml
The mygame/verbs/<class>/ convention mirrors default/verbs/. Each
subdirectory holds verbs whose --on references that root class.
bootstrap.load_verbs walks the tree recursively, so the directory
structure is purely organizational — only the --on line in each verb’s
shebang determines where the verb is attached.
A note on packaging
Today, moo_init only discovers bootstrap packages that are siblings of
moo/bootstrap/default/. There is no pip install-and-go path for an
out-of-tree package yet. The practical workflow is:
Develop your package in-tree by placing it under
moo/bootstrap/mygame/andmoo/bootstrap/mygame/.Add
"mygame"to thebuiltin_templateslist inmoo/core/management/commands/moo_init.py.
The directory structure and code shown below are identical to what an out-of-tree pip-installable package will eventually need. The closing section of this tutorial sketches the future shape; for now, the tutorial itself shows the in-tree variant.
Step 1: Create the package skeleton
From the django-moo repo root:
mkdir -p moo/bootstrap/mygame/verbs/room
mkdir -p moo/bootstrap/mygame/verbs/thing
mkdir -p moo/bootstrap/mygame/verbs/player
mkdir -p moo/bootstrap/mygame/tests
touch moo/bootstrap/mygame/__init__.py
touch moo/bootstrap/mygame/verbs/__init__.py
Each verb subdirectory will hold one .py file per verb. You don’t need
an __init__.py inside the subdirectories — load_verbs walks them as
filesystem trees, not as Python packages.
Step 2: Write the orchestrator
Create moo/bootstrap/mygame/bootstrap.py (the package’s __init__.py
is intentionally empty so test discovery doesn’t run the bootstrap as a
side effect of importing it):
import importlib.resources
import logging
from moo import bootstrap
from moo.core import code, lookup
from moo.core.models import Object
log = logging.getLogger(__name__)
_repo = bootstrap.initialize_dataset("mygame")
wizard = lookup("Wizard")
sys = Object.objects.get(pk=1)
_namespace = {
"log": log,
"bootstrap": bootstrap,
"lookup": lookup,
"wizard": wizard,
"sys": sys,
"repo": _repo,
}
_pkg = importlib.resources.files("moo.bootstrap") / "mygame"
_scripts = sorted(
(f for f in _pkg.iterdir() if f.name.endswith(".py") and f.name[0].isdigit()),
key=lambda f: f.name,
)
with code.ContextManager(wizard, log.info):
for _script in _scripts:
exec( # pylint: disable=exec-used
compile(_script.read_text(encoding="utf8"), _script.name, "exec"),
_namespace,
)
What this does:
bootstrap.initialize_dataset("mygame")creates theRepositoryrow, the System Object (#1), and the Wizard player. It is idempotent — a second call returns the existing repo._namespaceis the variable scope every numbered sub-script runs inside. Anything you put here is available as a bare name to those scripts.lintwill complain about “undefined-variable” inside sub-scripts; that’s expected, and a# pylint: disable=undefined-variablecomment at the top of each numbered file is conventional.importlib.resources.files(...).iterdir()finds every numbered.pyfile in the package and runs them in sorted name order — so file prefixes (010_,020_) determine load order.code.ContextManager(wizard, log.info)makes the bootstrap run as the Wizard player, so any objects created inherit Wizard ownership. The second argument is the writer callback — log lines fromprint()go throughlog.info.
This pattern is taken directly from moo/bootstrap/default/bootstrap.py.
Read that file alongside this one — your orchestrator should look very
similar.
Step 3: Define your root classes
Create moo/bootstrap/mygame/010_classes.py:
# pylint: disable=undefined-variable
root, _ = bootstrap.get_or_create_object("MyGame Root", unique_name=True)
sys.set_property("root_class", root)
root.set_property("description", "")
room, _ = bootstrap.get_or_create_object("MyGame Room", unique_name=True, parents=[root])
sys.set_property("room", room)
room.set_property("description", "An empty space.")
thing, _ = bootstrap.get_or_create_object("MyGame Thing", unique_name=True, parents=[root])
sys.set_property("thing", thing)
player, _ = bootstrap.get_or_create_object("MyGame Player", unique_name=True, parents=[root])
sys.set_property("player", player)
Two things matter here:
bootstrap.get_or_create_objectis the idempotent equivalent ofObject.objects.create(). Calling it twice returns the existing object the second time. Never usecreate()at the top level of a bootstrap script —moo_init --syncwould raiseIntegrityErroron the second run.sys.set_property("room", room)registers the class on the System Object so verbs can reference it as$roomin their shebang. The same goes for$thing,$player,$root_class. Without these registrations,--on $roomwon’t resolve whenload_verbsruns.
You are deliberately not parenting on Generic Room or any other
class from default. This is a fresh world.
Step 4: Create the starting rooms
Create moo/bootstrap/mygame/020_rooms.py:
# pylint: disable=undefined-variable
_rooms = {}
start, _ = bootstrap.get_or_create_object(
"Starting Square",
unique_name=True,
parents=[lookup("MyGame Room")],
)
start.set_property("description",
"A flat stone square at the centre of an empty plain. "
"Doors of carved wood face you in three directions.")
_rooms["start"] = start
sys.set_property("player_start", start)
north_hall, _ = bootstrap.get_or_create_object(
"North Hall",
unique_name=True,
parents=[lookup("MyGame Room")],
)
north_hall.set_property("description",
"A long stone hall lit by sputtering torches.")
_rooms["north_hall"] = north_hall
sys.set_property("player_start", room) makes that room the spawn point
for new players and the destination of home for players without a
custom home.
The _rooms dict is local to this script — variables defined in one
sub-script aren’t visible to the next. To pass references forward (so
the exit script can reference rooms), stash them on a class or on the
namespace dict. The simplest approach is to look them up by name in the
next script with lookup("Starting Square").
Step 5: Wire the exits
Create moo/bootstrap/mygame/030_exits.py. This step is intentionally
sketchy — every game’s exit model is different. The pattern below
mirrors default’s, where exits are first-class Objects with a dest
property. You may want something simpler.
# pylint: disable=undefined-variable
exit_class, _ = bootstrap.get_or_create_object(
"MyGame Exit",
unique_name=True,
parents=[lookup("MyGame Root")],
)
sys.set_property("exit", exit_class)
start = lookup("Starting Square")
north_hall = lookup("North Hall")
exit_north, _ = bootstrap.get_or_create_object(
"north",
parents=[exit_class],
location=start,
)
exit_north.set_property("dest", north_hall)
exit_south, _ = bootstrap.get_or_create_object(
"south",
parents=[exit_class],
location=north_hall,
)
exit_south.set_property("dest", start)
You’ll need to write a go verb (later) that consults dest and moves
the player. None of default’s exit/move.py applies here — you’re
defining the contract yourself.
Step 6: Add a verb
Create moo/bootstrap/mygame/room/look.py:
#!moo verb look --on $room --dspec none
from moo.sdk import context, NoSuchPropertyError
try:
desc = this.get_property("description")
except NoSuchPropertyError:
desc = "There's nothing remarkable to see."
print(this.name)
print(desc)
contents = list(this.contents.exclude(pk=context.player.pk))
if contents:
names = ", ".join(obj.name for obj in contents)
print(f"Here: {names}")
The shebang says: this is a verb named look that lives on whatever
object is registered as _.room (your MyGame Room class), and it
takes no direct object. Every room you create inherits this verb because
each room is parented on MyGame Room.
This is a complete, working look verb — but it is yours. There is no
inherited look from default. The same applies to every command you
want players to type: take, drop, go, say, inventory, and so
on. Each is one file under mygame/verbs/<class>/. See Your First MOO Verb
for the verb-authoring basics.
Step 7: Finalize and load verbs
Create moo/bootstrap/mygame/999_finalize.py:
# pylint: disable=undefined-variable
bootstrap.load_verbs(repo, "moo.bootstrap.mygame.verbs", replace=True)
load_verbs walks the verb package recursively, parses each shebang,
resolves the --on target, and creates or updates the corresponding
Verb row. replace=True means moo_init --sync will overwrite verb
source in place rather than skipping files whose verbs already exist.
Putting load_verbs in 999_finalize.py (rather than directly in
__init__.py after the script loop) matches the default package’s
convention and keeps the orchestrator focused on running scripts.
Step 8: Register the dataset
Edit moo/core/management/commands/moo_init.py and append "mygame" to
the builtin_templates list near the top of the file:
builtin_templates = ["default", "mygame"]
Today this whitelist is the only way to make a custom dataset selectable
via --bootstrap. Removing the whitelist in favour of filesystem
discovery is a separate follow-up; until that lands, this manual step is
required.
Step 9: Run the bootstrap
docker compose run webapp manage.py migrate
docker compose run webapp manage.py moo_init --bootstrap mygame
moo_init runs your orchestrator, which runs each numbered script in
order, then loads the verbs. If anything goes wrong it rolls back the
entire transaction — you can fix the bug and re-run.
Once it succeeds:
docker compose run webapp manage.py createsuperuser --username wizard
docker compose run webapp manage.py moo_enableuser --wizard wizard Wizard
ssh -p 8022 Wizard@localhost
Type look and you should see your starting room.
Step 10: Iterate
When you change a verb file, run:
docker compose run webapp manage.py moo_init --bootstrap mygame --sync
--sync re-runs your bootstrap against the existing database without
resetting it. get_or_create_object skips objects that already exist;
load_verbs(..., replace=True) updates verb source in place.
Step 11: Test it
Tests for your dataset live under mygame/tests/. Use the
t_init fixture with your dataset name:
import pytest
from moo.core import code, parse
from moo.sdk import lookup
from moo.core.models import Object
@pytest.mark.django_db(transaction=True, reset_sequences=True)
@pytest.mark.parametrize("t_init", ["mygame"], indirect=True)
def test_starting_room_has_description(t_init: Object, t_wizard: Object):
start = lookup("Starting Square")
assert "stone square" in start.get_property("description")
@pytest.mark.django_db(transaction=True, reset_sequences=True)
@pytest.mark.parametrize("t_init", ["mygame"], indirect=True)
def test_look_prints_description(t_init: Object, t_wizard: Object):
printed = []
with code.ContextManager(t_wizard, printed.append) as ctx:
parse.interpret(ctx, "look")
assert any("stone square" in line for line in printed)
The t_init fixture in moo/conftest.py accepts any in-tree bootstrap
name; passing ["mygame"] causes it to bootstrap your dataset before
the test runs.
Run the suite with:
uv run pytest -n auto moo/bootstrap/mygame/tests/
Where to look for more
moo/bootstrap/default/— the canonical real-world bootstrap package. Readbootstrap.pyfor the orchestrator pattern,000_initialize.pythrough999_finalize.pyfor concrete examples of class definition, room creation, player setup, and finalization.moo/bootstrap/default/verbs/<class>/— verb files organised by root-class name. Even though your world doesn’t reuse the verbs, the directory layout, shebang conventions, and SDK usage are all worth copying.The companion moo-agent project — ships a second from-scratch bootstrap (
zork1) contributed via the samemoo.bootstrap.*namespace asdefault. Its parallel root-class hierarchy (Zork Room,Zork Thing,Zork Container,Zork Actor) does not inherit fromdefault’s classes; its player commands (take,drop,examine, …) target a custom$playeralias rather than the LambdaCore one. Read it for a concrete example of supporting an alternate command vocabulary, and of out-of-tree bootstrap packaging via namespace packages.Bootstrapping Reference — function reference for
initialize_dataset,get_or_create_object,load_verbs,load_verb_source, andparse_shebang.Bootstrap Recipes — additional bootstrap patterns: property inheritance flags, ACL setup, multi-file organisation.
moo-agent’s
extras/zil_import/translator — if your world is itself a translation from another text-adventure source language, this package shows one approach (parser + IR + translator + generator) to producing a bootstrap from foreign data.
Out-of-tree packaging (future direction)
The pattern shown above keeps your bootstrap inside django-moo’s tree
because that’s what moo_init supports today. The package shape —
orchestrator, numbered scripts, sibling verb directory — is identical to
what a pip-installable out-of-tree package would need. When entry-point
discovery lands, the change will be straightforward:
Move
mygame/andmygame/verbs/out ofmoo/bootstrap/into their own repository.Add an entry-point row to your
pyproject.tomlalong the lines of[project.entry-points."moo.bootstrap"]declaring the bootstrap package.moo_init --bootstrap mygamethen resolves the package viaimportlib.metadata.entry_pointsinstead of the in-tree filesystem scan, and the whitelist inmoo_init.pygoes away.
Until that infrastructure ships, the in-tree path described in this tutorial is the supported route.