Eight objects fill the pool. A ninth request fails. Release slot 0 and the next request reuses it: SLOT 0 OK 1. A bullet, enemy or effect can use that index while the helper handles occupancy and the game decides when the object finishes.

Put it in your project
Run make in the source folder. Enter build/demo.fth one line at a time in the original Jupiter Ace FORTH ROM, on the unexpanded 3 KiB configuration. These are ASCII keyboard-entry sources, not a saved dictionary or tape image. Reset before loading another complete variant to reclaim dictionary space and avoid binding an old definition.
The generated file loads routines.fth, storage.fth, caller.fth and settings.fth, then runs demo.fth. Keep that order: FORTH resolves a referenced word when it compiles its caller. For a game, keep the first four files and replace the demonstration with your owner.
: PNEW SLOTS PC @ PINIT ;
: NEWOBJ PGET DUP IF OVER 2 * PAYLOAD + 60 SWAP ! THEN ;
: PSHOW CLS ." SLOT " PK @ U. ." OK " PF @ U. ;PNEW leaves an initialisation flag. Consume it and stop if it is zero. NEWOBJ leaves slot, then success flag. Consume the flag before using the slot. On success, the owner writes 60 to that slot’s payload cell. On failure, discard the sentinel without indexing payload. The earlier data-stack entries belong to the caller and are preserved.
Allocate and retain the storage
CREATE SLOTS
77 C, 77 C, 77 C, 77 C, 77 C, 77 C, 77 C, 77 C, 99 C,
CREATE PAYLOAD
60 , 60 , 60 , 60 , 60 , 60 , 60 , 60 , 65535 ,SLOTS contains eight occupancy bytes plus guard byte 99. PAYLOAD contains eight two-byte cells plus guard cell 65535. Neither guard is a ninth object. A zero occupancy byte means free; every nonzero value means occupied. Acquisition writes 1 and release writes 0. Payload belongs entirely to the owner.
Pass a live caller-owned dictionary buffer with at least the configured number of bytes. Do not FORGET it while selected. The helper checks that the base is at least 15441, below HERE, and has room for the selected extent before HERE, before indexed access. That range check cannot discover a CREATE object’s true boundary or distinguish data from compiled words. An arbitrary dictionary address or undersized allocation is not a supported buffer. ROM, screen and arbitrary external RAM buffers are outside this contract.
Initialise, acquire and release
DECIMAL
0 VARIABLE PB
0 VARIABLE PC
0 VARIABLE PS
0 VARIABLE PK
0 VARIABLE PF
0 VARIABLE PX
0 VARIABLE PI
: PCHK 0 PF ! 1 PX !
PS @ DUP 0= SWAP 9 U< 0= OR IF EXIT THEN
PB @ 15441 U< IF EXIT THEN
PB @ HERE U< 0= IF EXIT THEN
HERE PB @ - PS @ U< IF EXIT THEN 0 PX ! ;
: PINIT PC ! PB ! 0 PS ! 0 PK !
0 PF ! 1 PX !
PC @ DUP 0= SWAP 9 U< 0= OR IF 0 EXIT THEN
PC @ PS ! PCHK PX @ IF 0 PS ! 0 EXIT THEN
0 PI ! PS @ 0 DO 0 PB @ I + C! LOOP
PS @ PK ! 1 PF ! PF @ ;
: PGET PS @ PK ! PCHK
PX @ IF PK @ 0 EXIT THEN
0 PI ! BEGIN PI @ PS @ U< WHILE
PB @ PI @ + C@ 0= IF
1 PB @ PI @ + C! PI @ PK ! 1 PF ! PK @ 1 EXIT
THEN PI @ 1+ PI ! REPEAT PK @ 0 ;
: PREL PK ! PCHK PX @ IF 0 EXIT THEN 2 PX !
PK @ PS @ U< 0= IF 0 EXIT THEN
PB @ PK @ + C@ 0= IF 0 EXIT THEN
0 PB @ PK @ + C! 1 PF ! 0 PX ! 1 ;| Word | Stack effect |
|---|---|
PINIT |
( base capacity -- ok ) |
PGET |
( -- slot ok ) |
PREL |
( slot -- ok ) |
PINIT selects and clears occupancy for capacity 1..8. PGET occupies the lowest free slot, or returns the extent and zero. PREL releases an occupied slot and rejects a free or invalid slot.
Reserve the seven context cells and helper word names for one mutable pool. PB is the selected base; PC stores the requested capacity; PS is the private selected extent; PK is the last result or release index; PF is success; PX is error; and PI is private scan state. The helper is not reentrant. PCHK is internal validation, not an allocation operation.
Valid init clears only the configured occupancy bytes. It preserves payload and unused storage. Invalid init disables the pool with PS=0, returns zero and writes no occupancy bytes, even after a valid earlier selection. Changing PC later does not resize PS; deliberately call PNEW to initialise again. Reinitialisation invalidates all old object references.
Full is a valid outcome: PK=PS, PF=0, PX=0. Invalid context gives PX=1; the returned slot is still a sentinel and must not be used as a subscript. Release checks context and the unsigned index before access. An out-of-range or already-free release returns zero with PX=2. Successful release leaves payload unchanged.
An occupied object keeps its index until release. Afterwards, an old saved index may refer to a new occupant. There are no generation numbers or stale-handle checks. End the owner’s references when its object finishes. Keep drawing, collision, lifetime countdowns and spawning outside these occupancy words; the loop choices explain where those decisions fit.
The search uses bounded BEGIN/WHILE/REPEAT, so its early EXIT does not abandon a DO loop. The original manual’s LEAVE waits until the next LOOP; it is not a modern immediate break. The init clear has a positive bounded DO loop which completes before returning. The caller uses the ROM words 2 * for a two-byte cell offset; do not assume the exercise word 2* is already defined.
Use fewer slots
8 PC !4 PC !The custom build selects four slots; build/single.fth selects one. The same allocation supports either smaller extent. The owner still checks success before touching payload. Changing the supported maximum needs a fresh review of storage, arithmetic, dictionary room and native checks.
This complete small demonstration makes nine requests at every capacity, releases slot 0, then requests again:
PNEW U.
NEWOBJ DROP DROP
NEWOBJ DROP DROP
NEWOBJ DROP DROP
NEWOBJ DROP DROP
NEWOBJ DROP DROP
NEWOBJ DROP DROP
NEWOBJ DROP DROP
NEWOBJ DROP DROP
NEWOBJ DROP DROP
0 PREL DROP
NEWOBJ DROP DROP PSHOW
PAYLOAD @ U. SLOTS 8 + C@ U.
PAYLOAD 16 + @ U.When a finished object still occupies its slot
The wrong owner replaces 0 PREL DROP with this fragment:
0 DROP
Restore the release before the next acquisition. Enter build/repaired.fth in a fresh ROM session: slot 0 is reused with success one. It is byte-for-byte identical to the normal program. Working, wrong and repaired all enter and complete; a ROM error would not demonstrate this ownership fault.
Costs and native checks
The original ROM adds 656 dictionary bytes for the helper, 53 for storage and 110 for the caller: 819 dictionary bytes in total, including headers and compiled words. Numeric storage is 14 context bytes, nine occupancy/guard bytes and 18 payload/guard bytes: 41 bytes. Source-file length is a different measurement.
The external NUMBER observer costs another 19 dictionary bytes; with it, HERE ends at 16279 and RAM top remains 16384. The empty data stack starts at HERE+12. Interpreter input and CPU/data stacks share the remaining room. Observed stopped-at-prompt separations are not a peak-stack measurement or a budget for adding more words. This does not establish that this pool, the separate session sample and a game fit together in stock RAM.
Native checks cover all 274 occupancy masks across capacities 1, 4 and 8, using occupied bytes 1, 128 and 255. There are 1,843 complete operation observations, including 99 invalid-capacity, extent, pointer and index cases, and 300 single-slot release/reuse cycles. Distinct payload seeds, unused bytes, both guards, upper cell bits and the caller’s lower stack tag remain observable. Five further fresh boots execute every line of the complete generated callers.
The retained evidence binds source, typed recipes, original ROM and Emu198x 0.44.1. Independent models check every helper field, array element, result and compiled dictionary byte. Failure controls challenge coverage, code, storage, stack and pixels. CI checks retained proof without booting the ROM. These observations qualify this configuration and workload, not original hardware, another FORTH or arbitrary caller stack depths.
Both figures preserve the original 320×288 PNG bytes. Every decoded pixel remains unchanged and agrees with native screen and character RAM. Primary reference: Jupiter Ace User’s Manual (1982), control flow and chapter 24’s dictionary/data-stack layout.
For Maths for Games, occupancy is a finite set: acquire chooses its smallest missing index and release removes a member. The practical design lesson is to separate storage reuse from the object’s meaning and lifetime.