frankiegame
Introduced in v1.20, with image and audio API parity added in v1.21. A tiny terminal game engine — fixed-timestep loop, double-buffered ANSI rendering, non-blocking keys. Zero dependencies, built entirely on stdlib termios.
frankiegame shares its entire API with frankiecanvas (the browser renderer) — write your game once, and the stitch line picks where it renders.
Quick Start
stitch "frankiegame"
g = game_new(width: 40, height: 20, fps: 15)
on_key(g, "left") do |game|
game["px"] -= 1
end
on_tick(g) do |game|
clear(game)
draw(game, game["px"], 10, "🧟")
text(game, 0, 0, "score: #{game["score"]}", color: "green")
end
run_game(g)
Start a playable template in one command:
frankiec new --game mygame
cd mygame && frankiec run main.fk
Keys
Single characters plus "up", "down", "left", "right", "space", "enter", "esc", "tab", "backspace". q quits by default (configurable via quit_on_q:).
Arrow keys read raw bytes directly from the terminal file descriptor (fixed in v1.20.1 — earlier builds decoded every arrow as esc).
Headless / CI Mode
Without a tty, rendering and input silently no-op — your test suite can run the game loop for real. Pair with max_ticks: to bound it:
g = game_new(width: 10, height: 5, max_ticks: 40)
run_game(g) # runs exactly 40 ticks, then returns — no tty needed
render_frame(g) returns the current buffer as a string, so tests can assert on what would have been drawn.
API Reference
game_new(width: 40, height: 20, fps: 15, bg: " ", max_ticks: 0, quit_on_q: true)
Creates the game state hash. max_ticks > 0 stops the loop automatically (useful headless). Stash your own state in the returned hash freely — it's just a hash.
clear(g)
Resets the frame buffer to the background character.
draw(g, x, y, s, color: nil)
Writes a string into the buffer at cell (x, y). Multi-character strings run horizontally; out-of-bounds is silently ignored.
text(g, x, y, s, color: nil)
Alias of draw — reads nicely for HUD text.
draw_sprite(g, x, y, sprite, color: nil)
Draws a multi-line sprite with transparency: spaces in the sprite string do not overwrite what's already drawn beneath them.
sprite = "
/\\
/ \\
"
draw_sprite(g, 4, 2, sprite)
load_image(path) (v1.21)
Loads an image file for use with draw_image. On frankiegame, this just remembers the path — the terminal can't show real pixels, so draw_image renders a placeholder block instead. Call it once before run_game, not per frame.
zombie = load_image("zombie.png")
The same call works unchanged against frankiecanvas, which decodes the real file — see frankiecanvas for the pixel version.
draw_image(g, x, y, image, w: 1, h: 1) (v1.21)
Draws the loaded image at cell (x, y), sized w × h cells. On frankiegame this renders a ▒ placeholder block over that area — since a terminal can't show a real bitmap, the point is that the function calls stay identical across both engines, so a game written against frankiegame still compiles and runs (with placeholder graphics) if you swap in frankiecanvas later, and vice versa.
on_tick(g) do |game|
clear(game)
draw_image(game, 10, 4, zombie, w: 2, h: 2)
end
synth_play(g, freq: 440, wave: "sine", dur: 0.08, gain: 0.06) (v1.21)
On frankiegame, this maps to the terminal bell — the parameters are accepted but ignored, so cross-engine code still compiles. See frankiecanvas for real synthesized tones.
collide?(x1, y1, w1, h1, x2, y2, w2, h2)
Axis-aligned bounding box overlap test.
on_key(g, key) do |game| ... end
Handle one specific key.
on_any_key(g) do |game, key| ... end
Handle every key press.
on_tick(g) do |game| ... end
Called once per frame — draw here.
game_beep(g)
Rings the terminal bell. Since v1.21 this is a thin wrapper over synth_play(g, freq: 520, wave: "square", dur: 0.07, gain: 0.06) — behavior is unchanged, it's just built on the new synth primitive underneath. (A real WebAudio blip in frankiecanvas.)
stop_game(g)
Ends the loop after the current frame.
render_frame(g)
Returns the current buffer as one string — also what the engine prints; handy for headless tests.
run_game(g)
Enters the blocking game loop (q or stop_game to exit). Restores the terminal even if a handler raises.
Colors
8 named colors are available via color: on draw/text — see GAME_COLORS in the stitch source for the full list.
Example: Snake
The bundled showcase project demonstrates the whole engine API in about 80 lines:
frankiec examples snake
cd snake && frankiec run main.fk
Quick Reference
| Function | Description |
|---|---|
game_new(width:, height:, fps:, bg:, max_ticks:, quit_on_q:) |
Create the game state |
clear(g) |
Reset the frame buffer |
draw(g, x, y, s, color: nil) / text(g, x, y, s, color: nil) |
Draw a glyph string |
draw_sprite(g, x, y, sprite, color: nil) |
Multi-line sprite with transparent spaces |
load_image(path) |
Load an image — real pixels on frankiecanvas, path-only here (v1.21) |
draw_image(g, x, y, image, w: 1, h: 1) |
Draw a loaded image — ▒ placeholder block here (v1.21) |
synth_play(g, freq:, wave:, dur:, gain:) |
Terminal bell here, real tone on frankiecanvas (v1.21) |
collide?(x1, y1, w1, h1, x2, y2, w2, h2) |
AABB collision test |
on_key(g, key) do \|game\| ... end |
Handle one key |
on_any_key(g) do \|game, key\| ... end |
Handle every key |
on_tick(g) do \|game\| ... end |
Called once per frame |
game_beep(g) |
Terminal bell |
stop_game(g) |
End the loop |
render_frame(g) |
Buffer as a string (headless tests) |
run_game(g) |
Enter the blocking loop |