A poison effect needs both a lifetime and a rule for when it hurts. Keeping those counters separate prevents a final partial interval from extending the effect forever. This helper returns pulses and remaining duration; your game supplies the consequence.

Include and initialise
Reserve R/H/D/P/A/F/O/E and lines 3000–3230. There are no arrays or private scratch globals. Combine numbered source with build.py and prevent fall-through using END, STOP or a jump. C64/BBC use GOSUB; Spectrum uses GO SUB.
Set D to an integral duration from zero to 255 and P to an integral period from one to 255. Call 3000 to clear remaining R, phase H, apply A, pulse F and O/E, retaining D/P. The effect starts inactive. Each independent effect needs separate state and numbered references, or a deliberate wrapper that loads/stores its context.
Apply and service
Set A=1 to apply or refresh, then call 3100. It reloads R from D and H=0. The same call consumes the first active tick. Set A=0 on ordinary later services. An active service decrements R, advances H and returns F=1 at a period boundary. Expiration clears H. A period-boundary pulse on the final active tick is valid, even though the returned R is zero.
For duration six and period two:
| Service | Remaining | Pulse |
|---|---|---|
| Apply | 5 | 0 |
| 2 | 4 | 1 |
| 3 | 3 | 0 |
| 4 | 2 | 1 |
| 5 | 1 | 0 |
| 6 | 0 | 1 |
Process the fresh F result rather than requiring R to remain positive after service; that would lose the final pulse. D=0 with A=1 cancels the effect without a pulse. Valid inactive calls clear H and return F=0. O=1,E=0 means the call is valid, not that the effect is still active.
R/D must be integral bytes, P positive, H in 0..P-1 and A exactly zero or one. E=1 rejects timer/configuration state; E=2 rejects an invalid apply flag. Invalid calls preserve R/H and clear F/O. Configuration is validated before refresh, so changing P below the old phase needs an explicit phase/reset policy rather than expecting apply to repair inconsistent state.
The reusable routine
Reusable numbered routines
3000 LET R=0: LET H=0: LET A=0: LET F=0: LET O=0: LET E=0: RETURN
3100 LET F=0: LET O=0: LET E=1
3110 IF R<0 OR R>255 OR R<>INT (R) THEN RETURN
3120 IF D<0 OR D>255 OR D<>INT (D) THEN RETURN
3130 IF P<1 OR P>255 OR P<>INT (P) THEN RETURN
3140 IF H<0 OR H>=P OR H<>INT (H) THEN RETURN
3150 LET E=2
3160 IF A<>0 AND A<>1 THEN RETURN
3170 LET O=1: LET E=0
3180 IF A=1 THEN LET R=D: LET H=0
3190 IF R=0 THEN LET H=0: RETURN
3200 LET R=R-1: LET H=H+1
3210 IF H=P THEN LET H=0: LET F=1
3220 IF R=0 THEN LET H=0
3230 RETURNDuration decides how many active ticks remain. Phase decides whether this tick emits a pulse. Testing the period boundary before clearing expired phase keeps a valid final pulse while preventing a leftover partial interval from becoming extra lifetime.
Run through expiry
The working caller applies once and services eight times. It checks that the duration expires and that three pulses occur. The custom duration five/period three produces one pulse; its final partial interval expires without an extra pulse. The earlier table also tests cancellation, period one, maximum values, final pulses and invalid state.
Example calls from the maintained caller
70 LET D=6: LET P=2: GOSUB 3000: LET A=1: LET NN=0: LET AA=0: LET BB=0: FOR JJ=1 TO 8
80 GOSUB 3100: GOSUB 2000: LET NN=NN+F: LET A=0: NEXT JJ
90 IF R=0 THEN LET AA=1
100 IF NN=3 THEN LET BB=1
110 PRINT "TIME RESULTS";AA;BB
120 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: STOPLine 2000, the mailbox, NN/AA/BB and JJ (Z on Spectrum) are diagnostic machinery. A game calls 3100, consumes the fresh F result once and uses R for its own status display. The helper neither changes health nor plays sound.
Customise the effect
Choose D and P for a poison pulse, blinking effect or another periodic event. Reapplying resets both duration and phase here. Stacking durations, preserving phase on refresh or limiting refreshes are different rules and need different state/tests. Changing D alone affects a later application; it does not truncate an already-running R.
The units are service calls. They can be turns, simulation updates or clock-driven ticks if that is what the caller supplies. Withholding calls pauses the effect. An elapsed-time deadline or accumulator suits variable update spacing; a timed turn still needs its chosen clock policy. There is no installed interrupt, hidden frame wait or real-time guarantee.
P=1 can pulse on every active call, including an apply with D=1 that also expires immediately. A nonzero duration shorter than the period can expire with no pulse. Neither case is an error; the game chooses whether that behaviour fits the effect.
Mistake and repair
The wrong caller sets A=1 before every service. Remaining duration repeatedly reloads, while phase repeatedly restarts at zero. With these periods, no pulse arrives and the duration never finishes. Both saved checks print zero.
The repair applies once and retains timer state through later A=0 updates. Repaired source and loaded tokenised program match the working caller exactly.

Cost and further steps
Each service performs a bounded sequence of scalar checks/arithmetic with no array scan. BASIC scalar storage is not eight machine-code bytes. There is no measured hardware-cycle claim. A single expiry deadline is cheaper when no periodic pulses are needed; several independently stacked effects require more state and a deliberate ownership policy.
Maths for Games connects countdowns, modular phase and discrete intervals. The Craft helps choose how pulses and expiry are communicated so the player can understand the rule.
Machine and measured size
C64 BASIC V2 recognises only the first two significant characters of numeric variable names. Short names make the ownership visible; check for collisions before including another component. This helper uses scalar numeric fields and allocates no arrays. The diagnostic uses a mailbox at $C000, outside the normal BASIC heap.
The numbered helper occupies 368 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, JJ and RAM mailbox are observation machinery, not required game state.
Execution evidence
Working, custom, deliberately wrong and exact repaired callers executed through C64’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-timelines.py timed-status commodore-64 --tools /path/to/extracted/releases and inspect with python3 scripts/check-basic-timelines.py timed-status.
Explore further
Compare the assembly technique. The BASIC game-flow routine makes session boundaries explicit. Complete source and native evidence.