Skip to content

Game States and Restart on Amiga

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 Amiga 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 caller-owned byte fields: state/events/changed/fresh/error.
; D0/CCR may change; D1-D7/A0-A6 survive. No chipset or IRQ operations.
; States Title0 Play1 Over2 Paused3; events Start1 Pause2 Loss4 Restart8.
; One decision per update. Loss beats Pause; Over/Restart beats Start.
; FRESH asks the owner to reset session data once, never on resume.
; Error1 invalid state or2 high event bits: preserve state, clear pulses.
flow_init:
    clr.b flow_state
    clr.b flow_events
    clr.b flow_changed
    clr.b flow_fresh
    clr.b flow_error
    rts
flow_step:
    clr.b flow_changed
    clr.b flow_fresh
    clr.b flow_error
    move.b flow_state,d0
    cmpi.b #4,d0
    blo.s flow_valid_state
    move.b #1,flow_error
    rts
flow_valid_state:
    move.b flow_events,d0
    andi.b #$f0,d0
    beq.s flow_valid_events
    move.b #2,flow_error
    rts
flow_valid_events:
    move.b flow_state,d0
    tst.b d0
    beq.s flow_title
    cmpi.b #1,d0
    beq.s flow_playing
    cmpi.b #2,d0
    beq.s flow_over
    move.b flow_events,d0
    andi.b #2,d0
    beq.s flow_done
    moveq #1,d0
    bra.s flow_commit
flow_title:
    move.b flow_events,d0
    andi.b #1,d0
    bne.s flow_new_game
    rts
flow_playing:
    move.b flow_events,d0
    andi.b #4,d0
    beq.s flow_check_pause
    moveq #2,d0
    bra.s flow_commit
flow_check_pause:
    move.b flow_events,d0
    andi.b #2,d0
    beq.s flow_done
    moveq #3,d0
    bra.s flow_commit
flow_over:
    move.b flow_events,d0
    andi.b #8,d0
    bne.s flow_new_game
    move.b flow_events,d0
    andi.b #1,d0
    beq.s flow_done
    moveq #0,d0
    bra.s flow_commit
flow_new_game:
    move.b #1,flow_fresh
    moveq #1,d0
flow_commit:
    move.b d0,flow_state
    move.b #1,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.

D0/CCR may change; D1–D7/A0–A6 survive. The five component fields are bytes in private writable RAM in the relocated Hunk. A direct call uses a four-byte return address. There is no status result in carry: read the explicit error/pulse bytes.

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:
    bsr flow_init
    clr.b previous
    clr.b score
    clr.b resets
    move.b #RESET_HEALTH,health
    moveq #0,d4
trace_loop:
    lea held_samples,a0
    move.b 0(a0,d4.w),d0
    move.b d0,held
    move.b previous,d0
    not.b d0
    and.b held,d0
    move.b d0,flow_events
    move.b held,previous
    cmpi.b #1,flow_state
    bne.s trace_decide
    addq.b #1,score
trace_decide:
    bsr checked_step
    ifne BROKEN
    tst.b flow_changed
    beq.s trace_after_reset
    cmpi.b #1,flow_state
    bne.s trace_after_reset
    else
    tst.b flow_fresh
    beq.s trace_after_reset
    endc
    bsr reset_session
trace_after_reset:
    cmpi.l #8,d4
    bne.s trace_not_resume
    move.b score,resumed
trace_not_resume:
    cmpi.l #10,d4
    bne.s trace_record
    move.b score,finished
trace_record:
    bsr emit_flow
    move.b score,d0
    bsr emit
    move.b previous,d0
    bsr emit
    addq.l #1,d4
    cmpi.l #15,d4
    bne trace_loop
    move.b resumed,preview
    move.b resets,preview+1
    move.b flow_state,preview+2
    move.b score,preview+3
    move.b health,preview+4
    move.b finished,preview+5
    move.b invalid_seen,preview+6
    move.b flow_changed,preview+7
    rts
reset_session:
    clr.b score
    move.b #RESET_HEALTH,health
    addq.b #1,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
    section code,code_c
RESET_HEALTH = 3
BROKEN = 0
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "flow.inc"
    include "display.inc"
    include "../turn-deadline/font.inc"
code_end:
flow_state: dc.b 0
flow_events: dc.b 0
flow_changed: dc.b 0
flow_fresh: dc.b 0
flow_error: dc.b 0
previous: dc.b 0
held: dc.b 0
score: dc.b 0
health: dc.b 0
resets: dc.b 0
resumed: dc.b 0
finished: dc.b 0
invalid_seen: dc.b 0
    even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1405,0
    even
signature: dc.b "198XFLOWAMIGA!!",0
    even
copperlist:
    dc.w $08e,$2c81,$090,$2cc1,$092,$0038,$094,$00d0
    dc.w $100,$1200,$102,0,$104,0,$108,0,$10a,0
    dc.w $180,$012,$182,$fff
copbpl:
    dc.w $0e0,0,$0e2,0
    dc.w $ffff,$fffe
plane: dcb.b 10240,0

The A500 OCS/PAL caller cold-boots an OFS ADF with Kickstart1.3, then takes over IRQ, DMA, Copper and its private bitplane. Code and writable fields are relocated from a Hunk; it does not return to the OS. 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
    section code,code_c
RESET_HEALTH = 5
BROKEN = 0
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "flow.inc"
    include "display.inc"
    include "../turn-deadline/font.inc"
code_end:
flow_state: dc.b 0
flow_events: dc.b 0
flow_changed: dc.b 0
flow_fresh: dc.b 0
flow_error: dc.b 0
previous: dc.b 0
held: dc.b 0
score: dc.b 0
health: dc.b 0
resets: dc.b 0
resumed: dc.b 0
finished: dc.b 0
invalid_seen: dc.b 0
    even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1405,0
    even
signature: dc.b "198XFLOWAMIGA!!",0
    even
copperlist:
    dc.w $08e,$2c81,$090,$2cc1,$092,$0038,$094,$00d0
    dc.w $100,$1200,$102,0,$104,0,$108,0,$10a,0
    dc.w $180,$012,$182,$fff
copbpl:
    dc.w $0e0,0,$0e2,0
    dc.w $ffff,$fffe
plane: dcb.b 10240,0

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
    section code,code_c
RESET_HEALTH = 3
BROKEN = 1
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "flow.inc"
    include "display.inc"
    include "../turn-deadline/font.inc"
code_end:
flow_state: dc.b 0
flow_events: dc.b 0
flow_changed: dc.b 0
flow_fresh: dc.b 0
flow_error: dc.b 0
previous: dc.b 0
held: dc.b 0
score: dc.b 0
health: dc.b 0
resets: dc.b 0
resumed: dc.b 0
finished: dc.b 0
invalid_seen: dc.b 0
    even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1405,0
    even
signature: dc.b "198XFLOWAMIGA!!",0
    even
copperlist:
    dc.w $08e,$2c81,$090,$2cc1,$092,$0038,$094,$00d0
    dc.w $100,$1200,$102,0,$104,0,$108,0,$10a,0
    dc.w $180,$012,$182,$fff
copbpl:
    dc.w $0e0,0,$0e2,0
    dc.w $ffff,$fffe
plane: dcb.b 10240,0

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 Amiga 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 214 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.