Press 1. The count advances once. Leave the machine alone and the world waits for you. That is enough to give an adventure its rhythm: look around, make a choice, then let the world respond.
Here the world is just a counter. Replace that update with a room change, a board move or an opponent’s response. Input, persistent state and presentation still have different jobs.

Start under the normal firmware
This is a Dragon 32 PAL caller, not a replacement ROM. Keep the three maintained source files together and build from the code-samples root:
python3 scripts/build-dragon-ace-loops.py --assembler /path/to/asm198x --system dragon-32Enter the generated build/working/load.bas line by line after a fresh firmware boot. The loader is generated from the assembled payload; you do not have to transcribe a second hand-maintained list of bytes.
Dragon uses CLEAR 200,&H6FFF to keep BASIC below the payload at $7000–$70E9. A small numbered READ/POKE program installs its 234 bytes, then CLS:EXEC &H7000 starts it. This verifies keyboard entry and EXEC, rather than cassette loading. Reserve that range in a mixed BASIC/assembly project.
Origin, settings and the two includes
; SPDX-License-Identifier: MIT
org $7000
INITIAL equ 0
STEP equ 1
RESET_EACH equ 0
DRAW equ 1
SOUND equ 1
include "caller.inc"
include "routines.inc"Initialise once; wait, update and show the result
; EXEC $7000 under Dragon 32 BASIC. Reserve above CLEAR 200,&H6FFF.
start:
pshs cc,dp,d,x,y,u
lda #INITIAL
jsr turn_init
jsr draw_box
main:
lda #RESET_EACH
beq keep_state
clra
jsr turn_init
keep_state:
jsr show_turn
poll:
jsr $8006
tsta
beq poll
cmpa #'Q'
beq finished
cmpa #'1'
bne main
jsr turn_advance
jsr trigger_sound
bra main
finished:
puls cc,dp,d,x,y,u,pcQ returns to BASIC through EXEC’s original stack frame. Other keys leave the state alone. The display uses hexadecimal: $0A means ten, $FF means 255, and the next accepted action wraps to $00.
Take the reusable parts
Include routines.inc once. Put the starting byte in A and call turn_init once; call turn_advance when your own command handler accepts an action. STEP is a compile-time byte. Neither routine reads input or touches the screen.
State update, presentation and diagnostic sound as separate calls
; State and output are independent: only turn_init/turn_advance change count.
; A in; A/CC out. One byte, addition modulo 256.
turn_init:
sta turn_count
rts
turn_advance:
lda turn_count
adda #STEP
sta turn_count
rts
; Presentation: overwrites text cells $0400..$0407, no cursor changes.
show_turn:
ldx #$0400
lda #20
sta ,x+
lda #21
sta ,x+
lda #18
sta ,x+
lda #14
sta ,x+
lda #32
sta ,x+
lda #36
sta ,x+
lda turn_count
lsra
lsra
lsra
lsra
jsr hex_digit
sta ,x+
lda turn_count
anda #15
jsr hex_digit
sta ,x
rts
hex_digit:
cmpa #10
blo digit
adda #7
digit:
adda #48
anda #63
rts
; Three cells per row; $0440/0460/0480. Preserves all other display cells.
draw_box:
lda #DRAW
beq box_done
lda #24
sta $0440
sta $0441
sta $0442
sta $0460
sta $0462
sta $0480
sta $0481
sta $0482
lda #32
sta $0461
box_done:
rts
; Blocking one-bit pulse train. Save/restore data, DDR and control register.
; No pitch/timing guarantee; ROM IRQs remain enabled.
trigger_sound:
lda #SOUND
beq sound_done
lda $FF23
pshs a
anda #$F3
sta $FF23
lda $FF22
pshs a
ora #2
sta $FF22
lda $FF23
ora #4
sta $FF23
lda $FF22
pshs a
ldx #200
sound_pulse:
eora #2
sta $FF22
ldb #100
sound_delay:
decb
bne sound_delay
leax -1,x
bne sound_pulse
puls a
sta $FF22
lda $FF23
anda #$FB
sta $FF23
puls a
sta $FF22
puls a
sta $FF23
sound_done:
rts
turn_count:
fcb 0| Routine | Contract |
|---|---|
turn_init |
A becomes turn_count. Changes one labelled byte; flags. |
turn_advance |
Add STEP modulo 256. Changes A, flags and that byte. |
show_turn |
Display the current byte. Changes A/X/flags and eight text cells. |
draw_box |
Draw three rows of original text. Changes A/flags and nine text cells. |
trigger_sound |
Blocking diagnostic pulse train. Changes A/B/X/flags and temporary PIA state. |
The Dragon caller asks the ROM for delivered characters through $8006. A zero result means no input; TSTA makes that branch explicit. The retained Dragon 32 run checks a held 1, but other firmware and Dragon 64 repeat policies need their own checks. This is not a simultaneous-key scanner.
The complete entry saves CC, DP, D, X, Y and U on the existing S stack. It keeps ROM interrupts running and returns with the original stack intact. Do not replace S or the firmware services while relying on this reader.
The text adapter writes MC6847 codes at $0400–$0407, then the box at $0440/$0460/$0480, three cells per row. X is code 24 here, not ASCII 88. It assumes normal 32-column text mode; PMODE graphics needs a different adapter. The routine leaves the cursor alone and does not clear your other UI.
Sound temporarily selects single-bit PIA1 PB1 output and disables the sound multiplexer. It saves and restores the control, direction and data registers, preserving other data bits. Do not share those registers concurrently with cassette or another sound driver. The pulse loop blocks, and interrupts can stretch it: this is an audible effect, not a measured pitch or background player. The complete caller’s initial register save needs ten stack bytes; the sound call adds five for its return and saved PIA values, with firmware/interrupt stack needs in addition.
Customise it
Build with --variant step-three to use this complete alternative. Two actions now produce $03 → $06. Code and state size stay the same.
Same caller, three units per accepted action
; SPDX-License-Identifier: MIT
org $7000
INITIAL equ 0
STEP equ 3
RESET_EACH equ 0
DRAW equ 1
SOUND equ 1
include "caller.inc"
include "routines.inc"
Common mistake: resetting inside the loop
This variant is deliberately broken. RESET_EACH=1 runs initialisation inside each iteration. An action updates the byte, then the next redraw erases the result. The screen stays at $00.
Broken: erase persistent state before each redraw
; Deliberately broken: initialisation inside every iteration.
; SPDX-License-Identifier: MIT
org $7000
INITIAL equ 0
STEP equ 1
RESET_EACH equ 1
DRAW equ 1
SOUND equ 1
include "caller.inc"
include "routines.inc"
Use the original startup-only initialisation to repair it. The repaired caller is run separately. A bad return convention or a conflicting screen allocation would be different faults; these images show the state-lifetime mistake.
Decide when time matters
This loop spends CPU waiting, but does not spend a game turn. A timed decision needs a separate clock, an input policy that checks it and a rule for a command arriving with the deadline. Continuing display or firmware interrupts do not imply that the world should advance on every tick.
Compare the loop choices and the BASIC version. These are different resource and input agreements, not a compulsory ladder from one language to another.
Source and verification
Native-emulator executed: Emu198x 0.44.1, Asm198x 0.0.71, the named machine and firmware configuration above. Actual keyboard input drives the program; host writes do not supply the count. The images are native framebuffer captures.
The maintained source includes the working variation and deliberate fault. The retained record identifies sources and observations. Eight cold starts also check 254 → 255 → 0, a nonzero start, loaded-byte equality, held input, omitted drawing/sound and a return to interpreter arithmetic. An independent checker decodes screen RAM and rejects altered counts, missing stages, changed sources and changed captures.
This qualifies the small workload and named configuration. It is not an original-hardware, pitch, cassette-transfer or whole-emulator claim.
Primary references
Inside the Dragon (Addison-Wesley, 1983), pp. 103–104 and Appendix 3, pp. 348–349 (ROM input/output contracts), pp. 212–213 (single-bit sound). The direct keyboard entry is $8006; $800C is screen output.