Skip to main content

🌱
Wanderland

A top-down Wanderland grid: a red character facing a locked yellow door, a key, and a goal ring

Open in marimo Open on notebook.link

An interactive low-poly 3D coding playground as an anywidget, built for Python notebooks. Write simple Python commands and watch a charming little character — Mo the Mossball — animate through a stylized world, collecting gems and reaching goals.

It captures the joy of a learn-to-code playground — write code, watch the character act it out — with an original character, original art, and a small Python command API, running entirely inside an interactive notebook.

# Create and show the world (it has its own ▶ Run My Code button)
# Any anywidget-compatible notebook works: Jupyter, marimo, VS Code...
import wanderland as mp
from wanderland import move_forward, turn_right, collect_gem

# For marimo, you might wrap this in mo.ui.anywidget()
world = mp.World(mp.puzzles.gem_path())
world  # renders the 3D scene + the in-scene Run button

# Your program (editing this just *loads* it; the character doesn't move yet)
# gem_path's gems are non-blocking: walk the character onto each one, then collect_gem().
def solution():
    move_forward()
    move_forward()
    move_forward()
    collect_gem()   # first gem

    turn_right()
    move_forward()
    move_forward()

    turn_right()
    move_forward()
    move_forward()
    move_forward()
    collect_gem()   # second gem

    turn_right()
    turn_right()

    move_forward()
    move_forward()
    move_forward()

# Hand the program to the widget
world.load(solution)
# ...now press ▶ Run My Code in the scene to animate it once.

# Read the outcome back in Python (synchronous, after load)
world.success            # True
world.gems_collected     # 2
world.reached_goal       # True

The ▶ Run My Code button lives inside the widget. Editing the program reloads its timeline silently; the character only moves when you press Run, and then stays at their final pose.


Install & run

pip install wanderland          # or: uv add wanderland

The published package ships the prebuilt 3D frontend — no Node required. It works in any notebook that supports anywidget (marimo, Jupyter). Open the example notebook to play:

uv run marimo edit example.py   # the teaching playground

You'll see your character (like Mo the Mossball) standing in a warm low-poly world; running a program animates them through your commands step by step. Drag to orbit the camera.

Develop from source (rebuild the frontend)

Requires Python ≥ 3.10 and Node ≥ 18.

npm install && npm run build     # build the 3D bundle -> src/wanderland/static/index.js
uv venv && uv pip install -e ".[dev]"
uv run marimo edit example.py

Solve puzzles with code

A program is an ordinary Python function. Inside it you call commands in the order you want them to happen:

command what it does
move_forward() step one tile in the direction faced
turn_left() / turn_right() rotate 90° — turning is egocentric (relative to the current heading)
pickup() take the object in the cell faced (a key/ball/box, or a blocking gem) into your hand — carry limit one. You don't move.
drop() drop the carried object onto the empty floor cell faced
toggle() open/close the door faced (a locked door opens with a matching-color key, which you keep); open a box to reveal its contents
collect_gem() collect the non-blocking gem on the tile you're standing on (walk on, then collect; scores, not carried)
move_backward() step back without turning — free-play only, off the canonical action set

Interaction is always on the cell you face, standing adjacent — you never walk onto a blocking object. A blocked move is animated by why it failed: the character teeters at the brink of the world's edge (a near-fall), and bonks off a wall, door, or object.

Action space

Every world declares the exact set of verbs it permits — explicitly, with no default and no canonical bundle. That declared set is the action space you're allowed to use, and it's enforced: calling a verb the world didn't list raises.

world.action_space     # ('move_forward', 'turn_left', 'turn_right', 'pickup', 'drop', 'toggle')
world.actions_doc      # [{'name': 'pickup', 'doc': '...'}, ...]  — documentation for the actions

Running a program

Three ways to drive the character, depending on who pulls the trigger:

  • world.load(solution) — the recommended notebook flow. Captures the commands, simulates them, and hands the timeline to the widget without playing. The user presses the widget's own ▶ Run My Code button to animate it once; the character stays at their final pose. Editing the program reloads silently.
  • world.run(solution) — captures and plays immediately (no button). Handy for programmatic or headless use; returns the result dict.
  • @world.program — decorator form of run(); plays whenever the defining cell re-executes.

All three capture the command sequence and simulate it in Python (the source of truth); world.success and friends are available synchronously regardless of which you use. The example notebook uses load() + the in-scene button.

Reading the outcome

Because the simulation runs in Python, results are available synchronously right after the program runs (and work even without a browser):

world.success          # all (non-blocking) gems collected AND goal reached
world.gems_collected   # int
world.total_gems       # int
world.reached_goal     # bool
world.result           # the full dict: final pose, what's carried, ...

For reactive readback (like in marimo), read world.value (or world.state) in another cell — the frontend writes a playback report there when the animation finishes.

For Educators: Creating Custom Worlds

Wanderland makes it easy to design your own levels and assignments for students. You can sketch out puzzles visually using simple ASCII text strings.

from wanderland import from_ascii, World

level_design = """
> . # .
. . Ly .
Ky . # O
"""

allowed_actions = (
    "move_forward",
    "turn_left",
    "turn_right",
    "pickup",
    "toggle",
)

puzzle = from_ascii(
    "Locked Room",
    level_design,
    actions=allowed_actions
)

# You can also pick a different character!
world = World(puzzle, character="rover")  # A hovering drone-bot instead of Mo

Each cell is one whitespace-separated token; the top row is north, columns go east:

token meaning
^ > v < start tile and the character's facing (N/E/S/W) — S also works with heading=
. # ~ ! O floor · wall · water (impassable) · lava (walkable but deadly) · goal
g / G non-blocking gem (walk on, then collect_gem()) / blocking gem (pickup() from the front)
Kc Bc Xc key / ball / box of color c (r g b p y e) — Xc:obj gives a box hidden contents
Dc Lc closed / locked door of color c

actions= is required. Built-in worlds live in mp.puzzles (first_steps, gem_path, spiral, locked_room).

Rendering: floor, water, walls, gems, and the colored objects (keys, balls, boxes, doors) all render in 3D, and pickup/drop/toggle animate — the carried item floats above the character, doors unlock and swing open, boxes open to their contents. (Box contents stay hidden when printing the world state.)

License

MIT

Metadata

Release files for wanderland 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for wanderland 0.1.2
File Size Uploaded
wanderland-0.1.2.tar.gz 300.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for wanderland 0.1.2
File Interpreter ABI Platform
wanderland-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 480.6 kB

Release files / wanderland-0.1.2.tar.gz

Download URL wanderland-0.1.2.tar.gz
Size 300.6 kB
Tags Source
SHA-256 checksum
How to use checksums
d1da332624d3d4a68a4404dd4227721647db82ee41de059e99ff5291d44fa15a
BLAKE2b-256 checksum
How to use checksums
722fcd66c1b25e1b886a48fe34728d1104422ff954618488c9e78c2b06b18858
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.6 {"installer":{"name":"uv","version":"0.10.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / wanderland-0.1.2-py3-none-any.whl

Download URL wanderland-0.1.2-py3-none-any.whl
Size 180.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6679de9d0f6e4b36be9554ef5fae1f3ab322b14249f37aa3dd369d39598789bb
BLAKE2b-256 checksum
How to use checksums
2d2e6c270476b7b08bfa66f371c982dded7c59659eac8fecc5bd068524c79343
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.6 {"installer":{"name":"uv","version":"0.10.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page