funkey

A game engine for the terminal. Written in Rust. A game draws pixels and reads keys; funkey does the rest.

Rust Unlicense Linux | macOS Stay Amazing

What it is

A game is one type with two methods: update reads the keys and moves things, draw paints a frame of pixels. funkey runs the loop at a fixed rate and puts the frame on the terminal. That is the whole contract, and Doom fits it.

Pixels, two ways

Half blocks in any terminal: two pixels a cell, in full colour, only the cells that changed. Real pixels through the kitty graphics protocol where the terminal has it, over shared memory in glass.

Keys that go up

A terminal never says when a key is released. glass and kitty do, through the kitty keyboard protocol, so a held arrow walks and a tap takes one step. Elsewhere a key counts as held while its repeats come.

Sound

A mixer in the engine, piped to pw-play, paplay or aplay. WAV files, Doom lumps, or sounds made on the spot: tones, slides, noise, and tunes written as notes. Channels loop and change pitch while they play.

2D, 3D and Doom

Sprites from rows of characters or PNG files, tile maps with platformer physics, particles, two fonts. A software 3D rasterizer with a depth buffer and fog. A WAD loader and a sector renderer that draws Doom's own levels.

The games

Seventeen games come with the engine. Each has its own version, shown on its title screen and tagged in the repo; the crate version is the engine's. Every one runs with cargo run --release --example <name>, and with FUNKEY_PIXELS=kitty in glass or kitty for real pixels. funkeys shows them as cards and plays the one you pick; every release carries it and the games as binaries.

chomp: a yellow muncher in a blue maze of dots, three chasers out in the corridors and one still in its house

chomp

In the spirit of Pac-Man
chomp-v1.0

A muncher eats the dots of a maze while four chasers hunt it by the arcade's rules. They scatter to their corners and chase by turns. One comes straight for you, one aims ahead, one closes in from the other side, and one loses heart up close. A power dot turns the hunt around for a few seconds. The maze, the art and the sound are new.

cargo run --release --example chomp
kart: a red and a cyan kart side by side on a grey road, in the long shadow of a tree, with more karts ahead and oaks and birches behind the red and white barrier

kart

In the spirit of Super Mario Kart
kart-v1.2

Eight karts on four tracks: a meadow, a beach, an icy pass and a desert. The road is flat and painted the way that game painted it, with the karts on it in 3D. Hop into a bend and hold the drift until the sparks turn blue or orange, then let go for a push. Boxes hand out six items, kinder ones to those at the back. A cup is four races, at 50, 100 or 150cc.

cargo run --release --example kart
again: three selves in a room of doors and gates, and under it their steps as rows of arrows

again

A time-loop puzzle
again-v1.0

A puzzle you solve with your own past. A life is a handful of steps in a small room. Then time starts over, and the self you just were walks beside you, pressing the same keys. One of you stands on a plate while the next walks through the door it opens. Twenty rooms, and a search proves each needs every self it gives. The round that plays gains a voice with every self.

cargo run --release --example again
marble: a blue marble on a patch of ice, a pool of acid on the road behind it and a green spring waiting ahead

marble

A tribute to Marble Madness
marble-v1.0

A glass ball down six sloping courses that hang in the dark, against the clock. It gathers speed downhill, flies off a ramp and breaks after a long fall. Steel balls shove it, green springs eat it, acid melts it, and on ice the keys do little. Each loss costs time, and the seconds left at a goal go on to the next course. Left alone, it plays itself.

cargo run --release --example marble
vector: a heart-shaped web, a pulsar lighting its lane white, the claw moving off along the rim

vector

A tribute to Tempest
vector-v1.0

Glowing lines on black. Sixteen webs seen down their length, a claw on the rim, and what climbs the lanes toward it: flippers, tankers, spikers, fuseballs and pulsars. A superzapper clears the web once. Then the flight down to the next web, past the spikes. The lines add their light where they cross and fade as on a vector tube. Left alone, it plays itself.

cargo run --release --example vector
raid: a gunship shot down over a lake, a second one coming in, a flak gun in the target box

raid

A gunship over fractal mountains
raid-v1.2

Six missions in the spirit of Comanche, from a radar post at dawn to a fortress at night. A gun, rockets and guided missiles against trucks, tanks, flak, gunboats, missile sites and other gunships. The gunship follows the ground at the height you set. Hills are cover: a missile site cannot see what flies low behind one. Hover over the pad to rearm and repair.

cargo run --release --example raid
stack: the game playing itself, a combo of two, the next five pieces on the right

stack

A tribute to Tetris
stack-v1.1

Seven pieces of four blocks, a well ten wide. The pieces turn and kick off the walls as in modern Tetris, and a bag of seven deals them. Hold one, see the next five, drop soft or hard. Four rows at once, a T spun into its slot and clears in a row all score more. Korobeiniki plays, faster when the well fills. Left alone, it plays itself.

cargo run --release --example stack
eliminator: a sellsword fights an Araxi by torchlight, the dice of every roll shown below the map

eliminator

A dungeon by the Amar RPG rules
eliminator-v1.3

The maze beneath the royal castle of Amar. Five levels up to the gate, and the Raven Demon guarding it. Every roll is shown: the open-ended O6, the totals and the sum. Stances, double attacks, wounds, light, fear, and marks that raise your skills. New to Amar? i on the title shows the three tiers, the O6 and a blow in three pages.

cargo run --release --example eliminator
salvo: the ship flies past a volcano throwing rocks, a row of fans coming at it

salvo

A tribute to Gradius
salvo-v1.2

Seven stages, a boss at the end of each: a volcano, stone heads, crystals, living cells, the sun, a high-speed maze and the fortress. Capsules light the power-up bar one step at a time: speed, missiles, double, laser, options that follow your path, a shield.

cargo run --release --example salvo
gems: a bear jumps on the wall of an isometric castle lined with gems

gems

A tribute to Crystal Castles
gems-v1.3

Five castles seen from the corner, their walls lined with gems. Take them all before the gem eaters do. Trees walk, a crystal ball rolls, bees swarm, and the witch comes in each castle's third wave. The hat makes you safe for a while, and there is a warp for those who know.

cargo run --release --example gems
castle: the gatehouse and keep across the moat, banners flying

castle

A walk to a castle
castle-v1.0

From the hills, down the path, over the drawbridge and in through the gate. Stone, slate and wood as textures over the walls, the sun and its shadows baked into every corner, banners in the wind, torches, pines, mountains and clouds, painted on every core at 960 by 600. Nothing to win, only to look at.

cargo run --release --example castle
soar: mountains, water and clouds under a low sun

soar

A flight over fractal mountains
soar-v1.1

A height map ray-cast a column at a time, the way Comanche did it, with the sun and its shadows baked in, haze, water that glitters, and clouds drifting over. The autopilot flies; take the controls when you like. There is nothing to win, only to look at.

cargo run --release --example soar
Doom: a zombie approaches down a corridor, the pistol raised

doom

The game, from any Doom WAD
doom-v1.0

Every monster from both Dooms with Doom's own frame timings, all nine weapons, pickups, keys, doors, lifts, crushers, teleports, switches, the status bar with the face, the automap, the intermission, the sounds. Freedoom's WADs are free; Doom's own load the same way.

cargo run --release --example doom
jumpman: girders, ladders and bombs

jumpman

Platformer, Jumpman Junior in spirit
jumpman-v1.1

Five levels of girders, ladders and ropes. Take every bomb, jump the bullets, dodge the robots, and do not fall too far. One bomb reveals hidden ladders, one takes girders away. A title tune, a high score.

cargo run --release --example jumpman
invaders: five rows of eleven above four shields

invaders

Space Invaders in spirit
invaders-v1.0

Fifty-five of them, marching down to the four-note beat that quickens as the rows thin. Shields crumble pixel by pixel from either side, a mystery ship crosses the top, and each wave starts a row lower.

cargo run --release --example invaders
drive: a red car on a road over green hills

drive

Driving, on the 3D rasterizer
drive-v1.0

A car on a winding road over the hills. Fetch the packages before the clock runs out, then more of them with less time. An arrow points at the nearest. Leave the road and the grass slows you.

cargo run --release --example drive
climb: bricks, ladders and coins

climb

The first game, 200 lines
climb-v1.0

Ladders, coins, blobs. The smallest game on the engine, and the one to read first when you want to write your own.

cargo run --release --example climb

Doom, in the terminal

The game's own frames and sounds, written straight to video by a scripted run. Every pixel is what the terminal shows.

Make your own

A bouncing ball is the whole shape of a game. Give it a name, a Game impl, and run it.

use funkey::*;

struct Ball { x: f32, y: f32, vx: f32, vy: f32 }

impl Game for Ball {
    fn update(&mut self, input: &Input, dt: f32) -> Flow {
        if input.pressed(Key::Char('q')) { return Flow::Quit; }
        self.x += self.vx * dt;
        self.y += self.vy * dt;
        if self.x < 0.0 || self.x > 156.0 { self.vx = -self.vx; }
        if self.y < 0.0 || self.y > 96.0 { self.vy = -self.vy; }
        Flow::Continue
    }
    fn draw(&mut self, f: &mut Frame) {
        f.clear(0x102030);
        f.rect(self.x as i32, self.y as i32, 4, 4, 0xffcc00);
    }
}

fn main() {
    run(&mut Ball { x: 10.0, y: 10.0, vx: 60.0, vy: 40.0 }, Config { width: 160, height: 100, fps: 60 });
}
# Cargo.toml
[dependencies]
funkey = { git = "https://github.com/isene/funkey", package = "fe2o3-funkey" }

From there: Sprite::from_rows for art, Tilemap and Body for a level that can be walked, Audio with Sample::tone for a sound made of nothing, Raster for a third dimension. With FUNKEY_SCRIPT set, a game runs with no terminal and writes its frames, so it can be tested, and filmed. Return Flow::Pause and the game stands still, silent and at no cost, until a key is pressed. It does the same by itself when you switch to another window.