Skip to content

Game States and Restart on Spectrum

Separate title, play, pause and game over, preserve a paused session and reset a new one exactly once.

Pause a game, then resume it. Your score should still be there. Start a new game and it should be cleared. Both actions enter Playing, but they mean different things. This routine makes that distinction explicit, so a reset cannot quietly sneak into the resume path.

The working caller preserves score 4 across pause/resume, finishes that session, restarts and performs two more playing updates. It records two fresh-session resets. The deliberately wrong caller resets on every entry to Playing: the resumed score becomes 0, and it records an extra reset.

The Spectrum caller shows 04 02 01 02 03 06 01 00: score survives resume and two fresh sessions reset once each.
The first field is the score immediately after resume. The second counts fresh-session resets. Neither should change merely because a paused game resumes.

One decision per update

The state numbers preserve the existing shared policy: Title 0, Playing 1, Game Over 2, Paused 3. Supply a logical event byte to FLOW_EVENTS (or the lowercase named field in the Z80/68000 source), then call flow_step once. Events are Start 1, Pause 2, Loss 4, Restart 8. Start and Pause normally come from new presses; Loss comes from game rules. Restart is an explicit menu/action request.

Current state Accepted request, in priority order Result
Title Start Playing; fresh session
Playing Loss, then Pause Game Over or Paused
Game Over Restart, then Start Fresh Playing session, or Title
Paused Pause Playing; preserve session

Other requests have no effect in that state. Loss beats Pause if both arrive together. In Game Over, an explicit Restart beats Start. Start returning from Game Over to Title preserves the established shared example; direct Restart is an additional deliberate request. A state does not immediately run the next state’s update on the same event.

Reusable initialisation and one-step transition routine

flow.inc
; SPDX-License-Identifier: MIT
; Five private bytes: state/events/changed/fresh/error. AF changes only.
; State Title0 Play1 Over2 Paused3; events Start1 Pause2 Loss4 Restart8.
; One transition per call, Loss wins Pause, Restart wins Start in Over.
; Error1 bad state,2 high event bits; preserve state, clear pulses on error.
; Single owner; caller consumes fresh for session reset, preserves sampler.
flow_init:
    xor a
    ld (flow_state),a
    ld (flow_events),a
    ld (flow_changed),a
    ld (flow_fresh),a
    ld (flow_error),a
    ret
flow_step:
    xor a
    ld (flow_changed),a
    ld (flow_fresh),a
    ld (flow_error),a
    ld a,(flow_state)
    cp 4
    jr c,flow_valid_state
    ld a,1
    ld (flow_error),a
    ret
flow_valid_state:
    ld a,(flow_events)
    and $f0
    jr z,flow_valid_events
    ld a,2
    ld (flow_error),a
    ret
flow_valid_events:
    ld a,(flow_state)
    or a
    jr z,flow_title
    cp 1
    jr z,flow_playing
    cp 2
    jr z,flow_over
    ld a,(flow_events)
    and 2
    jr z,flow_done
    ld a,1
    jr flow_commit
flow_title:
    ld a,(flow_events)
    and 1
    jr nz,flow_new_game
    ret
flow_playing:
    ld a,(flow_events)
    and 4
    jr z,flow_check_pause
    ld a,2
    jr flow_commit
flow_check_pause:
    ld a,(flow_events)
    and 2
    jr z,flow_done
    ld a,3
    jr flow_commit
flow_over:
    ld a,(flow_events)
    and 8
    jr nz,flow_new_game
    ld a,(flow_events)
    and 1
    jr z,flow_done
    xor a
    jr flow_commit
flow_new_game:
    ld a,1
    ld (flow_fresh),a
flow_commit:
    ld (flow_state),a
    ld a,1
    ld (flow_changed),a
flow_done:
    ret
component_end:

flow_init clears the five private bytes: current state, supplied events, changed, fresh and error. Call it at boot; it is not your game’s reset routine. On each flow_step, CHANGED pulses when a transition occurs, FRESH pulses when the caller should initialise a new session, and ERROR reports invalid state (1) or illegal upper event bits (2). Pulses/error clear on the next call. Read and act on them before calling again.

Invalid input leaves the state unchanged. Diagnose ERROR in the caller; do not use it as a reason to reset silently or index a drawing table with an invalid state. A valid no-op has no error and emits no entry/reset pulse. The routine performs no drawing, audio, input reads or payload reset.

Only AF changes; BC/DE/HL/IX/IY survive. The five named component fields follow the code in private writable RAM. A direct call uses a two-byte return address.

Reset the session, preserve the sampler

The maintained client computes new presses from one logical held sample and its previous sample. It updates the old Playing state before deciding transitions, matching the shared policy that the current update’s simulation/collisions finish before pause or loss. Thus an update entering Pause counts as one playing update; the resume transition itself does not.

After flow_step, call reset_session only when FRESH is set. Here it clears score and restores health; a game can also clear its object flags, reset room state, reseed a deliberately repeatable RNG and restart appropriate timers. Choose what belongs to a session rather than clearing all RAM. Resuming skips those actions and preserves progress.

Keep previous-held input history outside that reset. A held Start/Pause button must not become a new press simply because a state changed. For a real input reader at startup, seed previous-held from the actual initial sample. The scripted client begins with a deliberate neutral sample; it is not a hardware-reader demonstration.

Compute edges, update once and reset only a fresh session

exercise.inc, excerpt
preview_flow:
    call flow_init
    xor a
    ld (previous),a
    ld (score),a
    ld (resets),a
    ld (trace_index),a
    ld a,RESET_HEALTH
    ld (health),a
trace_loop:
    ld a,(trace_index)
    ld l,a
    ld h,0
    ld de,held_samples
    add hl,de
    ld a,(hl)
    ld (held),a
    ld a,(previous)
    cpl
    ld b,a
    ld a,(held)
    and b
    ld (flow_events),a
    ld a,(held)
    ld (previous),a
    ld a,(flow_state)
    cp 1
    jr nz,trace_decide
    ld a,(score)
    inc a
    ld (score),a
trace_decide:
    call checked_step
    if BROKEN
    ld a,(flow_changed)
    or a
    jr z,trace_after_reset
    ld a,(flow_state)
    cp 1
    jr nz,trace_after_reset
    else
    ld a,(flow_fresh)
    or a
    jr z,trace_after_reset
    endif
    call reset_session
trace_after_reset:
    ld a,(trace_index)
    cp 8
    jr nz,trace_not_resume
    ld a,(score)
    ld (resumed),a
trace_not_resume:
    ld a,(trace_index)
    cp 10
    jr nz,trace_record
    ld a,(score)
    ld (finished),a
trace_record:
    call emit_flow
    ld a,(score)
    call emit
    ld a,(previous)
    call emit
    ld a,(trace_index)
    inc a
    ld (trace_index),a
    cp 15
    jp nz,trace_loop
    ld a,(resumed)
    ld (preview),a
    ld a,(resets)
    ld (preview+1),a
    ld a,(flow_state)
    ld (preview+2),a
    ld a,(score)
    ld (preview+3),a
    ld a,(health)
    ld (preview+4),a
    ld a,(finished)
    ld (preview+5),a
    ld a,(invalid_seen)
    ld (preview+6),a
    ld a,(flow_changed)
    ld (preview+7),a
    ret
reset_session:
    xor a
    ld (score),a
    ld a,RESET_HEALTH
    ld (health),a
    ld a,(resets)
    inc a
    ld (resets),a
    ret

This small client deliberately supplies a fixed fifteen-update logical trace, including held input, release/repress, pause, resume, loss and direct restart. The native packet retains state, changed/fresh/error, score and input history at every update, rather than inferring behaviour from one still image.

Include and adapt it

Buildable caller and exact include order

demo.asm
    org $8000
RESET_HEALTH equ 3
BROKEN equ 0
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "flow.inc"
    include "../../physics/rectangle-overlap/display.inc"
code_end:
flow_state: defb 0
flow_events: defb 0
flow_changed: defb 0
flow_fresh: defb 0
flow_error: defb 0
previous: defb 0
held: defb 0
score: defb 0
health: defb 0
resets: defb 0
resumed: defb 0
finished: defb 0
invalid_seen: defb 0
case_state: defb 0
case_events: defb 0
trace_index: defb 0
ready: defb 0
preview: defs 8,0
OUTPUT: defs 1405,0
    end start

The 48K PAL standalone caller enters through ROM tape loading and BASIC USR at $8000, then disables interrupts, owns the bitmap/attributes/border and uses ROM glyphs for diagnostics. Mutable fields and output follow its code. It remains in its hold loop rather than restoring BASIC. The reusable helper owns none of those hardware resources. Reserve its five bytes and include it once; your loop supplies events and consumes the results. Keep one owner for updates, and defer requests from interrupts instead of changing the state halfway through a decision.

The eight hexadecimal fields are resumed score, reset count, final state, final score, reset health, score at loss, observed invalid-state error, final changed pulse. The working row is 04 02 01 02 03 06 01 00. This diagnostic score is a wrapping byte counting Playing updates, not a complete scoring system.

Make it your own

The custom caller changes reset health from 3 to 5. The fifth output field becomes 05. No extra component code or state is needed; it is a caller policy. Keep that value in reset_session, so changing a session’s starting health cannot alter resume behaviour.

Change initial health without changing transitions

custom.asm
    org $8000
RESET_HEALTH equ 5
BROKEN equ 0
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "flow.inc"
    include "../../physics/rectangle-overlap/display.inc"
code_end:
flow_state: defb 0
flow_events: defb 0
flow_changed: defb 0
flow_fresh: defb 0
flow_error: defb 0
previous: defb 0
held: defb 0
score: defb 0
health: defb 0
resets: defb 0
resumed: defb 0
finished: defb 0
invalid_seen: defb 0
case_state: defb 0
case_events: defb 0
trace_index: defb 0
ready: defb 0
preview: defs 8,0
OUTPUT: defs 1405,0
    end start

For more states or event bits, change the policy deliberately and add competing-event and invalid-input cases. Those are extensions, not safe edits to the existing byte contract. Entry/exit hooks and table-driven rules can be useful later; this component deliberately returns pulses instead of imposing callbacks or an engine structure.

Common mistake: reset whenever Playing is entered

The labelled wrong caller tests CHANGED and Playing instead of FRESH before resetting session data. That also matches resume. The first field falls from 04 to 00, the reset count rises from 02 to 03, and the score at loss falls from 06 to 02. The program completes the full trace. Its transition routine is still correct; the bug is in the caller’s reset condition.

Deliberate fault: treat resume as a new session

wrong.asm
    org $8000
RESET_HEALTH equ 3
BROKEN equ 1
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "flow.inc"
    include "../../physics/rectangle-overlap/display.inc"
code_end:
flow_state: defb 0
flow_events: defb 0
flow_changed: defb 0
flow_fresh: defb 0
flow_error: defb 0
previous: defb 0
held: defb 0
score: defb 0
health: defb 0
resets: defb 0
resumed: defb 0
finished: defb 0
invalid_seen: defb 0
case_state: defb 0
case_events: defb 0
trace_index: defb 0
ready: defb 0
preview: defs 8,0
OUTPUT: defs 1405,0
    end start

The smallest fix is the FRESH test in the normal client. The repaired executable matches the working executable exactly; a Spectrum tape’s filename can differ while its loaded CODE remains identical.

The Spectrum caller shows 00 03 01 02 03 02 01 00 after a reset on resume.
The wrong reset condition loses progress on resume and counts a third reset. The observed trace identifies the offending transition.

Cost, neighbours and verification

The helper uses 139 code bytes plus five state bytes at this layout. It takes one bounded branch path per update, with no object scan or wait. Instruction/storage counts do not establish a whole-game frame budget. Your input sampling, simulation, rendering, sound and entry work still have their own costs.

The shared state-machine entry explains the policy. Input edges explain one press versus a held button. Object slots make one possible session-reset responsibility concrete. Resetting a pool is separate from restoring pixels or resetting audio; each resource needs its owner.

Four native runs with Asm198x 0.0.72 / Emu198x 0.44.2 cover normal, changed health, the reset-on-resume fault and exact repair. Amiga media uses Build198x 0.2.9. An independent policy model checks all 64 valid state/event pairs, all 252 invalid state bytes, twelve upper-event-bit rejection cases and the complete fifteen-update trace. Preserved-register sentinels and positive completion distinguish a real result from a stalled caller. The packet is 1,405 bytes; the optional display shows a small summary of it.

The observer checks native startup, actual code, source/tool/configuration identities, capture hashes and text pixels. Corrupting retained observations must fail the gate. Firmware is hashed rather than distributed. This is logic execution on the named configurations, not a new hardware-reader test, physical-hardware timing measurement or complete-game acceptance test.

The existing interactive Spectrum state loop remains the keyboard-driven companion. This reusable logical routine does not replace its controls or published route.

Maintained source and native evidence.