ZIL Importer Reference
The moo.zil_import package converts Infocom ZIL source files into a
DjangoMOO bootstrap package. It produced the zork1 dataset (covered in
django-moo’s bootstrapping reference) from the upstream dungeon.zil
and actions.zil files released by Microsoft / Activision under the MIT
License in 2025.
The importer is a build tool, not a runtime component. The output it
produces — moo/bootstrap/zork1/ — is committed to the repository.
Day-to-day use of django-moo never invokes the importer; it runs only
when the importer itself is being changed or when a new upstream ZIL
source is being absorbed. Every generated file carries a
# Generated by moo/zil_import — do not edit by hand header.
For background — what ZIL is, why translation is structured this way, and how the pipeline is laid out — see Why the ZIL Importer Exists.
Pipeline
The package is a four-stage pipeline; each stage is independently testable and exposes a small public surface:
*.zil ──► parser ──► converter ──► translator ──► generator ──► moo/bootstrap/<name>/
(tokens (IR dataclasses) (per-routine (per-bootstrap
+ AST) Python text) file emission)
Stage |
Module |
Public entry points |
|---|---|---|
Parser |
|
|
Converter |
|
|
Translator |
|
|
Generator |
|
|
Migration gate |
|
|
Coverage audit |
|
|
Snapshot |
|
|
Verb metadata |
|
|
Game config |
|
|
CLI |
|
|
Running the importer
uv run python -m moo.zil_import dungeon.zil actions.zil --output moo/bootstrap/zork1
moo.zil_import.__main__ delegates to cli.main, which parses each
input file, runs converter.extract_all, then generator.generate_all.
The bootstrap output is committed to the repository; subsequent
regenerations are an importer-development activity, not a deployment
step.
CLI flags:
Flag |
Default |
Effect |
|---|---|---|
|
required, repeatable |
ZIL source files, parsed in order. Manifests ( |
|
|
Per-game configuration slug; choices are |
|
|
Bootstrap output directory. Defaults to the selected game-config’s |
|
off |
Drop log level to |
|
off |
Run pylint on every generated file; fail when the score drops below |
|
|
Minimum acceptable pylint score (only used with |
IR dataclasses
The intermediate representation is a small set of dataclasses defined
in moo.zil_import.ir. Every stage downstream of the converter
operates on these — never on raw AST nodes.
- class moo.zil_import.ir.ZilRoom(atom, desc, ldesc, fdesc, exits=<factory>, flags=<factory>, globals=<factory>, action=None, value=0, pseudo=<factory>)
One ZIL
<ROOM …>form: title, descriptions, exits, flags, ACTION.
- class moo.zil_import.ir.ZilObject(atom, location, synonyms=<factory>, adjectives=<factory>, desc=None, ldesc=None, fdesc=None, text=None, flags=<factory>, action=None, capacity=0, size=5, value=0, tvalue=0, vtype=None)
One ZIL
<OBJECT …>form: location, synonyms, descriptions, flags, ACTION.
- class moo.zil_import.ir.ZilExit(direction, dest, message, condition, else_message, per_routine)
One direction’s exit record from a ZIL ROOM property.
- class moo.zil_import.ir.ZilRoutine(name, params, aux_vars, body, raw_zil, initial_values=<factory>)
One ZIL
<ROUTINE …>form: name, params, aux locals, body AST.
- class moo.zil_import.ir.ZilTable(name, values)
One ZIL
<GLOBAL name <TABLE …>>or<LTABLE …>value list.
ZilRoutine.initial_values keys parameter and aux-var names (in
UPPER-KEBAB form) to their declared default expressions. A missing
key means the variable has no declared default and the translator
emits None (parameters) or 0 (aux locals — Z-machine semantics).
Parser
- moo.zil_import.parser.tokenize(source)
Tokenize ZIL source text.
- moo.zil_import.parser.parse(tokens)
Parse a flat token list into a list of top-level nodes.
- Parameters:
tokens (
list[Token]) – Token stream fromtokenize().- Return type:
list[Union[str,int,None,list,tuple]]- Returns:
Top-level AST nodes (lists, tuples, atoms, ints,
None).- Raises:
ParseError – On unmatched
<>/()brackets.
- moo.zil_import.parser.parse_file(path)
Parse a ZIL source file.
- Parameters:
path (
str) – Filesystem path to the ZIL source.- Return type:
tuple[list[Union[str,int,None,list,tuple]],str]- Returns:
(nodes, source_text)wherenodesis the AST andsource_textis the raw file contents.
- class moo.zil_import.parser.Str
A ZIL string literal — distinct from bare atoms at the type level.
See ZIL Importer Reference for the rationale.
- class moo.zil_import.parser.Token(kind, value, line, offset=0)
One lexed ZIL token: kind, raw text, source line, and byte offset.
- class moo.zil_import.parser.ParseError
Raised on a malformed ZIL source — unmatched
<>/()brackets or an unknown token.
Str is a str subclass used to distinguish ZIL string literals
("hello") from bare atoms (HELLO). They look identical at the
character level after the parser strips quotes, but the translator
needs to emit one as a Python string literal and the other as either
an object reference or a state-variable read. isinstance(node, Str)
discriminates.
The number tokenizer uses a negative lookahead so ZIL predicate atoms
like 0? and 1? are recognised as single atoms rather than
number, atom pairs.
Converter
- moo.zil_import.converter.extract_all(nodes)
Walk parsed ZIL AST and extract every IR collection.
compound_verb_dictmaps(verb, particle)→ V-routine for forms like<SYNTAX TURN OFF OBJECT = V-LAMP-OFF>so the generator can emit particle-aware dispatchers.- Parameters:
nodes (
list) – Top-level AST nodes frommoo.zil_import.parser.parse().- Return type:
tuple[dict[str,ZilRoom],dict[str,ZilObject],dict[str,ZilRoutine],dict[str,ZilTable],dict[str,object],dict[str,list[tuple[int,str]]],dict[str,list[str]],dict[tuple[str,str],str],dict[str,list[tuple[int,str]]]]- Returns:
9-tuple of
(rooms, objects, routines, tables, globals_dict, syntax_dict, synonyms_dict, compound_verb_dict, bare_syntax_dict).
extract_all walks the parsed AST and returns a 9-tuple of dicts —
rooms, objects, routines, tables, globals, syntax rules, synonyms,
compound verbs, and bare-syntax rules — keyed by ZIL atom name (for
example, "WHITE-HOUSE", "TROLL", "ROBBER-FUNCTION").
Forms it does not recognise (free-standing strings, <COMPILATION-FLAG …>,
etc.) are silently dropped. Any form that matches a known head but
fails extraction is logged at WARNING level with the atom name and
the cause, and the form is skipped.
Converter notes
ZIL <TABLE> and <LTABLE> literals are extracted by
_extract_table_values. The helper resolves bare atoms (@-prefixed
references retained as references for late lookup), preserves <> nil
slots in villain records, and skips any (PURE)-style flag groups —
the IR carries only the data slot values.
Translator
- class moo.zil_import.translator.ZilTranslator(routine, object_atoms=None, routine_atoms=None, ambiguous_object_atoms=None, action_owner=None, owner_overrides=None, pre_handler_routines=None, display_names=None, substrate_display_names=None, routine_to_verbs=None, strictly_zero_object=None, allows_bare_invocation=None, lint_active=False, game_config=None)
Translate a single ZilRoutine body into Python verb source.
See ZIL Importer Reference (ZilTranslator state) for the full meaning of each constructor argument.
- Parameters:
routine (
ZilRoutine) – ZIL routine IR to translate.object_atoms (
set[str] |None) – Atoms naming a Room/Object — drivelookup("name")emission for atom refs.routine_atoms (
set[str] |None) – Atoms naming another ZIL routine — drive zero-arg dispatch on bare atoms in expression position.action_owner (
tuple[str,bool] |None) –(atom, is_room)of the owning room/object, orNonefor global helpers.owner_overrides (
dict[str,str] |None) – Per-routine--on $<owner>shebang overrides (uppercase routine name → owner property).pre_handler_routines (
set[str] |None) – V-routine names whosePRE-Xhandler should be inlined at the top of the substrate body.display_names (
dict[str,str] |None) – Atom → globally-unique display name.substrate_display_names (
dict[str,str] |None) – Substrate snake-name → display name.routine_to_verbs (
dict[str,list[str]] |None) – V-routine name → list of player verbs that dispatch to it via SYNTAX rules.strictly_zero_object (
set[str] |None) – V-routine names whose only SYNTAX form is 0-OBJECT (emit no--dspec).lint_active (
bool) – When set, emit only format-intrinsic pylint disables in the verb-file header.game_config (
GameConfig|None) – Per-game configuration; defaults toZORK1_CONFIG.
- f_constants_found()
Return the list of F-* constants handled by this routine.
- Return type:
list[str]- Returns:
F-* constants in clause order, or
[].
- has_f_dispatch()
Return True if this routine dispatches on F-* constants via COND/MODE.
- Return type:
bool- Returns:
Truewhen an F-* dispatch COND is present.
- has_m_dispatch()
Return True if this routine dispatches on M-* constants via COND/RARG.
- Return type:
bool- Returns:
Truewhen an M-* dispatch COND is present.
- has_verb_dispatch()
True if the routine has a per-clause-splittable VERB? COND.
Only action-owner routines get split; global helpers don’t.
- Return type:
bool- Returns:
Truewhen the routine is splittable.
- m_constants_found()
Return the list of M-* constants handled by this routine.
- Return type:
list[str]- Returns:
M-* constants in clause order, or
[].
- translate()
Return the full verb-file body, or empty when the residual is a no-op.
M-/F-/VERB? clauses that get split into per-clause files are pruned here so the residual god-verb carries only the catch-all body.
- Return type:
str- Returns:
The complete verb-file source, or
""when the generator should skip emission.
- translate_combined_clauses(skip_constants=None)
Emit one verb file that handles every M-/F-clause this routine dispatches on.
The body is a single
if rarg == "M-X": ... elif rarg == "M-Y": ...ladder mirroring ZIL’s<COND (<EQUAL? .RARG ,M-X> …) (<EQUAL? .RARG ,M-Y> …)>. All M-clause role names (preturnfuncfor M-BEG,turnfuncfor M-END, …) plus the F-clause roles plus the routine atom itself end up in the shebang as aliases so the parser /do_commandinvocations that name a specific role still resolve.Returns
""when every clause translates to a no-op (the substrate dispatch path handles the routine without per-clause overrides).- Return type:
str- Returns:
Complete verb-file source, or
"".
- translate_f_clause(f_constant)
Translate one F-* combat branch into a verb-file body.
Mirrors
translate_m_clausebut keyed on.MODE/F-...; the emitted file’s verb is taken fromM_TO_VERB(F-DEAD→f_dead) and dispatches on the routine’saction_owner(the villain).- Parameters:
f_constant (
str) – The F-* constant whose clause to extract (e.g."F-DEAD").- Return type:
str- Returns:
The clause body as a complete verb-file source, or
""when the body is a no-op.
- translate_m_clause(m_constant)
Translate one M-* branch into a verb-file body.
- Parameters:
m_constant (
str) – The M-* constant whose clause to extract (e.g."M-BEG").- Return type:
str- Returns:
The clause body as a complete verb-file source, or
""when the clause is a no-op (<>/ RFALSE / RTRUE) so the substrate verb runs via normal parser dispatch.
- translate_object_function_combined()
Emit one combined-callback file for an OBJECT-FUNCTION’s VERB? dispatch.
Phase 7 collapse: instead of emitting one
--on "<obj_name>"file per VERB? clause (rusty_knife/attack.py,rusty_knife/take.py, …), emit a single<routine_atom>.pywhose body is anif verb_name == "attack": ... elif verb_name == "take": ...ladder. Invoked exclusively viadispatch_object_function(obj, verb_name, prep, iobj)which looks upobj.actionand callsobj.invoke_verb(action_atom, …); the file is parser-inert (--dspec none).Replaces the per-VERB? split files for OBJECT action_owners. Pure ROOM-FUNCTION routines continue through
translate_combined_clauses().Returns
""for action_owner=None / room owners / verb-less routines / all-noop bodies (the caller should fall back to the legacy emission path or skip emission entirely).- Return type:
str
- translate_verb_clause(verb_atoms, extra_test, body_forms)
Translate one VERB? clause as a complete verb file body.
- Parameters:
verb_atoms (
list[str]) – ZIL verb atoms the clause dispatches on.extra_test – Optional extra test from an enclosing AND, or
None.body_forms (
list) – Body forms of the clause.
- Return type:
str- Returns:
Verb-file source, or
""for a no-op body.
- verb_clauses_for_split()
Yield
(verb_atoms, extra_test, body_forms)for each splittable clause.- Return type:
list[tuple[list[str],list,list]]- Returns:
A list of clause tuples.
- moo.zil_import.translator.translate_routine(routine, *, game_config=None)
Translate a ZilRoutine to a complete verb-file string.
- Parameters:
routine (
ZilRoutine) – The ZIL routine IR to translate.game_config (
GameConfig|None) – Optional per-game configuration; defaults toZORK1_CONFIG.
- Return type:
str- Returns:
Complete verb-file source.
- moo.zil_import.translator.translate_m_clause(routine, m_constant, *, game_config=None)
Translate one M-* clause from an ACTION routine.
- Parameters:
routine (
ZilRoutine) – The ZIL routine IR to translate.m_constant (
str) – The M-* constant whose clause to extract.game_config (
GameConfig|None) – Optional per-game configuration; defaults toZORK1_CONFIG.
- Return type:
str- Returns:
Verb-file source for the clause, or
""for a no-op.
- moo.zil_import.translator.translate_f_clause(routine, f_constant, *, game_config=None)
Translate one F-* combat clause from a per-villain ACTION routine.
- Parameters:
routine (
ZilRoutine) – The ZIL routine IR to translate.f_constant (
str) – The F-* constant whose clause to extract.game_config (
GameConfig|None) – Optional per-game configuration; defaults toZORK1_CONFIG.
- Return type:
str- Returns:
Verb-file source for the clause, or
""for a no-op.
- moo.zil_import.translator.has_m_dispatch(routine)
Return True if routine dispatches on M-* constants.
- Parameters:
routine (
ZilRoutine) – The ZIL routine IR.- Return type:
bool- Returns:
Truewhen an M-* dispatch COND is present.
- moo.zil_import.translator.has_f_dispatch(routine)
Return True if routine dispatches on F-* combat constants.
- Parameters:
routine (
ZilRoutine) – The ZIL routine IR.- Return type:
bool- Returns:
Truewhen an F-* dispatch COND is present.
- moo.zil_import.translator.sanitize_ident(name)
Convert a ZIL atom into a valid Python identifier.
See ZIL Importer Reference (Identifier sanitization) for the full rule set.
- Parameters:
name (
str) – The ZIL atom to sanitize.- Return type:
str- Returns:
A valid, non-shadowing Python identifier.
ZilTranslator accepts two optional sets, object_atoms and
routine_atoms, that disambiguate bare atoms in expression context:
An atom in
object_atomstranslates tolookup("name")— a real DjangoMOO object reference suitable for attribute access.An atom in
routine_atoms(a known ZIL routine) translates to a zero-arg verb dispatch on the routine’s owner (or_.thingfor free helpers).An atom appearing as a key in
GLOBAL_MAP(intranslator/constants.py) translates to the canonical Python expression for that ZIL global —WINNERandPLAYERmap tocontext.player,HEREtocontext.player.here(),SCOREtocontext.player.zstate_get('SCORE'), and so on.Anything else falls back to
context.player.zstate_get('NAME').
The generator passes the union of room and object atoms as
object_atoms, the set of routine names as routine_atoms, and a
GameConfig whose npc_atom_map extends GLOBAL_MAP per-game (e.g.
,THIEF → lookup("thief")).
Translator package layout
moo.zil_import.translator is a package with five files:
File |
Role |
|---|---|
|
The |
|
Atom maps, dispatch-mode tables ( |
|
Pure-function helpers for naming: |
|
Statement-level form-head handlers — one function per ZIL form ( |
|
Expression-level form-head handlers — arithmetic, comparison, flag predicates, table operators, |
Adding a new ZIL idiom is a one-line dispatch-table entry plus a small handler function.
Translation idioms
A representative sample of the translator’s recurring patterns:
ZIL form |
Python emission |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Bare |
|
|
|
|
|
|
|
|
|
|
|
For predicate routines (whose ZIL name ends in ?) the translator
wraps the trailing expression of the body in return so the implicit
ZIL return value becomes an explicit Python one.
Identifier sanitization
ZIL allows -, ?, and a leading . (local-var deref) in atoms;
Python does not. sanitize_ident (in translator/identifiers.py):
strips leading
.,replaces
-with_and trailing?with_p,collapses any other non-alphanumeric character to
_,prefixes leading digits with
v_,suffixes Python keywords and shadowed builtins (
set,list,dict,print, …) with_v.
The result is always a valid, non-shadowing Python identifier.
ZilTranslator state
ZilTranslator.__init__ accepts a routine plus several optional
context dictionaries supplied by the generator. The most commonly
relevant:
object_atoms,routine_atoms— atom-disambiguation sets described above.action_owner—(atom, is_room)of the room/object whoseACTIONproperty points at this routine. Drives the--onshebang on the generated verb file.display_names,substrate_display_names— atom → display-name maps so--on "<display>"shebangs resolve at verb-load time.routine_to_verbs— V-routine → list of player verbs that dispatch to it via SYNTAX rules.pre_handler_routines— V-routines whosePRE-Xhandler should be inlined at the top of the substrate body.lint_active— when set, the verb file emits only format-intrinsic pylint disables (no file-level escape hatches).game_config—GameConfiginstance (defaults toZORK1_CONFIG) whosenpc_atom_mapextendsGLOBAL_MAPper-game.
Game configuration
- class moo.zil_import.game_config.GameConfig(name, dataset_name, banner, manifest_files, license_blurb, npc_atom_map=<factory>, zork_number=1, exit_condition_overrides=<factory>, player_avatar_atoms=frozenset({'ADVENTURER', 'ME', 'PLAYER', 'WINNER'}), reset_body_filename='_reset_state_body.py', synonym_expansions=<factory>, adjective_expansions=<factory>, verb_atom_expansions=<factory>, avatar_aliases=(), skip_routines=frozenset({}))
Per-game knobs the translator and generator read at runtime.
- Variables:
name – Human-readable game name (used in docstrings and banners).
dataset_name – Bootstrap dataset key — also the directory name under
moo/bootstrap/and the value passed tobootstrap.initialize_dataset(...)andpytest.mark.parametrize("t_init", [...]).banner – Multi-line banner emitted by the bootstrap loader.
{rooms}and{objects}placeholders are filled in by the generator.manifest_files – Names of the canonical ZIL source files (used in the bootstrap docstring).
license_blurb – Licensing/credit paragraph for the bootstrap module docstring.
npc_atom_map – ZIL NPC atoms → DjangoMOO object names. The translator merges this into its global atom→Python-expr map so
,THIEF/,TROLLetc. compile to the rightlookup("...")call.zork_number – Numeric ZORK-NUMBER constant used by Infocom titles to gate text variants in shared source files. Only meaningful when translating an Infocom game with the
%<COND ... ZORK-NUMBER ...>macro; defaults to1for Zork 1. Other titles set their own value (Zork II → 2, Zork III → 3); games without the macro can ignore it.exit_condition_overrides – Game-specific exit guards keyed on
(room_atom, direction). The canonical ZIL emits room ACTION routines that block movement on flags (TROLL-MELEE blocks all four exits while TROLL-FLAG is set); the auto-translator only catches per-direction CEXIT/FEXIT guards. This map lets a game force a condition_flag + nogo_msg on the generated exit when the ACTION-based guard would have caught it.player_avatar_atoms – ZIL ACTORBIT atoms that name the player-character placeholder rather than a real NPC (ME, ADVENTURER for Zork; ARTHUR, FORD, TRILLIAN, ZAPHOD for HHG). These keep
Actoras their parent so they don’t get an anonymousPlayerrow.reset_body_filename – Filename (under
moo/zil_import/scripts/) whose contents become the generated099_reset_state.py. Defaults to Zork’s_reset_state_body.pyfor back-compat; HHG overrides to its own_hhg_reset_state_body.py.synonym_expansions – Per-game alias-expansion map. ZIL truncates dictionary entries to 6 chars (
ASPIRIstands foraspirin,ANALGEforanalgesic). The generator adds the value as an extra alias on any object whose synonym list contains the key, so player-typed full words still parse. Keys are uppercase truncated ZIL atoms; values are full-word lowercase aliases.adjective_expansions – Same shape as
synonym_expansionsbut applied to ADJECTIVE atoms. ZIL truncates adjective atoms the same way (DEMOLIfordemolition,OFFICIforofficial). Used by the generator when emitting cross-product<adj> <syn>multi-word aliases sotake junk mailresolves.verb_atom_expansions – Same shape applied to verb atoms in
syntax_dict. ZIL truncates verb atoms too (INVENTforinventory); the generator pulls the expanded form into the emitted dispatcher’s alias list so players can type the full word.
ZORK1_CONFIG is the default instance and supplies the Zork 1 banner,
manifest list, license blurb, and NPC atom map. HHG_CONFIG is a
second registered config (Infocom’s Hitchhiker’s Guide to the
Galaxy) that extends GameConfig with three HHG-specific knobs:
player_avatar_atoms— the multi-POV atomsARTHUR,FORD,TRILLIAN,ZAPHODthat name protagonist placeholders rather than real NPCs.synonym_expansions— full-word aliases for ZIL’s 6-char-truncated dictionary entries (e.g.ASPIRI→aspirin).reset_body_filename— the script undermoo/zil_import/scripts/whose contents become099_reset_state.pyfor this dataset.
Both configs are registered in GAME_CONFIGS and the CLI’s
--game-config flag selects between them. To target a third title,
construct a new GameConfig, register it in GAME_CONFIGS, and
either run python -m moo.zil_import … --game-config <slug> or pass
it directly to generate_all / ZilTranslator.
Generator
- moo.zil_import.generator.generate_all(rooms, objects, routines, output_dir, *, ir=None, options=None)
Generate the full
moo/bootstrap/<dataset>/tree.The ZIL IR dicts (tables, globals, syntax rules, synonyms, compound verbs, bare-form syntax) are bundled into
GeneratorIR; the optional linter and per-game knobs intoGeneratorOptions.- Parameters:
rooms (
dict[str,ZilRoom]) – All ZIL rooms keyed by atom.objects (
dict[str,ZilObject]) – All ZIL objects keyed by atom.routines (
dict[str,ZilRoutine]) – All ZIL routines keyed by name.output_dir (
Path) – Target directory (created if missing).ir (
GeneratorIR|None) – ZIL IR bundle (tables / globals / syntax / synonyms / compound-verb / bare-syntax dicts). Defaults to an emptyGeneratorIR.options (
GeneratorOptions|None) – Linter and per-game knobs. Defaults to a freshGeneratorOptionswithZORK1_CONFIG.
- Return type:
None
- class moo.zil_import.generator.GeneratorIR(tables=<factory>, globals_dict=<factory>, syntax_dict=<factory>, synonyms_dict=<factory>, compound_verb_dict=<factory>, bare_syntax_dict=None, rules=<factory>)
Optional intermediate-representation dicts produced by the converter.
Each is keyed by a ZIL atom; values describe tables, scalar globals, SYNTAX rules, SYNONYM aliases, compound-verb particles, and bare-form syntax rules. Absent keys default to empty dicts so the generator can run on partial IR.
- Variables:
tables – Atom → ZIL table values.
globals_dict – Atom → scalar GLOBAL initial value.
syntax_dict – Verb → list of
(arity, V-routine)tuples.synonyms_dict – Verb → synonym list.
compound_verb_dict –
(verb, particle)→ V-routine.bare_syntax_dict – Bare-form syntax rules;
Nonemeans “usesyntax_dict”.rules – Verb → typed
ZilSyntaxRulelist. The per-syntax-row emitter consumes this; legacy dispatcher emission reads the dict views above.
- class moo.zil_import.generator.GeneratorOptions(linter=None, game_config=None)
Per-run options for
generate_all.- Variables:
linter – Optional
moo.zil_import.lint.Linter. When set, the generator runs per-file pylint and raises on threshold breach.game_config – Per-game knobs (banner, dataset name, NPC atom map). Defaults to
ZORK1_CONFIGwhenNone.
generate_all accepts the parsed rooms/objects/routines plus a
GeneratorIR (tables, globals, syntax tables) and GeneratorOptions
(linter, game config). The output layout:
Output file |
Source |
|---|---|
|
empty (so test discovery doesn’t run the bootstrap) |
|
orchestrator that loads the numbered scripts |
|
rendered template — root-class hierarchy + Wizard parent |
|
ZIL |
|
one stanza per |
|
one stanza per |
|
ZIL |
|
per-room exit lists |
|
seed/reset records for realtime-scheduled daemons |
|
non-wizard player avatar plus Player record migration |
|
canonical world-state reset (per-game body via |
|
|
|
one file per translated routine; owner is the action_owner |
|
combined M-/F-clause emission; aliases every clause-role name in its shebang |
|
one parser-entry runner per |
|
passive V-routine helper invoked programmatically from the syntax-row runner |
|
|
|
room sanity assertions |
|
object sanity assertions, ambiguous-name aware |
|
structural exit assertions |
|
meta-test that every translated verb file loads |
Empty M-/F- clauses (whose ZIL body is <>) are skipped — the
generator does not register a no-op verb for them. The combined
M-/F-emission is implemented by
ZilTranslator.translate_combined_clauses and produces a single
verb file per action-owning routine with an if rarg == "M-X": …
ladder; the per-event filename layout the legacy generator emitted
(<verb>__m_<event>.py) is no longer used.
Migration gate
- moo.zil_import.migration.MIGRATED_VERBS: set[str]
Verb atoms whose dispatch lands on the new syntax-row emitter. Phase 4 starts with single-rule verbs (no compound particles, no prep-iobj variants). Multi-rule verbs from the originally-planned Batch A (take, drop, look, examine, read, open) require either per-particle parser disambiguation or in-body dispatch logic and land in later phases.
MIGRATED_VERBS is the per-verb switch for the syntax-row dispatcher
refactor. Verbs listed in this set emit through
templates/syntax_row.py.j2 — one runner per
(verb, particle, iobj_prep, arity) cell — and their substrate
--dspec is rewritten to none so the runner is the only parser
entry point. Verbs not in the set continue through the legacy
per-verb-atom dispatcher under verbs/actor/dispatchers/. The module
will be removed once the legacy path retires.
Coverage audit
- class moo.zil_import.audit.RegenAudit(game, routines=<factory>)
Accumulator for one regen. The generator instantiates one per game, calls the
add_*/record_*methods at decision points, and serialises it tocoverage.jsonafter the routines loop.- add_drop(name, kind, **details)
Record one drop on the named routine.
detailsbecomes the JSON entry alongsidekind. Convention: include enough identifying info that the drop is human-actionable (constant name, verb atoms, source-form snippet).- Parameters:
name (
str) – ZIL routine name.kind (
str) – Drop category ("m_clause","verb_clause","syntax_rule","unhandled_form", …).details (
Any) – Free-form identifying fields.
- Return type:
None
- record_emitted(name, *, action_owner=None, files=None)
Mark a routine as successfully emitted.
- Parameters:
name (
str) – ZIL routine name.action_owner (
tuple[str,bool] |None) –(atom, is_room)of the owning room or object, orNonefor substrate routines.files (
list[str] |None) – Verb-file paths the emission produced.
- Return type:
None
- record_f_clause(name, constant, mode)
Record how an F-* combat clause was emitted.
- Parameters:
name (
str) – ZIL routine name.constant (
str) – F-* constant (e.g."F-DEAD").mode (
str) –"combined"or"per_clause".
- Return type:
None
- record_file(name, file_path)
Attach an emitted verb-file path to a routine record.
- Parameters:
name (
str) – ZIL routine name.file_path (
str) – Relative path under the bootstrap output dir.
- Return type:
None
- record_m_clause(name, constant, mode)
Record how an M-* clause was emitted.
- Parameters:
name (
str) – ZIL routine name.constant (
str) – M-* constant (e.g."M-BEG").mode (
str) –"combined"(combined emission) or"per_clause"(legacy per-clause file).
- Return type:
None
- record_skipped(name, reason)
Mark a routine as deliberately skipped (e.g. in
_SKIP_ROUTINES).- Parameters:
name (
str) – ZIL routine name.reason (
str) – Short tag explaining why (e.g."_SKIP_ROUTINES").
- Return type:
None
- to_dict()
Render the accumulated audit as the
coverage.jsonpayload.- Return type:
dict[str,Any]- Returns:
A dict with
game,generated_at, asummaryblock, and the per-routineroutinesmap.
- write(output_dir)
Write
coverage.jsoninto the bootstrap output dir.- Parameters:
output_dir (
Path) – Bootstrap output directory.- Return type:
Path- Returns:
Path to the written file.
RegenAudit accumulates per-routine drop records during emission and
writes coverage.json next to the bootstrap output. Tracked drop
kinds are m_clause_dropped, f_clause_dropped,
verb_clause_dropped, syntax_rule_dropped, and unhandled_form.
The ratchet test at tests/test_translator_coverage.py compares
coverage.json against a checked-in baseline: a new drop fails as a
regression; a healed drop fails so the baseline can be re-collected
via tests/_collect_coverage_baseline.py.
Snapshot and restore
- moo.zil_import.snapshot.capture_snapshot(site, repo, snapshot_path)
Capture every Object on
sitetosnapshot_pathas JSON.- Parameters:
site – Active
django.contrib.sites.models.Site. Used to scope the Object query and recorded in the file header.repo (
str) – Dataset name ("zork1","hhg") — recorded so a restore can warn on dataset mismatch.snapshot_path (
Path) – Destination JSON file. Parent dir is created on demand.
- Return type:
Path- Returns:
Path to the written snapshot file.
- moo.zil_import.snapshot.restore_snapshot(snapshot_path, site)
Restore an Object snapshot onto
site.Re-asserts
site_pkandsite_domainbefore any write. The file’s recorded site MUST match thesiteargument’s actual values — otherwise raisesSnapshotSiteMismatchand aborts.- Parameters:
snapshot_path (
Path) – JSON file written bycapture_snapshot().site – Active
django.contrib.sites.models.Site.
- Return type:
None
- exception moo.zil_import.snapshot.SnapshotSiteMismatch
Snapshot’s recorded site doesn’t match the site argument.
Use the snapshot helpers when a --reset alone doesn’t unwind enough
state — long sessions accumulate mutated object locations, daemon
counters, and NPC flags that the avatar-only reset can’t fix.
capture_snapshot freezes every Object on the target site
(location_pk, properties, flags) into a JSON file;
restore_snapshot re-asserts site_pk and site_domain against the
recorded values before any write, raising SnapshotSiteMismatch if
they disagree.
Verb-file metadata
- moo.zil_import.verb_metadata.parse_verb_file(path)
Parse a verb file’s shebang line.
- Parameters:
path (
Path) – Verb file path. Read once; first line scanned for#!moo verband the rest returned as the body.- Return type:
tuple[VerbShebang,str] |None- Returns:
(shebang, body)tuple, orNoneif the file has no shebang.
- moo.zil_import.verb_metadata.iter_verb_files(bootstrap_root)
Walk every
.pyunder<bootstrap_root>/verbs/and yield the ones with a valid shebang.- Parameters:
bootstrap_root (
Path) – Directory containingverbs/(e.g.moo/bootstrap/zork1).- Return type:
Iterator[tuple[Path,VerbShebang,str]]
- class moo.zil_import.verb_metadata.VerbShebang(names, on, dspec, ispec)
Parsed
#!moo verbline.
Used by tests/test_bootstrap_consistency.py to enforce
shebang/body alignment across every generated verb file — every
verb-name literal in the body must appear in the shebang’s names
list, and every .perform("X", …) call must name a verb registered
somewhere in the bootstrap.
Verb tree layout
The static templates under moo/zil_import/verbs/ are copied
verbatim into the bootstrap output. Each top-level directory is a
verb---on owner:
Directory |
Owner |
Contents |
|---|---|---|
|
|
Parser shims, dispatch helpers ( |
|
|
Predicates and primitives every object inherits — |
|
|
Substrate verbs plus subdirs: |
|
|
Player-side verbs and state — |
|
|
The |
|
game-specific (Zork 1 only) |
Per-game overrides — e.g. |
|
game-specific (HHG only) |
HHG-specific overrides ( |
When the translator emits a routine whose ACTION property points at
a specific room or object, the per-action-owner verb file lands under
verbs/<owner-slug>/. After regen the directory tree therefore mixes
the static template owners above with action-owner directories like
verbs/sailor/, verbs/granite_wall/, verbs/troll/, etc.
If you add a new routine head to the translator’s recognition table
(SDK_HEADS in translator/constants.py), make sure the
corresponding SDK verb exists under verbs/root/ or
verbs/thing/ before regenerating.
Regenerating the bootstrap
After changing the parser, converter, translator, generator, or any
verb template — or after pulling a new upstream dungeon.zil /
actions.zil — regenerate with:
uv run python -m moo.zil_import \
~/path/to/zork1/dungeon.zil \
~/path/to/zork1/actions.zil \
--output moo/bootstrap/zork1
Then sync the running database:
docker compose run webapp manage.py moo_init --bootstrap zork1 --sync
--sync overwrites verb source in place via replace=True. Object
creation is idempotent because the bootstrap uses
bootstrap.get_or_create_object.