Skip to content

Choose a Winning Request in Spectrum BASIC

Select the highest-priority request while keeping the first equal-priority winner and explicit batch ownership.

A pickup sound and a death sound may be requested in the same update. If only one can play, decide which request wins before touching the sound hardware. This small selector works just as well for choosing a message, animation request or another single shared resource.

Spectrum BASIC shows both retained behaviour checks passing.
The working caller completes with both saved checks equal to one.

Include and initialise

Combine the numbered routine with your caller, reserving lines 3000–3200. Call 3000 once before a batch of requests. It clears F/I/P/R/A/O/E. F says whether a winner exists; I holds its ID and P its priority. Callers may use IDs and priorities from 0 to 255. Zero is a valid ID and priority, so F must distinguish no request from request zero.

Spectrum uses GO SUB, while C64 and BBC use GOSUB. Keep END, STOP or an explicit jump before the helper. The supplied build.py combines numbered files and rejects duplicate lines; it does not depend on a BASIC MERGE command.

Submit and read

Put a request ID in R and its priority in A, then call 3100. A larger priority wins. O=1 means this request became the winner; O=0 means the previous winner remains. An equal priority keeps the first request. After all producers have submitted, check F and use I to dispatch the winner once. Do not dispatch every time O=1 or lower-priority work may already have escaped before a later request wins.

E=1 rejects damaged F/I/P state and E=2 rejects an invalid request. Numeric IDs and priorities must be integral 0–255; F must be exactly zero or one. Invalid calls clear O but preserve the winner. An empty selector can contain stale I/P, but F=0 means the next valid request wins regardless of those stale values.

The routine

Reusable numbered routines

routines.bas
3000 LET F=0: LET I=0: LET P=0: LET R=0: LET A=0: LET O=0: LET E=0: RETURN
3100 LET O=0: LET E=1
3110 IF F<>0 AND F<>1 THEN RETURN
3120 IF I<0 OR I>255 OR I<>INT (I) THEN RETURN
3130 IF P<0 OR P>255 OR P<>INT (P) THEN RETURN
3140 IF R<0 OR R>255 OR R<>INT (R) THEN LET E=2: RETURN
3150 IF A<0 OR A>255 OR A<>INT (A) THEN LET E=2: RETURN
3160 LET E=0
3170 IF F=0 THEN GO TO 3200
3180 IF A<=P THEN RETURN
3200 LET F=1: LET I=R: LET P=A: LET O=1: RETURN

A batch of competing requests

The retained client below submits five requests. ID20 has the highest priority, and an equal-priority ID40 arrives later. ID20 remains selected. The complete diagnostic also tests priority boundaries, equal values and a damaged present flag.

Calls to line 2000 in this excerpt write diagnostic observations; that writer and the mailbox setup are in the complete source. In your own game, omit those observation calls and the final mailbox write. Keep the public helper calls, and supply your game state as described above.

Maintained retained-state caller

demo.bas, excerpt
  70 GO SUB 3000: FOR J=1 TO 5
  80 READ R,A: GO SUB 3100: GO SUB 2000: NEXT J
  90 LET Y=0: LET D=0: IF I=20 THEN LET Y=1
 100 IF P=200 THEN LET D=1
 110 PRINT "PICK RESULTS";Y;D
 120 POKE W+995,Y: POKE W+996,D: POKE W+997,G: POKE W+998,Z-256*INT (Z/256): POKE W+999,INT (Z/256): POKE W+1000,239: STOP

Ownership and customisation

The helper owns F/I/P/R/A/O/E, with no private loop scratch. It does not own sound channels, sprites or messages. This selects one winner, not one winner per channel; use a separate state set for each independently arbitrated resource. Rename all references and line numbers for a second instance. The diagnostic’s W/Z/G and Y/D are not required by a game.

The custom caller changes the game-chosen priority values while keeping the same ordering and tie policy. Agree on priority bands across producers: for example, reserve a higher range for events that must interrupt ordinary feedback. The actual numbers have no built-in meaning. Reordering producers can change the winner of a tie, so make that ordering deliberate. To prefer the last equal request, change the comparison and add tests/captures that establish that different contract.

Mistake and repair

The wrong caller initialises before each request. Every request looks like the first, so the last low-priority request wins. Both saved checks become 0,0: the winner’s ID and priority are wrong. The selector itself is obeying the empty-state contract.

The repair initialises once at the batch boundary and retains state through all submissions. Its source and loaded tokenised program match the working caller.

Spectrum BASIC shows two failed checks from the deliberate caller mistake.
The wrong caller runs to completion, but both saved behaviour checks are zero.

Costs and alternatives

Each request takes constant scalar work and a comparison; a batch of N requests is O(N). It stores a winner instead of an entire queue. That loses information deliberately: if every request must eventually run, use an event queue or priority queue. A long-running sound also needs its own rule for whether a new batch may interrupt it; this selector has no playback timer. The Craft helps choose what deserves precedence; Maths for Games connects stable maximum selection to ordering and ties.

Machine and measured size

This helper uses scalar variables and needs no array allocation. AND/OR in these listings join comparisons; neither is being used as a bitwise operator. The diagnostic reserves its mailbox above CLEAR 59999.

The numbered helper occupies 393 tokenised program bytes on this interpreter, including line headers. This excludes the caller, test tables, numeric scalar/array storage, interpreter stacks and ROM. Tokenised size is not machine-code size or a hardware-cycle count. No real-time or whole-game performance budget is inferred from the diagnostic. The caller’s W/Z/G, Y/D and RAM mailbox are observation machinery, not required game state.

Execution evidence

Working, custom, deliberately wrong and exact repaired callers executed through Spectrum’s stock BASIC interpreter under Emu198x 0.44.2. Build198x 0.2.9 tokenises C64 PRG and Spectrum TAP programs. C64 enters stock RUN, Spectrum uses ROM tape loading, and BBC enters every numbered source line through MOS/BASIC. Actual loaded program bytes, complete state packets, positive completion/count/guards and visible result pixels are checked. Firmware is not distributed.

An independent mathematical/order model checks the trace and rejects altered source, configuration, loaded program, state, completion and image evidence. Host exhaustive checks complement the native boundary/sequential examples; those host ranges are not claimed as natively exhaustive. The wrong caller prints 0,0 and the normal/custom/repaired callers print 1,1.

Public CI checks retained evidence; it does not execute restricted firmware. Refresh with python3 scripts/verify-basic-rules.py priority-arbiter sinclair-zx-spectrum --tools /path/to/extracted/releases and inspect with python3 scripts/check-basic-rules.py priority-arbiter.

Explore further

Compare the assembly technique. The BASIC game-flow routine makes session boundaries explicit. Complete source and native evidence.