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 stock 3 KiB Jupiter Ace 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 jupiter-aceEnter the generated build/working/load.fth 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.
The Ace loader defines TURNS as 15616 CALL, checks that HERE has not passed 15616, pads the dictionary to $3D00, then compiles the 212-byte payload with C,. HERE advances past $3DD3, protecting the bytes from subsequent definitions. The padding also costs dictionary space. Start from a fresh boot; the loader aborts if the dictionary has already passed the chosen address. Do not move this absolute-address code with LOAD or REDEFINE.
Origin, settings and the two includes
; SPDX-License-Identifier: MIT
org $3D00
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
; FORTH: reserve dictionary through code_end; execute 15616 CALL.
; IX/IY and the data stack stay untouched. SP is the ROM return stack.
start:
ld a,INITIAL
call turn_init
call draw_box
main:
ld a,RESET_EACH
or a
jr z,keep_state
xor a
call turn_init
keep_state:
call show_turn
call new_action
or a
jr z,finished
call turn_advance
call trigger_sound
jr main
finished:
jp (iy)Q returns to FORTH after Q has been released. 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
; A in; A/flags out. One byte, addition modulo 256.
turn_init:
ld (turn_count),a
ret
turn_advance:
ld a,(turn_count)
add a,STEP
ld (turn_count),a
ret
; Release both command keys, then wait. A=1 for 1, A=0 for Q.
; Q wins if both are down. BC/flags clobbered; not a general keyboard scanner.
new_action:
release_keys:
ld bc,$F7FE
in a,(c)
bit 0,a
jr z,release_keys
ld b,$FB
in a,(c)
bit 0,a
jr z,release_keys
wait_key:
ld bc,$FBFE
in a,(c)
bit 0,a
jr z,quit_key
ld b,$F7
in a,(c)
bit 0,a
jr nz,wait_key
ld a,1
ret
quit_key:
in a,(c)
bit 0,a
jr z,quit_key
xor a
ret
; Default ASCII character set from ROM; eight display cells, no cursor change.
show_turn:
ld hl,label
ld de,$2400
ld bc,6
ldir
ld a,(turn_count)
rrca
rrca
rrca
rrca
and 15
call hex_digit
ld ($2406),a
ld a,(turn_count)
and 15
call hex_digit
ld ($2407),a
ret
hex_digit:
cp 10
jr c,digit
add a,7
digit:
add a,48
ret
label:
db "TURN $"
draw_box:
ld a,DRAW
or a
ret z
ld a,88
ld ($2440),a
ld ($2441),a
ld ($2442),a
ld ($2460),a
ld ($2462),a
ld ($2480),a
ld ($2481),a
ld ($2482),a
ld a,32
ld ($2461),a
ret
; OUT pushes speaker out; IN pushes it in. Reads are not silent on the Ace.
; Blocking pulse train; BC/DE/A/flags clobbered, no pitch guarantee.
trigger_sound:
ld a,SOUND
or a
ret z
ld d,100
sound_pulse:
ld bc,$FEFE
xor a
out (c),a
ld e,100
sound_delay_out:
dec e
jr nz,sound_delay_out
in a,(c)
ld e,100
sound_delay_in:
dec e
jr nz,sound_delay_in
dec d
jr nz,sound_pulse
ret
turn_count:
db 0
code_end:| 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/BC/DE/HL/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/BC/DE/flags and speaker state. |
The Ace input routine samples two keyboard half-rows: $F7FE bit 0 for 1, $FBFE bit 0 for Q. Zero means pressed. It waits for both command keys to be released before accepting another action; Q wins a simultaneous sample and is released before the routine returns. Other keys are ignored. BC/A/flags change. This is a small command reader, not a general character scanner.
A top-level JP (IY) returns to FORTH. Internal helpers use RET. Confusing those boundaries can send the interpreter into unrelated memory. IX/IY and the data stack remain untouched; SP stays on FORTH’s balanced return stack. Other main registers may change. Two nested internal return addresses need four bytes, with interpreter and interrupt stack use in addition.
The text adapter uses the ROM’s normal ASCII glyphs and writes $2400–$2407, plus three cells per row at $2440/$2460/$2480. It leaves the cursor and other UI alone. Replacing a shared character definition would also change this box.
The speaker and keyboard share an I/O effect: OUT pushes the speaker out, while IN pulls it in. The sound routine alternates them. The input reader also performs IN, so its first read can change the speaker’s level. The retained no-sound control checks that the deliberate pulse train rises above this baseline. Do not run another speaker driver concurrently; this blocking effect makes no pitch-accuracy claim.
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 $3D00
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 $3D00
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 FORTH 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
Jupiter Ace User’s Manual, issue 2 (Jupiter Cantab, 1982), chapters 12 and 16 (input and conditional loops), chapter 24 (memory), chapter 25, pp. 144–146 (machine-code CALL and return), chapter 26, p. 152 (keyboard and speaker I/O); Ace schematic (keyboard half-row bit positions).