A row of three identical tiles can be stored as a count and a tile value. Stretching it back out is straightforward until the data ends halfway through a pair, or a run is larger than the space you reserved. This decoder stops safely and tells you what it produced.

Include and initialise
Reserve I(13) for the encoded source, B(8) for decoded output, public W/E/S/T/L and private J/K/V. Reserve lines 3000–3240, combine the helper and caller with build.py and keep END, STOP or a jump before the helper. C64/BBC use GOSUB and Spectrum GO SUB. Do not borrow private scratch for an enclosing loop.
Call 3000 at a deliberate reset boundary. It clears all source/output array cells and all public scalars. Supply source bytes afterwards. The maintained 13-byte source includes both a valid asset and deliberate malformed examples; these bytes are caller data, not a required immutable format table.
Decode a selected span
S is an inclusive zero-based source byte offset, T the exclusive end, with 0 <= S <= T <= 13. L is the available output capacity from zero to eight. Set them and call 3100. Pairs are positive count followed by byte value. S=0 refers to I(1), and decoded bytes begin at B(1).
| Field | Use |
|---|---|
| I(1)..I(13), S/T | Caller-owned encoded bytes and selected span; preserved |
| B(1)..B(8), L | Owned destination and supplied capacity |
| W / E | Actual bytes produced / fresh completion or error code |
E=0 means the selected span completed, E=1 invalid bounds or non-byte source data, E=2 a truncated pair, E=3 a zero count, E=4 insufficient output space. Input bounds and all 13 source values are validated before writing. W resets to zero each call. An empty span succeeds without output, even at zero capacity.
On a stream error, B contains a valid decoded prefix of W bytes. Bytes after that prefix are preserved. The routine can stop inside a run when capacity is exhausted. It does not roll back that prefix and returns no continuation cursor. Re-decoding with more capacity starts at S again. If you need an all-or-nothing asset, use a staging buffer and publish it only when E=0.
The reusable routine
Reusable numbered routines
3000 LET W=0: LET E=0: LET S=0: LET T=0: LET L=0
3010 FOR J=1 TO 13: LET I(J)=0: NEXT J
3020 FOR J=1 TO 8: LET B(J)=0: NEXT J: RETURN
3100 LET W=0: LET E=1
3110 IF S<0 OR S>13 OR S<>INT (S) THEN RETURN
3120 IF T<S OR T>13 OR T<>INT (T) THEN RETURN
3130 IF L<0 OR L>8 OR L<>INT (L) THEN RETURN
3140 FOR J=1 TO 13
3150 IF I(J)<0 OR I(J)>255 OR I(J)<>INT (I(J)) THEN RETURN
3160 NEXT J: LET E=0: LET J=S+1
3170 IF J>T THEN RETURN
3180 IF J+1>T THEN LET E=2: RETURN
3190 LET K=I(J): LET V=I(J+1)
3200 IF K=0 THEN LET E=3: RETURN
3210 IF W=L THEN LET E=4: RETURN
3220 LET W=W+1: LET B(W)=V: LET K=K-1
3230 IF K>0 THEN GO TO 3210
3240 LET J=J+2: GO TO 3170The loop checks for a complete pair and rejects zero counts before emitting its run. Every store checks remaining capacity. Filling the destination exactly can succeed if that also finishes the selected stream; a full buffer alone is not proof of success.
Decode and use the returned length
The normal source begins 3,65,2,66,1,67. It expands to AAABBC: six values, with capacity eight. The custom caller offers five cells, producing AAABB and E=4. Both are expected, useful outcomes with different completeness.
Example calls from the maintained caller
100 GO SUB 3000: LET AA=0: LET BB=0
110 LET I(1)=3: LET I(2)=65: LET I(3)=2: LET I(4)=66
120 LET I(5)=1: LET I(6)=67: LET I(7)=0: LET I(8)=90
130 LET I(9)=2: LET I(10)=68: LET I(11)=5: LET I(12)=69
140 LET I(13)=1
150 FOR Z=1 TO 8: LET B(Z)=99: NEXT Z
160 LET S=0: LET T=6: LET L=8: GO SUB 3100
170 GO SUB 2000: LET SS=0: FOR Z=1 TO W: LET SS=SS+B(Z): NEXT Z
180 IF W=6 THEN LET AA=1
190 IF SS=394 THEN LET BB=1
200 LET S=12: LET T=13: LET L=8: GO SUB 3100: GO SUB 2000
210 LET S=6: LET T=8: LET L=8: GO SUB 3100: GO SUB 2000
220 LET S=0: LET T=0: LET L=0: GO SUB 3100: GO SUB 2000
230 PRINT "RLE RESULTS";AA;BB
240 POKE MB+995,AA: POKE MB+996,BB: POKE MB+997,GD: POKE MB+998,WC-256*INT (WC/256): POKE MB+999,INT (WC/256): POKE MB+1000,239: STOPThe full diagnostic also exercises truncated and zero runs, empty spans, invalid bounds and output exhaustion. Line 2000, AA/BB/SS/JJ/KK (Z/Q on Spectrum) and the mailbox are observers. Your game calls 3100 and accepts only the first W cells, applying its own policy to E.
Customise capacity and data
Counts describe repeats, not byte offsets. Values can be tiles, pixels or any byte-valued token. Runs of one work but cost two encoded bytes, so alternating input may grow. There is no zero terminator or literal-packet escape in this small local codec.
L never exceeds the allocated eight cells. A larger decoder needs corresponding arrays, limits and tests, and perhaps wider counters. A streaming decoder must retain pair/run progress explicitly. If assets never change, build-time expansion may remove runtime decode work entirely.
Mistake and repair
The wrong caller replaces W with L after decoding. It advertises eight output cells although only six were produced; two old values become part of the alleged asset. The length and consumed-data checks both fail. The repair preserves the decoder’s returned W and consumes exactly that prefix. The retained trace checks E as well as the output. Repaired source and loaded program match the normal caller.

Cost and Maths for Games
Validation scans 13 source values. Decoding visits the selected pairs and performs at most eight output stores. A count of 255 still cannot overrun the reserved destination. This bounded interpreter routine is not a measured frame-time guarantee.
Maths for Games connects run lengths, sums and bounds. Compare encoded size 2*number_of_runs with the original length before choosing compression. The Craft helps decide whether partial output is useful or whether loading should commit only a complete asset.
Machine and measured size
Spectrum FOR variables must be single letters: this diagnostic uses Z/Q instead of the other callers’ JJ/KK. 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 582 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 AA/BB, Z/Q 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. After the positive completion marker, the observer advances two complete display frames before capturing, so a partially scanned final character cannot masquerade as a finished picture. 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 supplied diagnostic callers deliberately end with STOP; the interpreter report is expected. 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-packing.py bounded-rle sinclair-zx-spectrum --tools /path/to/extracted/releases and inspect with python3 scripts/check-basic-packing.py bounded-rle.
Explore further
Compare the assembly technique. The BASIC game-flow routine makes session boundaries explicit. Complete source and native evidence.