🌱
Wanderland
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 ofrun(); 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/toggleanimate — 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d1da332624d3d4a68a4404dd4227721647db82ee41de059e99ff5291d44fa15a
|
|
| MD5 |
8d3fbf59a31b650db632faac559c8dd7
|
|
| BLAKE2b-256 |
722fcd66c1b25e1b886a48fe34728d1104422ff954618488c9e78c2b06b18858
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6679de9d0f6e4b36be9554ef5fae1f3ab322b14249f37aa3dd369d39598789bb
|
|
| MD5 |
31d1da3067c4a1b3fc3b2a392eabc9e4
|
|
| BLAKE2b-256 |
2d2e6c270476b7b08bfa66f371c982dded7c59659eac8fecc5bd068524c79343
|