Press Space: one turn. Wait: the world waits with you. Now press T and leave the keyboard alone. Four seconds later, 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-64/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 a PAL C64 with stock BASIC V2 and KERNAL. The caller clears the normal text display and reserves $C000–$C013 (49152–49171) for an optional diagnostic observer. Keep sprites, machine code and other owners out of that area. Q leaves the screen and observer behind.
The caller owns the game policy
10 PRINT CHR$(147);
20 N=0:S=1:EX=0:DU=240:LC=0:MB=49152
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:RETURN
8000 PRINT CHR$(19);:PRINT "SPACE:TURN T:TIMER Q:QUIT"
8010 PRINT "TURN";N;" EX";EX;" TIMER";DA;" "
8020 RETURNCombine this caller, the existing command-loop routine below, routines.bas, clock.bas and monitor.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 C64 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, resetting after 5,184,000 ticks (24 hours). A PAL video field is not a TI tick. Keep the normal KERNAL interrupt service running. Tape I/O stops this timer, and assigning TI$ changes the shared clock; neither is harmless while a deadline depends on 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=49152
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:RETURN
8000 PRINT CHR$(19);:PRINT "SPACE:TURN T:TIMER Q:QUIT"
8010 PRINT "TURN";N;" EX";EX;" TIMER";DA;" "
8020 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 diagnostic caller occupies 1030 tokenised program bytes. Its observed scalar variable table occupies 133 bytes after the checked interactions; this is a snapshot, not a peak-memory promise. String contents and interpreter workspace/stack are additional. The optional observer reserves another 20 bytes. In the recorded disabled-idle snapshot, successive service samples were 20 jiffies apart with the observer present; that includes interpreted caller/diagnostic work and is not the cost of the helper alone.
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.
Optional 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. Four cold stock-firmware runs cover the public caller, half-duration customisation, baseline-reset fault and repaired adapter. The verifier types the complete program, observes guarded native state, and checks untimed idle/action, no-input expiry, restart and disable. Native captures above show the real text display.
A second BASIC caller executes twelve synthetic-clock boundary cases through the same interpreter: no elapsed time, wrap, exact/overshot expiry and zero/maximum duration. An independent arithmetic oracle checks the returned bytes. These establish arithmetic behaviour; they are not a wall-clock run across an entire rollover. Retained-record checks reject missing variants, changed source and falsified native observations. No original-hardware, other-ROM or other-region claim is made.
The maintained modules and native evidence include loading order and repeatable commands. The second caller shows the same helper used without the demo’s input policy.
Primary references
Commodore, Commodore 64 Programmer’s Reference Guide (1983), “Using the GET Statement”, TI/TI$ BASIC reference, KERNAL UDTIM/RDTIM and BASIC program/variable storage pointers. These are the clock, input and memory contracts used here.