Adder Commodore 64

Adder · a reactive language for the Commodore 64 · Language v0.1.0 · Lab & Guide v0.1.1

Adder Quickstart

Welcome to Adder! In the next ten minutes, you will go from a single line of code to a complete, playable Commodore 64 Jupiter Lander arcade game—and see how the language and the 8-bit hardware work together at every step.

The Commodore 64 (1982) packs 64 kilobytes of memory, a video chip with sixteen colors and eight hardware sprites, a three-voice SID synthesizer, and a 1 MHz processor into a single keyboard case. Instead of poking magic numbers into memory addresses, Adder lets you describe what the screen, sprites, and sound chip should show with live <- bindings, update game state in when event handlers, and rely on the compiler to prove your number ranges and frame timing before the program ever runs.

Light Up the Border

This single line is a complete Adder program:

System.screen.border <- Color(System.screen.frames % 16)
// The border steps through its sixteen colors, one a frame.

Fifty times a second on European (PAL) machines—and sixty times a second on North American (NTSC) machines—the video chip paints a complete television picture called a frame.

  • System.screen.frames counts up by one on every frame.
  • System.screen.frames % 16 wraps that count from 0 to 15, matching the sixteen built-in C64 colors.
  • The binding arrow <- works like a live formula in a spreadsheet: whenever System.screen.frames changes, System.screen.border updates automatically. You need no main function, no manual redraw loop, and no hardware register addresses.

You can also step through all sixteen colors at your own pace with .every(n) and .cycle(Color) on the frame event:

System.screen.border <- System.screen.frame.every(8).cycle(Color)
// The border steps to the next color every 8th frame.

Types and Helpful Compiler Checks

Every value in Adder has a type. System.screen.border expects a Color, while System.screen.frames is a number. Adder never converts one kind of value into another behind your back—if you forget Color(...), the compiler tells you right away and shows the fix:

RefusedwrongType
System.screen.border <- System.screen.frames
Compiler says
This is a Number<u16>, but a Color is needed here.
Color codes run from 0 to 15. Write Color(value) to convert a number explicitly.
Fix: border <- Color(System.screen.frames % 16)

When you want a specific color, note, or key by name, use its dot member (Color.cyan, Note.c4, Key.space). Wherever Adder already knows the expected type, you can drop the type name and write the leading dot: System.screen.border <- .cyan.

Keep Score with State, Events, and Text

A game needs remembered state that changes when something happens, and a heads-up display on the 40-by-25 character screen:

const step = 1
var score: Number<0...99> = 0
when System.keyboard.space.pressed
  score = score +| step
end
System.screen.text <- Text(row: 0, col: 2, ink: .cyan,
  "LANDINGS ", (score.digits(2), .white))

Look at the three ways a name is introduced:

  1. A constant (const step = 1) never changes. The compiler bakes 1 straight into the machine instructions.
  2. State (var score: Number<0...99> = 0) is remembered across frames. It changes only when a when event handler assigns it a new value with =. Here, when System.keyboard.space.pressed runs in the exact frame the space bar goes down, and score = score +| step adds 1 (clamping at 99).
  3. A binding (<-) stays wired up continuously. Text(row: 0, col: 2, ink: .cyan, "LANDINGS ", (score.digits(2), .white)) places "LANDINGS " in cyan at row 0, column 2, followed immediately at column 11 by the two-digit score in white! Whenever score changes, those two digits update on the very same frame.

Numbers Have Edges

In Jupiter Lander, the pilot has three reserve ships (lives), fuel runs from 0 to 99, and both horizontal drift vx (Number<-1.5...1.5>) and vertical speed vy (Number<-1.5...2.0>) hold fractions as gravity pulls the ship down and the thrusters push it back. Giving numbers explicit ranges lets Adder pack them tightly on the 8-bit processor (Number<0...3> or Number<0...99> takes 1 byte; Number<24...296> or a fractional Number<-1.5...2.0> takes 2 bytes) and lets you choose what happens at the boundaries:

  • + and - check: stepping past either end stops the program and prints the line number on screen.
  • +% and -% wrap: stepping past one end loops around to the other end.
  • +| and -| clamp: stepping past an end stops cleanly at that boundary.

Compare checked subtraction - with a guarded subtraction on our three lives:

var lives: Number<0...3> = 3
when System.keyboard.space.pressed
  lives = lives - 1
end
System.screen.text[0][0..<1] <- lives.digits(1)
// Pressing space four times steps 3, 2, 1, 0, then stops with
// "out of range at line 3".

To stop at 0 without halting, either clamp with lives = lives -| 1 or check if lives > 0 then first:

var lives: Number<0...3> = 3
when System.keyboard.space.pressed
  if lives > 0 then
    lives = lives - 1
  end
end
System.screen.text[0][0..<1] <- lives.digits(1)
// The count goes 3, 2, 1, 0 and stays at 0.

Because if lives > 0 then guarantees lives is at least 1 inside the block, the compiler proves lives - 1 can never dip below 0 and omits the runtime overflow check!

You can also clamp or wrap any whole-number expression inline with .clamp(lo...hi) and .wrap(lo..<hi), or pass an inline range to .cycle(0..<width):

const width = 40
column <- System.screen.frame.cycle(0..<width)
System.screen.text[0][0..<2] <- column.digits(2)

Steer the Lander with the Joystick

The Commodore 64's video chip draws eight independent sprites—movable 24-by-21 pixel shapes that float smoothly over the character screen without erasing the text beneath them.

You draw a sprite right in your source file with Shape([...]), using X for lit pixels and . for transparent ones. And to steer a sprite across the screen with the joystick in Control Port 2 (System.joystick[2].dx, which reads -1 left, 0 center, 1 right), use .sum(from:, in:):

const lander = Shape([
  "...X...............X..X.",
  "....X....XXXXXX....XXX..",
  ".....X.XXXXXXXXXXX..XX..",
  ".....XX....X......XX.XX.",
  "....XXXXX..XXXXXXXXX....",
  "....X...X..X..X....X....",
  "....X...XXXX..XXXXXX....",
  "....X...XXXX..X....X....",
  "....X...XXXX.X......X...",
  "...XXXXXXXXXXXX....XX...",
  "..X............XXXXX.X..",
  "..X.............X.X..X..",
  "..X..XXXX.......X.XXXX..",
  "..X..XXXXX......X.X..X..",
  "...XXXXXXXXXXXXXXXXXXX..",
  "....XX.X..XXXX..X.XX....",
  "...XX..XXX....XXX..XX...",
  "..XX...X........X...XX..",
  "..XX................XX..",
  ".XXXX..............XXXX.",
  "XXXXXX............XXXXXX"
])

lander_x <- (System.joystick[2].dx * 2).sum(from: 148, in: 24...296)
System.screen.sprite[0] <- (x: lander_x, y: 120, shape: lander, color: .white)
// The lander glides left and right at 2 pixels a frame and stops
// cleanly inside the 24...296 flight corridor.

Because the C64 has eight sprites, stacking a second sprite at the exact same (x, y) coordinates lets us add gray cabin windows and deck trim (lander_trim), while a third sprite below the nozzle (lander_plume) draws the flickering yellow-and-orange rocket exhaust whenever the thruster fires!

Line Up with the Landing Pad

When our descending lander reaches the height of the landing pad (y = 166), how do we check whether its landing gear is squarely over the pad? By testing whether lander_x and pad_x are within 12 pixels of each other!

We write that check as a reusable function (fn):

fn on_pad(a, b)
  if a > b then a - b < 12 else b - a < 12 end
end

lander_x <- (System.joystick[2].dx * 2).sum(from: 148, in: 24...296)
pad_x <- System.screen.frame.cycle(64...256)
System.screen.border <- if on_pad(lander_x, pad_x) then .light_green else .black end
System.screen.sprite[0] <- (x: lander_x, y: 120, color: .white)
System.screen.sprite[1] <- (x: pad_x, y: 136, color: .light_gray)

Notice that fn on_pad(a, b) needs no type annotations: Adder infers the parameter ranges and bool return type directly from the call site on_pad(lander_x, pad_x)! (You can always write explicit types like fn on_pad(a: Number<24...296>, b: Number<64...256>): bool whenever you want to document a boundary.)

Play Sound Effects and Switch Scenes

Two last building blocks complete our game:

1. Sound Effects on the SID Chip

The C64's synthesizer has three voices (System.sound[1].voice[1] through voice[3]). Bind a voice with <- to a .noise waveform for a continuous rocket thruster roar while holding fire, or call .play(...) inside a when handler for one-shot landing and crash sounds:

const chime = Tone(.c6, decay: .short)
const boom = Tone(.c2, waveform: .noise, decay: .long)
const thrust = Tone(.c3, waveform: .noise)
System.sound[1].voice[2] <- if System.joystick[2].fire then thrust else .silence end
when System.keyboard.space.pressed
  System.sound[1].voice[1].play(chime)
end
when System.keyboard.return.pressed
  System.sound[1].voice[1].play(boom)
end

2. Game Scenes with Enums and Match

Our game moves through three scenes: title, playing, and over. By writing a match scene block at the top level of the program, each scene gets its own screen text, border color, sprites, state variables, and when handlers—which exist and run only while that scene is active:

enum Scene
  case title, playing, over
end
var scene: Scene = .title

match scene
  title ->
    System.screen.border <- .black
    System.screen.text <- Text(row: 12, col: 14, ink: .white, " PRESS FIRE ")
    when System.joystick[2].fire.pressed
      scene = .playing
    end
  playing ->
    System.screen.border <- .black
    System.screen.text <- Text(row: 12, col: 14, ink: .light_green, " DESCENDING ")
    when System.joystick[2].fire.pressed
      scene = .over
    end
  over ->
    System.screen.border <- .red
    System.screen.text <- Text(row: 12, col: 14, ink: .yellow, " MISSION END")
    when System.joystick[2].fire.pressed
      scene = .title
    end
end

Whenever scene switches to .playing, every var declared inside playing -> (ships, fuel, lander_x, lander_y, vx, vy, gravity) starts fresh from its initial value automatically!

Check What Every Line Costs

On a 1 MHz Commodore 64, a PAL frame has 19,656 cycles and an NTSC frame has 17,095 (16,768 on early NTSC chips). Adder counts the exact bytes of every line and proves an upper bound on your frame cycles—both in the Lab gutter and via bun tools/costs.ts:

Terminal
bun tools/costs.ts ../border.adder
Cost report
source  bytes  cycles
border.adder:1     23     31+  System.screen.border <- Color(System.screen.frames % 16)
                    3 bytes of mutable data, including variables and scratch
frame work, worst case: PAL 70 of 19,656 cycles, NTSC 70 of 17,095, early NTSC 70 of 16,768
trusted: 0 bytes under 0 hand-written blocks; compiled: 23 bytes in the program's lines
runtime and shared tables (line 0): 283 bytes (269 code, 14 data and padding); startup worst case 12,458 cycles, not counting the raster wait of standard detection; program file: 308 bytes including the load address
file data (initial values, tables, constants, padding): 14 bytes; resident RAM while running: 321 bytes (the file without its load address, reserved runs and zero page cells)

This line becomes 23 bytes of instructions and at most 31 cycles a frame—leaving over 99% of the processor free.

The cost report also shows why guarding a range with if saves both bytes and cycles. Compare the two lives programs from earlier:

Cost report
   3     42     37+  lives = lives - 1
Cost report
   3      6      8+  if lives > 0 then
   4     10      13  lives = lives - 1

With if lives > 0 then, the compiler proves lives - 1 cannot underflow and emits a plain 10-byte, 13-cycle subtraction.

Put It All Together: Jupiter Lander

Here is the complete Jupiter Lander arcade game! Just like HAL Laboratory's 1982 original, each descent starts in low orbit drifting right (vx = 0.5) while gentle gravity (0.015625 pixels/frame², stepping up slightly after each landing) pulls the ship down. Nudge joystick 2 left and right to fire the side thrusters (vx = vx -| 0.03125 / +| 0.03125) and hold fire to burn the main rocket thruster (vy = vy -| 0.046875). Watch the landing pad's approach beacons: they glow green when both your descent rate (vy <= 0.625) and lateral drift (vx within -0.5...0.5) are safe for touchdown, and turn red when you are coming in too fast!

enum Scene
  case title, playing, over
end
var scene: Scene = .title
var score: Number<0...99> = 0

const chime = Tone(.c6, decay: .short)
const boom = Tone(.c2, waveform: .noise, decay: .long)
const thrust = Tone(.c3, waveform: .noise)
const rcs = Tone(.c4, waveform: .noise)
const touchdown_y = 166

const lander = Shape([
  "...X...............X..X.",
  "....X....XXXXXX....XXX..",
  ".....X.XXXXXXXXXXX..XX..",
  ".....XX....X......XX.XX.",
  "....XXXXX..XXXXXXXXX....",
  "....X...X..X..X....X....",
  "....X...XXXX..XXXXXX....",
  "....X...XXXX..X....X....",
  "....X...XXXX.X......X...",
  "...XXXXXXXXXXXX....XX...",
  "..X............XXXXX.X..",
  "..X.............X.X..X..",
  "..X..XXXX.......X.XXXX..",
  "..X..XXXXX......X.X..X..",
  "...XXXXXXXXXXXXXXXXXXX..",
  "....XX.X..XXXX..X.XX....",
  "...XX..XXX....XXX..XX...",
  "..XX...X........X...XX..",
  "..XX................XX..",
  ".XXXX..............XXXX.",
  "XXXXXX............XXXXXX"
])

const lander_trim = Shape([
  "........................",
  ".........XXXXXX.........",
  ".......XXXXXXXXXXX......",
  "........................",
  "........................",
  "........X..X..X.........",
  "........XXXX..XXXXXX....",
  "........XXXX..X.........",
  "........XXXX.X..........",
  "........................",
  "....XXXXXXXXXXXX........",
  "....XXXXXXXXXXXX........",
  ".........XXXXXXX........",
  ".........XXXXXXX........",
  "........................",
  ".........XXXXXX.........",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................"
])

const lander_plume = Shape([
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  ".........XXXXXX.........",
  "........XXXXXXXX........",
  "........XXXXXXXX........",
  ".........XXXXXX.........",
  ".........XXXXXX.........",
  "..........XXXX..........",
  "..........XXXX..........",
  "...........XX...........",
  "...........XX..........."
])

const pad = Shape([
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "XXXXXXXXXXXXXXXXXXXXXXXX",
  ".XXXXXXXXXXXXXXXXXXXXXX.",
  "..XX..XX..XXXX..XX..XX..",
  "..XX..XX...XX...XX..XX..",
  ".XXX..XX...XX...XX..XXX.",
  "XXXX................XXXX",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................"
])

const pad_beacons = Shape([
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "XX........XXXX........XX",
  "..XX....XX....XX....XX..",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................",
  "........................"
])

fn on_pad(a, b)
  if a > b then a - b < 12 else b - a < 12 end
end

preview_x <- (System.joystick[2].dx * 2).sum(from: 148, in: 24...296)

System.screen.background <- .black
System.screen.text[0][0..<2] <- score.digits(2)
System.screen.text[0][3..<38] <- "LANDINGS // FUEL:     SHIPS:       "
System.screen.ink[0][3..<38] <- .cyan
System.screen.text[23][3..<37] <- "====[ JUPITER LANDING SECTOR ]===="
System.screen.ink[23][3..<37] <- .dark_gray

match scene
  title ->
    System.screen.border <- .black
    System.screen.text[10][9..<31] <- "JUPITER LANDER // 1982"
    System.screen.text[12][14..<26] <- " PRESS FIRE "
    System.screen.sprite[0] <- (x: preview_x, y: 86, shape: lander, color: .white)
    System.screen.sprite[1] <- (x: preview_x, y: 86, shape: lander_trim, color: .gray)
    when System.joystick[2].fire.pressed
      score = 0
      scene = .playing
    end
  playing ->
    var ships: Number<0...3> = 3
    var fuel: Number<0...99> = 99
    var pad_x: Number<64...256> = 160
    var lander_x: Number<24.0...296.0> = 64.0
    var lander_y: Number<44.0...166.0> = 48.0
    var vx: Number<-1.5...1.5> = 0.5
    var vy: Number<-1.5...2.0> = 0.0
    var gravity: Number<0.015625...0.03125> = 0.015625
    burning <- System.joystick[2].fire and fuel > 0
    steering <- System.joystick[2].dx != 0 and fuel > 0
    safe_speed <- vy <= 0.625 and vx >= -0.5 and vx <= 0.5
    System.screen.border <- .black
    System.screen.text[0][21..<23] <- fuel.digits(2)
    System.screen.text[0][32..<33] <- ships.digits(1)
    System.screen.sprite[0] <- (x: lander_x.floor(), y: lander_y.floor(), shape: lander, color: .white)
    System.screen.sprite[1] <- (x: lander_x.floor(), y: lander_y.floor(), shape: lander_trim, color: .gray)
    System.screen.sprite[2] <- (
      x: lander_x.floor(),
      y: if burning then lander_y.floor() + 6 else -30 end,
      shape: lander_plume,
      color: if System.screen.frames % 2 == 0 then .yellow else .orange end
    )
    System.screen.sprite[3] <- (x: pad_x, y: 170, shape: pad, color: .light_gray)
    System.screen.sprite[4] <- (
      x: pad_x,
      y: 170,
      shape: pad_beacons,
      color: if safe_speed then .light_green else .light_red end
    )
    System.sound[1].voice[2] <- if burning then thrust else .silence end
    System.sound[1].voice[3] <- if steering then rcs else .silence end
    when System.screen.frame
      if System.joystick[2].dx < 0 and fuel > 0 then
        vx = vx -| 0.03125
      end
      if System.joystick[2].dx > 0 and fuel > 0 then
        vx = vx +| 0.03125
      end
      if System.joystick[2].fire and fuel > 0 then
        vy = vy -| 0.046875
        if System.screen.frames % 6 == 0 then
          fuel = fuel -| 1
        end
      else
        vy = vy +| gravity
      end
      lander_x = lander_x +| vx
      lander_y = lander_y +| vy
      guard lander_y >= touchdown_y
      if on_pad(lander_x.floor(), pad_x) and vy <= 0.625 and vx >= -0.5 and vx <= 0.5 then
        score = score +| 1
        fuel = fuel +| 25
        gravity = gravity +| 0.00390625
        System.sound[1].voice[1].play(chime)
      else
        ships = ships -| 1
        System.sound[1].voice[1].play(boom)
      end
      pad_x = Number.random(64...256)
      lander_x = 64.0
      lander_y = 48.0
      vx = 0.5
      vy = 0.0
      if ships == 0 then scene = .over end
    end
  over ->
    System.screen.border <- .red
    System.screen.text[10][11..<29] <- "ALL LANDERS LOST  "
    System.screen.text[12][14..<26] <- " MISSION END"
    when start
      wait(seconds: 3)
      scene = .title
    end
end

Where Next

Now that you have built a complete game, explore the Adder Language Reference whenever you want to reach for more of the machine:

  • Types & Expressions — lists, structs, optionals (T?), .map / .filter, .clamp / .wrap, bit flags (bit_is_set, toggle_bit), and sliding list windows (xs[left..<(left + 40)]).
  • Declarations — struct methods (fn and mut fn), stateful op declarations (.previous()), and multi-file projects (include).
  • Events and Handlers & Statements — .hold, .cooldown, .rising / .falling, guard, wait, forever, and for loops.
  • The System Device Tree — raster color bands, scrolling text regions (System.screen.region), bitmap pixel canvases (System.screen.canvas), custom fonts, mouse, and 3-voice SID synthesis.
  • Costs and the Frame & Assembly — fast / small marks, multi-frame work functions, Queue buffers, and inline 6510 asm.