Tutorial: Authoring a World

Two routes to a world: a Python generator in tools/worlds/ (how every campaign world is made) or the in-game 3D editor (how players make theirs).

True 3D, no conversion The original's 2D map of a mission gives its identity, theme, mechanics, difficulty and progression, and nothing else. No conversion, extrusion, tracing, side-view "2.5D", lane locking or row-to-row mapping: each world is its own hand-authored 3D design, using only the game's existing tiles and objects as the original uses them.

Route A: a Python generator

Every campaign world is the output of a script, tools/worlds/worldNNN.py, and the committed worlds3d/worldNNN.vwr is its build product. Change the script, never the file. The worked example here is mission 012, the crate lesson of the blue machine world.

1. Read the brief and the reference

plans/briefs/world012.md records what the original teaches (with the gameplay video's time span), the 3D design, and the checks that were run. For world 012: rooms climbing to a ledge too high to jump, each solved with crates (push one to the red dot, pull one out of a corner, stack two, then three). The 3D design turns the rooms into five machine terraces climbing away from the start, each higher than the last by its lesson's ledge. The original's map is worlds/world012.txt in the sibling repository mobile-eggbert; tools/world_inventory.py summarises every world.

2. The generator

A generator has a docstring naming its brief and how to run it, a main() that builds a World and saves it, and, where the checker cannot model a gate, a variant that writes the "solved" state. Shortened from tools/worlds/world012.py:

sys.path.insert(0, str(Path(__file__).resolve().parent))

from kit import MACHINE_FACES, MACHINE_FLOOR, MONITOR, O, World, props, rect, wall  # noqa: E402

RED_DOT = 3                 # the red ball tile marking where a crate goes
X0, X1 = 40, 58
# Terraces: (z0, z1, top); Blupi walks at top + 1.
T0 = (62, 70, 0)
T1 = (50, 61, 3)
...
SPAWN = (48, 1, 68)

def main(argv) -> int:
    solved = len(argv) > 1 and argv[1] == "--solved"
    rng = random.Random(12)
    w = World(mission=12, sky=0)
    for z0, z1, top in (T0, T1, T2, T3, T4):
        terrace(w, z0, z1, top, rng)
    ...
    def crate(x, y, z, final):
        w.place(O.CRATE, *(final if solved else (x, y, z)))

    # 1. Push the crate forward (-Z) to the red dot under the 3-high ledge of T1.
    w.set(48, 1, T1[1], RED_DOT)
    crate(48, 1, 66, (48, 1, 62))
    w.hint_zone(0, X0, 1, T0[0], X1, 3, T0[1])
    ...
    w.place(O.EXIT, 49, top, 18)
    w.spawn = SPAWN
    out = Path(argv[2]) if solved else Path(__file__).resolve().parents[2] / "worlds3d" / "world012.vwr"
    w.save(out)

The authoring API is tools/worldkit.py: World(mission, sky), set, get, clear, fill, box, stairs, place (objects, with an optional patrol end), bigdecor, hint_zone, rotate_plate, spawn, save. Coordinates are the raw grid (0-99, Y up); a block at height y makes y + 1 the walking level. Block numbers are icons of object-m.png; object types are Def::ObjectType values (ObjectType in worldkit names the common ones). The kits add building pieces: tools/worlds/kit.py for theme 1 (wall, slab, rail, props, lift, mine, sign_niche, rect, line), and one module per theme (palace.py, village.py, cheese.py, station.py, slime.py, toys.py, skyworks.py, clods.py, cobbles.py). The world format is described on The .vwr Format.

3. Regenerate

python3 tools/worlds/world012.py                     # writes worlds3d/world012.vwr
python3 tools/worlds/world012.py --solved solved.vwr # crates where the lessons put them
git diff --stat worlds3d/

Generators are deterministic (any randomness is seeded), so re-running an unchanged script reproduces the committed file byte for byte; a diff after you changed nothing is a bug in the script.

4. Check it

python3 tools/levelcheck.py worlds3d/world012.vwr      # FAIL by design: the crates must move first
python3 tools/levelcheck.py solved.vwr                  # the exit is reachable
python3 tools/campaigncheck.py
python3 tools/watercheck.py worlds3d/world012.vwr
python3 tools/audit/patrol_envelope.py
python3 tools/audit/lift_envelope.py
ctest --test-dir build-cna-opengl3 -R VerifyShippedWorlds

levelcheck.py is conservative: it models walking, jumps of up to two blocks or two cells, falls, lifts, swimming and teleporters, but not crates, dynamite, vehicles or timing. That is why world 012 needs its solved variant (both results were reproduced on 2026-11-07). Use flightcheck.py when a helicopter matters. Other generators have staged variants with their own options; read their docstrings.

5. Play it

cd build-cna-opengl3
./GalaxyEggbert --mission 12
./GalaxyEggbert --mission 12 --spawn 48,1,68,0     # x,y,z in raw grid, yaw in degrees

The build directory holds a staged copy of worlds3d/, refreshed by a post-build step of the game target, so it does not change when only a world file changed: copy the regenerated file over the staged one, or look at it with tools/qa_live.sh, which links the repository's worlds3d/ directly (Testing & QA).

6. Script the playthrough

Every campaign mission has a scripted playthrough in tools/autoplay/. The start of world012.txt:

# Mission 012 played through (plans/briefs/world012.md).
wait 30
expect mission = 12
expect lives >= 3
# 1. Push the crate four cells -Z to the red dot under T1 (it stops at T1's face), climb it.
go 48.00 62.89 tol 0.05 max 400
go 48 62 jump 1.4
until ground = 1
expect y >= 1.95
tools/autoplay.sh build-cna-opengl3 tools/autoplay/world012.txt 12

Write a section, run it, read the trace, repeat (trace N logs every tick, shots N saves frames). Jumps need a run-up; pickups need Blupi within about 0.6; synchronise on cycles with temp, blitz or crush, and on lifts with lift X Y Z. A changed route needs its waits retuned: mine, lift and cycle phases are tick-bound. A theme hub's script (world010.txt) includes its missions' scripts, so replay the hub too.

7. Finish

Update the brief (reference, design, checks) and the audit rows the change affects (python3 tools/audit/world_lessons.py --check, python3 tools/audit/lesson_inventory.py). Commit the script, the .vwr, the playthrough and the brief together, one world per commit, saying what the original does and how the 3D version does it.

Rules for a fair 3D world

Route B: the in-game 3D editor

The main menu's 3D World Editor button opens the slot browser: ten slots for each of the three players. Inside a world you fly the camera, aim with the mouse (or touch, or a gamepad) and place the game's own blocks, objects, BigDecor, Blupi's start, the background and the music from an icon palette in Eggbert 2's order; undo and redo cover every edit, and a box can be filled, copied and stamped. The dice saves and starts a play-test; Pause's Back returns to the editor. The controls are on 3D World Editor.

Editing a shipped world in the game The typed word xnjttjpo opens the editor on the world being played and saves over the shipped file (keeping a backup). That is a player feature. For the repository, a campaign world's source is its generator: change the script.