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):

shell
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release --target jai_gloom jai_crawler jai_demoreel jaiscript_runner

Binaries 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):

GLOOM in play on E1M1: the pistol fires and its flash lights the brick walls and the flagstone floor orange; a grunt bursts in a white flash just ahead, its blood pooled on the floor, under a beamed ceiling; the status bar reads health 90, pistol 40, and the message line says GRUNT destroyed.
E1M1, the Dimlit Antechamber: the muzzle flash lights the room as a grunt goes down.
The GLOOM title screen: the word GLOOM in pale, blood-dripping letters over a dark sky, a wall of fire burning along the bottom with embers drifting up, the line 'a terminal raycaster, written in JaiScript', 'press FIRE to descend' and the keys.
The title, burning.
The floor tally after E1M1: FLOOR CLEARED in gold block letters, kills 7 / 7 and secrets 1 / 1 at 100% with green bars, and a time of 1:06 against a par of 0:55.
The tally: every kill and the secret, a little over par.

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.

shell
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 300

Expected: --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.

keyaction
w / smove forward / back
a / dstrafe
left / rightturn
space / ctrlfire
euse: doors, suspicious walls, the exit switch
shiftrun
1 2 3pistol / scattergun / hexcaster
mautomap
esc or Ctrl+Cquit (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.

The GRUBWELL cockpit on floor 2, turn 187, in the mossy Mossrot Warrens: down the corridor ahead a glowing orange ember bolt flies at a Grublin, lighting the walls and floor around it; around the view sit the log, the map, the views behind, left and right with their wall sconces, the compass facing east, the character panel at 17 of 28 HP and the loot in sight.
Turn 187 of the seed 6 smoke run: an Emberbolt in flight lights the corridor on its way to a Grublin.
The first frame of a run: the banner FLOOR 1, MOSSROT WARRENS over the forward view, JAI's statue a few steps ahead, a blade glinting on the floor among bones, and the log saying that something grunts past the statue.
Floor 1 opens on a banner, JAI's statue, and the last one down's blade.
Mid-run hot reload: the banner RELOADED, YOUR RUN KEPT over the forward view as a bright scanline sweeps down every pane; the log reads Reloaded 14 scripts, and Kept: floor 1, turn 60, 3 monsters, 2 items, HP 20 of 20, under an earlier refused reload of a missing file.
A reload mid-run names what it kept; the refused one above it changed nothing.
The GRUBWELL title screen: the name in green and gold block letters above a stone well that spirals down to a glowing green light, embers rising out of the glow and pairs of eyes in the brickwork, a menu offering a new descent, how to play, and leave, and a line saying R reloads the scripts mid-run, and so does saving one.
The title looks down the Well.
The victory page: VICTORY in cream and gold letters over rainbow flames rising from the bottom of the screen, and a box titled THE COLOR RETURNS with the run's hero, level, kills, gold, turns and floor.
Victory over Vexadrach (seed 5, god mode).
shell
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 6

The 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.

keyaction
w / upstep forward; into a monster, attack
s / downstep back
a d / left rightturn 90 degrees
q / esidestep left, right
.wait a turn
zclass skill, down your facing
gpick up
ithe pack: a letter to look closer, then e use or equip, d drop
p / Equaff the first potion / equip the best upgrade
>descend (on the stairs)
mthe overhead map; there wasd, hjkl and the arrows step by compass direction, and your facing follows
vthe forward view alone, or with LEFT, RIGHT and BEHIND around it
c / ?character sheet / keys
escpause: s save and quit, q quit, r reload the scripts
Rreload the scripts (saving one does the same)
S / Qsave 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:

CHROME: three mirrored spheres, gold, red and glass, bouncing over a red and cream checkerboard under a sunset sky, small chrome satellites orbiting them, each sphere reflecting the floor and the sky.
CHROME, a ray tracer: one ray per pixel, every core (9 at 11).
EIGHT's FOUR slide: the word FOUR and the caption HOT RELOAD, a code card of a loop importing rules.jai, a checklist, the 8-cube turning in a window over the hyperbolic plane, and a live panel where a reloaded script spins a star.
EIGHT counts the language in, one slide a feature: FOUR reloads a script mid-scene (1 at 23.8).
FINALE: a night skyline of towers lit as level meters over black water, fireworks bursting in the sky, and the credits listing the twelve scenes between them.
FINALE: fireworks over towers that meter the score, and the credits (12 at 24).
shell
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, timing

jai_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.

keyaction
rhot-reload every scenes/*.jai from disk
hstep 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
ffreeze the clock (render keeps running)
n / p / space / arrowsnext / previous scene
1..9, 0jump straight to a scene (0 = scene 12, the finale; keys are one-based, --capture indices are zero-based)
q / ESCquit

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:

Gator Quest in Swampton at seven in the morning: stone streets and timber houses seen from above, the hero gator among townsfolk and guards, and the side panel with HP and MP bars, gold, the place and time, the equipped armor, a minimap and the journal log.
Swampton in the morning.
The Gator Quest title: GATOR QUEST and 'A SWAMP IN NEED OF A HERO' in green and yellow pixel letters over the town, a name prompt reading CHOMPS in mud brown, and a side menu to play alone, host or join, with the story and the controls.
The title: name your gator, pick a color, play alone, host or join.
The same streets at ten at night: the town in deep blue shadow, lit only around the torches and the forge.
The same streets after dark, lit by torches and the forge.
shell
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 loopback

Expected: -- 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::.

shell
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_HASH

The 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:

JaiDoom on Freedoom's E1M1: a pink creature takes pistol fire in front of a row of monsters on a ledge above blue water, a large tentacled creature looms at the right, and the status bar along the bottom shows ammo 49, health 100% and the marine's face.
E1M1 by the water: the textured BSP renderer, sprites, the weapon and the status bar, in half blocks.
The Freedoom Phase 1 title picture: an armored marine with a gun and a tentacled monster on a red background, the Freedoom logo above and PRESS ANY KEY below.
The title screen, from the WAD's own TITLEPIC.
A courtyard of the same map: a horned creature stands before a pillar marked with a blue cross, over a dirt floor with a patch of grass and a green potion, the pistol raised.
A courtyard of the same map, mid-fight.

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::reader and a binary::layout, and binary::writer builds the WAV, MIDI and BMP files it writes (Containers & Strings).
  • The playsim runs 16.16 fixed-point math with BAM angles on int64 under 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).

jaiscript
string key = read_key();
while (!key.empty()) {
	if (key == "esc") { /* request exit */ }
	key = read_key();
}