Chapter 14 - A Small Matrix Game
Every chapter since the sixth has handed you an instrument. The matrix profile draws, timers and ramps schedule, shapes and sounds and the LCD announce, arrays hold boards, parts split files, and cards turn a program into a game with screens. Each arrived alone, solving one small program’s problem. A game asks for all of them at once, and this chapter spends them together.
The game is Skyfall. Blocks fall from the top row in random columns. You slide a three-pixel paddle along the bottom with held 4 and 6. Catch a block and the score climbs on the seven-segment display, a chirp sounds, and the next block falls a little faster. Miss, and a buzz takes one of your three lives. A splash card waits for any key, a game-over card names the ending on the LCD, and after a short pause any key starts the sky falling again.
Before a line of it is written, this chapter teaches the habit that makes games this size comfortable: design first, in Glimmer’s own terms. A Glimmer game’s declarations are its design - the facts, the moments, the schedules, the resources, the screens. Settle those on paper and the blocks that remain are small Z80 exercises, each with one job you have already named.
The game on paper
Start with the facts Skyfall must remember between frames:
| Fact | Type | Job |
|---|---|---|
PadX |
byte | the paddle’s left column, 0..5 |
DropX |
byte | the falling block’s column |
DropY |
byte | the falling block’s row |
Score |
word | catches so far, shown on the seven-segment display |
Lives |
byte | misses left, shown on the LCD |
Armed |
byte | game-over gate: restart allowed |
Then the moments, and where each comes from and goes to:
| Moment | Fired by | Consumed by |
|---|---|---|
LeftP, RightP |
held 4 and 6 | sliding the paddle |
FallTick |
the gravity timer | the drop falls one row |
AnyKeyP |
any key | leaving Splash; restarting from GameOver |
GateP |
a one-shot timer | opening the restart gate |
Two schedules drive them. Gravity is an oscillator with period 18,
and the period is the game’s difficulty: catches will write it
smaller, the move chapter 7 taught. Wait is a one-shot word timer,
idle at zero until the game-over card arms it - chapter 7’s delayed
moment, spent at last.
The resources: one green 3x1 shape for the paddle, a high sound
for catches, a low one for misses, and six text strings for the LCD.
And the screens: three cards, Splash, Playing, GameOver, joined
in a loop - any key leaves Splash, the last life leaves Playing, and
an armed press leaves GameOver for Splash again.
Add the budget check that ends every design pass. Facts, moments, and
CurrentCard each take one of the program’s 32 change-flag cells:
six facts, five moments, one card cell - twelve, with room to spare.
Timer cells carry no flag, and FrameCount costs nothing in a program
that never names it. Skyfall fits, on paper, before any block exists.
The entry file
The design above, typed in. Skyfall follows the shape chapter 12 taught: an entry file holding the declarations, and one part holding the cards and blocks.
; Skyfall - catch the falling blocks.
; Declarations here; the cards and blocks live in skyfall-rules.glim.
program Skyfall
platform tec1g-mon3
display matrix8x8
part "skyfall-rules.glim"
; --- facts ---
state PadX : byte = 3 ; paddle's left column, 0..5
state DropX : byte ; falling block's column
state DropY : byte ; falling block's row
state Score : word ; catches so far
state Lives : byte ; misses left before game over
state Armed : byte ; game-over gate open: restart allowed
; --- moments ---
pulse LeftP
pulse RightP
pulse FallTick
pulse AnyKeyP
pulse GateP
; --- schedules ---
timer Gravity : byte = 18 -> FallTick ; writable: catches quicken it
timer Wait : word = 0 -> GateP once ; armed on game over
; --- input ---
bind key KEY_4 held period 4 -> LeftP
bind key KEY_6 held period 4 -> RightP
bind key any rising -> AnyKeyP
; --- resources ---
shape Paddle color green
"XXX"
end
sound Catch len 8 div 2
sound Miss len 40 div 10
text MsgTitle "SKYFALL "
text MsgRun "CATCH THE BLOCKS"
text MsgOver "GAME OVER "
text MsgAny "PRESS ANY KEY"
text MsgLives "LIVES "
text MsgPad " "
Fifty lines: the design again, in a form the build can check. Two details reward a closer look.
No fact carries the changed modifier. Every program so far used
changed to draw its first picture, and Skyfall’s screens all belong
to cards - each card’s enter block will re-raise what its renders
need, the pattern chapter 13 taught. Startup takes care of itself.
The row-one messages are all padded to sixteen characters. The LCD
keeps whatever was last written, and three cards take turns with the
same two rows, so each message is sized to cover the longest message
that ever shares its row. MsgPad, six spaces, does the same job for
the tail of row two.
A helper and the splash card
The rules file opens with a routine, declared before the first card
line so it reads as belonging to the whole game:
; Skyfall's rules - a part of skyfall.glim.
; A = a random column, 0..7. Both spawns call it.
routine RandCol
begin
ld c,ApiRandom
rst $10 ; A = random byte, destroys B
and %00000111
end
card Splash
enter SplashShow
begin
call FbClear
call HudBlankDig
lcd_row MsgTitle, LcdRow1
lcd_row MsgAny, LcdRow2
end
effect StartGame
on AnyKeyP
goto Playing
end
RandCol is where the sky gets its randomness. MON-3’s API dispatcher
sits behind rst $10 with the call number in C, the same doorway the
lcd_row op walks through, and ApiRandom is one of the equates
every generated file carries: it returns a random byte in A and
destroys B. Masking with %00000111 folds the byte to a column,
0..7. Two blocks will spawn drops - the round’s first, and every
respawn after a landing - so the three lines live in a routine both
can call.
The card itself is chapter 13’s opening move: SplashShow runs once
on entry, darkens both board displays, and writes the title and the
invitation, and StartGame routes any press straight to play.
Starting a round
card Playing
enter StartRound
updates Score, Lives, PadX, DropX, DropY, Gravity
begin
lcd_row MsgRun, LcdRow1
ld hl,0
ld (Score),hl
ld a,3
ld (Lives),a
ld (PadX),a ; 3: the paddle starts centred
ld a,18
ld (Gravity),a ; the pace every round climbs from
call RandCol ; first drop: random column, top row
ld (DropX),a
xor a
ld (DropY),a
end
One block resets the whole round: score to zero, three lives, paddle
centred, gravity back to its opening pace, and a first drop at the
top of a random column. The updates line is the startup story. Its
marks reach the card’s renders the same frame, so the board, the
score, and the lives readout all appear the moment play begins, on
the first round and on every replay.
Gravity in that list echoes chapter 7: a timer cell carries no
change flag, so its entry compiles to nothing, and the line stands as
the block’s declaration that it writes the pace - for the dependency
report, and for you.
Steering the paddle
effect SlideLeft
on LeftP
updates PadX
begin
ld a,(PadX)
or a
jr z,_stop ; at the left edge: stay
dec a
ld (PadX),a
_stop:
end
effect SlideRight
on RightP
updates PadX
begin
ld a,(PadX)
cp 5
jr nc,_stop ; column 5 puts the right edge at 7: stay
inc a
ld (PadX),a
_stop:
end
Mover’s rules from chapter 1, with one number moved: the right stop is 5, because the paddle’s own width claims the last two columns - its right edge reaches 7, so every column a block can fall in is catchable. The held period of 4 makes the paddle quick. Crossing the whole board costs twenty frames, a third of a second, which the late game will demand.
The drop
One rule carries the whole game. Every FallTick, the drop moves
down a row; the frame it would enter row 7, the paddle’s row, the
landing resolves instead.
effect Fall
on FallTick
updates DropY, DropX, Score, Lives, Gravity, CurrentCard
begin
ld a,(DropY)
inc a
cp 7
jr c,_store ; rows 1..6: keep falling
; row 7 is the paddle's row: resolve the landing
ld a,(PadX)
ld b,a
ld a,(DropX)
sub b ; how far right of the paddle's left edge?
cp 3
jr nc,_miss ; 3 or more - or underflowed: beside the paddle
ld hl,(Score)
inc hl
ld (Score),hl
call Snd_Catch
ld a,(Gravity) ; every catch quickens the fall, floor at 6
cp 7
jr c,_next
dec a
ld (Gravity),a
jr _next
_miss:
call Snd_Miss
ld a,(Lives)
dec a
ld (Lives),a
jr nz,_next
ld a,Card.GameOver ; the last life: leave the board
ld (CurrentCard),a
_next:
call RandCol ; a fresh drop at the top
ld (DropX),a
xor a ; back to the top row
_store:
ld (DropY),a
end
The catch test is three instructions. After sub b, A holds the
drop’s offset from the paddle’s left edge; the paddle covers offsets
0, 1, and 2. A drop left of the paddle underflows to 253 or higher,
so the one unsigned cp 3 sorts every landing, both sides of the
paddle included: carry means caught.
A catch scores, chirps, and turns the difficulty screw: dec a and a
store into Gravity, the timer’s next reload counting from the new
period, with cp 7 holding a floor of 6. Pacing is the same ordinary
write it was in Drip - here it answers the score instead of a ramp.
A miss buzzes and spends a life, and the last life writes
Card.GameOver into CurrentCard - conditional navigation, chapter
13’s rule for transitions that depend on a runtime test. Either way
the block falls into _next: a new drop spawns at the top of a
random column, and _store files the row. The switch to GameOver
lands at the next frame start, so this frame’s renders still run and
the final board reaches the screen.
Pictures and numbers
Three renders, one per instrument.
render DrawBoard
on PadX, DropX, DropY
begin
call FbClear
ld a,(DropX)
ld b,a ; B = x
ld a,(DropY)
ld c,a ; C = y
ld a,COLOR_YELLOW
call FbPlot
ld a,(PadX)
ld b,a
ld c,7 ; the bottom row
ld hl,Shape_Paddle
call ShapeDraw
end
render ShowScore
on Score
begin
ld hl,(Score)
call HudWriteU16
end
render ShowLives
on Lives
begin
lcd_row MsgLives, LcdRow2
ld a,(Lives) ; the cursor sits after the string: add the digit
add a,'0'
ld c,ApiCharToLcd
rst $10
ld hl,MsgPad ; blank the rest of the old row-2 message
ld c,ApiStringToLcd
rst $10
end
DrawBoard repaints the whole scene whenever anything on it moved:
clear, plot the drop, draw the paddle shape. FbPlot clobbers B, DE,
and HL, so the paddle’s arguments load after the drop is plotted -
the register hygiene chapter 9 attached to these calls.
ShowLives extends the lcd_row idiom by one step. The op positions
the LCD cursor and streams its string, and the cursor advances with
every character written - so when MsgLives ends, the cursor rests
exactly where the digit belongs. One ApiCharToLcd call drops it in,
and MsgPad streams six spaces over whatever the previous card left
behind: a render that owns a row writes the whole row.
Game over, gated
card GameOver
enter GameOverShow
updates Armed, Wait
begin
lcd_row MsgOver, LcdRow1
xor a
ld (Armed),a ; close the gate
ld hl,90 ; a second and a half before restart arms
ld (Wait),hl
end
effect OpenGate
on GateP
updates Armed
begin
lcd_row MsgAny, LcdRow2
ld a,1
ld (Armed),a
end
; Conditional navigation: restart only once the gate is open.
effect Restart
on AnyKeyP
updates CurrentCard
begin
ld a,(Armed)
or a
jr z,_wait
ld a,Card.Splash
ld (CurrentCard),a
_wait:
end
The gate answers a playtesting problem: the player who dies mashing
the keys would sail straight through an ungated game-over screen
without reading it. GameOverShow closes the gate and arms the
one-shot - ninety frames, a second and a half at the scan’s sixty-odd
frames a second. When GateP arrives, OpenGate writes the
invitation on row two and opens the gate; until then, Restart
swallows every press at jr z,_wait.
The press that finally restarts fires AnyKeyP once. Card switches
land at the next frame start and pulses clear at frame end, so Splash
wakes to a quiet keypad and waits for a press of its own - three
distinct presses walk the loop from game over to falling blocks, and
each card hears exactly one.
The design, printed
The chapter began with the design as tables. The toolchain ends it by printing the same design back, computed from the program itself:
glimmer --deps skyfall.glim
program Skyfall
PadX : state byte
raised by: StartRound, SlideLeft, SlideRight
triggers: DrawBoard (render)
DropX : state byte
raised by: StartRound, Fall
triggers: DrawBoard (render)
DropY : state byte
raised by: StartRound, Fall
triggers: DrawBoard (render)
Score : state word
raised by: StartRound, Fall
triggers: ShowScore (render)
Lives : state byte
raised by: StartRound, Fall
triggers: ShowLives (render)
Armed : state byte
raised by: GameOverShow, OpenGate
triggers: (nothing)
LeftP : pulse
raised by: key KEY_4 (held)
triggers: SlideLeft (logic)
RightP : pulse
raised by: key KEY_6 (held)
triggers: SlideRight (logic)
FallTick : pulse
raised by: timer Gravity
triggers: Fall (logic)
AnyKeyP : pulse
raised by: key any (rising)
triggers: StartGame (logic), Restart (logic)
GateP : pulse
raised by: timer Wait
triggers: OpenGate (logic)
Gravity : timer
raised by: StartRound, Fall
triggers: (nothing)
Wait : timer once
raised by: GameOverShow
triggers: (nothing)
CurrentCard : card state (built-in; cards: Splash, Playing, GameOver)
raised by: StartGame, Fall, Restart
triggers: SplashShow (logic), StartRound (logic), GameOverShow (logic)
Read it against the tables from the start of the chapter and the
match is line for line - every fact, every moment, every raiser. A
few entries teach on their own. AnyKeyP triggers two blocks in two
different cards, and card gating keeps them from ever both running.
Gravity shows triggers: (nothing) even though the whole game
dances to it: the hidden countdown is its only consumer, and
FallTick, one line up, carries the announcement. And CurrentCard
- raised by three blocks, triggering three enters - is the game’s mode graph in four lines.
And skyfall.main.asm tells the startup story in two bytes:
Changed0: .db %00000000 ; flags dispatch tests
Changed1: .db %00001000 ; flags dispatch tests
Twelve flag cells fill bank 0 and spill into bank 1, and at boot
every bit is clear except one: bit 3 of Changed1, which is
CurrentCard’s. The whole game unfolds from that single set bit -
SplashShow runs on the first frame, the title appears, and
everything after follows from presses and ticks.
Build it, run it under Debug80, and play. The first drop falls at a stroll; ten catches in, the sky has an opinion; a few more and survival hangs on the paddle’s top speed. When a run ends, the LCD says so, holds you for a breath, and asks for a key.
Summary
- Design a game in Glimmer’s own terms before writing blocks: facts, moments, schedules, resources, and cards, with a flag-cell budget check - Skyfall spends 12 of the 32.
- The entry file holds the declarations and the part holds the cards and blocks: the design and the craft, one file each.
ApiRandomis MON-3’s random byte, called withrst $10and the call number in C; it returns A and destroys B. Mask it for a column.- Scoring and pacing are ordinary state changes: a catch increments a word and writes a smaller period into the gravity timer, floor included.
- One unsigned compare resolves a landing: subtract the paddle’s
column and
cpthe width, and underflow sends both misses to the same branch. - Cards need no
changedmodifiers: each card’senterblock re-raises what its renders need, and boot itself is one set bit -CurrentCard’s - in the change flags. - A one-shot timer and a gate fact turn “press any key” into “read the screen, then press any key”.
Skyfall is a complete Glimmer game, and yours to bend: wider paddle, faster floor, two drops. Next, the same instruments under real pressure - reading Tetro, the largest matrix game in the repository.