Skip to content

Land While Crossing a One-way Platform in Spectrum BASIC

Compare old and proposed feet against a platform top, using widened horizontal spans and a deliberate drop-through flag.

A fast fall can carry a character from above a thin platform to below it in one update. The feet never equal the top at either endpoint. Compare the path they crossed, and a landing can still be found without a per-pixel fall loop.

Spectrum BASIC shows both retained behaviour checks passing.
The working caller completes with both saved checks equal to one.

Include and initialise

Reserve public L/S/X/W/P/N/A/B/Y/D/O and private E/F. Reserve lines 3000–3240, combine routines.bas and a caller with build.py and keep END, STOP or a jump before the helper. C64/BBC use GOSUB; Spectrum uses GO SUB. Private E/F hold widened horizontal endpoints.

Call 3000 at a reset boundary. It clears all public fields. Supply actor horizontal X/W, old bottom P, proposed bottom N, platform X/width A/B, top Y and byte-valued drop flag D afterwards. Vertical coordinates increase downwards and do not wrap.

Query one candidate platform

Call 3100. O=1 reports byte/integer inputs. L=1 means landing/contact and S is the snapped bottom Y. Otherwise L=0 and S is the proposed bottom N. All inputs survive. Invalid numeric input returns O=0,L=0,S=N; handle O before applying the result.

Field Use
X / W / P / N Actor horizontal start/width and old/proposed feet
A / B / Y / D Platform start/width/top and drop-through flag
L / S / O Contact, proposed/snapped bottom and valid flag

Horizontal spans are half-open: [X,X+W) and [A,A+B). Edge-only touching does not overlap. Width zero is a valid empty span with no contact. Endpoint sums may reach 510 and must stay wide. Landing requires horizontal overlap and P <= Y <= N, with D=0. Stationary feet exactly on top count as contact; upward motion passes through.

Any nonzero byte D bypasses contact. Use 0/1 flags and normalise a dialect’s−1 true value if needed. How long drop-through stays active belongs to the caller.

The reusable routine

Reusable numbered routines

routines.bas
3000 LET L=0: LET S=0: LET X=0: LET W=0: LET P=0: LET N=0: LET A=0: LET B=0: LET Y=0: LET D=0: LET O=0: RETURN
3100 LET L=0: LET S=N: LET O=0
3110 IF X<0 OR X>255 OR X<>INT (X) THEN RETURN
3120 IF W<0 OR W>255 OR W<>INT (W) THEN RETURN
3130 IF P<0 OR P>255 OR P<>INT (P) THEN RETURN
3140 IF N<0 OR N>255 OR N<>INT (N) THEN RETURN
3150 IF A<0 OR A>255 OR A<>INT (A) THEN RETURN
3160 IF B<0 OR B>255 OR B<>INT (B) THEN RETURN
3170 IF Y<0 OR Y>255 OR Y<>INT (Y) THEN RETURN
3180 IF D<0 OR D>255 OR D<>INT (D) THEN RETURN
3190 LET O=1
3200 IF W=0 OR B=0 OR D<>0 THEN RETURN
3210 LET E=X+W: LET F=A+B
3220 IF X>=F OR A>=E THEN RETURN
3230 IF P>Y OR N<Y THEN RETURN
3240 LET L=1: LET S=Y: RETURN

The helper checks the crossed vertical interval rather than requiring N to equal Y. Horizontal endpoints are calculated before comparing spans, without narrowing back to a byte. It returns a proposed bottom, not an automatic whole-object position or velocity update.

Keep old and proposed feet

The normal caller falls from 90 to 110 across top 100 and returns contact with S=100. The custom caller sets D=1, so the same path passes through and keeps S=110. Later calls check rising feet, stationary contact and horizontal edge-only touching.

Example calls from the maintained caller

demo.bas, excerpt
  70 GO SUB 3000: LET AA=0: LET BB=0
  80 LET X=10: LET W=10: LET P=90: LET N=110: LET A=15: LET B=10: LET Y=100: LET D=0: GO SUB 3100
  90 GO SUB 2000
 100 IF L=1 THEN LET AA=1
 110 IF S=100 THEN LET BB=1
 120 LET X=10: LET W=10: LET P=110: LET N=90: LET A=15: LET B=10: LET Y=100: LET D=0: GO SUB 3100: GO SUB 2000
 130 LET X=10: LET W=10: LET P=100: LET N=100: LET A=15: LET B=10: LET Y=100: LET D=0: GO SUB 3100: GO SUB 2000
 140 LET X=10: LET W=10: LET P=90: LET N=110: LET A=20: LET B=10: LET Y=100: LET D=0: GO SUB 3100: GO SUB 2000
 150 PRINT "LAND RESULTS";AA;BB
 160 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: STOP

Line 2000, AA/BB/JJ (Z on Spectrum) and mailbox writes are observations. A game supplies the previous and proposed geometry before committing its fall, calls 3100 and uses L/S/O to choose its movement and grounded result. The helper reads no controls or clock.

Customise platform policy

Query each plausible candidate and choose the nearest valid top along the fall. Accepting an arbitrary last result can put the actor through an earlier surface. Moving platforms, slopes and several collision axes require additional policy; this small query does not select a whole scene’s landing for you.

A fall that crosses the byte-coordinate seam is outside this non-wrapping vertical contract. Wider worlds need wider input/result bounds and verification. Horizontal start/width encoding can represent an endpoint beyond 255 internally; that is different from a byte-valued exclusive-edge API. Check contracts before connecting routines.

Mistake and repair

The wrong caller keeps contact only if N equals Y. It discards the correctly found 90→110 crossing of top 100 and restores the unsnapped bottom. Both saved checks fail. Compare the interval crossed, not just endpoint equality. Repaired source and loaded program match normal.

Spectrum BASIC shows two failed checks from the deliberate caller mistake.
The wrong caller runs to completion, but both saved behaviour checks are zero.

Cost and Maths for Games

There is fixed validation, two widened sums and interval comparisons, independent of fall distance. No pixel visits, allocation or rendering occur. BASIC numeric storage and tokenised size are separate from a packed byte representation. No cycle or frame-time guarantee is made.

Maths for Games connects half-open spans, inclusive crossing and widened arithmetic. The Craft connects intentional pass-through, forgiving landing and which movement state the game should commit.

Global names and line numbers

BASIC numeric names, arrays and numbered lines are global. To combine helpers, rename public and private fields and renumber the helper together with every GO SUB/GO TO reference. Keep each state block under one owner. The maintained build.py rejects duplicate line numbers; it does not automatically namespace or renumber routines.

Machine and measured size

Spectrum FOR variables must be single letters: this diagnostic uses Z instead of the other callers’ JJ. The helper uses scalar numeric fields and allocates no arrays. 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 568 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 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-platforms.py one-way-platform sinclair-zx-spectrum --tools /path/to/extracted/releases and inspect with python3 scripts/check-basic-platforms.py one-way-platform.

Explore further

Compare the assembly technique. The BASIC game-flow routine makes session boundaries explicit. Complete source and native evidence.