The console library

Every cart on this site runs against the same library, and it travels with your upload — there is nothing to install and nothing to require. This is console 0.2.0, the version a new upload is pinned to.

A cart is a directory with a contract: an entry file naming a class after the directory, and whatever art and data it needs beside it. Zip that directory, upload it, publish it. Everything below is what goes in the file.

Get the console

A cart runs on the console: a directory of Ruby, this library, and a small tool that boots it. Unpack the download into a checkout and the library is in place.

Download the library — sign in to download it with a key that lets you publish carts from the console.

Your first cart

The smallest cart that moves

A cart is one class in one file, named after the directory it lives in: `carts/mine/app/mine.rb` defines `Mine`. Paths are relative to that directory, so `sprites/ship.png` means this cart's own file.

TITLE = 'mine'

class Mine
  def self.assets
    { sprites: { ship: 'sprites/ship.png' } }
  end

  def setup
    @x = 400.0
  end

  def update
    @x += 4 if input.held?(:right)
    @x -= 4 if input.held?(:left)
  end

  def render
    draw.sprite path: :ship, x: @x, y: 300
  end
end

Splitting it into scenes

A cart with scenes gets `enter`, `update` and `render` per scene instead of the cart's own `update`. `scene_data` is per-scene state that survives scene changes.

class Mine
  def setup
    scene :title, TitleScene
    scene :play, PlayScene
    goto :title
  end
end

class TitleScene
  def enter
    scene_data[:t] = 0
  end

  def update
    goto :play if input.pressed?(:accept)
  end

  def render
    draw.text 'PRESS SPACE',
              x: screen[:w] / 2.0, y: screen[:h] / 2.0,
              anchor_x: 0.5, anchor_y: 0.5,
              size_px: 32, color: :accent
  end
end

Playable before any art exists

`auto_sprite` synthesises a texture for any name that has no file, and never overwrites one that does. Generated textures are deterministic, so the same cart looks the same everywhere and can stand in for art later.

def setup
  auto_sprite :ship, 16, 16, :accent, :frame
  auto_sprite :star, 8, 8, :warn, :circle
end

def render
  draw.sprite path: :star, x: 120, y: 200
  draw.sprite path: :ship, x: 260, y: 200
end

Entities, animation and a camera

An entity is a hash you own. `animate` plays a numbered directory of frames from the cart's sprites, and the camera clamps itself to the world rectangle it is given.

WORLD = { x: 0, y: 0, w: 1600, h: 900 }

def setup
  @ship = spawn :ship, kind: :hero, x: 800, y: 120,
                 w: 28, h: 28, sprite: :ship
  animate @ship, :run, fps: 12
  camera.follow @ship
  camera.bounds = WORLD
end

def update
  @ship[:x] += 3 if input.held?(:right)
  @ship[:x] -= 3 if input.held?(:left)
  camera.update
end

def render
  draw.sprite path: @ship[:sprite], x: @ship[:x], y: @ship[:y]
end

Sound, and work that reschedules itself

Music is one track; effects are fire-and-forget. `every` runs a block every N frames, which covers spawners, timers and cooldowns without coroutines.

def setup
  music :theme
  every 90, immediate: true do
    next_wave
  end
end

def update
  sfx :laser if input.pressed?(:accept)
end

def next_wave
  spawn :enemy, kind: :foe, x: 800, y: 900, w: 24, h: 24,
                sprite: :enemy, dx: -1
end

The hooks

Every hook is optional. A cart that defines none of them still boots — it just does nothing.

self.assets before boot Names the art the cart owns. Declaring it is what lets the site check the cart at upload time and tell you a sprite is missing before anyone plays it.
setup once, at boot Build your state here: spawn things, load a level, start music.
update every frame Move, read input, advance timers. Only used when the cart defines no scenes.
render every frame, last Draws after whichever scene drew, so it is where a HUD that persists across scenes belongs.

Sending a cart here

No archive to build and no upload form. With the bundle unpacked and your key in it:

./publish-site arcade
  1. Download the console. Signed in, the download arrives with a key in dragonstation.json beside the library.
  2. Build a cart. ./run --cart arcade boots it locally.
  3. Send it: ./publish-site arcade. It arrives here as a draft under your account.
  4. Play it, then publish it from the cart's page.

The same upload, without the browser. Everything the form refuses it refuses — traversal, symlinks, expansion bombs, art a cart names but does not own — and it says so in the message.

The endpoint, if you are writing your own client
POST /api/carts
Authorization: Bearer <the api_key from dragonstation.json>
archive=<the cart ZIP, as multipart/form-data>

201 {"slug","title","status":"draft","url",...}
401 {"error":"unauthorised"}          a key that is not yours, or one a newer download replaced
422 {"error":...,"problems":[...]}    the refusals below, one per line
400 {"error":"no archive"}             nothing was sent
503 {"error":"no console library ..."} nothing to pin the cart to

The API

20 modules, described by the library's own comments. Nothing here is written by hand, so it cannot drift from the code it describes.