Code status: complete maintained caller, executed through the 48K ROM. The automated check drives real Q and Space events through title, play, pause, resume and game over. It checks held-key behaviour and the session clock; it does not establish an entire game’s frame budget.
Audit execution record · Scope and remaining checks
Four states, one update loop
A title screen should not keep moving the player behind the menu. Pausing should freeze the current game, then resume it rather than start another one. This small caller makes those choices visible with the border: blue for title, green for play, yellow for pause and red for game over.
Build demo.tap with the adjacent Makefile and load it on a 48K Spectrum. Space starts, pauses and resumes; Q ends a running game; Space returns from game over to the title. Reset the machine to leave. The caller starts at $8000, owns its stack at $FF00, keeps the 48K ROM visible for IM 1 interrupts, and uses the maintained QAOP/Space reader and frame waiter.
A complete state-machine caller
; 48K PAL, entered through BASIC USR32768. ROM IM1 remains visible.
; Space: title->play, play<->pause, game over->title. Q: lose while playing.
; Blue=title, green=playing, yellow=paused, red=game over. Reset to leave.
; This caller owns the stack, border/MIC/beeper port and the declared state.
org $8000
start:
di
ld sp,$ff00
ld iy,$5c3a
im 1
xor a
ld (game_state),a
ld hl,0
ld (game_ticks),hl
call read_controls
ld (previous),a ; held controls at entry are not new presses
ld hl,$cafe
ld (ready),hl
ei
main_loop:
call wait_frame
call read_controls
ld b,a
ld a,(previous)
cpl
and b
ld (pressed),a
ld a,b
ld (previous),a
call update_state
call draw_state
jr main_loop
; One state consumes one sampled update. AF/BC/HL change; DE/IX/IY survive.
; State is0..3. Entry/reset actions happen only on a transition.
update_state:
ld a,(game_state)
or a
jr z,state_title
cp 1
jr z,state_playing
cp 2
jr z,state_game_over
cp 3
jr z,state_paused
invalid_state:
di ; expose corrupted state instead of masking it
halt
jr invalid_state
state_title:
ld a,(pressed)
and 16
ret z
ld hl,0 ; a fresh session, not a resume
ld (game_ticks),hl
ld a,1
ld (game_state),a
ret
state_playing:
ld hl,(game_ticks)
inc hl
ld (game_ticks),hl
ld a,(pressed)
bit 0,a
jr nz,state_lose ; loss takes priority over pause
and 16
ret z
ld a,3
ld (game_state),a
ret
state_lose:
ld a,2
ld (game_state),a
ret
state_game_over:
ld a,(pressed)
and 16
ret z
xor a
ld (game_state),a
ret
state_paused:
ld a,(pressed)
and 16
ret z
ld a,1 ; retain the current session's counter
ld (game_state),a
ret
draw_state:
ld a,(game_state)
ld l,a
ld h,0
ld de,state_colours
add hl,de
ld a,(hl)
out ($fe),a
ret
signature: defm "198XSTATES!"
ready: defw 0
game_state: defb 0
previous: defb 0
pressed: defb 0
game_ticks: defw 0
state_colours: defb 1,4,2,6
include "../game-loop/frame.inc"
include "../input-to-sound/input.inc"
end startFit it into a game
Sample controls once per update. Compute new presses from that sample and the previous sample, then dispatch the current state. The history survives transitions: holding Space must not start a game and immediately pause it on the next update. Seeding history on entry also prevents a key held during loading from becoming a new press.
update_state changes AF, BC and HL, preserves DE, IX and IY, and consumes writable game_state, pressed and game_ticks. States are 0–3; an invalid value deliberately halts for diagnosis. Q takes priority if loss and pause arrive together. Replace those demo actions with your game’s rules.
game_ticks increments only in play. Starting a fresh session clears it; resuming preserves it. It is a wrapping 16-bit demonstration counter, not a permanent score or wall clock. The transition update counts as a playing update before entering pause or game over. draw_state also changes DE and writes the border/MIC/beeper port; replace it if another routine owns that output.
Keep drawing separate from state updates. Menus, play and pause can share drawing without sharing movement. Add work incrementally and measure the complete update before assuming it fits the frame.
Related: Game loop, Keyboard reading.
For a component you can call from another loop, game states and restart separate transition decisions, entry pulses and fresh-session resets, with a verified example of accidentally resetting on resume.