A coin pickup, a door opening and an enemy disappearing can all happen in one update. A bounded queue lets one part of the game record requests and another consume them in order. A full queue reports failure instead of quietly losing an older event.

Include and initialise
Reserve DIM Q(8), choose integral capacity C from 1 to 8, combine the numbered helper and your caller, then call 3000 once at a new-session boundary. It clears all eight payload cells, head H, tail T, count N, value V and results O/E. It retains C. Reserve lines 3000–3410, with END/STOP or a jump before the helpers. The supplied build.py combines the numbered files and rejects duplicate line numbers.
Queue positions are zero-based, but payload Q uses indices 1–8: slot H lives in Q(H+1). Your game should normally use the public calls rather than touch these indices. Q is a numeric array, not packed byte storage.
| Call | Supply | Result |
|---|---|---|
| 3100 — enqueue | V, an integral event value 0–255 | O=1 if stored; O=0 if full |
| 3200 — dequeue | retained queue | O=1 and V=oldest event; O=0 if empty |
A failed full insertion preserves all events. An empty dequeue leaves V unchanged; test O before dispatching V, or you may process the previous event twice. Popped cells retain their old payload until reused. N determines which cells are live. The helper never interprets event values: zero can be a real event.
The reusable routine
Reusable numbered routines
3000 LET H=0: LET T=0: LET N=0: LET V=0: LET O=0: LET E=0
3010 FOR I=1 TO 8: LET Q(I)=0: NEXT I: RETURN
3100 LET O=0: GOSUB 3300
3110 IF E<>0 THEN RETURN
3120 IF V<0 OR V>255 OR V<>INT (V) THEN LET E=2: RETURN
3130 IF N=C THEN RETURN
3140 LET Q(T+1)=V: LET T=T+1
3150 IF T=C THEN LET T=0
3160 LET N=N+1: LET O=1: RETURN
3200 LET O=0: GOSUB 3300
3210 IF E<>0 THEN RETURN
3220 IF N=0 THEN RETURN
3230 LET V=Q(H+1): LET H=H+1
3240 IF H=C THEN LET H=0
3250 LET N=N-1: LET O=1: RETURN
3300 LET E=1
3310 IF C<1 OR C>8 OR C<>INT (C) THEN RETURN
3320 IF H<0 OR H>=C OR H<>INT (H) THEN RETURN
3330 IF T<0 OR T>=C OR T<>INT (T) THEN RETURN
3340 IF N<0 OR N>C OR N<>INT (N) THEN RETURN
3350 LET U=H+N-C*INT ((H+N)/C)
3360 IF T<>U THEN RETURN
3365 IF N=0 THEN LET E=0: RETURN
3370 FOR I=0 TO N-1
3380 LET U=H+I-C*INT ((H+I)/C)
3390 IF Q(U+1)<0 OR Q(U+1)>255 OR Q(U+1)<>INT (Q(U+1)) THEN RETURN
3400 NEXT I
3410 LET E=0: RETURNOwnership and damaged state
The helper owns C/H/T/N/V/O/E, array Q and private scratch I/U. Do not wrap calls in a caller’s FOR I loop. M belongs to the maintained diagnostic dispatcher: 1 enqueue, 2 dequeue. A game can call the public line numbers directly. B/Z/G, A/D and the verification mailbox are diagnostic storage; J/K remain caller loop variables.
Both services validate capacity, head, tail and count before mutation. They require T = (H+N) modulo C, including when empty or full. Every live payload is checked before either operation changes the queue. E=1 means damaged queue state; E=2 means an invalid value supplied to enqueue. Ordinary full/empty outcomes leave E=0 and O=0. Neither the pending events nor indices change on rejection. Initialisation assumes your DIM already exists and C will be valid before the next service.
There is one owner and no interrupt synchronisation. A second queue needs distinct globals, array, line numbers and references. A queue is not automatically safe to share between an interrupt and main code. If an event needs several fields, store an object handle or extend each queued record deliberately; one byte-valued event alone cannot carry an arbitrary payload.
A complete caller
The caller first exercises twenty varied, boundary and invalid states. It then fills the queue, rejects an extra event, consumes two, inserts three more to wrap the tail, and drains through empty. Every state includes all eight payload cells, so stale storage and live contents cannot be confused. This is a diagnostic client you can replace with actual game requests.
Complete caller with retained state observations
10 PRINT CHR$(147)
20 DIM Q(8)
30 LET B=49152: LET Z=0: LET G=165: POKE B+1000,0: POKE B+1001,90
40 LET C=8: LET M=0: GOSUB 3000: GOSUB 2000
50 FOR J=1 TO 20
60 READ C,H,T,N,V,M,O,E
70 FOR K=1 TO 8: READ Q(K): NEXT K
80 GOSUB 1500: GOSUB 2000: NEXT J
90 LET C=8: GOSUB 3000
100 FOR J=0 TO C: LET V=10+J: LET M=1: GOSUB 1500: GOSUB 2000: NEXT J
110 LET M=2: GOSUB 1500: LET A=O: LET D=0
120 IF V=10 THEN LET D=1
130 GOSUB 2000: GOSUB 1500: GOSUB 2000
140 FOR J=0 TO 2: LET V=100+J: LET M=1: GOSUB 1500: GOSUB 2000: NEXT J
150 FOR J=1 TO C+2: LET M=2: GOSUB 1500: GOSUB 2000: NEXT J
160 PRINT "FIFO RESULTS";A;D
170 POKE B+995,A: POKE B+996,D: POKE B+997,G: POKE B+998,Z-256*INT (Z/256): POKE B+999,INT (Z/256): POKE B+1000,239: STOP
1500 IF M=1 THEN GOSUB 3100: RETURN
1510 GOSUB 3200: RETURN
2000 POKE B+Z,C: LET Z=Z+1
2010 POKE B+Z,H: LET Z=Z+1
2020 POKE B+Z,T: LET Z=Z+1
2030 POKE B+Z,N: LET Z=Z+1
2040 POKE B+Z,V: LET Z=Z+1
2050 POKE B+Z,M: LET Z=Z+1
2060 POKE B+Z,O: LET Z=Z+1
2070 POKE B+Z,E: LET Z=Z+1
2080 POKE B+Z,Q(1): LET Z=Z+1
2090 POKE B+Z,Q(2): LET Z=Z+1
2100 POKE B+Z,Q(3): LET Z=Z+1
2110 POKE B+Z,Q(4): LET Z=Z+1
2120 POKE B+Z,Q(5): LET Z=Z+1
2130 POKE B+Z,Q(6): LET Z=Z+1
2140 POKE B+Z,Q(7): LET Z=Z+1
2150 POKE B+Z,Q(8): LET Z=Z+1
2160 RETURN
5000 DATA 8,0,0,0,0,1,9,9,0,17,34,51,68,85,102,119
5010 DATA 8,1,2,1,1,2,9,9,1,18,35,52,69,86,103,120
5020 DATA 8,2,4,2,255,1,9,9,2,19,36,53,70,87,104,121
5030 DATA 8,3,6,3,128,2,9,9,3,20,37,54,71,88,105,122
5040 DATA 8,4,0,4,0,1,9,9,4,21,38,55,72,89,106,123
5050 DATA 8,5,2,5,1,2,9,9,5,22,39,56,73,90,107,124
5060 DATA 8,6,4,6,255,1,9,9,6,23,40,57,74,91,108,125
5070 DATA 8,7,6,7,128,2,9,9,7,24,41,58,75,92,109,126
5080 DATA 8,0,0,8,0,1,9,9,8,25,42,59,76,93,110,127
5090 DATA 8,1,1,0,1,2,9,9,9,26,43,60,77,94,111,128
5100 DATA 8,2,3,1,255,1,9,9,10,27,44,61,78,95,112,129
5110 DATA 8,3,5,2,128,2,9,9,11,28,45,62,79,96,113,130
5120 DATA 0,4,7,3,0,1,9,9,12,29,46,63,80,97,114,131
5130 DATA 8,8,1,4,1,2,9,9,13,30,47,64,81,98,115,132
5140 DATA 8,6,4,5,255,1,9,9,14,31,48,65,82,99,116,133
5150 DATA 8,7,5,9,128,2,9,9,15,32,49,66,83,100,117,134
5160 DATA 9,0,7,7,0,1,9,9,16,33,50,67,84,101,118,135
5170 DATA 8,1,1,8,255,2,9,9,17,255,51,68,85,102,119,136
5180 DATA 8,2,2,0,255,1,9,9,18,35,52,69,86,103,120,137
5190 DATA 8,3,4,1,128,2,9,9,19,36,53,70,87,104,121,138Customise it
The custom caller lowers logical capacity to four. It keeps DIM Q(8), so this reduces the event budget and validation work without claiming smaller array allocation. Raising the bound requires coordinated changes to DIM, initialisation, validators, model and tests. Use a different policy if dropping the newest request is unacceptable: reserve a slot for critical events, coalesce repeated movement requests, or return the failure to the producer for a deliberate retry. Do not reset the queue to make room.
A per-frame queue can be drained after collision detection, avoiding object removal while traversing the objects. A turn-based game may drain it after each command. Neither case requires a timer inside the queue. How those routines fit into a whole game belongs in the curriculum; this component owns only storage and FIFO ordering.
A mistake you can recognise
The wrong caller initialises the queue again at the consumer boundary. All recorded events disappear. The first dequeue fails and the expected oldest value is absent, giving two saved result digits 0, 0. The normal caller prints 1, 1. Later new events still work, which is why a single successful enqueue would not expose the lost earlier work.
The repair moves initialisation back to the start of the session. Its source and actual loaded tokenised program match the working caller exactly. The capture shows the executed mistake.

The wrap-around arithmetic
H points at the next event to read; T points at the next free cell to write. Incrementing an index past C-1 wraps it to zero. Count N distinguishes empty from full when H and T are equal. Keeping all three costs one extra scalar but makes those two situations unambiguous.
Validation computes remainders with division and INT. The update itself uses an increment and comparison. A power-of-two assembly queue may use a bit mask, but Spectrum BASIC’s logical AND is not that operation. This version accepts every capacity from one to eight.
The Craft connects delayed events to update ordering. Maths for Games supplies the modular arithmetic behind the circular storage. An immediate function call is cheaper when nothing needs deferring; a queue is useful when order and deferred ownership matter.
Machine and cost
C64 BASIC V2 uses only the first two significant characters of a numeric variable name. The single-letter names are deliberate: check your project for collisions before pasting them. The diagnostic mailbox at $C000 sits outside the normal BASIC heap.
The helper occupies 653 tokenised program bytes on this interpreter, including numbered-line headers. This excludes the caller, tables, numeric variables/arrays, interpreter stacks and ROM. It is not a machine-code size or cycle count. Each service checks up to N live payloads before changing one cell. This safety scan costs O(N); the circular insertion/removal after validation is constant work. DIM Q(8) allocates eight numeric cells on Spectrum and nine on C64/BBC, including their unused zero cell. Numeric cells and descriptors cost more than byte storage. No measured hardware-cycle or frame-budget claim is made.
Verified examples
All four working/custom/wrong/repaired callers ran through C64’s stock BASIC interpreter under Emu198x 0.44.2. Build198x 0.2.9 tokenises C64 PRG and Spectrum TAP files; C64 enters stock RUN, Spectrum uses ROM tape loading, and BBC enters the complete numbered listing through stock MOS/BASIC. Actual loaded programs, complete state traces, positive completion, guards and visible result pixels are checked. Firmware is not distributed.
The independent deque model checks twenty varied/boundary/error rows and a retained fill, reject, wrap and drain session. Custom callers change logical capacity. These are configuration-specific observations, not a claim about every physical machine or possible game.
Public CI checks retained records and rejects altered source, configuration, loaded program, state, completion and image evidence; it does not execute restricted firmware. Refresh with python3 scripts/verify-basic-components.py event-queue commodore-64 --tools /path/to/extracted/releases; inspect with python3 scripts/check-basic-components.py event-queue.
Explore further
The BASIC object pool offers another caller-owned component. Compare the assembly version for its representation and cost choices. Exact source and native observations.