Skip to content

Give a BASIC turn a deadline on Electron

Use a reusable elapsed-clock helper, keep untimed play valid, and let a deadline expire without blocking on input.

Press Space: one turn. Wait: the world waits with you. Now press T and leave the keyboard alone. Four seconds later, the next turn arrives by itself. Your adventure might give a player time to escape a flooding room; your board game might put a limit on a move. The turn rule can stay the same while its timing changes.

Native Electron BASIC caller showing TURN 3, one automatic expiry and timing enabled.
Two Space actions, then an automatic third turn. Original example captured in Emu198x 0.44.1.

Try the complete caller

Run make -C acorn-electron/patterns/basic/framework/turn-deadline in code-samples. Enter the generated build/demo.bas as one numbered program, then type RUN. Space advances, T switches timing on or off, and Q ends the example. Use brief taps at first. The shared caller compares uppercase T/Q; use Caps Lock or Shift if you change the machine’s case setting. The status shows the turn, expiry count and active flag; it is not a live countdown.

Use a stock 32 KiB PAL Electron with its OS and BBC BASIC ROMs. This reuses the BBC BASIC deadline modules unchanged, but the records and captures are independent Electron runs. The Makefile delegates to that shared folder and copies generated complete listings into this folder’s build directory. The caller selects MODE 6 and sets HIMEM=&3000, leaving &3000–&3013 for an optional diagnostic observer. Keep the program and heap below that reservation. Q does not restore the previous mode or HIMEM: reset or restore your own memory layout before loading another program.

The caller owns the game policy

caller.bas
10 MODE 6:HIMEM=&3000
20 N%=0:S%=1:EX%=0:DU%=400:LC%=0:MB%=&3000
30 DC%=TIME AND &FFFF:GOSUB 3000
40 ? (MB%+16)=165:? (MB%+17)=90
50 GOSUB 8000
100 GOSUB 5000
110 K$=INKEY$(0)
130 IF DX% THEN EX%=EX%+1:IF EX%>255 THEN EX%=0
140 IF DX% THEN 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 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
8000 PRINT TAB(0,0);:PRINT "SPACE:TURN T:TIMER Q:QUIT"
8010 PRINT "TURN";N%;" EX";EX%;" TIMER";DA%;"   "
8020 RETURN

Combine this caller, turn.bas, routines.bas, clock.bas and monitor.bas. The Makefile does that for you. BASIC executes by line number, so reserve the shown ranges and update every GOSUB/GOTO target if you renumber. Reset/NEW before entering a complete alternative: old numbered lines must not survive accidentally.

A bounded turn update, independent of time

turn.bas
1000 N%=(N%+S%)AND255:RETURN

The N% expression comes from the existing sampled turn loop. That entry still offers physical press-edge handling; this caller deliberately consumes delivered characters instead. Both counters are small demonstration state: replace their effects with your own game rules.

Take the reusable helper

The helper does no input, drawing or turn advancement. It accepts two samples of a clock, subtracts the elapsed ticks and reports whether time ran out.

Initialise, start and poll a deadline

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

Set DU% to your duration and DC% to the current clock, then call 3000 once. This initialises disabled state and its previous sample. Call 3100 to start or restart: it copies duration into remaining time. Zero duration leaves it disabled.

Each service iteration, including while disabled, set DC% and call 3200. Read DX% immediately afterwards: 1 means this poll expired. The helper disables that deadline and clamps remaining to zero; the next poll clears the event. Your caller decides whether to spend a turn or start another deadline.

Global Contract
DU% Caller duration: whole ticks, 0..65535; 0 disables
DC% Caller current clock: whole ticks, 0..65535
DP% Helper-owned previous sample
DR% Helper-owned remaining ticks, saturated at zero
DA% Helper-owned active flag, 0 or 1
DX% Helper-owned expiry event, 0 or 1
DE% Helper-owned elapsed ticks from this poll

These are BASIC globals. Rename them consistently to avoid collisions. The % suffix selects signed 32-bit integers. Differences of two 16-bit samples, including negative differences before correction, fit comfortably. The helper reserves no fixed RAM, IRQ vector, hardware timer, array or string. It uses BASIC GOSUB/RETURN frames and is one mutable context, rather than a re-entrant service.

Starting preserves the most recent sampled baseline. Poll immediately before starting if that instant matters. Disable by setting DA%=0. There is no preserved pause state: the demo clears remaining on disable and gives a fresh duration on re-enable.

Read a clock without waiting for a command

The machine-specific clock adapter

clock.bas
5000 DC%=TIME AND &FFFF:GOSUB 3200:RETURN

TIME counts hundredths of a second. This adapter deliberately takes its low 16 bits, wrapping every 655.36 seconds. It does not claim to expose the full five-byte MOS clock or all of BASIC TIME. Keep the normal MOS interrupt/timer service running. The Electron’s physical real-time interrupt source is 50 Hz; centisecond units do not imply a separate 100 Hz interrupt or one-centisecond response granularity.

Poll before one whole clock cycle passes. 65530 → 5 is eleven ticks; missing an entire extra cycle would give the same samples. A clock reset is not a rollover. Reinitialise your context after another owner changes the clock. This adapter never resets it.

INKEY$(0) reads without waiting for Return. It consumes characters delivered by firmware, including typeahead and repeat. A held key is not promised to count once. Compare the sampled press-edge loop when the physical key state is the rule you need. Putting blocking INPUT into this caller would also stop deadline service. A clock does not make BASIC run two tasks at once.

The caller polls, reads one character, then handles expiry. Expiry wins if both arrive in the same iteration, discarding that character, including T or Q. Otherwise Space advances once and restarts an already active deadline. An untimed action stays untimed. An overdue deadline produces one turn; it does not replay every turn that might have happened during a delay.

Make it your own

The same helper with half the duration

customise.bas
10 MODE 6:HIMEM=&3000
20 N%=0:S%=1:EX%=0:DU%=200:LC%=0:MB%=&3000
30 DC%=TIME AND &FFFF:GOSUB 3000
40 ? (MB%+16)=165:? (MB%+17)=90
50 GOSUB 8000
100 GOSUB 5000
110 K$=INKEY$(0)
130 IF DX% THEN EX%=EX%+1:IF EX%>255 THEN EX%=0
140 IF DX% THEN 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 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
8000 PRINT TAB(0,0);:PRINT "SPACE:TURN T:TIMER Q:QUIT"
8010 PRINT "TURN";N%;" EX";EX%;" TIMER";DA%;"   "
8020 RETURN

This changes only DU%=400 to DU%=200: roughly four seconds becomes two. The units matter as much as the number. Maths for Games explores the same habits: choose a representation, give quantities units and check where arithmetic wraps. Subtraction here is elapsed time, not frame pacing.

Common mistake: resetting the baseline every time

Deliberately broken adapter below. It stores “now” as the previous sample just before asking how much time elapsed. Every subtraction becomes zero.

Broken: throwing away elapsed time before polling

reset-baseline.bas
5000 DC%=TIME AND &FFFF:DP%=DC%:GOSUB 3200:RETURN
Faulty native Electron BASIC caller still showing TURN 1, zero expiries and timing enabled after more than a full duration.
The clock advanced, but the faulty adapter kept replacing the baseline. Remaining time stayed at the full duration and the automatic turn never arrived.

Restore clock.bas to remove the assignment to DP%. A separate fresh repaired run advances normally. Keep this replacement out of working builds: its line number intentionally replaces the correct adapter.

What does it cost?

The routine performs bounded work per poll; it does not loop once per elapsed tick. Interpretation, GOSUB lookup and your other work still determine how often you can service it. A native clock accumulates time while BASIC draws or calculates, but expiry is noticed only when you poll again. Restarting at observation time also introduces drift.

The native interpreter stored 218 bytes of tokenised helper lines, including line headers. The complete diagnostic caller occupies 1096 tokenised program bytes. Its observed dynamic variable heap occupies 99 bytes after the checked interactions; this is a snapshot, not a peak-memory promise. BBC BASIC’s resident single-letter integer table is outside that dynamic heap. The optional observer reserves another 20 bytes. In the recorded disabled-idle snapshot, successive service samples were 11 centiseconds apart with the observer present; that includes interpreted caller/diagnostic work and is not the cost of the helper alone.

The optional 20-byte observer adds six scratch/monitor globals and several byte writes every iteration. Remove GOSUB 9000, its guards, MB%/LC% initialisation and monitor.bas when you do not need it. It is deliberately readable, rather than a fast game implementation. The caller redraws only on actions, toggles and expiry. No whole-game frame budget follows from these measurements.

Optional monitor used by the complete caller

This is diagnostic feedback, separate from the deadline helper. Include it for the complete caller above, or remove the observer calls and reservations as described before omitting it.

Optional diagnostic snapshot

monitor.bas
9000 ?(MB%+19)=0
9010 ?(MB%)=N%:?(MB%+1)=EX%:?(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 ?(MB%+18)=LC%:?(MB%+19)=239:RETURN
9100 H%=INT(V%/65536):V%=V%-H%*65536
9110 M%=INT(V%/256):V%=V%-M%*256
9120 ?(MB%+O%)=V%:?(MB%+O%+1)=M%:?(MB%+O%+2)=H%:RETURN

Longer clocks, several independent deadlines, preserved pause time, carrying lateness into the next turn, queued commands or fixed-step catch-up are useful extensions when a game needs them. Each changes either the policy or the resource cost. See choosing progression and clocks for those distinctions.

What was checked

Code status: emulator-executed BASIC. Four cold stock-firmware runs cover the public caller, half-duration customisation, baseline-reset fault and repaired adapter. The verifier types the complete program, observes guarded native state, and checks untimed idle/action, no-input expiry, restart and disable. Native captures above show the real text display.

A second BASIC caller executes twelve synthetic-clock boundary cases through the same interpreter, with a faulty exact-zero comparison required to fail the same independent oracle: no elapsed time, wrap, exact/overshot expiry and zero/maximum duration. An independent arithmetic oracle checks the returned bytes. These establish arithmetic behaviour; they are not a wall-clock run across an entire rollover. Retained-record checks reject missing variants, changed source and falsified native observations. The working idle observation advanced 202 centiseconds in 101 native fields, including mailbox settling. This is a serviced-clock observation, not a response-latency promise. No original-hardware, other-ROM, minimum-RAM or other-region claim is made.

The maintained modules and native evidence include loading order and repeatable commands. The shared implementation supplies every maintained listing above. The second caller shows the same helper used without the demo’s input policy.

Primary references

Acorn Electron User Guide, chapter 29, OSWORD A=1 and OSBYTE 129; The Advanced User Guide for the Acorn Electron, printed p.88, OSWORD 1. Captures are Code198x’s own program output in Emu198x 0.44.1, with native capture geometry preserved.