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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wanderland-0.1.2.tar.gz (300.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wanderland-0.1.2-py3-none-any.whl (180.0 kB view details)

Uploaded Python 3

File details

Details for the file wanderland-0.1.2.tar.gz.

File metadata

  • Download URL: wanderland-0.1.2.tar.gz
  • Upload date:
  • Size: 300.6 kB
  • Tags: Source
  • Uploaded using 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}

File hashes

Hashes for wanderland-0.1.2.tar.gz
Algorithm Hash digest
SHA256 d1da332624d3d4a68a4404dd4227721647db82ee41de059e99ff5291d44fa15a
MD5 8d3fbf59a31b650db632faac559c8dd7
BLAKE2b-256 722fcd66c1b25e1b886a48fe34728d1104422ff954618488c9e78c2b06b18858

See more details on using hashes here.

File details

Details for the file wanderland-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: wanderland-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 180.0 kB
  • Tags: Python 3
  • Uploaded using 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}

File hashes

Hashes for wanderland-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 6679de9d0f6e4b36be9554ef5fae1f3ab322b14249f37aa3dd369d39598789bb
MD5 31d1da3067c4a1b3fc3b2a392eabc9e4
BLAKE2b-256 2d2e6c270476b7b08bfa66f371c982dded7c59659eac8fecc5bd068524c79343

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page