Skip to content

Detect a Whole Button Chord Once in Spectrum BASIC

Require a set of held buttons, exclude forbidden buttons and report a rising edge for the complete chord.

Press two required buttons one after another and the chord becomes active when the second arrives. Keep holding them and it should not trigger again on every update. Compare the chord’s whole condition with its previous condition, rather than demanding that every button have a new edge at once.

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

Include and initialise

Reserve public W/H/R/F/A/E/O, private B/C/D/I/U/V/X/T and lines 3000–3240. There are no arrays. Private I must not be the caller’s outer FOR variable. Combine routines.bas and a caller using build.py and prevent fall-through with END, STOP or a jump. C64/BBC use GOSUB; Spectrum uses GO SUB.

Call 3000 at the intended reset boundary. It clears all public fields. Set nonempty required mask R and forbidden mask F afterwards. These masks must not overlap. H is the supplied held-button snapshot; bit meanings come from the caller’s logical mapping.

Query a complete held snapshot

Call 3100. A becomes one when every required bit is held and every forbidden bit is clear. E becomes one only when A rises from the previous whole-chord state W. The helper then retains W=A. O=1 reports valid state/configuration, and H/R/F survive.

Reusable numbered routines

routines.bas
3000 LET W=0: LET H=0: LET R=0: LET F=0: LET A=0: LET E=0: LET O=0: RETURN
3100 LET A=0: LET E=0: LET O=0
3110 IF W<>0 AND W<>1 THEN LET W=0: RETURN
3120 IF H<0 OR H>255 OR H<>INT (H) THEN LET W=0: RETURN
3130 IF R<1 OR R>255 OR R<>INT (R) THEN LET W=0: RETURN
3140 IF F<0 OR F>255 OR F<>INT (F) THEN LET W=0: RETURN
3150 LET B=H: LET C=R: LET D=F: LET A=1: LET T=0
3160 FOR I=1 TO 8
3170 LET U=B-2*INT (B/2): LET V=C-2*INT (C/2): LET X=D-2*INT (D/2)
3180 IF V=1 AND X=1 THEN LET T=1
3190 IF V=1 AND U=0 THEN LET A=0
3200 IF X=1 AND U=1 THEN LET A=0
3210 LET B=INT (B/2): LET C=INT (C/2): LET D=INT (D/2): NEXT I
3220 IF T=1 THEN LET A=0: LET W=0: RETURN
3230 IF A=1 AND W=0 THEN LET E=1
3240 LET W=A: LET O=1: RETURN
Fields Use
W Previous whole-chord state, zero or one
H / R / F Held / required / forbidden masks
A / E / O Active level / new chord edge / valid flag

H/R/F are integral bytes, R is nonzero and W is exactly zero or one. Invalid state or overlapping masks clear W/A/E/O. This deliberately removes a stale active latch: a later valid active chord can produce a new edge. Handle O before acting on the outputs.

Press, hold, interrupt and press again

The normal caller requires mask three and forbids bit two. It feeds no buttons, one required button, both, both again, both plus the forbidden button, both again, then release. Exactly two rising edges occur; the repeated held snapshot has E=0. The custom caller moves the required combination to bits seven and zero and forbids bit one.

Example calls from the maintained caller

demo.bas, excerpt
  70 GO SUB 3000: LET AA=0: LET BB=0
  80 LET R=3: LET F=4: LET NN=0
  90 LET H=0: GO SUB 3100: GO SUB 2000: LET NN=NN+E
 100 LET H=1: GO SUB 3100: GO SUB 2000: LET NN=NN+E
 110 LET H=3: GO SUB 3100: GO SUB 2000: LET NN=NN+E
 120 LET H=3: GO SUB 3100: GO SUB 2000: LET NN=NN+E
 130 IF E=0 THEN LET BB=1
 140 LET H=7: GO SUB 3100: GO SUB 2000: LET NN=NN+E
 150 LET H=3: GO SUB 3100: GO SUB 2000: LET NN=NN+E
 160 LET H=0: GO SUB 3100: GO SUB 2000: LET NN=NN+E
 170 IF NN=2 THEN LET AA=1
 180 PRINT "CHRD RESULTS";AA;BB
 190 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/NN/JJ (Z on Spectrum) and the mailbox are diagnostics. A game consumes E for a one-time action or A for continuous chord behaviour. The eight logical bits do not promise that any particular physical keyboard or controller can report eight simultaneous buttons. Device scanning, matrix limitations and logical mapping belong to its input reader.

Customise mapping and ownership

F=0 permits extra held buttons; forbidding every bit outside R instead asks for exactly that chord. Choose the intended behaviour rather than treating these as equivalent. Separate chords need separate history and renamed fields/line references.

Changing masks while retaining W compares a new condition with history from the old one. Reset deliberately on remapping or focus change when that is the intended edge policy. Ordered combinations need a sequence recogniser; a chord query has no memory of which required button arrived first and reads no timer.

Mistake and repair

The wrong caller clears W before each query. Every held active snapshot appears to be a new chord press. The saved total-edge and repeated-snapshot checks both fail. Retain the whole-chord condition between services. The repaired source and loaded program 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

Each valid query walks eight bits arithmetically to test required, forbidden and held membership. Native bitwise operators or assembly can make that cheaper after checking width and signedness. Boolean button arrays are another representation when compact masks are less useful. There is no device read, allocation or wait here.

Maths for Games connects subsets, disjoint sets and rising edges. The Craft connects intentional multi-button actions to clear, reliable controls.

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 705 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/NN, 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-input-models.py input-chord sinclair-zx-spectrum --tools /path/to/extracted/releases and inspect with python3 scripts/check-basic-input-models.py input-chord.

Explore further

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