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.framescounts up by one on every frame.System.screen.frames % 16wraps that count from0to15, matching the sixteen built-in C64 colors.- The binding arrow
<-works like a live formula in a spreadsheet: wheneverSystem.screen.frameschanges,System.screen.borderupdates automatically. You need nomainfunction, 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:
wrongTypeSystem.screen.border <- System.screen.framesWhen 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:
- A constant (
const step = 1) never changes. The compiler bakes1straight into the machine instructions. - State (
var score: Number<0...99> = 0) is remembered across frames. It changes only when awhenevent handler assigns it a new value with=. Here,when System.keyboard.space.pressedruns in the exact frame the space bar goes down, andscore = score +| stepadds1(clamping at99). - 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! Wheneverscorechanges, 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)
end2. 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
endWhenever 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:
bun tools/costs.ts ../border.addersource 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:
3 42 37+ lives = lives - 1 3 6 8+ if lives > 0 then
4 10 13 lives = lives - 1With 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
endWhere 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 —
structmethods (fnandmut fn), statefulopdeclarations (.previous()), and multi-file projects (include). - Events and Handlers & Statements —
.hold,.cooldown,.rising/.falling,guard,wait,forever, andforloops. - 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/smallmarks, multi-frameworkfunctions,Queuebuffers, and inline 6510asm.