An enemy disappearing should make its slot available for the next enemy. A small pool keeps allocation predictable.

Use it in a project
Reserve arrays F(8) and L(8), then set C to the number of usable slots, from 1 to 8. Call line 3000 once at a new-session boundary. It clears all eight flags and lifetimes, plus M, S, R, O and E. Set payloads after initialisation. Line numbers 3000–3450 must be free in your project; move all references together if you renumber them.
| Call | Supply | Result |
|---|---|---|
GOSUB 3100 — acquire |
C; retained flags |
O=1 and R=0..C-1, or O=0, R=255 when full |
GOSUB 3200 — release |
S=0..C-1 |
O=1, R=S on a live release; otherwise O=0, R=255 |
GOSUB 3300 — age |
retained flags and live lifetimes | decrement each live lifetime; O counts expiries and R is the last expired slot, or 255 |
Spectrum spells the call GO SUB; C64 and BBC use GOSUB. A returned slot is zero-based, so its flag is F(R+1) and its lifetime is L(R+1). Check O before indexing R+1. A full pool returns 255; it is not an array index.
After acquiring a slot, initialise every payload field before another part of the game reads it. Set L(R+1) to 1–255 if you will call ageing. Position, velocity, sprite allocation and sound ownership remain your game’s responsibility. An acquired slot does not automatically become a fully constructed enemy.
Release clears its occupancy flag only. The old lifetime and your other payload remain; initialise them when reusing the slot. A slot number is not a durable object identity: an old reference can name a different object after reuse. Use generations or discard old references deliberately.
Reusable initialisation, allocation, release and ageing
3000 FOR I=1 TO 8
3010 LET F(I)=0: LET L(I)=0
3020 NEXT I
3030 LET M=0: LET S=0: LET R=0: LET O=0: LET E=0: RETURN
3100 LET R=255: LET O=0: GOSUB 3400
3110 IF E<>0 THEN RETURN
3120 FOR I=1 TO C
3130 IF F(I)=0 THEN LET F(I)=1: LET R=I-1: LET O=1: RETURN
3140 NEXT I
3150 RETURN
3200 LET R=255: LET O=0: GOSUB 3400
3210 IF E<>0 THEN RETURN
3220 IF S<0 OR S>=C OR S<>INT (S) THEN RETURN
3230 IF F(S+1)=0 THEN RETURN
3240 LET F(S+1)=0: LET R=S: LET O=1: RETURN
3300 LET R=255: LET O=0: GOSUB 3400
3310 IF E<>0 THEN RETURN
3320 FOR I=1 TO C
3330 IF F(I)=0 THEN GOTO 3360
3340 IF L(I)<1 OR L(I)>255 OR L(I)<>INT (L(I)) THEN LET E=1: RETURN
3360 NEXT I
3370 FOR I=1 TO C
3380 IF F(I)=0 THEN GOTO 3395
3385 LET L(I)=L(I)-1
3390 IF L(I)=0 THEN LET F(I)=0: LET R=I-1: LET O=O+1
3395 NEXT I
3398 RETURN
3400 LET E=1
3410 IF C<1 OR C>8 OR C<>INT (C) THEN RETURN
3420 FOR I=1 TO C
3430 IF F(I)<>0 AND F(I)<>1 THEN RETURN
3440 NEXT I
3450 LET E=0: RETURNInclude and call
Keep routines.bas with your caller and combine the numbered listings in numeric order before tokenising or typing them. The supplied build.py and Makefile do that. C64 BASIC V2 has no built-in MERGE command; concatenating the listings at authoring time avoids inventing one. Do not paste the helper between executing statements without a STOP, END or explicit jump around its numbered subroutines.
The maintained caller first runs boundary cases, then fills the pool, attempts one extra acquisition, releases slot one, reacquires it, and ages the live slots twice. M belongs to its diagnostic dispatcher: 1 acquire, 2 release, 3 age. Your game can call the public line numbers directly. The packet writer and mailbox are verification machinery, not required game state.
Complete maintained caller and state observations
10 MODE 7:HIMEM=&6000
20 DIM F(8): DIM L(8)
30 LET B=24576: LET Z=0: LET Q=165
40 ?(B+1000)=0: ?(B+1001)=90
50 LET C=8: GOSUB 3000: GOSUB 2000
60 RESTORE
70 FOR J=1 TO 16
80 READ C,M,S,R,O,E
90 FOR K=1 TO 8: READ F(K): NEXT K
100 FOR K=1 TO 8: READ L(K): NEXT K
110 GOSUB 1500: GOSUB 2000
120 NEXT J
130 LET C=8: GOSUB 3000
140 FOR J=1 TO C
150 LET M=1: GOSUB 1500
160 IF O=1 THEN LET L(R+1)=2
170 GOSUB 2000: NEXT J
180 LET M=1: GOSUB 1500: GOSUB 2000
190 LET M=2: LET S=1: GOSUB 1500: LET H=O: GOSUB 2000
200 LET M=1: GOSUB 1500: LET A=O
210 IF O=1 THEN LET L(R+1)=2
220 GOSUB 2000
230 LET M=3: GOSUB 1500: GOSUB 2000
240 GOSUB 1500: GOSUB 2000
250 PRINT "POOL RESULTS";H;A;S;R;O;E
260 ?(B+995)=H: ?(B+996)=A: ?(B+997)=Q: ?(B+998)=Z-256*INT (Z/256): ?(B+999)=INT (Z/256): ?(B+1000)=239: STOP
1500 IF M=1 THEN GOSUB 3100: RETURN
1510 IF M=2 THEN GOSUB 3200: RETURN
1520 GOSUB 3300: RETURN
2000 ?(B+Z)=C: LET Z=Z+1
2010 ?(B+Z)=M: LET Z=Z+1
2020 ?(B+Z)=S: LET Z=Z+1
2030 ?(B+Z)=R: LET Z=Z+1
2040 ?(B+Z)=O: LET Z=Z+1
2050 ?(B+Z)=E: LET Z=Z+1
2060 FOR K=1 TO 8: ?(B+Z)=F(K): LET Z=Z+1: NEXT K
2070 FOR K=1 TO 8: ?(B+Z)=L(K): LET Z=Z+1: NEXT K
2080 RETURN
5000 DATA 8,1,0,0,0,0,0,0,0,0,0,0,0,0,1,2,255,0,1,2,255,0
5010 DATA 8,2,7,0,0,0,1,0,0,0,0,0,0,0,2,255,0,1,2,255,0,1
5020 DATA 8,3,8,0,0,0,1,1,0,0,0,0,0,0,255,0,1,2,255,0,1,2
5030 DATA 8,1,255,0,0,0,1,1,1,1,0,0,0,0,0,1,2,255,0,1,2,255
5040 DATA 8,2,0,0,0,0,1,1,1,1,1,1,1,1,1,2,255,0,1,2,255,0
5050 DATA 8,3,7,0,0,0,0,0,0,0,0,0,0,0,2,255,0,1,2,255,0,1
5060 DATA 8,1,8,0,0,0,1,0,0,0,0,0,0,0,255,0,1,2,255,0,1,2
5070 DATA 8,2,255,0,0,0,1,1,0,0,0,0,0,0,0,1,2,255,0,1,2,255
5080 DATA 8,3,0,0,0,0,1,1,1,1,0,0,0,0,1,2,255,0,1,2,255,0
5090 DATA 8,1,7,0,0,0,1,1,1,1,1,1,1,1,2,255,0,1,2,255,0,1
5100 DATA 8,2,8,0,0,0,0,0,0,0,0,0,0,0,255,0,1,2,255,0,1,2
5110 DATA 8,3,255,0,0,0,1,0,0,0,0,0,0,0,0,1,2,255,0,1,2,255
5120 DATA 0,1,0,0,0,0,1,1,0,0,0,0,0,0,1,2,255,0,1,2,255,0
5130 DATA 8,2,7,0,0,0,2,1,1,1,0,0,0,0,2,255,0,1,2,255,0,1
5140 DATA 9,3,8,0,0,0,1,1,1,1,1,1,1,1,255,0,1,2,255,0,1,2
5150 DATA 8,1,255,0,0,0,0,0,0,0,0,0,0,0,0,1,2,255,0,1,2,255Ownership and invalid state
The helper owns numeric globals C, S, R, O, E, arrays F and L, and private loop scratch I; initialisation also clears diagnostic M. Do not use I as a caller’s surrounding FOR variable. Independent pools need separate variables, arrays, line numbers and references. There are no interrupts, hidden input reads, frame waits or drawing operations in the reusable routines.
Every service validates integral capacity 1–8 and active flags exactly zero or one. E=1 rejects damaged state before mutation. An ordinary full pool, invalid requested slot or double release returns E=0, O=0. Ageing additionally validates every occupied lifetime as an integer 1–255 before decrementing any of them. A zero live lifetime cannot silently wrap to a long-lived object.
BBC BASIC arrays include element zero, which this one-based interface leaves unused. The diagnostic reserves its mailbox above HIMEM=&6000; the reusable routines do not change HIMEM or the screen mode.
Customise it
The custom caller uses four live slots instead of eight. The routine and allocated arrays remain the same size; the smaller logical capacity reduces the scan and object budget, not the declared array storage. To change allocation size or raise the supported capacity, update the DIM statements, initialisation bound, validation and tests together. Merely changing C beyond eight is rejected.
Give different object classes different initial lifetimes, or use the flags without ageing when lifetime is irrelevant. Keep the lifetime array allocated for this maintained initializer. A flag-only variant could omit it, but would have a different contract.
Mistake and repair
The wrong caller loses the live slot reference and asks to release slot 255. Release correctly refuses it; the pool is still full, so the next acquisition also fails. The screen retains both results: 0, 0 instead of 1, 1. The two later ageing calls still run, so a final empty pool must not hide the earlier failed request.
The repair restores the intended slot-one release. repaired.bas is identical to demo.bas, and their actual loaded tokenised programs match. The Spectrum tape filenames differ; the loaded program is the relevant identity.

Cost and alternatives
The numbered helper occupies 676 tokenised program bytes on the qualified interpreter. This includes its line headers, not the caller, test data or interpreter ROM. Source-character count is a different quantity. Numeric arrays are interpreter-managed, so eight flags are not eight machine-code bytes. Spectrum allocates eight cells per array; C64 and BBC also allocate the unused zero cell. Scalar descriptors, array headers, interpreter stacks and the diagnostic storage add further cost.
Acquire scans at most C flags; release validates up to C flags and changes one; ageing validates the pool and lifetimes before its update pass. The extra safety pass costs runtime but prevents partial ageing when one lifetime is corrupt. No hardware-cycle or whole-game performance claim is made. A free list can reduce search, while a bitset saves flag storage at the cost of bit manipulation. The assembly pool is a useful comparison when interpreter overhead matters.
Single-owner updates keep this simple. Queue spawn/release requests until after traversal if immediate mutation would update a new object twice. Different genres can choose a different full-pool policy; this helper rejects rather than evicting an existing object.
Execution evidence
Working, four-slot custom, deliberately wrong and exact repaired callers execute on BBC Micro’s stock BASIC interpreter under Emu198x 0.44.2. C64 and Spectrum programs are tokenised by Build198x 0.2.9; C64 enters through the stock ROM and RUN, Spectrum through ROM tape loading. BBC source is entered through its stock interpreter, with every loaded line identity and the native tokenised program retained.
Each caller emits its complete state after sixteen varied/boundary/error cases and a retained-state client. An independent set/lifetime model checks the whole trace, positive completion, mailbox guards, actual loaded program and the visible release/reacquire digits. The independent host checks cover every occupancy mask at capacities four and eight; those are host checks, not a claim that all 256 masks ran natively. Native observations qualify these configurations and examples, not every input or physical machine timing. Firmware and snapshots are not distributed.
The public evidence gate checks retained observations and rejects altered source, configuration, loaded program, state, completion and picture claims. It does not run restricted firmware. Refresh with python3 scripts/verify-basic-pools.py bbc-micro --tools /path/to/extracted/releases; validate with python3 scripts/check-basic-pools.py and python3 -m unittest discover -s scripts -p test_basic_pools.py -v.
Where it fits
Compare the C64 assembly pool and generation handles when compact storage or durable references matter. Maths for Games develops bounds and invariants; The Craft places object ownership in a game update. Source and native evidence.