wasmcart

Lua

Write games in Lua 5.4. Ship them as cartridges.

A LÖVE-style API on a prebuilt engine — love.load, love.update(dt), love.draw. There is no compiler in your loop.

the wasmcart-lua release licence

Try it in one command

Eight prebuilt carts ship with every release — including Cavern, a complete LÖVE adventure game. Download a .wasc and run it:

npx wasmcart cavern.wasc

Nothing to install but Node 22+. The cart carries the Lua engine, the game, and its assets.

Start your own game

Everything below lives in wasmcart/wasmcart-lua.

The engine wasm ships in the repo, so there is no toolchain to set up. Copy the template and run it:

git clone https://github.com/wasmcart/wasmcart-lua
cp -r wasmcart-lua/template my-game
cd my-game
cp ../wasmcart-lua/build/engine.wasm main.wasm

npx wasmcart .

That opens a window. Edit app/main.lua and run npx wasmcart . again — that is the whole loop. The template also ships a run.sh that does exactly those two steps if you would rather type less.

A complete game

This is a whole cart. Gamepad in, shapes out, 60 times a second:

local x, y = 640, 360

function love.update(dt)
  if love.pad.isDown("left")  then x = x - 6 end
  if love.pad.isDown("right") then x = x + 6 end
end

function love.draw()
  love.graphics.setColor(0.4, 0.8, 1)
  love.graphics.circle("fill", x, y, 36)
  love.graphics.print("hello from lua!", 40, 40)
end

wasmcart games are gamepad games: design for d-pad, face buttons and sticks. On a desktop the arrows and z/x map onto pad 1 so you can test without a controller.

What you get

Real Lua 5.4

Not a subset or a dialect: coroutines, metatables, varargs, the garbage collector, string.format, pcall, integer division. Pure-Lua libraries drop into app/ and require works.

Batched GL2D drawing

One atlas and one draw call for a whole frame of sprites, with circle coverage computed in the fragment shader. A software rasterizer lives in the same binary for hosts with no GL.

Shaders and meshes

love.graphics.newShader takes LÖVE's own effect and position functions and applies to every draw path, solids and sprites and text alike. newMesh gives arbitrary triangles with per-vertex colour and UVs, in fan, strip or triangles mode.

Box2D physics

Box2D 3.2 compiled with WASM SIMD, behind both a windfield-style API and LÖVE-shaped entry points. Multiple independent worlds are supported.

Transpilers ride along

Anything that compiles to Lua works unchanged: Teal, Fennel, YueScript, MoonScript. Transpile to .lua, drop it in app/.

Deterministic by design

love.math.random and math.random are both seeded by the host, so the same seed and inputs produce byte-identical frames on every host.

Audio and canvases

16 mixer voices at 48kHz, WAV and OGG, plus render targets you can draw into and then draw like an image.

Ship it

When you want a single shippable file:

npx wasmcart pack --wasm main.wasm --assets app --name my-game \
  --width 1280 --height 720 -o my-game.wasc

One .wasc that runs on every wasmcart host: the CLI, a browser, RetroArch through the libretro core, or a handheld.

A real LÖVE game runs on it

Cavern, a complete open-source LÖVE adventure game, is ported in ports/cavern/ with all 97 of its Lua files byte-identical to upstream — only its physics wrapper is swapped for the engine's native one. That port is what drove Box2D, the Lua 5.1 compatibility layer, SpriteBatch and a dozen other gaps into the engine.

← All languages