Skip to content

Game States and Restart on C64

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 C64 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

6502.inc
; SPDX-License-Identifier: MIT
; Caller owns FLOW_STATE/EVENTS/CHANGED/FRESH/ERROR (five private bytes).
; States TITLE0 PLAY1 OVER2 PAUSED3. Events START1 PAUSE2 LOSS4 RESTART8.
; step performs at most one transition; LOSS wins PAUSE, RESTART wins START.
; CHANGED is an entry pulse; FRESH asks the caller to reset session data once.
; Invalid state -> ERROR1, high event bits -> ERROR2; state stays unchanged.
; All calls change A/flags, preserve X/Y and decimal mode. Single owner.
flow_init:
    lda #0
    sta FLOW_STATE
    sta FLOW_EVENTS
    sta FLOW_CHANGED
    sta FLOW_FRESH
    sta FLOW_ERROR
    rts
flow_step:
    lda #0
    sta FLOW_CHANGED
    sta FLOW_FRESH
    sta FLOW_ERROR
    lda FLOW_STATE
    cmp #4
    bcc flow_valid_state
    lda #1
    sta FLOW_ERROR
    rts
flow_valid_state:
    lda FLOW_EVENTS
    and #$f0
    beq flow_valid_events
    lda #2
    sta FLOW_ERROR
    rts
flow_valid_events:
    lda FLOW_STATE
    beq flow_title
    cmp #1
    beq flow_playing
    cmp #2
    beq flow_over
; paused: resume the current session, not a new one.
    lda FLOW_EVENTS
    and #2
    beq flow_done
    lda #1
    bne flow_commit
flow_title:
    lda FLOW_EVENTS
    and #1
    bne flow_new_game
    rts
flow_playing:
    lda FLOW_EVENTS
    and #4
    beq flow_check_pause
    lda #2
    bne flow_commit
flow_check_pause:
    lda FLOW_EVENTS
    and #2
    beq flow_done
    lda #3
    bne flow_commit
flow_over:
    lda FLOW_EVENTS
    and #8
    bne flow_new_game
    lda FLOW_EVENTS
    and #1
    beq flow_done
    lda #0
    beq flow_commit
flow_new_game:
    lda #1
    sta FLOW_FRESH
flow_commit:
    cmp FLOW_STATE
    beq flow_done
    sta FLOW_STATE
    lda #1
    sta FLOW_CHANGED
flow_done:
    rts
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.

A and flags change; X/Y and decimal mode survive. The supplied component fields occupy $80–$84; move all five symbols together into private writable storage in your game. The helper uses no scratch pointer. 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:
    jsr flow_init
    lda #0
    sta previous
    sta score
    sta resets
    sta trace_index
    lda #RESET_HEALTH
    sta health
trace_loop:
    ldx trace_index
    lda held_samples,x
    sta held
    lda previous
    eor #255
    and held
    sta FLOW_EVENTS
    lda held
    sta previous
    lda FLOW_STATE
    cmp #1
    bne trace_decide
    inc score
trace_decide:
    jsr checked_step
!if BROKEN {
    lda FLOW_CHANGED
    beq trace_after_reset
    lda FLOW_STATE
    cmp #1
    bne trace_after_reset
} else {
    lda FLOW_FRESH
    beq trace_after_reset
}
    jsr reset_session
trace_after_reset:
    lda trace_index
    cmp #8
    bne trace_not_resume
    lda score
    sta resumed
trace_not_resume:
    lda trace_index
    cmp #10
    bne trace_record
    lda score
    sta finished
trace_record:
    jsr emit_flow
    lda score
    jsr emit
    lda previous
    jsr emit
    inc trace_index
    lda trace_index
    cmp #15
    bne trace_loop
    lda resumed
    sta preview
    lda resets
    sta preview+1
    lda FLOW_STATE
    sta preview+2
    lda score
    sta preview+3
    lda health
    sta preview+4
    lda finished
    sta preview+5
    lda invalid_seen
    sta preview+6
    lda FLOW_CHANGED
    sta preview+7
    rts
reset_session:
    lda #0
    sta score
    lda #RESET_HEALTH
    sta health
    inc resets
    rts

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
FLOW_STATE=$80
FLOW_EVENTS=$81
FLOW_CHANGED=$82
FLOW_FRESH=$83
FLOW_ERROR=$84
case_state=$70
case_events=$71
ptr=$74
saved_y=$76
trace_index=$77
score=$78
health=$79
resets=$7a
resumed=$7b
finished=$7c
invalid_seen=$7d
previous=$7e
held=$7f
OUTPUT=$c000
preview=OUTPUT+1480
ready=OUTPUT+1488
RESET_HEALTH=3
BROKEN=0
!source "caller.inc"
!source "exercise.inc"
component_start:
!source "../../../../../cross-platform/patterns/framework/game-flow/6502.inc"
hex_font:
    !byte 0,0,0,0,0,0,0,0,0,56,40,40,40,56,0,0,0,16,48,16,16,56,0,0,0,56,8,56,32,56,0,0,0,56,8,56,8,56,0,0,0,40,40,56,8,8,0,0,0,56,32,56,8,56,0,0,0,56,32,56,40,56,0,0,0,56,8,8,8,8,0,0,0,56,40,56,40,56,0,0,0,56,40,56,8,56,0,0,0,16,40,56,40,40,0,0,0,48,40,48,40,48,0,0,0,56,32,32,32,56,0,0,0,48,40,40,40,48,0,0,0,56,32,48,32,56,0,0,0,56,32,48,32,32,0,0
code_end:

The standalone PAL PRG uses the stock BASIC/KERNAL startup, then disables IRQ sources and owns the screen, character RAM and colour RAM. Its diagnostic client also owns $70–$84 and an output buffer at $C000. It does not return to 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
FLOW_STATE=$80
FLOW_EVENTS=$81
FLOW_CHANGED=$82
FLOW_FRESH=$83
FLOW_ERROR=$84
case_state=$70
case_events=$71
ptr=$74
saved_y=$76
trace_index=$77
score=$78
health=$79
resets=$7a
resumed=$7b
finished=$7c
invalid_seen=$7d
previous=$7e
held=$7f
OUTPUT=$c000
preview=OUTPUT+1480
ready=OUTPUT+1488
RESET_HEALTH=5
BROKEN=0
!source "caller.inc"
!source "exercise.inc"
component_start:
!source "../../../../../cross-platform/patterns/framework/game-flow/6502.inc"
hex_font:
    !byte 0,0,0,0,0,0,0,0,0,56,40,40,40,56,0,0,0,16,48,16,16,56,0,0,0,56,8,56,32,56,0,0,0,56,8,56,8,56,0,0,0,40,40,56,8,8,0,0,0,56,32,56,8,56,0,0,0,56,32,56,40,56,0,0,0,56,8,8,8,8,0,0,0,56,40,56,40,56,0,0,0,56,40,56,8,56,0,0,0,16,40,56,40,40,0,0,0,48,40,48,40,48,0,0,0,56,32,32,32,56,0,0,0,48,40,40,40,48,0,0,0,56,32,48,32,56,0,0,0,56,32,48,32,32,0,0
code_end:

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
FLOW_STATE=$80
FLOW_EVENTS=$81
FLOW_CHANGED=$82
FLOW_FRESH=$83
FLOW_ERROR=$84
case_state=$70
case_events=$71
ptr=$74
saved_y=$76
trace_index=$77
score=$78
health=$79
resets=$7a
resumed=$7b
finished=$7c
invalid_seen=$7d
previous=$7e
held=$7f
OUTPUT=$c000
preview=OUTPUT+1480
ready=OUTPUT+1488
RESET_HEALTH=3
BROKEN=1
!source "caller.inc"
!source "exercise.inc"
component_start:
!source "../../../../../cross-platform/patterns/framework/game-flow/6502.inc"
hex_font:
    !byte 0,0,0,0,0,0,0,0,0,56,40,40,40,56,0,0,0,16,48,16,16,56,0,0,0,56,8,56,32,56,0,0,0,56,8,56,8,56,0,0,0,40,40,56,8,8,0,0,0,56,32,56,8,56,0,0,0,56,32,56,40,56,0,0,0,56,8,8,8,8,0,0,0,56,40,56,40,56,0,0,0,56,40,56,8,56,0,0,0,16,40,56,40,40,0,0,0,48,40,48,40,48,0,0,0,56,32,32,32,56,0,0,0,48,40,40,40,48,0,0,0,56,32,48,32,56,0,0,0,56,32,48,32,32,0,0
code_end:

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 C64 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 123 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.

Maintained source and native evidence.