Skip to content

Keep Interaction Focus Stable in Spectrum BASIC

Choose an eligible nearby target while retaining the current one within an explicit distance margin.

Two nearby doors should not make the interaction prompt flicker whenever the player moves a fraction closer to one of them. Choose an eligible target, then give the current target a small retention margin. The player gets a stable prompt without making a distant object impossible to leave.

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

Include and initialise

Include routines.bas, reserving lines 3000–3330. Public fields are S/O/C/A/R/M/D/E/F/G; private scratch names are I/K/P/U/V/T/B. Call 3000 at a chosen reset boundary. It clears public fields. Put a STOP, END or jump before the helper; call 3100 to choose focus.

C is the current target index, zero through three, or 255 for none. A is a four-bit active mask. D/E/F/G are the four target distances, R the maximum allowed distance and M the retention margin. All these values must be integral bytes, and A cannot exceed 15. Supply every distance, even for inactive targets.

Reusable numbered routines

routines.bas
3000 LET S=0: LET O=0: LET C=0: LET A=0: LET R=0: LET M=0: LET D=0: LET E=0: LET F=0: LET G=0: RETURN
3100 LET S=255: LET O=0
3110 IF C<0 OR C>255 OR C<>INT (C) THEN RETURN
3120 IF C>3 AND C<>255 THEN RETURN
3130 IF A<0 OR A>15 OR A<>INT (A) THEN RETURN
3140 IF R<0 OR R>255 OR R<>INT (R) THEN RETURN
3150 IF M<0 OR M>255 OR M<>INT (M) THEN RETURN
3160 IF D<0 OR D>255 OR D<>INT (D) THEN RETURN
3170 IF E<0 OR E>255 OR E<>INT (E) THEN RETURN
3180 IF F<0 OR F>255 OR F<>INT (F) THEN RETURN
3190 IF G<0 OR G>255 OR G<>INT (G) THEN RETURN
3200 LET O=1: LET B=256: LET T=255: LET U=0: LET V=0: LET K=1
3210 FOR I=0 TO 3
3220 LET P=D
3230 IF I=1 THEN LET P=E
3240 IF I=2 THEN LET P=F
3250 IF I=3 THEN LET P=G
3260 IF INT (A/K)-2*INT (A/(2*K))=0 OR P>R THEN GO TO 3300
3270 IF I=C THEN LET U=1: LET V=P
3280 IF P<B THEN LET B=P: LET T=I
3300 LET K=K*2: NEXT I
3310 LET S=T
3320 IF U<>0 AND V<=B+M THEN LET S=C
3330 RETURN

S is the selected index; 255 means none. O=1 means a valid query, including an empty eligible set. Invalid input returns S=255/O=0 and preserves all inputs. The routine does not change C: after a valid query the caller decides when to commit C=S. That includes clearing C when S=255.

Prefer the nearest, retain the current

A target is eligible only when its active bit is set and its distance is at most R. Without eligible current focus, the nearest eligible target wins; equal distances favour the lower index. With eligible current focus, retain it while its distance is no greater than the best distance plus M. An inactive or out-of-range current target gets no grace from the margin.

The normal caller starts with target one at distance ten and target zero at nine, using margin two. Target one remains selected. At distance eight, target zero is still only two closer, so focus stays. At seven it is three closer, and focus switches to zero.

Example calls from the maintained caller

demo.bas, excerpt
  70 GO SUB 3000: LET AA=0: LET BB=0
  80 LET C=1: LET A=3: LET R=20: LET M=2: LET D=9: LET E=10: LET F=30: LET G=30
  90 GO SUB 3100: GO SUB 2000
 100 IF S=1 THEN LET AA=1
 110 IF C=1 THEN LET BB=1
 120 IF O=1 THEN LET C=S
 130 LET D=8
 140 GO SUB 3100: GO SUB 2000
 150 IF O=1 THEN LET C=S
 160 LET D=7
 170 GO SUB 3100: GO SUB 2000
 180 IF O=1 THEN LET C=S
 190 LET A=16
 200 GO SUB 3100: GO SUB 2000
 210 IF O=1 THEN LET C=S
 220 PRINT "FOCS RESULTS";AA;BB
 230 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

The custom caller starts with target three and four equal distances. Even with margin zero, an eligible current target wins a tie. Moving target zero nearer switches focus; removing every active target returns a valid none. A later invalid mask returns O=0. Saved checks verify the first selection and the unchanged input C; diagnostics observe state before the caller’s commit.

Choose what distance means

The helper compares numbers supplied by the game. They might be Manhattan distance, a screen-space score or another consistent metric. R and M must use the same units. If using squared distances to avoid a square root, a fixed score margin is not a fixed physical distance margin. Choose and explain that behaviour deliberately.

Use separate state for separate players. Target identities must remain stable between queries. This four-target interface is useful for a small interaction set; a larger game can gather a bounded shortlist from a spatial query and translate stable identities explicitly. Recycling shortlist indices without translating C would silently change which object owns focus.

Mistake and repair

The wrong caller sets C to 255 before every query. The helper keeps choosing the nearest or lowest tied index because it never receives the old focus. Both saved first-selection/current-input checks fail. Retain the committed current index between queries and replace it only through the intended game policy. Repaired source and loaded programs match normal exactly.

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

The helper validates eight inputs and examines exactly four candidates. It uses no arrays, display access or timer. The best distance starts at 256, outside the valid byte range. Best distance plus margin can reach 510 and deliberately stays wide in BASIC; narrowing that sum to a byte would change the rule near its limit.

Nearest-target selection without memory is cheaper in state and may be exactly right for a single shot. Retention adds deliberate hysteresis. A dwell timer or separate enter/leave radii offer different stability and response costs; they are alternatives, not automatic upgrades.

Maths for Games connects ordering, tie rules, distance measures and hysteresis. The Craft connects predictable prompts and the balance between stability and responsiveness.

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 792 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-combat-focus.py interaction-focus sinclair-zx-spectrum --tools /path/to/extracted/releases and inspect with python3 scripts/check-basic-combat-focus.py interaction-focus.

Explore further

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