A guard remembers that you brought the key. A shopkeeper offers a different line after you have helped them. A few quest flags can give a conversation memory without baking every story branch into a nest of IF statements. This helper answers whether a choice is available and applies the chosen changes; it leaves words and presentation to your game.

Include and initialise
Combine routines.bas with your numbered caller using the supplied build.py. Reserve lines 3000–3330, numeric globals F/R/B/S/C/A/O/T/E and private scratch I/U/V/K/L/X. Call 3000 at a new-game boundary: it clears every public field, including the quest flags F. Load a saved quest state afterwards. Keep END, STOP or an explicit jump before the helper so execution cannot fall into it. C64/BBC use GOSUB; Spectrum uses GO SUB.
Ask, then accept
F is the retained quest mask. R contains required flags; B contains forbidden flags. Each mask is an integral number from 0 to 255: bits 1,2,4,8,16,32,64,128 are eight independent yes/no facts. Set S to flags to set and C to flags to clear after acceptance. Set A=0 to query or A=1 to accept, then call 3100.
O=1 means every required flag is present and no forbidden flag is present. T=1 means an available choice was actually accepted. A query can therefore return O=1,T=0 and leaves F unchanged. An accepted choice clears C then sets S; setting wins when those masks overlap. O describes availability before that update. If the choice becomes unavailable afterwards, query again to obtain its new availability.
E=1 rejects non-integral/out-of-range masks or an A other than zero/one. E=2 rejects a rule that requires and forbids the same flag. Both failures return O=T=0 and leave F unchanged. An ordinary unavailable choice has E=0. R=B=0 imposes no conditions.
The reusable routine
Reusable numbered routines
3000 LET F=0: LET R=0: LET B=0: LET S=0: LET C=0: LET A=0: LET O=0: LET T=0: LET E=0: RETURN
3100 LET O=0: LET T=0: LET E=1
3110 IF F<0 OR F>255 OR F<>INT (F) THEN RETURN
3120 IF R<0 OR R>255 OR R<>INT (R) THEN RETURN
3130 IF B<0 OR B>255 OR B<>INT (B) THEN RETURN
3140 IF S<0 OR S>255 OR S<>INT (S) THEN RETURN
3150 IF C<0 OR C>255 OR C<>INT (C) THEN RETURN
3160 IF A<>0 AND A<>1 THEN RETURN
3170 LET E=0: LET O=1: LET U=1
3180 FOR I=1 TO 8
3190 LET V=INT (F/U)-2*INT (F/(2*U))
3200 LET K=INT (R/U)-2*INT (R/(2*U))
3210 LET L=INT (B/U)-2*INT (B/(2*U))
3220 IF K=1 AND L=1 THEN LET O=0: LET E=2: RETURN
3230 IF K=1 AND V=0 THEN LET O=0
3240 IF L=1 AND V=1 THEN LET O=0
3250 LET U=U*2: NEXT I
3260 IF O=0 OR A=0 THEN RETURN
3270 LET T=1: LET X=0: LET U=1
3280 FOR I=1 TO 8
3290 IF INT (S/U)-2*INT (S/(2*U))=1 THEN LET X=X+U: GO TO 3320
3300 IF INT (C/U)-2*INT (C/(2*U))=1 THEN GO TO 3320
3310 IF INT (F/U)-2*INT (F/(2*U))=1 THEN LET X=X+U
3320 LET U=U*2: NEXT I
3330 LET F=X: RETURNThe bit tests use division and INT rather than assuming that every dialect’s AND is a bitwise operation. AND/OR in these listings join comparisons. The rule is the same across these three interpreters, while tokenised size and runtime cost differ.
Let a conversation remember
The maintained caller first asks about a choice, accepts it, then tries again after the acceptance has cleared its required flag. It later tests another choice against the new state. This makes a repeated selection meaningful: it cannot silently grant the same one-time reward forever.
Maintained retained-state caller
70 GO SUB 3000: LET F=1: LET R=1: LET B=2: LET S=2: LET C=1: LET A=0
80 GO SUB 3100: LET AA=O: GO SUB 2000: LET A=1
90 GO SUB 3100: LET BB=T: GO SUB 2000
100 GO SUB 3100: GO SUB 2000
110 LET R=2: LET B=1: LET S=16: LET C=0: GO SUB 3100: GO SUB 2000
120 LET A=0: GO SUB 3100: GO SUB 2000
130 PRINT "TALK RESULTS";AA;BB
140 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 complete caller also holds boundary/error tables. Its line 2000 writer, RAM mailbox, saved AA/BB digits and loop Z are observation machinery, not part of the reusable API. A game calls 3100 directly and uses O/T/E. Private I must not be a caller’s enclosing FOR variable.
Customise the conversation
The custom caller assigns quest facts to bits four/eight instead of one/two. Give each fact a meaning in your game and keep that assignment consistent when saving. An unconditional repeatable choice can leave S=C=0; a one-time choice can forbid the flag it sets. Do not clear F every time the dialogue opens. For more than eight facts, use several masks with a deliberate multi-mask query/commit policy; merely allowing F above 255 changes this contract.
The helper owns no strings, input, choice menu, rewards or inventory. Check T before granting a reward. If a reward can fail because an inventory is full, validate that condition before committing the quest change, or define an explicit transaction. Two separate calls do not automatically become atomic. Separate helpers need distinct globals, scratch and numbered lines.
Mistake and repair
The wrong caller clears its quest flags before asking about the choice. The required fact vanishes: the choice is unavailable and cannot be accepted. Both saved results are zero. The exact repair retains the quest state between questions; repaired source and loaded tokenised program match the working version.

Cost and further steps
A valid query checks eight bit positions; an accepted update scans eight more. That bounded arithmetic is more interpreter work than testing a single scalar flag. It saves a large collection of independent flag variables, but does not turn BASIC numbers into packed byte storage. A table-driven dialogue engine can use this query behind text nodes and choice records. The Craft connects remembered choices to player expectations; Maths for Games explains masks, powers of two and predicates.
Machine and measured size
Spectrum FOR variables must be single letters: this diagnostic uses Z instead of the other callers’ JJ. This helper uses scalar variables and needs no array allocation. 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 958 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 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-world.py dialogue-gates sinclair-zx-spectrum --tools /path/to/extracted/releases and inspect with python3 scripts/check-basic-world.py dialogue-gates.
Explore further
Compare the assembly technique. The BASIC game-flow routine makes session boundaries explicit. Complete source and native evidence.