Skip to content

Give a BASIC turn a deadline on Dragon 32

Keep untimed play valid, or use TIMER and a reusable countdown to advance without waiting for a command.

Native Dragon BASIC caller: TURN 3 E 1 T 1, after one automatic turn.
Two player actions, then one automatic turn with timing enabled. Emu198x 0.44.2, stock Dragon 32 PAL firmware.

Press Space for one turn. Wait and the world waits too. Press T, wait about 4.8 seconds, and a turn arrives without input. T disables timing or starts a fresh duration; Q returns to BASIC. This is a small rule you can use for a flooding room, a thinking opponent or a timed board-game move.

Build and use

Run make -C dragon-32/patterns/basic/framework/turn-deadline. Reset the stock Dragon 32 PAL interpreter, enter every numbered line of build/demo.bas, and type RUN. The Makefile combines caller, deadline helper, clock adapter, presentation, the maintained Commodore BASIC command-loop rule and its portable diagnostic serializer. No firmware is supplied.

CLEAR 200,&H6FFF reserves high RAM before variables are created. The optional observer owns $7000–$7013; the separate arithmetic fixture owns $7000–$7062. Do not run other machine code or graphics buffers there. The complete caller uses text mode, not a PMODE framebuffer. BASIC retains its normal RAM workspace, stack, keyboard and IRQ services.

The caller owns the game policy

caller.bas
5 CLEAR 200,&H6FFF
10 CLS
20 N=0:S=1:EX=0:DU=240:LC=0:MB=28672
30 DC=TIMER:GOSUB 3000
40 POKE MB+16,165:POKE MB+17,90
50 GOSUB 8000
100 GOSUB 5000
110 K$=INKEY$
130 IF DX THEN EX=EX+1:IF EX>255 THEN EX=0
140 IF DX THEN A$="A":GOSUB 1000:GOSUB 3100:GOSUB 8000:GOTO 100
145 IF K$="Q" THEN END
150 IF K$="T" THEN GOSUB 6000:GOSUB 8000:GOTO 100
160 IF K$<>" " THEN GOTO 190
170 A$="A":GOSUB 1000
180 IF DA THEN GOSUB 3100
185 GOSUB 8000
190 GOSUB 9000:GOTO 100
6000 IF DA THEN DA=0:DR=0:RETURN
6010 GOSUB 3100:RETURN

Take the helper

Initialise, start and poll

routines.bas
3000 DP=DC:DR=0:DA=0:DX=0:DE=0:RETURN
3100 DR=DU:DA=0:DX=0
3110 IF DU>0 THEN DA=1
3120 RETURN
3200 DX=0:DE=DC-DP
3210 IF DE<0 THEN DE=DE+65536
3220 DP=DC
3230 IF DA=0 THEN RETURN
3240 DR=DR-DE:IF DR>0 THEN RETURN
3250 DR=0:DA=0:DX=1:RETURN

Include routines.bas once. Its line ranges are 3000–3250; reserve or consistently renumber every call target. Set DU to an integer duration 0..65535, DC to a clock sample, then GOSUB 3000 once. This clears the context and starts disabled. GOSUB 3100 starts/restarts from DU, with zero meaning disabled. It retains the most recent sampled baseline: poll immediately before restarting if that instant matters.

Each service iteration sets DC and calls 3200. DX is the one-poll expiry event; read it before the next poll clears it. DR is remaining time, DA is enabled (0/1), DP the previous sample and DE elapsed ticks. DU/DC are caller inputs; DP/DR/DA/DX/DE belong to the helper. Seven numeric globals, only the first two name characters significant. The helper performs no I/O or game update, uses no array or fixed RAM, and is not re-entrant.

Subtraction is modulo 65536: 65526 to 10 is twenty ticks. Consecutive samples must be less than one complete cycle apart (about 21 minutes 51 seconds at 50 Hz). You cannot distinguish a missing extra cycle from the same two values. Reinitialise after a global TIMER reset; a reset is not a normal wrap. Disabled polls still update the baseline. Expiry clamps remaining to zero and disables the context. A late poll gives one event, without catch-up.

Clock, input and policy

Read TIMER without resetting it

clock.bas
5000 DC=TIMER:GOSUB 3200:RETURN

clock.bas reads TIMER without resetting it. The stock ROM’s video IRQ updates the counter. Retain that interrupt service; blocking sound, tape work or other code can delay observation or disturb normal clock service. These examples qualify the recorded Dragon 32 PAL firmware in Emu198x 0.44.2, not a Dragon 64 or every ROM/region.

INKEY$ returns a delivered character or an empty string. It does not wait for Return. This is not a general simultaneous-key scanner or a physical press-edge guarantee. Blocking INPUT is appropriate for the existing untimed example, but would prevent this deadline being serviced while the player thinks.

The caller polls, consumes one character, then handles expiry first. Expiry wins over a simultaneous character, including T or Q. Otherwise Space advances once and restarts only an active deadline. An untimed action stays untimed. Disable discards the remainder; enable gives a full duration. Restarting at observation time introduces drift; preserved pause, queued input and carried lateness are different policies.

The reusable command-loop rule is unchanged: A$=“A” commits S to the bounded byte-valued N; both stay in 0..255. EX is a demonstration expiry counter. Replace their effects with your game rules. Drawing changes only on action, toggle or expiry, not every poll.

Customise and diagnose

Half the duration, the same helper

customise.bas
5 CLEAR 200,&H6FFF
10 CLS
20 N=0:S=1:EX=0:DU=120:LC=0:MB=28672
30 DC=TIMER:GOSUB 3000
40 POKE MB+16,165:POKE MB+17,90
50 GOSUB 8000
100 GOSUB 5000
110 K$=INKEY$
130 IF DX THEN EX=EX+1:IF EX>255 THEN EX=0
140 IF DX THEN A$="A":GOSUB 1000:GOSUB 3100:GOSUB 8000:GOTO 100
145 IF K$="Q" THEN END
150 IF K$="T" THEN GOSUB 6000:GOSUB 8000:GOTO 100
160 IF K$<>" " THEN GOTO 190
170 A$="A":GOSUB 1000
180 IF DA THEN GOSUB 3100
185 GOSUB 8000
190 GOSUB 9000:GOTO 100
6000 IF DA THEN DA=0:DR=0:RETURN
6010 GOSUB 3100:RETURN

customise.bas changes DU from 240 to 120: about 4.8 seconds becomes 2.4, with the same helper and policy. Choose units before choosing the number. This is the same modular subtraction and representation problem explored in Maths for Games, rather than a frame wait.

mistakes/reset-baseline.bas is deliberately broken: DP=DC immediately before polling makes every elapsed interval zero. The firmware clock advances, but remaining time stays at the full duration and the automatic turn never arrives. Build build/wrong.bas to reproduce it, then restore clock.bas. A separate repaired cold boot verifies that repair; never concatenate both adapters into a working program.

Deliberately broken: discarding elapsed time

reset-baseline.bas
5000 DC=TIMER:DP=DC:GOSUB 3200:RETURN
Native faulty Dragon BASIC caller: TURN 1 E 0 T 1.
The timer is enabled, but every elapsed interval has been thrown away.

Cost and qualification

The interpreter stores 189 tokenised bytes for the helper, including linked line headers. The full instrumented caller uses 1,048 tokenised bytes. The observed scalar table is 133 bytes: nineteen seven-byte entries, including seven helper globals (49 bytes). These are snapshots, not peak stack/heap or minimum-RAM proofs. The 20-byte diagnostic buffer and serializer are optional; remove line 40, MB/LC, GOSUB 9000 and monitor.bas when omitting them. The normal helper does not require CLEAR or a fixed buffer by itself.

The instrumented BASIC caller commonly samples 17–23 ticks apart. That includes input, observation and interpretation; it is not the standalone helper cost. A native clock accumulates while BASIC works, but your game can only react when control returns to the poll. No whole-game frame budget is inferred.

scripts/verify-dragon-basic-deadlines.py --tools /path/to/released/tools --firmware /path/to/roms makes six independent stock-firmware keyboard-entered runs: working, half duration, wrong baseline, repair, arithmetic boundaries and a wrong-equality control. It checks idle, restart, automatic expiry and disable; complete native linked BASIC lines, variable allocation, producer executable/firmware hashes and screenshots are retained. No debugger memory writes are used.

The second caller supplies twelve synthetic clock pairs, including disabled, zero elapsed, maximum duration, exact/overdue expiry and wrap. Replacing > with >= completes but fails the unsigned oracle. Supplied samples qualify arithmetic, not a 22-minute real-time rollover. Simultaneous-command policy is explicit source logic; the BASIC live-input checks do not force an exact concurrent expiry.

Run scripts/check-dragon-basic-deadlines.py and python3 -m unittest discover -s scripts -p test_dragon_basic_deadlines.py. Ten changed-source/state/memory/capture controls must be rejected. The screenshot shows TURN 3 E 1 T 1 for the working caller, TURN 1 E 0 T 1 for the fault.

Sources

Dragon 32 Programmer’s Reference Guide (John Vander Reyden, Sunshine, 1983), pp.29 (INKEY$), 34 (TIMER), 98–99 (BASIC representation). Public scan: https://archive.worldofdragon.org/browse/downloads/Books/Dragon-32-Programmers-Reference-Guide.pdf . Inside the Dragon (Addison-Wesley, 1983), pp.196–197 (video IRQ and clock), Appendix 1 (BASIC workspace), Appendix 2 ($0112–$0113 TIMER). The manual’s period description is historical context; the retained live samples demonstrate this emulator configuration’s advancing counter.

The portable turn update and optional monitor used by the caller remain in their maintained files:

The bounded rule, independent of timing

routines.bas
1000 C=0
1010 IF A$<>"A" THEN RETURN
1020 N=N+S
1030 IF N>255 THEN N=N-256
1040 C=1
1050 RETURN

Text feedback

presentation.bas
8000 PRINT @0,"SPACE TURN T TIMER Q"
8010 PRINT @32,"TURN";N;" E";EX;" T";DA;" "
8020 RETURN

Optional observer

monitor.bas
9000 POKE MB+19,0
9010 POKE MB,N:POKE MB+1,EX:POKE MB+2,DA
9020 V=DR:O=3:GOSUB 9100
9030 V=DC:O=6:GOSUB 9100
9040 V=DU:O=9:GOSUB 9100
9050 V=DE:O=12:GOSUB 9100
9060 LC=LC+1:IF LC>255 THEN LC=0
9070 POKE MB+18,LC:POKE MB+19,239:RETURN
9100 H=INT(V/65536):V=V-H*65536
9110 M=INT(V/256):V=V-M*256
9120 POKE MB+O,V:POKE MB+O+1,M:POKE MB+O+2,H:RETURN

Maintained source · Native execution record · Untimed Dragon command loop · Choosing progression and clocks

The 6809 implementation samples the same firmware clock with a different context and register contract. Choose the language route you need; neither is a prerequisite for the other.