Press Space: one turn. Wait: the world waits with you. Now press T and leave the keyboard alone. After 240 firmware ticks, the next turn arrives by itself. Your adventure might give a player time to escape a flooding room; your board game might put a limit on a move. The turn rule can stay the same while its timing changes.

Try the complete caller
Run make -C commodore-vic-20/patterns/basic/framework/turn-deadline in code-samples. Enter the generated build/demo.bas as one numbered program, then type RUN. Space advances, T switches timing on or off, and Q ends the example. Use brief taps at first. The status shows the turn, expiry count and active flag; it is not a live countdown.
Use an unexpanded 5 KiB PAL VIC-20, with the recorded stock BASIC V2 and KERNAL ROMs. The optional diagnostic observer borrows $033C–$034F (828–847), part of the inactive cassette buffer. The native arithmetic fixture borrows ninety-nine bytes from the same buffer; both fit its 192-byte allocation. Do not perform tape I/O or share that buffer while using the observer. It is outside the BASIC program/variable/string heap. Q leaves the screen and observer bytes behind. The reusable helper itself needs no fixed RAM reservation.
The caller owns the game policy
10 PRINT CHR$(147);
20 N=0:S=1:EX=0:DU=240:LC=0:MB=828
30 DC=TI:GOSUB 3000
40 POKE MB+16,165:POKE MB+17,90
50 GOSUB 8000
100 GOSUB 5000
110 GET K$
130 IF DX THEN EX=EX+1:IF EX>255 THEN EX=0
140 IF DX THEN A$="A":GOSUB 1000:GOSUB 3100:GOSUB 8000:GOTO 100
145 IF K$="Q" THEN END
150 IF K$="T" THEN GOSUB 6000:GOSUB 8000:GOTO 100
160 IF K$<>" " THEN GOTO 190
170 A$="A":GOSUB 1000
180 IF DA THEN GOSUB 3100
185 GOSUB 8000
190 GOSUB 9000:GOTO 100
6000 IF DA THEN DA=0:DR=0:RETURN
6010 GOSUB 3100:RETURNCombine this caller, the existing C64 command-loop rule below, its maintained deadline routines.bas, clock.bas, monitor.bas, and the compact PET/VIC-20 presentation.bas. The Makefile does that for you. BASIC executes by line number, so reserve the shown ranges and update every GOSUB/GOTO target if you renumber. Reset/NEW before entering a complete alternative: old numbered lines must not survive accidentally.
A bounded turn update, independent of time
1000 C=0
1010 IF A$<>"A" THEN RETURN
1020 N=N+S
1030 IF N>255 THEN N=N-256
1040 C=1
1050 RETURNThis is the existing untimed command-loop routine, unchanged. The caller supplies A$=“A” for Space and expiry. S stays in 0..255 and N wraps within 0..255. Both counters are small demonstration state: replace their effects with your own game rules.
Take the reusable helper
The helper does no input, drawing or turn advancement. It accepts two samples of a clock, subtracts the elapsed ticks and reports whether time ran out.
Initialise, start and poll a deadline
3000 DP=DC:DR=0:DA=0:DX=0:DE=0:RETURN
3100 DR=DU:DA=0:DX=0
3110 IF DU>0 THEN DA=1
3120 RETURN
3200 DX=0:DE=DC-DP
3210 IF DE<0 THEN DE=DE+5184000
3220 DP=DC
3230 IF DA=0 THEN RETURN
3240 DR=DR-DE:IF DR>0 THEN RETURN
3250 DR=0:DA=0:DX=1:RETURNSet DU to your duration and DC to the current clock, then call 3000 once. This initialises disabled state and its previous sample. Call 3100 to start or restart: it copies duration into remaining time. Zero duration leaves it disabled.
Each service iteration, including while disabled, set DC and call 3200. Read DX immediately afterwards: 1 means this poll expired. The helper disables that deadline and clamps remaining to zero; the next poll clears the event. Your caller decides whether to spend a turn or start another deadline.
| Global | Contract |
|---|---|
| DU | Caller duration: whole ticks, 0..5183999; 0 disables |
| DC | Caller current clock: whole ticks, 0..5183999 |
| DP | Helper-owned previous sample |
| DR | Helper-owned remaining ticks, saturated at zero |
| DA | Helper-owned active flag, 0 or 1 |
| DX | Helper-owned expiry event, 0 or 1 |
| DE | Helper-owned elapsed ticks from this poll |
These are BASIC globals. Rename them consistently to avoid collisions. Only the first two characters distinguish VIC-20 names. These ordinary numeric variables use five-byte floating-point values; all the whole numbers in the stated ranges are represented exactly. The helper reserves no fixed RAM, IRQ vector, hardware timer, array or string. It uses BASIC GOSUB/RETURN frames and is one mutable context, rather than a re-entrant service.
Starting preserves the most recent sampled baseline. Poll immediately before starting if that instant matters. Disable by setting DA=0. There is no preserved pause state: the demo clears remaining on disable and gives a fresh duration on re-enable.
Read a clock without waiting for a command
The machine-specific clock adapter
5000 DC=TI:GOSUB 3200:RETURNTI reads the KERNAL jiffy clock: nominal 60 ticks per second. A PAL video field is 50 Hz, so a field is not a TI tick. The native BASIC caller is independently observed on the unexpanded PAL VIC-20; sharing syntax with C64 is not its execution evidence. The firmware counter resets at 5,184,000 ticks. Keep its normal interrupt service running. Tape I/O can interfere with timing and also conflicts with this observer’s buffer; assigning TI$ changes the shared clock. This adapter never resets it.
Poll before one whole clock cycle passes. 5,183,994 → 5 is eleven ticks; missing an entire extra cycle would give the same samples. A clock reset is not a rollover. Reinitialise your context after another owner changes the clock. This adapter never resets it.
GET K$ reads without waiting for Return. It consumes characters delivered by firmware, including typeahead and repeat. A held key is not promised to count once. The untimed command loop can use blocking INPUT because its world is allowed to wait. Putting blocking INPUT into this caller would also stop deadline service. A clock does not make BASIC run two tasks at once.
The caller polls, reads one character, then handles expiry. Expiry wins if both arrive in the same iteration, discarding that character, including T or Q. Otherwise Space advances once and restarts an already active deadline. An untimed action stays untimed. An overdue deadline produces one turn; it does not replay every turn that might have happened during a delay.
Make it your own
The same helper with half the duration
10 PRINT CHR$(147);
20 N=0:S=1:EX=0:DU=120:LC=0:MB=828
30 DC=TI:GOSUB 3000
40 POKE MB+16,165:POKE MB+17,90
50 GOSUB 8000
100 GOSUB 5000
110 GET K$
130 IF DX THEN EX=EX+1:IF EX>255 THEN EX=0
140 IF DX THEN A$="A":GOSUB 1000:GOSUB 3100:GOSUB 8000:GOTO 100
145 IF K$="Q" THEN END
150 IF K$="T" THEN GOSUB 6000:GOSUB 8000:GOTO 100
160 IF K$<>" " THEN GOTO 190
170 A$="A":GOSUB 1000
180 IF DA THEN GOSUB 3100
185 GOSUB 8000
190 GOSUB 9000:GOTO 100
6000 IF DA THEN DA=0:DR=0:RETURN
6010 GOSUB 3100:RETURNThis changes only DU=240 to DU=120: roughly four seconds becomes two. The units matter as much as the number. Maths for Games explores the same habits: choose a representation, give quantities units and check where arithmetic wraps. Subtraction here is elapsed time, not frame pacing.
Common mistake: resetting the baseline every time
Deliberately broken adapter below. It stores “now” as the previous sample just before asking how much time elapsed. Every subtraction becomes zero.
Broken: throwing away elapsed time before polling
5000 DC=TI:DP=DC:GOSUB 3200:RETURN
Restore clock.bas to remove the assignment to DP. A separate fresh repaired run advances normally. Keep this replacement out of working builds: its line number intentionally replaces the correct adapter.
What does it cost?
The routine performs bounded work per poll; it does not loop once per elapsed tick. Interpretation, GOSUB lookup and your other work still determine how often you can service it. A native clock accumulates time while BASIC draws or calculates, but expiry is noticed only when you poll again. Restarting at observation time also introduces drift.
The native interpreter stored 191 bytes of tokenised helper lines, including line headers. The complete instrumented program occupies 1,016 tokenised bytes. Its observed scalar table occupies 133 bytes, with four string bytes at the checked snapshot; the free BASIC gap is 2,430 bytes. These are native pointers and allocations, not peak heap/stack measurements. The helper’s seven numeric globals occupy seven seven-byte variable entries, 49 bytes including names and values.
The optional observer accounts for 274 tokenised bytes inside that program plus its borrowed 20-byte buffer. The recorded instrumented idle caller commonly samples intervals around 17–18 ticks. That includes input and serialisation work; it is not the cost of the helper alone. The shorter-duration program has the same size; the deliberately wrong baseline adapter adds six tokenised bytes.
The optional 20-byte observer adds six scratch/monitor globals and several byte writes every iteration. Remove GOSUB 9000, its guards, MB/LC initialisation and monitor.bas when you do not need it. It is deliberately readable, rather than a fast game implementation. The caller redraws only on actions, toggles and expiry. No whole-game frame budget follows from these measurements.
Optional monitor used by the complete caller
This is diagnostic feedback, separate from the deadline helper. Include it for the complete caller above, or remove the observer calls and reservations as described before omitting it.
Compact presentation shared by these two callers
8000 PRINT CHR$(19);:PRINT "SPACE TURN T TIMER Q"
8010 PRINT "TURN";N;" E";EX;" T";DA;" "
8020 RETURNOptional diagnostic snapshot
9000 POKE MB+19,0
9010 POKE MB,N:POKE MB+1,EX:POKE MB+2,DA
9020 V=DR:O=3:GOSUB 9100
9030 V=DC:O=6:GOSUB 9100
9040 V=DU:O=9:GOSUB 9100
9050 V=DE:O=12:GOSUB 9100
9060 LC=LC+1:IF LC>255 THEN LC=0
9070 POKE MB+18,LC:POKE MB+19,239:RETURN
9100 H=INT(V/65536):V=V-H*65536
9110 M=INT(V/256):V=V-M*256
9120 POKE MB+O,V:POKE MB+O+1,M:POKE MB+O+2,H:RETURNLonger clocks, several independent deadlines, preserved pause time, carrying lateness into the next turn, queued commands or fixed-step catch-up are useful extensions when a game needs them. Each changes either the policy or the resource cost. See choosing progression and clocks for those distinctions.
What was checked
Code status: emulator-executed BASIC on the named configuration. Six independent stock-firmware boots cover working, half-duration, wrong baseline, repair, boundaries and wrong equality. Source is typed through the real keyboard path. The observer checks coherent guarded state, idle/action, no-input expiry, active restart and disable; it also records the complete native tokenised program, line inventory, BASIC pointers and variable entries.
A second caller supplies twelve DATA cases to the same helper, including zero/one/maximum duration, exact and overdue expiry, an ordinary low-word crossing and the firmware clock modulus. Changing > to >= completes the fixture but fails the independent oracle. Those supplied samples qualify arithmetic, not a real-time wait across a whole rollover.
The retained checker independently decodes the visible native digits and rejects 28 falsified records across PET and VIC-20. The maintained source and evidence bind source, tools and firmware hashes. The shared helper remains maintained once. ROMs are not redistributed. No original-hardware, other-ROM/region, peak-memory or whole-game performance claim follows from these runs.
For buffer ownership and BASIC pointers, see the PET User Manual’s cassette guidance and memory map, and the VIC-20 Programmer’s Reference Guide memory map. A borrowed firmware buffer is useful only while its original owner is inactive.
Compare the separate VIC-20 assembly deadline for its different state representation, integration and cost. The untimed VIC-20 command loop remains useful without a clock.