Skip to content

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