Chapter 9 - Shapes, Sound and Displays on the Board

Every picture in the book so far has been built from single calls to FbPlot: a dot, a drop, a bar of pixels in a loop. A game’s character deserves a body - two pixels by two, or a whole 8x8 figure - and plotting it pixel by pixel inside every render buries the picture in code. The picture belongs in data, drawn where you can see its shape.

A game also speaks through more than the matrix. When the character hits a wall, the player should hear it. When the score changes, the player should be able to read it between moves. The TEC-1G board carries an instrument for each job: a speaker, the six-digit seven-segment display, and a four-row LCD, all sitting beside the matrix and all reachable from a block.

This chapter teaches one pattern four times over. You declare a resource in the .glim file - a shape, a sound cue, a text string - and Glimmer generates the data plus something callable to go with it. Your blocks call what was generated, and the declaration reads like the resource it describes.

Fanfare

The chapter’s program is a small celebration. A cyan spark, two pixels square, bounces around the matrix on its own timer. Every wall hit reverses its direction, beeps the speaker, and adds one to a score on the seven-segment display. The LCD announces the program from the first frame.

program Fanfare

platform tec1g-mon3
display matrix8x8

shape Spark color cyan
  "XX"
  "XX"
end

sound Bounce len 8 div 3

text MsgHello "FANFARE"

state SparkX : byte = 3 changed
state SparkY : byte = 2
state VelX   : byte = 1
state VelY   : byte = 1
state Score  : word = 0 changed
state Banner : byte = 0 changed

pulse Tick

timer Step : byte = 6 -> Tick

effect Move
    on Tick
    updates SparkX, VelX, SparkY, VelY, Score
begin
    ; step x, bounce off columns 0 and 6
    ld a,(VelX)
    ld b,a
    ld a,(SparkX)
    add a,b
    ld (SparkX),a
    or a
    jr z,_hitx      ; left wall
    cp 6
    jr nz,_xdone    ; open board: no bounce
_hitx:
    ld a,(VelX)
    neg
    ld (VelX),a
    ld hl,(Score)
    inc hl
    ld (Score),hl
    call Snd_Bounce
_xdone:
    ; step y, bounce off rows 0 and 6
    ld a,(VelY)
    ld b,a
    ld a,(SparkY)
    add a,b
    ld (SparkY),a
    or a
    jr z,_hity      ; top wall
    cp 6
    jr nz,_ydone
_hity:
    ld a,(VelY)
    neg
    ld (VelY),a
    ld hl,(Score)
    inc hl
    ld (Score),hl
    call Snd_Bounce
_ydone:
end

render DrawSpark
    on SparkX, SparkY
begin
    call FbClear
    ld a,(SparkX)
    ld b,a          ; B = x
    ld a,(SparkY)
    ld c,a          ; C = y
    ld hl,Shape_Spark
    call ShapeDraw
end

render ShowScore
    on Score
begin
    ld hl,(Score)
    call HudWriteU16
end

render Greet
    on Banner
begin
    lcd_row MsgHello, LcdRow1
end

Three declarations at the top are new: shape, sound, and text. The rest is chapter 7’s machinery - a timer fires Tick every 6 frames, Move runs on Tick - carrying a new idea in its state: velocity as a fact. VelX holds 1 travelling right or $FF travelling left, and adding $FF to a byte steps it down by one, so one add moves the spark whichever way it is going. After the step, a spark at column 0 or column 6 has an edge against a wall (the shape is 2 wide, so 6 is as far right as it fits), and the rule answers with three moves: negate the velocity, bump the score word, start the sound cue. Then the same again for y.

Build it and let it run. The spark ricochets, each hit chirps, and the digits climb - twice in quick succession when it rounds a corner.

A shape is pixel art with a name

shape Spark color cyan
  "XX"
  "XX"
end

A shape declares a bitmap you can read at a glance: quoted rows of X for a lit pixel and . for an empty one. Rows are rectangular, from 1x1 up to 8x8, and the colour is one of the matrix’s seven: red, green, blue, yellow, cyan, magenta, white. A tall cross, three wide and four high, would read:

shape Cross color red
  ".X."
  "XXX"
  ".X."
  ".X."
end

From each shape Glimmer emits a data table named Shape_<Name>, and because at least one shape exists, the profile library gains ShapeDraw, the routine that paints any of them. Its interface is three registers:

ld hl,Shape_Spark
ld b,3           ; x
ld c,2           ; y
call ShapeDraw

HL picks the shape, B and C place its top-left corner. ShapeDraw ORs each lit pixel’s colour bits into the framebuffer, so lit pixels land on top of whatever is there and empty pixels leave it alone - two overlapping shapes combine. DrawSpark starts with FbClear for the same reason as every moving picture since chapter 1: a moving shape redraws from a clean board.

Placement is the caller’s whole responsibility: ShapeDraw plots every lit pixel at x plus column, y plus row, straight into the framebuffer, and a row that hangs off the board writes into whatever memory follows it. Keep the whole shape inside the 8x8 matrix - for the 2x2 spark that means x and y each stay in 0..6, which is exactly the range Move enforces with its bounce tests. Register hygiene matters here too: the generated contract line declares that ShapeDraw clobbers A, BC, DE, and HL, so load its arguments last, as DrawSpark does.

Sound that keeps out of the way

sound Bounce len 8 div 3

A sound declares a cue: a short, non-blocking beep served by the matrix scan itself. The scan loop visits the speaker once per row, 8 ticks per frame, and len counts those ticks - len 8 sounds for about one frame. div sets the pitch as a divider: smaller values are higher. Cues of this kind are the vocabulary of a board game’s feedback - clicks, chirps, buzzes - with a long low len 200 div 9 about as mournful as the speaker gets.

Each cue compiles to a routine named Snd_<Name>, and calling it is the entire interface:

    call Snd_Bounce

The call starts the cue and returns at once; the scan plays it out over the following frames while your blocks keep running. One cue is active at a time, and starting a new cue replaces the current one - a fresh wall hit restarts the chirp from the top, which is exactly the feedback a fresh hit deserves.

Where the call sits is the lesson of Move. Sound accompanies an event, and the event lives inside a rule, behind a conditional - so the call Snd_Bounce sits inside the effect, on the branch where the wall hit happened. The quiet path steps past it. Feedback is one line in the rule that knows.

The score, on the seven-segment display

The six-digit seven-segment display is the board’s number instrument, and the same scan that serves the speaker serves it: one digit per row tick, refreshed forever. The profile library drives it with two routines:

  • HudWriteU16 - HL = value, shown as five decimal digits, 0 to 65535.
  • HudBlankDig - clear all six digits.

Score is a word, so ShowScore loads all sixteen bits and hands them over:

render ShowScore
    on Score
begin
    ld hl,(Score)
    call HudWriteU16
end

The startup code Glimmer generates calls FbClear and HudBlankDig before the first frame, so both displays begin dark; Score is declared changed, so ShowScore runs on frame one and the score opens at zero rather than blank.

Move’s header has one consequence to trace. updates lists every fact the block may change, and each listed fact is marked changed whenever the block runs - so ShowScore repaints its digits every step, quiet ticks included. The repaint writes the same six glyph bytes and costs a few dozen cycles in the blank window. When a score changes rarely and its redraw is heavy, split the rule; when the redraw is HudWriteU16, the broad updates reads better.

Words on the LCD

text MsgHello "FANFARE"

A text declares a zero-terminated string for the TEC-1G’s four-row LCD. The LCD is board hardware, alongside the keypad rather than part of any display profile, so text resources work the same on the matrix and, later in the book, on the TMS9918. Writing a string to a row is one line in a block:

    lcd_row MsgHello, LcdRow1

lcd_row is an AZM op: a named instruction sequence that the assembler owns, defined once in the generated file and expanded inline wherever it is invoked. You write it like an instruction - name, then arguments - and the assembler replaces it with its body, with the arguments substituted in. Glimmer emits the lcd_row op whenever a program declares text, and you can define ops of your own in hand-written AZM modules, which chapter 12 reaches. Here it packages the two MON-3 calls that position the LCD cursor and stream a string, taking the message label and a row constant: LcdRow1 through LcdRow4 come with it.

Greet shows the run-once pattern that puts a title on screen. Banner starts changed and appears in no block’s updates, so it changes exactly once, before the first frame - Greet runs on frame one, writes FANFARE to the top row, and rests for the rest of the program’s life. A fact that starts changed and never changes again is a startup hook, declared in one line of state.

The file, resource by resource

Each resource declaration left its mark on the generated file. Open fanfare.main.asm and find every declaration’s other half.

The text resource is its bytes, terminator included:

; --- text resources (zero-terminated LCD strings) ---
MsgHello:
        .db     "FANFARE", 0

The shape is a five-byte table: a header of width, height, and colour, then one mask byte per row, lit pixels packed from bit 7:

; --- shape resources ---
; Table format: width, height, colour, then left-aligned row masks.
Shape_Spark:
        .db     2, 2, COLOR_CYAN
        .db     %11000000
        .db     %11000000

ShapeDraw walks exactly this: read the header, then for each row shift the mask left and FbPlot every set bit at its offset from B and C. The declaration you drew in Xs and the table the routine consumes are the same picture at two zoom levels.

The sound cue is a three-instruction wrapper:

; --- sound cues ---
; Non-blocking matrix-profile cues. len is row ticks; div is the
; speaker divider. Starting a cue replaces the currently active cue.
.routine
Snd_Bounce:
        ld      a,8
        ld      c,3
        jp      SndStart

Your len and div became the two loads, and SndStart is the library routine that arms the scan’s speaker service - A carrying the duration in ticks, C the divider. Declaring a second cue would add a second wrapper over the same SndStart.

And the op, defined once near the end of the file:

; Position the LCD cursor at a row command, then write a string.
op lcd_row(msg imm16, row imm8)
        ld      b,row
        ld      c,ApiCommandToLcd
        rst     $10
        ld      hl,msg
        ld      c,ApiStringToLcd
        rst     $10
end

Two parameters, typed by size: msg is a 16-bit immediate, row an 8-bit one. The body is two MON-3 calls through rst $10, with the parameters standing where their values will go. At every invocation the assembler drops these six instructions in place, so Greet’s block body lands in the file with the invocation still readable:

; --- render block Greet ---
.routine
Glim_Greet:
    lcd_row MsgHello, LcdRow1
        ret

Your one line, verbatim, and the assembler finishes the job from the definition above.

Summary

  • A shape is a named bitmap: quoted rows of X and ., 1x1 up to 8x8, in one of seven colours. Glimmer emits a Shape_<Name> table and the ShapeDraw routine: HL = shape, B = x, C = y.
  • ShapeDraw ORs lit pixels into the framebuffer, overlaps combine, and placement that keeps the whole shape on the board is the caller’s job. It clobbers A, BC, DE, and HL.
  • A sound cue compiles to Snd_<Name>; one call starts it and the matrix scan plays it out. len counts row ticks (8 per frame), div sets pitch (smaller is higher), and a new cue replaces the active one. Call cues inside rules, on the branch where the event happened.
  • The seven-segment HUD shows a value with HudWriteU16 (HL = value, five decimal digits) and clears with HudBlankDig.
  • A text is a zero-terminated LCD string; lcd_row Msg, LcdRowN writes it to a row. An AZM op is an assembler-owned macro: Glimmer emits the definition, you invoke it like an instruction, and the assembler expands it inline.
  • A state cell that starts changed and appears in no updates is a run-once startup hook.

Fanfare’s whole world is one spark and one number. Next, the board itself becomes data: arrays and layout types, for games whose state is many related bytes.