The Example Games
Five complete, playable programs ship in examples/:
- GLOOM, a real-time raycast shooter that exists in five languages
- GRUBWELL, a first-person dungeon crawler and the hot-reload showpiece
- a demoscene reel whose scenes reload live while it plays
- Gator Quest, a multiplayer terminal RPG
- JaiDoom, a DOOM engine written in JaiScript, which fetches its game data on first launch
GLOOM, GRUBWELL and the reel each have a thin C++ host (a VT console, key input, a seeded Rng, a clock, file IO) with everything else in script, and a drag-drop shim: one .jai file that re-creates what the host provides, so the standalone runner plays the game with no host built. Those three also play in your browser. Gator Quest and JaiDoom have no C++ of their own and run on the runner directly.
Every game has a headless seeded run that prints hashes of what it simulated or drew, so each is also a determinism test. The C++ hosts run the VM; the runner's --backend=interp runs the same scripts on the interpreter, which is how the two backends are compared.
To start a game of your own, see A Terminal Game in One File. The screenshots are real frames: docs/site_tools/capture_examples.py runs each game headless at a fixed seed and draws its terminal output cell for cell.
Building them
The games are part of the normal CMake build (JAISCRIPT_BUILD_EXAMPLES=ON by default when JaiScript is the top-level project):
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release --target jai_gloom jai_crawler jai_demoreel jaiscript_runnerBinaries land in build/bin/. Play the Release builds: a Debug engine is many times slower (Performance). On Windows, use Windows Terminal; the legacy console parses the color escapes far more slowly. The shims take their arguments after --.
GLOOM (examples/gloom): the five-language yardstick
A real-time 2.5D raycast shooter in the terminal: a four-map episode of corridors, keycards, secrets and monsters, with textured floors and ceilings, dynamic lights, and sound synthesized at boot by the script (it plays on Windows and on the Play page). Frames of the seeded autopilot (--smoke --look showcase, seed 666):



The same game, to the same spec, against the same C++ host and seeded RNG, was reimplemented in Squirrel, Lua, ChaiScript and Python, each written the way a competent user of that language would write it, and each reproduces the JaiScript reference bit-exactly at fixed state-hash checkpoints. The ports and the harness that times them are not in the published JaiScript repository; the JaiScript side re-measures with jai_gloom --smoke.
In the measured row, LuaJIT runs a tick in 0.097 ms and Lua 5.4.8 in 0.397; the JaiScript VM takes 0.634 ms serial and 0.461 on eight workers, ahead of Squirrel 3.2's 1.165. The JaiScript version drives its enemy brains with coroutine methods held in fields, builds its HUD with template strings, and runs its wall rays and particle pool through parallel_transform, deterministic at any worker count.
There are two looks over one simulation. The showcase look, the default in play, casts a ray per pixel column and textures and lights every pixel. The reference look, which the ports reproduce and --smoke draws by default, casts one ray per cell column over a flat floor and ceiling.
build/bin/jai_gloom # play: seed 666, VM backend
build/bin/jai_gloom --pix sext # 2x3 subpixels (needs a Unicode-13 font, e.g. Cascadia Mono)
build/bin/jai_gloom --256color # a terminal without 24-bit color (the default where COLORTERM says nothing)
build/bin/jai_gloom --ascii # a terminal without block glyphs, such as the Linux text console
build/bin/jai_gloom --look reference # the frames the ports draw
build/bin/jai_gloom --smoke --ticks 300 # headless: the conformance + perf protocol
build/bin/jai_gloom --bench 300 # the autopilot plays while you read ms/frame
build/bin/jai_gloom --dev # edit scripts/tune.jai while you play; the look reloads on save
build/bin/jai_gloom --mute # no sound
build/bin/jaiscript examples/gloom/gloom.jai # no host build at all: the drag-drop shim
build/bin/jaiscript examples/gloom/gloom.jai -- --dev # the shim reloads the same way
build/bin/jaiscript examples/gloom/gloom.jai -- --smoke --ticks 300Expected: --smoke prints total time, ms/tick, the hash of the whole frame stream and the final STATE_HASH. At --seed 666 --ticks 300 those are 7fecc09815bb64e0 and 3580805725 at every --workers count; with --look showcase the frames hash to 1b6efa4fcae4e573 and the STATE_HASH is the same. The shim's -- --smoke checks the reference checkpoint table, which the README in examples/gloom lists.
--dev shows hot reload on a running game. scripts/tune.jai holds the look's dials (light, shake, bob, vignette, sound); save it while you play and the next frame shows the change. A mistake is rejected with its line and the game keeps the look it had. look.jai, lights.jai, feel.jai and sound.jai reload the same way. No reload can touch the simulation, so STATE_HASH cannot tell one happened.
The shim, built on the runner's console builtins, guesses the color depth through env_var: 24-bit where COLORTERM is truecolor or 24bit and in Windows Terminal, xterm-256 elsewhere. -- --truecolor and -- --256color override it, and it takes --ascii and --look the same way.
| key | action |
|---|---|
w / s | move forward / back |
a / d | strafe |
| left / right | turn |
| space / ctrl | fire |
e | use: doors, suspicious walls, the exit switch |
| shift | run |
1 2 3 | pistol / scattergun / hexcaster |
m | automap |
| esc or Ctrl+C | quit (the screen, cursor and colors come back) |
GRUBWELL: dungeons of the unwashed (examples/crawler)
A first-person, step-based dungeon crawler in about 6,700 lines of JaiScript, and under the pixels a complete roguelike: ten floors, kin and professions, skills, affix loot, identify-by-use potions, a boss. The view is a one-ray-per-column raycaster in half blocks, inside a cockpit of four views, a log, a map, a compass and a character panel.
It is the most approachable codebase of the five, and the hot-reload showpiece. Save any script while you play, or press R, and the game reloads it under the running dungeon and logs what it kept (floor, turn, monsters, items, HP). A broken edit is refused with its file, line and error, and the run goes on under the old rules.





build/bin/jai_crawler # the title screen, seed 1337
build/bin/jai_crawler --colors 256 # the xterm 256-colour palette (default: 24-bit when COLORTERM says so)
build/bin/jai_crawler --ascii # no Unicode block glyphs, for the Linux text console
build/bin/jai_crawler --smoke --seed 6 --turns 1500
build/bin/jai_crawler --smoke --seed 6 --turns 1500 --reload-every 100 # hot reloads mid-run, same hash
build/bin/jaiscript examples/crawler/scripts/crawler.jai # the runner shim: no host build needed
build/bin/jaiscript examples/crawler/scripts/crawler.jai -- smoke turns 1500 seed 6The cockpit wants a console of about 135x37; a smaller one gets the forward view alone, down to 80x24. Both hosts restore the screen, cursor and colours on every exit, Ctrl+C included.
Expected: the --smoke run prints STATE_HASH: 2459230215 (the pilot dies on floor 8 on turn 839). The hash folds player, monsters, items, RNG state and facing. With --reload-every 100 the pilot re-runs every script each hundred turns (its first try, of a missing file, is refused) and prints the same hash. The shim's smoke run prints one hash on both backends; the README in examples/crawler lists the seeds it was verified with, including a god-mode autopilot that kills the boss on floor 10 at the same turn on both.
| key | action |
|---|---|
w / up | step forward; into a monster, attack |
s / down | step back |
a d / left right | turn 90 degrees |
q / e | sidestep left, right |
. | wait a turn |
z | class skill, down your facing |
g | pick up |
i | the pack: a letter to look closer, then e use or equip, d drop |
p / E | quaff the first potion / equip the best upgrade |
> | descend (on the stairs) |
m | the overhead map; there wasd, hjkl and the arrows step by compass direction, and your facing follows |
v | the forward view alone, or with LEFT, RIGHT and BEHIND around it |
c / ? | character sheet / keys |
esc | pause: s save and quit, q quit, r reload the scripts |
R | reload the scripts (saving one does the same) |
S / Q | save and quit / quit (asks once more) |
jai_demoreel (examples/demoreel)
A cold open and twelve auto-advancing truecolor scenes, scored to their own procedural soundtrack. Every effect is coded in scenes/*.jai, every frame is drawn in Unicode sextants (six pixels a cell), and the scenes read the score's kick, snare, chords and hits every frame. Press r and every scene hot-reloads without restarting.
The scenes: EIGHT (the language's features counted off around an 8-cube), DONUT (a torus ray traced in closed form), JULIA (a Julia set morphing in frame), LAVALAMP (ray tracing), PIPEDREAM (the pipes screensaver in 3D), BOIDS (a murmuration that flies geometry), GLOBHUNTER (a textured raycaster), CANYONRUN (VoxelSpace terrain in 16.16 fixed point), CHROME (a ray tracer of chrome and glass), OSSUARY (a skull on eight legs spinning an orb web), MAELSTROM (Navier-Stokes ink under a 3D wave tank), FINALE (fireworks over a skyline, and the credits).
Frames from jaiscript demoreel.jai -- shot N at T, which writes scene N (from one) at T seconds to shot.txt and shot.bmp:



build/bin/jai_demoreel # the reel, 30 fps pacing
build/bin/jai_demoreel --smoke # headless: the frame-stream hash + perf
build/bin/jai_demoreel --ascii # one density character per cell, for terminals without block glyphs
build/bin/jai_demoreel --pix half # half blocks instead of sextants, for a font without them
build/bin/jai_demoreel --capture I # print scene I as plain text (ANSI stripped)
build/bin/jai_demoreel --precompiled # boot from demoreel.jaibite (saved on first run)
build/bin/jaiscript examples/demoreel/demoreel.jai # drag-drop shim: the reel on the bare runner, with its soundtrack
build/bin/jaiscript examples/demoreel/demoreel.jai -- color 256 # the shim in xterm-256 color (by default it reads the depth as the exe does)
build/bin/jaiscript examples/demoreel/demoreel.jai -- frames 60 # headless self-test: sizes, a stream hash, timingjai_demoreel draws in 24-bit color under COLORTERM=truecolor (or 24bit) and in Windows Terminal, and in the xterm-256 cube elsewhere; --truecolor and --256color override it. q, ESC, Ctrl+C and SIGTERM all restore the cursor, the colors and the main screen.
Expected: --smoke runs the whole reel headless and prints boot time, total time, ms/frame, cells per second and the hash of the entire frame stream, 04c2f0009cddf0e4. --expect H exits non-zero unless the hash is H. The README in examples/demoreel carries the verified hashes and frame counts.
| key | action |
|---|---|
r | hot-reload every scenes/*.jai from disk |
h | step the HUD: two quiet chips (the scene; ms per frame, cores and fps), then a full status bar and a key bar that fills as the scene plays, then off |
f | freeze the clock (render keeps running) |
n / p / space / arrows | next / previous scene |
1..9, 0 | jump straight to a scene (0 = scene 12, the finale; keys are one-based, --capture indices are zero-based) |
q / ESC | quit |
The reel doubles as a whole-program benchmark. At 100×40 cells a frame is 150×120 sextant pixels, and nine of the twelve scenes split their work across every core. The 1,700-frame smoke run hashes the same at every worker count, and the runner's frames are byte-identical on the VM and the interpreter.
Gator Quest (examples/gatorquest)
An open-world RPG for the terminal: you are a gator, and six tyrants each hoard a Relic Scale. Combat and magic, fishing, mining, smithing and cooking, quests and bosses, and a shared world over a network for up to 16 players.
It runs on the stock jaiscript runner with no game-specific C++: about 14,000 lines of script generate the world, the art, the sound and the music. It is the largest program in the repository, with client-side movement prediction against a host, plain-file saves, and a dedicated server mode.
Frames from the headless capture modes, -- shot title and -- tour screen 150x44:



build/bin/jaiscript examples/gatorquest/scripts/gatorquest.jai # play (single player, host or join from the title)
build/bin/jaiscript examples/gatorquest/scripts/gatorquest.jai -- smoke # headless: a scripted session, then STATE_HASH
build/bin/jaiscript examples/gatorquest/scripts/gatorquest.jai -- serve port 28777 # a dedicated host with no screen
build/bin/jaiscript examples/gatorquest/scripts/gatorquest.jai -- predtest # the movement-prediction test over loopbackExpected: -- smoke prints the scripted session's journal and plan, then one STATE_HASH line, the same on both backends; -- smoke ticks 2000 seed 1 bots 2 prints STATE_HASH: 3218840367. Use a terminal with 24-bit color and UTF-8, at least 100x30. examples/gatorquest/README.md has the controls, the multiplayer commands and the save layout.
The sixteen-player limit is exercised over loopback: -- nettest runs a host and sixteen client sessions in one process and checks that a seventeenth is refused. nettest2.ps1 runs a dedicated host and two scripted clients as three processes, and -- predtest a host and one client on a virtual clock at several simulated round-trip times.
Against Lua. A Lua 5.4 port of what the smoke session simulates, with generated LuaJIT and Luau twins, prints the JaiScript run's output line for line, STATE_HASH included, over 27 seed, tick and bot configurations. Like the GLOOM ports, it is not in the published repository. On this simulation the VM takes 1.18 to 1.21 times Lua 5.4's time per tick, 1.20 to 1.23 times Luau's interpreter, about 2.1 times Luau's native code, and 6.4 to 7.1 times LuaJIT (medians of five on one P core; the measured row has the times).
JaiDoom (examples/jaidoom)
A DOOM engine written from the public WAD and engine format documentation, in about 10,000 lines of JaiScript (12,800 with its test scripts). It plays the Freedoom IWADs, the shareware episode of DOOM, and registered doom.wad or doom2.wad data you supply:
- the textured BSP renderer with COLORMAP light diminishing and palette flashes
- a 35 Hz fixed-point playsim, and monsters with the full state-machine AI (infighting included)
- hitscan and projectile combat, five weapons, powerups
- doors, lifts, stairs, teleporters, secrets
- the status bar, the automap, menus, intermission tallies, savegames and demos
- networked deathmatch and co-op with slots for up to 16 players
There is no C++ in the game. Its host surface is the runner's generic builtins: the keyboard (read_key, key_down), the terminal's size, files, sound, the NetServer/NetChannel socket classes and, for the first-launch fetch, the Download class and zip_extract. Clocks and frame pacing come from the standard library's time::.
cd examples/jaidoom/scripts # or run ../play.bat, ../play.sh
../../../build/bin/jaiscript jaidoom.jai # play; the first run fetches the data
../../../build/bin/jaiscript jaidoom.jai -- freedoom2 MAP01 # another IWAD and map
../../../build/bin/jaiscript jaidoom.jai -- doom2 MAP07 # your own registered WAD from wads/
../../../build/bin/jaiscript jaidoom.jai -- host [port] [coop] # netplay: host (deathmatch or co-op)
../../../build/bin/jaiscript jaidoom.jai -- join <host> [port] # netplay: join
../../../build/bin/jaiscript jaidoom.jai -- bootstrap # fetch the data without the screen
../../../build/bin/jaiscript p10_timedemo.jai # headless: a recorded demo, then FRAME_HASH and STATE_HASHThe game finds its folder from its own location, so dropping jaidoom.jai onto jaiscript.exe starts it from any working directory. It draws in 24-bit color where COLORTERM is truecolor or 24bit, in Windows Terminal and in any Windows console, and in the xterm-256 palette elsewhere; -- --truecolor and -- --256color override it. examples/jaidoom/README.md has the controls and the rest of the commands.
The game data is not in the repository. On first launch the game downloads the Freedoom project's two free IWADs from Freedoom's own release and, if you accept the prompt, the shareware episode of DOOM, which id Software has allowed to be passed around unmodified since 1993. Each file is checked against a published SHA-256 (crypto::sha256_file) before it is kept, and wads/SOURCES.txt records where every file came from and under which license. When the game folder is read-only the downloads and saves go to the per-user data folder. JaiDoom is a fan project, not affiliated with or endorsed by id Software; DOOM is a trademark of id Software.
Frames from scripts/shots.jai, on Freedoom Phase 1:



Netplay. The wire protocol is script: a host session runs a NetServer star hub that assigns player slots, relays packets and is the co-op authority for monsters and doors; each client holds one NetChannel, and every packet is a small JSON map. The netplay gate, p9_netplay.jai, runs the real host session against three scripted peers over loopback sockets (two in co-op), and a real client session against a scripted host. No 16-client session is part of the tests.
What it exercises in the language:
- The WAD loader parses the binary format with
binary::readerand abinary::layout, andbinary::writerbuilds the WAV, MIDI and BMP files it writes (Containers & Strings). - The playsim runs 16.16 fixed-point math with BAM angles on
int64under checked overflow, so a math bug throws instead of corrupting the world. - The framebuffer and all hot map data are typed arrays, and enemy brains and platform movers are coroutines.
On the VM a full play session runs at about 1.3 ms a frame, against DOOM's 28.5 ms tic, and a recorded 3,984-tic playthrough replays hash-identically on both backends (Performance).
Polling keyboard input
The runner's read_key() is nonblocking: it consumes one queued key-down edge and returns an empty string when none is pending. Letters are lowercased; arrows, escape, enter and tab use named keys. key_down(name) checks a held key instead: use it for movement. These functions come from the runner, so an embedding host provides its own input bindings (A Terminal Game in One File).
string key = read_key();
while (!key.empty()) {
if (key == "esc") { /* request exit */ }
key = read_key();
}