wasmcart
CPython 3.13 and pygame-ce, compiled to WebAssembly.
Existing pygame games port largely as-is. Your cart ships Python source and assets — the interpreter is already built.
Eight prebuilt carts ship with every release. Download a .wasc and run it — no Python install, no pygame install, no build:
npx wasmcart boom_py.wasc
Each cart carries CPython 3.13, pygame-ce, and the game's own code and assets. Nothing to install but Node 22+.
Everything below lives in wasmcart/wasmcart-pygame.
Point the packer at a directory of Python files and assets:
bash pack_game.sh my_game_dir/ out/my_game.wasc "My Game"
Packing itself is npx wasmcart pack, the same command every cart uses. The wrapper script exists because a Python cart also needs the trimmed stdlib and the pygame helper modules staged alongside your game first, and it scrapes your declared resolution while it is there. Then run it, with npx as usual:
npx wasmcart out/my_game.wasc # SDL window, auto-detects GL
npx wasmcart out/my_game.wasc --term # ANSI terminal, SSH-friendly
npx wasmcart out/my_game.wasc --frames 120 --shot out.png # headless
No dialect, no shim layer to learn — the same calls you already write:
import pygame
pygame.init()
screen = pygame.display.set_mode((640, 480))
clock = pygame.time.Clock()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
raise SystemExit
screen.fill((0, 0, 0))
pygame.draw.circle(screen, (60, 200, 60), (320, 240), 40)
pygame.display.flip()
clock.tick(60)
wasmcart games are gamepad games. pygame's joystick API reaches the pad, and on a desktop the keyboard maps onto it so you can test without a controller.
The actual interpreter, not a reimplementation: classes, generators, decorators, dataclasses, json, re. The stdlib is trimmed to what a sandboxed cart can reach, so no sockets, threads or subprocesses.
The community edition, compiled to WASM against an SDL2 backend that targets the cart's framebuffer and audio ring instead of a window system.
moderngl, numpy, glm and pywavefront shims are included. The examples include a textured raycaster and a shadow-mapped GL scene with a skybox. The numpy shim indexes, slices and iterates, but it is not real numpy: per-pixel loops work, vectorized arithmetic like arr[:,:,0] *= 2 does not.
pygame.surfarray works, and pixels3d is a genuine live view: writes land in the Surface rather than in a copy you throw away. Built on the memoryview a Surface already exports, so procedural textures and image effects run against real pixels.
A host sizes its window before the cart runs, so the packer scrapes your game's resolution and records it. A 1600x900 game is announced as 1600x900.
Every example ships CC0 or CC-BY art and audio with per-asset provenance recorded, and the generators are in the repo, so nothing here is a binary you cannot rebuild.
The same .wasc plays on your desktop, in a browser, in a terminal, through RetroArch via the libretro core, or on a retro handheld or phone. No per-platform build.
SolarWolf, the arcade game by pygame's own author, is ported in ports/solarwolf/ with 37 of its 38 Python modules byte-identical to upstream. Only main.py is replaced, because a cart is driven by a frame callback rather than its own while loop. That port is what found eight runtime gaps, including a mixer.music that was silently a no-op and a clock reading wall time instead of frames, so timed transitions stalled while every frame still rendered perfectly.
A CPython cart is 10-12 MB, because it carries an entire interpreter and stdlib. That is the price of skipping the compiler: the same game written in C would be a few hundred kilobytes. If cart size matters more than authoring speed, the compiled path is the one you want.