Eight objects fill the pool. A ninth request reports full. Release slot 0 and the next request reuses it, while the object’s stored payload remains 60. This gives a game a fixed number of reusable places for bullets, enemies or effects.

Put it in your project
The source folder contains the modules and Makefile. Run make, then Merge ASCII from build/working.asc into a blank AMOS Professional 2.00 project. F2 checks syntax; after it finishes, F1 runs. Q leaves the result screen. Import each complete variant into a fresh blank project so labels do not collide. These are ASCII import files, not tokenised AMOS saves.
Hide : Palette $000,$FFF : Paper 0 : Pen 1 : Cls 0
Dim PL_OP_LIVE(8),PL_PAYLOAD(8),PL_LIFE(8)
For PL_I=0 To 8 : PL_OP_LIVE(PL_I)=99 : PL_PAYLOAD(PL_I)=60 : PL_LIFE(PL_I)=0 : Next PL_I
Gosub PL_OP_INIT
Print "Bounded slots: ";PL_OP_SIZE
For PL_CALL=0 To PL_OP_SIZE
Gosub PL_OP_ACQUIRE
Print "Slot: ";PL_OP_SLOT;" acquired: ";PL_OP_OK
Next PL_CALL
PL_OP_SLOT=0
Gosub PL_OP_RELEASE
Gosub PL_OP_ACQUIRE
Print "Reuse: ";PL_OP_SLOT;" acquired: ";PL_OP_OK
Print "Payload: ";PL_PAYLOAD(0);" guard: ";PL_OP_LIVE(8)
Repeat : Wait Vbl : Until Inkey$="q"
Show
EndThe builder puts settings and the caller before routines.amos. Keep the routines after End: falling into a Return without a matching Gosub is an error. Reserve the PL_OP_ names for one owner and one pool. This mutable context is not reentrant.
Dim PL_OP_LIVE(8) allocates nine elements, because AMOS includes the upper bound. Slots 0..7 support our maximum capacity of eight; element 8 is a diagnostic guard. Allocate the array before calling the helper and keep it alive. The helper cannot discover missing or undersized caller storage. Payload and lifetime arrays are separate caller-owned data.
Initialise, acquire and release
Rem Caller allocates PL_OP_LIVE(8); helper uses slots 0 to size-1 only.
Rem Private size is set by init; payload and interrupts belong to caller.
PL_OP_INIT:
PL_OP_OK=0 : PL_OP_ERROR=1 : PL_OP_SIZE=0 : PL_OP_SLOT=0
If PL_OP_CAP<1 or PL_OP_CAP>8 : Return : End If
PL_OP_SIZE=PL_OP_CAP
For PL_OP_I=0 To PL_OP_SIZE-1
PL_OP_LIVE(PL_OP_I)=0
Next PL_OP_I
PL_OP_SLOT=PL_OP_SIZE : PL_OP_ERROR=0 : PL_OP_OK=1
Return
PL_OP_CHECK:
PL_OP_OK=0 : PL_OP_ERROR=1
If PL_OP_SIZE<1 or PL_OP_SIZE>8 : Return : End If
PL_OP_ERROR=0
Return
PL_OP_ACQUIRE:
Gosub PL_OP_CHECK
PL_OP_SLOT=PL_OP_SIZE
If PL_OP_ERROR<>0 : Return : End If
For PL_OP_I=0 To PL_OP_SIZE-1
If PL_OP_OK=0
If PL_OP_LIVE(PL_OP_I)=0
PL_OP_LIVE(PL_OP_I)=1 : PL_OP_SLOT=PL_OP_I : PL_OP_OK=1
End If
End If
Next PL_OP_I
Return
PL_OP_RELEASE:
Gosub PL_OP_CHECK
If PL_OP_ERROR<>0 : Return : End If
PL_OP_ERROR=2
If PL_OP_SLOT<0 or PL_OP_SLOT>=PL_OP_SIZE : Return : End If
If PL_OP_LIVE(PL_OP_SLOT)=0 : Return : End If
PL_OP_LIVE(PL_OP_SLOT)=0 : PL_OP_OK=1 : PL_OP_ERROR=0
Return| Field | Contract |
|---|---|
PL_OP_CAP |
Init input: integer capacity 1..8 |
PL_OP_SIZE |
Private configured extent; zero disables the context |
PL_OP_SLOT |
Acquisition result or release input |
PL_OP_OK |
One on success, zero otherwise |
PL_OP_ERROR |
Zero for valid operation/full, one for invalid context/init, two for invalid or already-free release |
PL_OP_I |
Private scan index |
PL_OP_LIVE() |
Occupancy only: zero free, nonzero occupied |
Set capacity and call Gosub PL_OP_INIT. Valid init clears only the configured occupancy slots. It preserves payload and lifetime data. Invalid init disables the context and returns error one without writing occupancy, even after a previous valid configuration. Changing capacity takes effect only through another init; keep private size and scan state private.
Gosub PL_OP_ACQUIRE selects the lowest free index and marks it occupied. Check PL_OP_OK before indexing an array. Full returns slot equal to size, success zero and error zero. The bounded scan completes its For loop before returning, leaving no abandoned loop frame.
To release, supply the slot and call Gosub PL_OP_RELEASE. Context and both index bounds are checked before subscripting. An already-free slot is rejected. Release does not erase payload. Active objects keep their indices until released; once reused, an old saved index may refer to another object. This helper has no generation counters or stale-handle detection.
Let the game own lifetime
Rem The owner owns lifetimes; release during traversal, spawn only afterwards.
Hide : Palette $000,$FFF : Paper 0 : Pen 1 : Cls 0
Dim PL_OP_LIVE(8),PL_LIFE(8),PL_PAYLOAD(8)
PL_OP_CAP=4
For PL_I=0 To 8
PL_OP_LIVE(PL_I)=77 : PL_LIFE(PL_I)=77 : PL_PAYLOAD(PL_I)=65537
Next PL_I
PL_OP_LIVE(8)=99 : PL_LIFE(8)=99
Gosub PL_OP_INIT
For PL_I=0 To 3
Gosub PL_OP_ACQUIRE : Read PL_LIFE(PL_OP_SLOT)
Next PL_I
Reserve As Work 1,388
PL_MAILBOX=Start(1)
Locate 0,3 : Print "Bank: ";PL_MAILBOX
Locate 0,4 : Print "Size: ";Length(1)
For PL_FRAME=0 To 2
PL_EXPIRED=0
For PL_I=0 To PL_OP_SIZE-1
If PL_OP_LIVE(PL_I)<>0
Dec PL_LIFE(PL_I)
If PL_LIFE(PL_I)=0
PL_OP_SLOT=PL_I : Gosub PL_OP_RELEASE
PL_EXPIRED=PL_EXPIRED+1
End If
End If
Next PL_I
Rem The complete pass is over; an expired slot can now be reused.
PL_REUSED=PL_OP_SIZE
If PL_FRAME=0
Gosub PL_OP_ACQUIRE : PL_REUSED=PL_OP_SLOT
If PL_OP_OK<>0 : PL_LIFE(PL_OP_SLOT)=2 : End If
End If
PL_COUNT=0
For PL_I=0 To PL_OP_SIZE-1
If PL_OP_LIVE(PL_I)<>0 : Inc PL_COUNT : End If
Next PL_I
PL_AT=PL_MAILBOX+PL_FRAME*128
Loke PL_AT,PL_FRAME : Loke PL_AT+4,PL_COUNT
Loke PL_AT+8,PL_EXPIRED : Loke PL_AT+12,PL_REUSED
For PL_I=0 To 8
Loke PL_AT+16+PL_I*4,PL_OP_LIVE(PL_I)
Loke PL_AT+52+PL_I*4,PL_LIFE(PL_I)
Loke PL_AT+88+PL_I*4,PL_PAYLOAD(PL_I)
Next PL_I
Loke PL_AT+124,$1A2B3C4D
Next PL_FRAME
Loke PL_MAILBOX+384,$1A2B3C4D
Locate 0,0 : Print "Lifetime checks complete"
Locate 0,1 : Print "Count: ";3
Locate 0,2 : Print "Exit: ";0
Repeat : Wait Vbl : Until Inkey$="q"
Erase 1 : Show
End
Data 1,2,1,3The optional lifetime caller starts four objects with lifetimes 1, 2, 1 and 3. Each pass visits occupied slots, reduces lifetime and releases expired objects. It waits until the first complete traversal ends before spawning into slot 0 with lifetime two. Spawning during traversal would make update order decide whether the new object ages immediately.
Native observations give live counts 3, 2, 0 and expiry counts 2, 1, 2. One pass is your chosen game unit, not automatically one second. Input, clocks, drawing, spawning and payload policy stay outside the helper. This separation connects storage reuse to the game-loop choices; the owner decides when the world advances.
Change capacity without changing the helper
PL_OP_CAP=8PL_OP_CAP=4build/custom.asc uses capacity four; build/single.asc uses one. Both fill, report full and reuse the lowest released slot. Keep the allocation contract and success check when choosing a capacity. Enlarging the supported bound also requires revisiting storage and verification.
When a finished object still occupies its slot
The deliberate wrong owner replaces release followed by acquire with this fragment:
Rem Release omitted by wrong owner
Gosub PL_OP_ACQUIRE
Restore PL_OP_SLOT=0, Gosub PL_OP_RELEASE, then Gosub PL_OP_ACQUIRE. Import build/repaired.asc separately: reuse returns zero with success one. The working, wrong and repaired programs all run; a syntax error or missing completion would not demonstrate this ownership fault.
Costs and native checks
The six scalar helper integers hold 24 bytes of numeric payload. Nine occupancy elements add 36 bytes. The caller’s optional payload and lifetime arrays add another 36 bytes each. Names, array descriptors, labels, interpreter tables and return frames cost additional memory. These are not total heap, compiled-code, minimum-memory or peak-stack measurements. ASCII file lengths measure source text.
The small caller reserves no work bank. Separate diagnostics confirm native bank lengths of 190,468 bytes per mask run, 4,972 for boundaries, 388 for lifetime and 652 for signed-minimum cases. Runtime, screen and bank headers are additional. Completion does not establish frame time or throughput.
Original AMOS Professional 2.00, interpreted through Emu198x 0.44.1, ran all six small callers on a PAL A1200 with 2 MiB chip RAM and no fast RAM. Three 768-record mask runs cover every byte occupancy mask at capacities one, four and eight, observing init, acquisition, release, repeated release and invalid release. Separate runs cover 46 boundary records, three lifetime passes and six signed-minimum inputs. The guest constructs -2147483648 by decrementing -2147483647; full integer observations retain upper bits, unused elements, payload and guards.
The retained evidence binds source, imported programs, runtime, firmware and captures. Independent models check complete native banks and screen digits; corruption controls reject altered records and identities. CI rechecks retained proof without booting the licensed interpreter. This qualifies the named workload and configuration, not another compiler, original hardware, NTSC or smaller machines.
Both pictures export native captures at 768×576. Every omitted framebuffer column has an identical retained partner, checked across the full decoded image. Original captures and export identities remain in the evidence; no unique pixels are removed or invented.
For Maths for Games, occupancy is a finite set: acquire chooses its smallest missing member, and release removes a member. The practical design lesson is to keep that storage decision separate from the game’s lifetime and update order.