A game can keep a small stock of enemies, projectiles or effects without allocating a new block of memory every time. A pool gives each live object a stable slot. Releasing one slot makes room for another object without shuffling every payload.

Include and initialise
Define POOL_CAPACITY as an integer from one through 255 and reserve that many private flag bytes. Zero means free and one occupied. Include pool.inc once, then call pool_init at an intended reset boundary. Initialisation clears flags only: payload arrays belong to the caller. Call it once before use, rather than resetting ownership during ordinary spawning.
The Amstrad CPC example uses pool_flags for the flag base. A game chooses RAM it owns and allocates its payload arrays separately. A slot index can address the same position in each parallel array: position, direction, animation, kind or other chosen state.
Reusable slot ownership routines
; SPDX-License-Identifier: MIT
; pool_flags: POOL_CAPACITY private bytes, 1..255; 0 free, 1 occupied.
; init clears flags only. acquire: NC/A=index; full C/A=capacity.
; release A=index: NC on occupied valid slot; C on invalid/already free.
; A/flags, BC, HL change; DE, IX, IY survive. No I/O, IRQ or hidden state.
; Single owner; initialise payload after acquisition, before any reader.
pool_init:
ld hl,pool_flags
ld b,POOL_CAPACITY
xor a
pool_init_loop:
ld (hl),a
inc hl
djnz pool_init_loop
ret
pool_acquire:
ld hl,pool_flags
ld b,POOL_CAPACITY
ld c,0
pool_acquire_loop:
ld a,(hl)
or a
jr z,pool_claim
inc hl
inc c
djnz pool_acquire_loop
ld a,POOL_CAPACITY
scf
ret
pool_claim:
ld (hl),1
ld a,c
or a
ret
pool_release:
cp POOL_CAPACITY
jr nc,pool_reject
ld l,a
ld h,0
ld bc,pool_flags
add hl,bc
ld a,(hl)
or a
jr z,pool_reject
ld (hl),0
or a
ret
pool_reject:
scf
ret
component_end:Acquire, check, then write
Call pool_acquire. Check carry before using the returned index. Carry clear means the lowest free slot has been claimed. Initialise its payload before making it visible to another part of the game. Carry set means full; the returned index equals capacity and lies outside every valid slot.
AF/BC/HL change. DE/IX/IY survive. Acquisition returns carry clear with A=slot, or carry set with A=capacity. Release takes A=slot and reports success through carry.
Call pool_release with a valid occupied index to clear its flag. An invalid index or an already-free slot returns carry set and changes no flag. Release leaves payload intact. Those old bytes are not a live object: the ownership flag decides whether the game may use them.
Fill, release and reuse
The maintained caller claims slots zero, one and two, then fills the remaining slots. It observes a full-pool result, releases slot one and claims it again. A rejected release of index 255 leaves the pool unchanged in these eight- and four-slot examples.
Retained fill, release, reuse and expiry client
preview_pool:
call pool_init
call pool_acquire
ld (preview),a
call pool_acquire
ld (preview+1),a
call pool_acquire
ld (preview+2),a
preview_fill:
call pool_acquire
jr nc,preview_fill
ld a,255
ld (preview+3),a
ld a,1
if BROKEN=0
call pool_release
endif
call pool_acquire
jr nc,preview_reused
ld a,255
preview_reused:
ld (preview+4),a
ld a,0
adc a,0
ld (preview+8),a
ld a,255
call pool_release
ld a,0
adc a,0
ld (preview+5),a
ld hl,pool_life
ld b,POOL_CAPACITY
ld c,0
preview_life:
ld a,c
and 1
inc a
ld (hl),a
inc hl
inc c
djnz preview_life
call pool_age
call occupancy
ld (preview+6),a
call pool_age
call occupancy
ld (preview+7),a
retThe default pool has eight slots; custom.asm uses four. Both reuse slot one after its release. The supplied occupancy display is an eight-bit diagnostic, so that client needs capacity three through eight. The build rejects an unsuitable capacity for that display instead of silently truncating it. Separate native boundary callers exercise capacity one and 255, with complete flag arrays. The helper’s wider contract does not make the small display wider.
Expire objects without moving them
Optionally include age.inc and reserve a separate lifetime byte for each slot. Before the first age call, every occupied slot must have a lifetime from one through 255. One pool_age call is one chosen simulation step. It decrements occupied lifetimes and releases those reaching zero, keeping every other object at the same index.
Optional fixed-slot lifetime policy
; Optional lifetime policy. Live life must be 1..255. Spawn after the pass.
; AF/BC/HL change; DE/IX/IY survive. Fixed indices, no compaction.
; Saved loop counter plus nested release call use stack space on expiry.
pool_age:
ld b,POOL_CAPACITY
ld c,0
pool_age_loop:
ld l,c
ld h,0
push bc
ld bc,pool_flags
add hl,bc
ld a,(hl)
pop bc
or a
jr z,pool_age_next
ld l,c
ld h,0
push bc
ld bc,pool_life
add hl,bc
dec (hl)
pop bc
jr nz,pool_age_next
ld a,c
push bc
call pool_release
pop bc
pool_age_next:
inc c
djnz pool_age_loop
ret
age_end:The example assigns alternating lifetimes one and two. After one step the odd slots remain; after the next, none remain. The maximum-capacity native caller also checks all 255 lifetimes changing from 255 to 254, then from one to zero with every slot released.
The caller owns the clock. An untimed game can omit this policy, and a timed-turn game can invoke it at its chosen boundary. Queue new spawns until after the pass: otherwise whether a new object ages immediately would depend on which slot it received. Passing zero as a live lifetime violates this policy’s contract and can wrap the decrement.
Keep one owner
The routines use ordinary call/return stack space and no device I/O. They are not re-entrant. Do not let an interrupt or another owner mutate the same flags during a traversal. Optional expiry adds a nested release call, and the Z80 policy saves a loop counter on the stack.
A released index can later name a different object. Long-lived references need a generation or another identity policy; a bare index does not solve that problem. Multiple pools need separate storage and renamed labels, or a deliberately designed context-pointer interface. Compacting objects is another approach with different reference and movement costs.
Mistake and repair
The wrong caller omits the release before trying to reuse slot one. The pool remains full. It returns carry set, and the caller records 255 as its diagnostic failure marker instead of a usable slot. Both saved reuse-index and success checks fail. Check the status and release ownership at the right boundary; do not treat stale payload bytes as spare capacity. Repaired source and emitted machine code match normal exactly.

The other native callers check full pools, invalid and repeated releases, unchanged guards, preserved context and all 256 ownership masks for the eight- and four-slot configurations. Capacity-255 tests include the high-bit indices and the full result 255; that result must never be used as a table index.
Machine layout and measured cost
The CPC464 firmware loader defines two diagnostic characters before lowering BASIC memory to &7FFF. The firmware rejects the reverse order with Improper argument. Code begins at $8000, flags at $9000, lifetimes at $9200 and the observation buffer at $9800. The entry wrapper saves AF/BC/DE/HL/IX/IY and returns through firmware CALL; the helper takes over no display or interrupt service.
These are explicit layouts for the named stock-firmware verification, not a general-purpose memory allocator. The complete diagnostic buffer reserves 4 KiB; it is not required pool state. A game needs capacity flag bytes and, if using expiry, capacity lifetime bytes, plus its own payload. The helper emits 56 bytes, with 39 additional bytes for expiry, on this CPU/tool configuration. Those sizes exclude the loader, exhaustive caller, observation buffers, BASIC runtime and payloads.
Initialisation, acquisition and a full expiry pass visit at most capacity slots. Release checks and changes at most one flag. A bitset can reduce flag storage at the cost of mask work. A free-list can avoid a scan but needs links and an explicit free-order policy. No measured cycle budget, original-hardware result or whole-game speed claim follows from these bounded examples.
Native evidence and Maths for Games
All six programs load through the stock BASIC interpreter’s generated READ/POKE source, with every actual machine-code byte compared before entry. Normal, custom, wrong, exact repair and the two capacity-boundary programs are independent runs. Full ownership/lifetime packets, actual emitted extent, completion, guards, register/context checks, BASIC arithmetic after return and independent screenshot indicators are required. Captures follow two complete frames after completion.
The set model chooses the lowest member of the free set and compares the complete ownership result. Public CI rechecks retained records and rejects altered source, producer, program, state, completion and coupled wrong captures; it does not execute restricted firmware. The retained cases name Emu198x 0.44.2 and Asm198x 0.0.72. Publishing separately checks the existing assembler pin. Host checks complement the native configurations; neither is claimed as every possible capacity, firmware or timing condition.
Run make with ASM198X pointing to the assembler. Each build produces the flat image and its generated stock loader under build/. Follow that loader’s reservation and wait for the editor before its CALL, EXEC or USR entry. The site includes maintained source and pins the complete repository; these payloads use the existing source-delivery path. A game normally includes the helper in its own assembly project and supplies its own layout.
Maths for Games connects finite sets, lowest-free selection, bounds and stable indices. The Craft connects object lifetime, spawn limits and feedback when no slot is available.