Picking up five arrows should not quietly credit one arrow unless the game has chosen a partial-pickup rule. These routines accept or reject the whole requested quantity. They are useful for ammunition, keys, ingredients or other fixed categories of counted objects.

Include and initialise
Reserve DIM Q(4), select a common integral per-category capacity C from 1 to 255, combine the numbered listings and call 3000. It clears Q(1)..Q(4), S/A/M/O/E, retaining C. Reserve lines 3000–3260 and private loop variable I. Do not wrap the call in a caller’s FOR I loop. END, STOP or a jump must prevent falling into the helpers.
The four slots are fixed categories, not four interchangeable item stacks. Your game defines what slot zero, one, two and three mean. A public slot S=0 maps to Q(1). C64/BBC arrays also allocate an unused element zero; Spectrum arrays start at one. Spectrum spells calls GO SUB; C64/BBC use GOSUB. The supplied build.py combines source lines and rejects duplicates.
Add or remove a quantity
Supply S=0..3, A=0..255 and M=0 to add or M=1 to remove, then call 3100. O=1 means the complete quantity was applied. O=0 with E=0 means insufficient stock or insufficient free capacity. No quantity changes on rejection. A zero request is a successful no-op.
E=1 rejects invalid capacity or any damaged retained count before touching a slot. E=2 rejects an invalid slot, amount or mode. Every count and input is integral, with each Q between zero and C. Validating all four categories costs time but prevents a request from mutating an already inconsistent inventory.
The routine
Reusable numbered routines
3000 FOR I=1 TO 4: LET Q(I)=0: NEXT I
3010 LET S=0: LET A=0: LET M=0: LET O=0: LET E=0: RETURN
3100 LET O=0: LET E=1
3110 IF C<1 OR C>255 OR C<>INT (C) THEN RETURN
3120 FOR I=1 TO 4
3130 IF Q(I)<0 OR Q(I)>C OR Q(I)<>INT (Q(I)) THEN RETURN
3140 NEXT I
3150 LET E=2
3160 IF S<0 OR S>3 OR S<>INT (S) THEN RETURN
3170 IF A<0 OR A>255 OR A<>INT (A) THEN RETURN
3180 IF M<>0 AND M<>1 THEN RETURN
3190 LET E=0
3200 IF M=1 THEN GO TO 3250
3210 IF A>C-Q(S+1) THEN RETURN
3220 LET Q(S+1)=Q(S+1)+A: LET O=1: RETURN
3250 IF A>Q(S+1) THEN RETURN
3260 LET Q(S+1)=Q(S+1)-A: LET O=1: RETURNA retained stock example
The caller starts one unit below capacity, tries to add two, and checks that the request is refused without changing stock. It then fills the final space, removes a full stack, tries an unavailable removal and exercises another category. The earlier table in the complete diagnostic covers varied counts, invalid slots, damaged state and invalid modes.
Calls to line 2000 in this excerpt write diagnostic observations; that writer and the mailbox setup are in the complete source. In your own game, omit those observation calls and the final mailbox write. Keep the public helper calls, and supply your game state as described above.
Maintained retained-state caller
80 LET C=99: GO SUB 3000: LET Q(1)=C-1: LET S=0: LET M=0: LET A=2
90 GO SUB 3100: GO SUB 2000: LET Y=0: LET D=0
100 IF O=0 THEN LET Y=1
110 IF Q(1)=C-1 THEN LET D=1
120 LET S=0: LET A=1: LET M=0: GO SUB 3100: GO SUB 2000
130 LET S=0: LET A=99: LET M=1: GO SUB 3100: GO SUB 2000
140 LET S=3: LET A=1: LET M=1: GO SUB 3100: GO SUB 2000
150 LET S=2: LET A=5: LET M=0: GO SUB 3100: GO SUB 2000
160 LET S=2: LET A=5: LET M=1: GO SUB 3100: GO SUB 2000
170 LET S=1: LET A=0: LET M=0: GO SUB 3100: GO SUB 2000
180 PRINT "PACK RESULTS";Y;D
190 POKE W+995,Y: POKE W+996,D: POKE W+997,G: POKE W+998,Z-256*INT (Z/256): POKE W+999,INT (Z/256): POKE W+1000,239: STOPOwnership and customisation
The helper owns S/A/M/C/O/E, Q and scratch I. It owns no item names, icons, prices, carry weight or object handles. To display “arrows”, the caller looks up its own name for that category. Separate inventories need separate arrays, globals and numbered references; sharing Q would share the contents.
The custom caller lowers C from 99 to 9. DIM Q(4) remains unchanged, so there is no claimed allocation saving. Different capacities per item need a capacity array and matching validation. More categories require changing DIM, the clear/validation loops, slot bounds and tests together. The current interface rejects slot 4 instead of guessing that a fifth category exists.
A partial pickup can be useful, but it needs an output reporting the accepted quantity and a caller that leaves the remainder in the world. Silently reducing A changes the meaning of success. Likewise, a weighted inventory or an equipment-slot system is a different contract, not just another title for these four counters.
Mistake and repair
The wrong caller clips the requested addition from two to one before calling. That smaller request succeeds and fills the stack. Its saved checks are 0,0: it was not rejected, and stock no longer equals its original value. The helper cannot know the original request was larger.
The repair passes the original amount and handles failure explicitly. Its source and loaded program match the working caller exactly.

Costs and alternatives
Every call validates four counts, then updates at most one. The fixed four-category scan is bounded; expanding the category count grows validation work. Numeric arrays are not packed bytes. A simple scalar is cheaper for a single ammunition count; typed stacks with item IDs are useful when categories are not fixed. A transfer between inventories must validate both sides before either changes. The Craft is the place to choose the player’s pickup/capacity rules; Maths for Games develops the bounds and conservation checks.
Machine and measured size
Spectrum numeric arrays start at one. AND/OR in these listings join comparisons; neither is being used as a bitwise operator. The diagnostic reserves its mailbox above CLEAR 59999.
The numbered helper occupies 525 tokenised program bytes on this interpreter, including line headers. This excludes the caller, test tables, numeric scalar/array storage, interpreter stacks and ROM. Tokenised size is not machine-code size or a hardware-cycle count. No real-time or whole-game performance budget is inferred from the diagnostic. The caller’s W/Z/G, Y/D and RAM mailbox are observation machinery, not required game state.
Execution evidence
Working, custom, deliberately wrong and exact repaired callers executed through Spectrum’s stock BASIC interpreter under Emu198x 0.44.2. Build198x 0.2.9 tokenises C64 PRG and Spectrum TAP programs. C64 enters stock RUN, Spectrum uses ROM tape loading, and BBC enters every numbered source line through MOS/BASIC. Actual loaded program bytes, complete state packets, positive completion/count/guards and visible result pixels are checked. Firmware is not distributed.
An independent mathematical/order model checks the trace and rejects altered source, configuration, loaded program, state, completion and image evidence. Host exhaustive checks complement the native boundary/sequential examples; those host ranges are not claimed as natively exhaustive. The wrong caller prints 0,0 and the normal/custom/repaired callers print 1,1.
Public CI checks retained evidence; it does not execute restricted firmware. Refresh with python3 scripts/verify-basic-rules.py inventory-stacks sinclair-zx-spectrum --tools /path/to/extracted/releases and inspect with python3 scripts/check-basic-rules.py inventory-stacks.
Explore further
Compare the assembly technique. The BASIC game-flow routine makes session boundaries explicit. Complete source and native evidence.