Skip to content

Repository files navigation

termcade

A terminal arcade. Classic games rendered at sub-cell resolution with block pixels, built on Bubble Tea. Every game is a sandboxed WebAssembly package — including the three that ship with it — and anyone writes them against the SDK and publishes them for players to termcade add.

Games

Three games are bundled, unpacked into your games directory the first time the arcade runs:

Game What it is
asteroid Asteroids-style rock shooter
tetris falling-block stacker
brickough Breakout-style brick breaker

They are ordinary .tcade packages running in the same sandbox as anything you install, built from source in termcade-games and vendored here as packages. Nothing is compiled into the binary as a game.

Uninstall one and it stays uninstalled — the arcade seeds a game once, not every run. The TUI opens directly on Marketplace; Library includes installed games even while signed out.

Run

With Go installed, straight from the internet:

go run github.com/aviorstudio/termcade@latest

Or grab a prebuilt archive from the releases. From a checkout (toolchain pinned via misemise install): go run ..

Needs a truecolor terminal at 80×24 or larger.

Pixel density

TERMCADE_PIXELS picks how each terminal cell is subdivided. Every setting draws the same playfield into the same cells; only sharpness and font requirements change.

Value Pixels per cell Notes
quad (default) 2×2 Unicode 1.0 block elements; renders anywhere.
sextant 2×3 Sharpest, near-square pixels. Needs Unicode 13 (Symbols for Legacy Computing) or you get tofu.
half 1×2 The original look, for fonts that struggle with the rest.
ascii 3×3 Pure ASCII art: a density ramp plus line characters. No block glyphs at all, so it works in any font (and any 1980s sensibility).
TERMCADE_PIXELS=sextant go run .

In the arcade, the pixel-style control in Settings or the pause menu cycles the style for the next game start and persists it to ~/.config/termcade/settings.json; TERMCADE_PIXELS overrides the saved choice for a run. Games never change for any of this — the renderer owns the look.

Input latency

Terminals send no key-release events, so a held key is normally guessed from auto-repeat — which stalls for the OS repeat delay (often ~500ms) after the first press, and makes the paddle feel like it ignores you.

termcade asks for the Kitty keyboard protocol's event-type reporting. On terminals that support it (kitty, Ghostty, foot, WezTerm, recent xterm) held keys are exact and that stall disappears. Elsewhere it silently falls back to the auto-repeat heuristic.

Controls

Key Action
Tab / Shift+Tab focus navigation, page content, or actions; move between form controls
↑/↓ select a row or pause-menu item
←/→ (actions) select the focused action
enter activate a focused item/button; ordinary game rows open details
Page Up / Page Down page lists or scroll long detail/documentation text
←/→ (or a/d, h/l) move paddle / turn ship / shift piece
↑ (or w) thrust (Asteroid) · rotate (Tetris)
↓ (or s) soft drop (Tetris)
space or z A button: launch ball / fire / hard drop
x B button
esc or p pause
esc (outside gameplay) dismiss/back; stop waiting and reconcile a pending form mutation
Ctrl+C quit and save local scores

While a game runs, game inputs cannot operate the sidebar. Pause first, then Tab into navigation. The sidebar is 22 columns wide at 120+ terminal columns; below that it overlays the content on demand. Non-game content is capped at 100 columns. Gameplay keeps its fixed cell footprint and hides the sidebar when needed to fit. See TUI workflows for the complete contract.

High scores persist to ~/.config/termcade/scores.json, and are yours whether or not you have an account — see Your history.

The marketplace

The TUI opens on Marketplace and shares the web app's Marketplace, Docs, Library, owner/game pages, and account/settings destinations. Browsing is anonymous. Library joins account games with installed packages, with clear installation/membership badges and Continue Playing entries.

In the TUI, Add saves account membership without downloading. Play installs a missing compatible account game; healthy installed games remain playable offline. Remove from Library changes only account membership; Uninstall here removes only the local package, after confirmation. No automatic updates replace an existing local copy.

Sign in from Settings without exiting the TUI: it shows the trusted pairing URL and one-time code, and opens a browser only when you choose Open browser. Account authentication stays in the browser. Settings also supports handles, org/member administration, CLI-session revocation, account deletion, and revoke-then-clear sign-out. The normal pairing destination remains https://app.termca.de/pair.

The gate is a product decision, not a security boundary: packages are public GitHub release assets, so an account is not what keeps anyone out. What it does is give account games a library that follows you between devices.

Existing shell commands keep their established behavior: termcade add saves membership and installs; termcade remove removes membership and the local copy; termcade login restores missing account packages; termcade sync adds missing packages without deleting local-only games or automatically updating existing copies. The new TUI's separate actions do not silently change those command contracts.

Creating an account in the app claims a username — your publishing handle, and the author segment of every game you release. nicodes/pong and aviorstudio/tetris are the same kind of name: the second belongs to an org, which is a studio more than one person can publish under. Being a member is enough to publish; admin governs the studio itself.

The same works from the command line:

termcade login                           # approve this CLI in your browser
termcade add aviorstudio/brickough       # add straight from the marketplace
termcade dev install <file>.tcade        # install a local build while developing
termcade whoami                          # who you are, and what you publish as
termcade username <name>                 # claim, rename, or check one is free
termcade org new aviorstudio             # a studio to publish under
termcade sync                            # bring your library to this machine
termcade keys new ci <username>          # a key for a release workflow
termcade list                            # what's here
termcade remove author/slug              # take one off (updates your library too)

Your history

Your high scores live in scores.json and always have. The arcade plays with no account and no network, and a score set that way is not waiting on either.

What an account adds is that the same history follows you. Every finished run is recorded locally and queued; the queue drains when there is a session and a network, which may be now, may be after you sign in, and may be next week. On another machine, signing in brings the account's side down. Both directions only ever raise a value — a high set here and a higher one set there both survive — so the two can disagree, sync in either order, or never sync, and the worst outcome is that a score stays where it was set.

The library also marks a game the marketplace has moved past, comparing the version in the installed package's own manifest with what the catalog publishes. termcade add again is what updates it.

These are personal records, not a leaderboard. A score comes from a game running in your own sandbox, and nothing on the other end can check it. It is your history, shown back to you; ranking anybody by it would need evidence that does not exist yet, so nothing here pretends to.

There is no way to install an older version, and that is deliberate. A game is not a dependency — nothing builds against one — so the reasons package managers pin (reproducible builds, lockfiles, a transitive bump breaking you) do not apply. add gives you what the author currently ships. The one thing that does narrow the choice is the ABI: the registry picks the newest release your arcade can actually run, so a game that has moved on to a later ABI tells you to update termcade instead of handing you a package the host would refuse.

The registry is https://api.termca.de. TERMCADE_REGISTRY overrides it, which is how you reach a local termcade-be stack (make dev serves one on port 8080):

TERMCADE_REGISTRY=http://127.0.0.1:8080 termcade add you/mygame

The registry stores no packages. It is an index: a game's releases live on its GitHub releases, and the registry records where each one is and what it hashed to when it fetched and validated them. Publishing is open source only for now — the registry checks that a repository is publicly visible before it will serve anything from it.

Packages come back through the API rather than from GitHub directly. That is what lets an install require an account and lets the arcade and the app share one path. termcade add asks the registry which release to install, streams it from the registry, and refuses to install anything whose sha256 does not match what the registry recorded — so a release asset swapped after publishing fails rather than reaching a player.

Installed games are WebAssembly modules that run sandboxed (no filesystem, no network) under wazero. A broken install shows up dimmed on the menu with the reason; it can never take the arcade down.

Writing games

The arcade you play with is also the whole dev kit — termcade dev new you/mygame scaffolds a playable game, termcade dev build packages it, and termcade dev install build/mygame.tcade puts it on your own menu.

To put it on everyone else's, attach the .tcade to a GitHub release and tell the marketplace where it is:

termcade publish https://github.com/you/mygame v1.0.0 mygame.tcade

The registry fetches that asset once, validates it against the same manifest rules the arcade enforces, reads the game's identity and version out of the manifest inside it, and records its digest. Nothing you pass here asserts what the package is — only where it is. The author segment of the id must be a handle you already control: your username, claimed at signup or with termcade username, or an org you belong to. Publishing under a name nobody has claimed is refused rather than minting it.

A game is a Go package implementing sdk.Game — see docs/sdk.md for the author quickstart, docs/packaging.md for the manifest and .tcade format, and docs/abi.md for the frozen wasm ABI (for non-Go toolchains). termcade dev build turns a game directory into an installable package.

termcade-games is three worked examples — complete games depending only on the sdk module, each buildable into a .tcade via its cmd/wasm entrypoint, each published to the marketplace the way yours will be.

Architecture

  • sdk/ — the public, stdlib-only module game authors depend on: the Game contract (fixed 60 TPS ticks, structured HUD, 8-key input), pixel canvas (pluggable cell shapes, run-length ANSI batching), vector math, Bresenham rasterization, held-key tracking, and sdk/tcgame, the wasm-export glue. Games draw in square logical units and never see the pixel grid.
  • internal/engine — host-side registry types and SafeGame, which contains any game panic to a crash screen instead of a dead arcade.
  • internal/plugin — the wazero host: sandboxed instantiation, per-call watchdog deadlines, install-dir discovery.
  • manifest/termcade.toml parsing/validation and the .tcade zip format. Public rather than internal: the registry validates uploads against this same package, so a game the marketplace accepts and a game the arcade will run are decided by one piece of code.
  • internal/shell — the Bubble Tea arcade frame: menu, 60 TPS tick loop that steps off the measured clock rather than drifting, pause/game-over/crash overlays.
  • internal/scores — atomic JSON high-score persistence, namespaced by author/slug.

Development

go build -o bin/termcade .   # local builds land in bin/ (gitignored)
go test ./...                # unit + shell tests
go test ./internal/plugin/   # includes wasm end-to-end tests (builds guests)
go vet ./...

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages