Press Space: the counter advances. Wait: nothing happens. Press T, though, and waiting becomes a decision. After 240 jiffies, roughly four seconds, the game takes a turn for you. Space takes your turn sooner and starts that deadline again. Another T gives you your thinking time back.
The game rule still knows nothing about clocks. A small caller decides whether this turn needs a deadline, checks for expiry, and keeps the keyboard working while you think.

Run it, then take the pieces you need
Build the maintained source:
make -C commodore-vic-20/patterns/assembly/framework/turn-deadline ASM198X=/path/to/asm198xThe checked assembler is Asm198x 0.0.72. Load build/demo.prg as a BASIC program and RUN; the included SYS line enters the machine-code caller. Reset leaves the demo. The checked setup is an unexpanded 5 KiB PAL VIC-20, 22-column text and normal KERNAL IRQ service. Its own PRG importer enters the stock BASIC RUN/SYS path. The C64 and VIC-20 load addresses differ; use this machine’s build.
Keep the whole samples checkout: the caller uses relative includes. It shares the twelve-byte 6502 deadline helper and the BBC/Electron policy dispatcher. The Commodore adapters use KERNAL calls; they do not use MOS calls or take over the interrupt vector.
DEADLINE = $1c00
STATE = $1c20
CLOCK = $1c30
DURATION = 240!source "settings.inc"
OSWRCH = $ffd2
* = $1001
!word basic_end
!word 10
!byte $9e
!text "4112"
!byte 0
basic_end:
!word 0
!fill $1010-*,0
start:
cld
cli
jsr $ffcc
lda #147
jsr OSWRCH
lda #0
ldx #7
clear_clock:
sta CLOCK,x
dex
bpl clear_clock
lda #0
ldx #9
clear_state:
sta STATE,x
dex
bpl clear_state
lda #165
sta DEADLINE+12
lda #90
sta DEADLINE+13
lda #<DURATION
sta DEADLINE
lda #>DURATION
sta DEADLINE+1
jsr sample_clock
jsr deadline_init
jsr display
next_sample:
jsr sample_clock
jsr deadline_poll
jsr sample_keys
jsr dispatch
lda STATE+7
beq next_sample
jsr display
lda #0
sta STATE+7
jmp next_sample
commit_turn:
inc STATE+4
bne turn_committed
inc STATE+5
turn_committed:
inc STATE+7
rts
!source "../../../../../commodore-64/patterns/assembly/framework/turn-deadline/clock.inc"
!source "../../../../../commodore-64/patterns/assembly/framework/turn-deadline/input.inc"
!source "../../../../../bbc-micro/patterns/assembly/framework/turn-deadline/dispatch.inc"
!source "../../../../../cross-platform/patterns/framework/elapsed-deadline/6502.inc"
; Draw only after changes, leaving the service loop free to poll.
display:
lda #19
jsr OSWRCH
lda STATE+5
jsr hex_byte
lda STATE+4
jsr hex_byte
lda #32
jsr OSWRCH
lda STATE+6
jsr hex_byte
lda #32
jsr OSWRCH
lda DEADLINE+4
jsr hex_byte
rts
hex_byte:
pha
lsr
lsr
lsr
lsr
jsr hex_digit
pla
and #15
hex_digit:
tax
lda digits,x
jmp OSWRCH
digits: !text "0123456789ABCDEF"Use these include paths as the working order. Keep returning routines outside the main execution path: an RTS needs a matching call. You can keep just the helper and clock adapter when your own game already has input, turn rules and drawing.
A countdown you can call
; DEADLINE owns 12 bytes. A=clock low, X=clock high; decimal mode clear.
; Poll at least once per 65536 clock ticks. A/X/status clobbered; Y preserved.
deadline_init:
sta DEADLINE+8
stx DEADLINE+9
lda #0
sta DEADLINE+2
sta DEADLINE+3
sta DEADLINE+4
sta DEADLINE+5
sta DEADLINE+6
sta DEADLINE+7
rts
deadline_start:
lda DEADLINE
sta DEADLINE+2
lda DEADLINE+1
sta DEADLINE+3
ora DEADLINE
beq deadline_off
lda #1
deadline_off:
sta DEADLINE+4
lda #0
sta DEADLINE+5
rts
deadline_poll:
sta DEADLINE+10
stx DEADLINE+11
sec
sbc DEADLINE+8
sta DEADLINE+6
txa
sbc DEADLINE+9
sta DEADLINE+7
lda DEADLINE+10
sta DEADLINE+8
lda DEADLINE+11
sta DEADLINE+9
lda #0
sta DEADLINE+5
lda DEADLINE+4
beq deadline_done
sec
lda DEADLINE+2
sbc DEADLINE+6
sta DEADLINE+2
lda DEADLINE+3
sbc DEADLINE+7
sta DEADLINE+3
bcc deadline_expired
ora DEADLINE+2
bne deadline_done
deadline_expired:
lda #0
sta DEADLINE+2
sta DEADLINE+3
sta DEADLINE+4
lda #1
sta DEADLINE+5
deadline_done:
rtsThe context at $1C00 owns twelve bytes:
| Offsets | Meaning |
|---|---|
| 0–1 | Duration, unsigned little-endian ticks; zero disables |
| 2–3 | Remaining ticks, clamped at zero |
| 4 | Active flag |
| 5 | Expiry event for this poll |
| 6–7 | Elapsed ticks in this poll |
| 8–9 | Previous clock sample |
| 10–11 | Scratch sample |
Set duration, clear decimal mode, sample the clock into A low, X high, then call deadline_init. It starts disabled. deadline_start copies duration into remaining time and enables a nonzero duration. It uses the last sampled baseline, so sample/poll immediately before starting when that instant matters.
Call sample_clock, then deadline_poll, on every service iteration, even while disabled. Read offset 5 before the next poll clears it. Polling may expire and disable the countdown; the caller decides whether that means a turn, a warning, or something else. The helper changes A/X/P and preserves Y. The adapters and surrounding firmware calls may change A/X/Y/P.
An untimed action stays untimed. This caller restarts an active deadline after Space, and after an automatic expiry. T disables and discards remaining time; re-enabling starts fresh. Preserving a paused remainder would be a different caller policy.
A daily reset is not a binary wrap
; Keep normal KERNAL IRQs running. RDTIM: A=low, X=middle, Y=high.
; CLOCK: previous raw 24-bit sample (0..2), daily offset (3..4), raw (5..7).
; Initialise all eight bytes to zero once, before the first call.
; Output A=normalised low byte, X=high byte; A/X/Y/P clobbered.
; Poll within 65536 ticks; no other caller may SETTIM while in use.
sample_clock:
jsr $ffde
normalise_clock:
sta CLOCK+5
stx CLOCK+6
sty CLOCK+7
sec
sbc CLOCK
txa
sbc CLOCK+1
tya
sbc CLOCK+2
bcs clock_forward
; Midnight reset removes 5184000 ($4f1a00) from the raw clock.
; Restore its low word to the virtual 16-bit clock.
clc
lda CLOCK+4
adc #$1a
sta CLOCK+4
clock_forward:
ldx #2
clock_save:
lda CLOCK+5,x
sta CLOCK,x
dex
bpl clock_save
clc
lda CLOCK+5
adc CLOCK+3
pha
lda CLOCK+6
adc CLOCK+4
tax
pla
rts
clock_end:KERNAL’s RDTIM returns a coherent three-byte clock in A low, X middle, Y high. There is a trap in the original C64 guide: it reverses the outer registers. Commodore’s original KERNAL listing, RDTIM at $F6DD, shows the implemented order. The named native firmware runs agree with it.
Ordinary low-word wrap is easy: previous=65530 and now=5 means eleven ticks. But the KERNAL clock also resets at 5,184,000 jiffies, a nominal day. That is not a multiple of 65536. Simply taking the low word would turn midnight into a large false elapsed interval.
The adapter remembers the previous full sample in eight bytes at $1C30. If the raw clock moves backwards, it adds the low word of one day to its offset before returning a virtual 16-bit clock. normalise_clock is callable separately with a raw A/X/Y sample; the native boundary fixture is a second caller of that same routine.
Clear all eight bytes once before the first sample. Keep samples less than 65536 jiffies apart, about 18.2 minutes at the nominal 60 Hz rate. Another part of your program must not SETTIM or reset TI$: the adapter would mistake that backward jump for midnight. Reinitialise it and the countdown after an intentional clock change.
This connects directly to Maths for Games: units, number representation and modular subtraction determine the result. A day wraps at a different modulus from a 16-bit integer. Neither is the same thing as one display field.
Keep normal KERNAL interrupts running. The adapter does not install an IRQ or add UDTIM calls. RDTIM itself enables interrupts, so this is a mainline routine, not a service to call inside an interrupt-disabled critical section. Long IRQ blocking can lose clock service; the helper cannot recover time that was never counted.
Keyboard delivery and turn order
; GETIN uses the current input channel. Caller starts with CLRCHN.
; Delivered Space/T characters become action/toggle bits in STATE+8.
; Buffered input and firmware repeat are retained, not physical press edges.
sample_keys:
lda #0
sta STATE+8
jsr $ffe4
cmp #32
bne check_toggle
inc STATE+8
rts
check_toggle:
cmp #84
bne key_done
lda #2
sta STATE+8
key_done:
rtsThe caller restores default channels with CLRCHN before using GETIN. Space becomes action bit 0; T becomes toggle bit 1. GETIN is channel-sensitive: a game using CHKIN must restore its keyboard channel or provide another sampler.
These are delivered characters, not physical press edges. The firmware buffer and key repeat still exist. Choose a physical-key sampler if holding a key must never repeat an action; this example does not make that promise.
; Consume expiry before input; one overdue event, no catch-up turns.
dispatch:
lda DEADLINE+5
beq consider_input
inc STATE+6
jsr commit_turn
jmp deadline_start
consider_input:
lda STATE+8
and #2
beq consider_action
lda DEADLINE+4
beq enable_timer
lda #0
sta DEADLINE+4
sta DEADLINE+2
sta DEADLINE+3
inc STATE+7
rts
enable_timer:
inc STATE+7
jmp deadline_start
consider_action:
lda STATE+8
and #1
beq dispatch_done
jsr commit_turn
lda DEADLINE+4
beq dispatch_done
jsr deadline_start
dispatch_done:
rtsThe order is deliberate: poll time, read one character, then dispatch. Expiry wins over that iteration’s character, including T. It spends one turn and starts one new deadline; missed time does not create a catch-up burst. Otherwise T wins over Space when both action bits are supplied by another sampler. Drawing happens after changes, leaving the service loop free to poll.
Make it your own
Change DURATION = 240 to 120 in settings.inc and rebuild. The same caller now gives the player half as many jiffies. That exact change has its own independently executed custom-duration record. It changes neither the clock rate nor code size.
The helper can represent durations from 1 to 65535 ticks. That upper limit does not remove the sampling limit. Multiple timers, preserved pauses and catch-up rules are useful extensions, but need separate state and their own qualification.
When nothing happens at expiry
Deliberately broken dispatcher: this version checks for input before consuming the one-poll expiry event.
; WRONG: expiry is a one-poll event. No input means the event is lost.
dispatch:
lda STATE+8
beq dispatch_done
lda DEADLINE+5
beq consider_input
inc STATE+6
jsr commit_turn
jmp deadline_start
consider_input:
lda STATE+8
and #2
beq consider_action
lda DEADLINE+4
beq enable_timer
lda #0
sta DEADLINE+4
sta DEADLINE+2
sta DEADLINE+3
inc STATE+7
rts
enable_timer:
inc STATE+7
jmp deadline_start
consider_action:
lda STATE+8
and #1
beq dispatch_done
jsr commit_turn
lda DEADLINE+4
beq dispatch_done
jsr deadline_start
dispatch_done:
rts
The countdown reaches zero and becomes inactive. With no character, though, the dispatcher returns before spending a turn or asking for a redraw. The screen stays 0001 00 01. That last 01 is stale presentation; native state says the timer is off. A still image alone cannot establish the fault, so the evidence also retains the advancing clock and countdown state.
Use the normal dispatcher above as the fix. The independently run repaired program is byte-identical to working and shows 0002 01 01 after the same input/wait sequence. Consume expiry every service iteration, whether input arrives or not.
Costs and what was checked
The normal PRG loads 483 bytes, including its BASIC stub and padding; the disk file adds a two-byte load address. Inside that total are the 140-byte helper and 63-byte clock adapter. The caller owns another 32 bytes: twelve deadline bytes, two guards, ten caller-state bytes and eight clock-adapter bytes, plus ordinary hardware stack use. The BASIC stub starts at $1001 and the caller at $1010; the normal image ends at $11E3. State lives at $1C00/$1C20/$1C30, below the $1E00 screen. The larger boundary fixture also fits stock RAM. BASIC is suspended inside SYS: do not allow another allocator or routine to use those reservations.
Retained runs measured 35–36 jiffies in 30 PAL fields. That shows why the service clock and raster clock must stay separate. It does not measure helper cycles or establish a whole-game budget. Rendering, sound and input work still have to leave enough room for clock service and timely polling.
Each machine has seven independent Emu198x 0.44.2 runs: working, half-duration, wrong dispatcher, repaired, boundaries, wrong equality and wrong midnight correction. The observer checks the whole loaded executable, guarded state and native captures. The retained checker separately decodes the on-screen digits.
A larger native caller exercises twelve helper boundary cases, six raw-clock pairs including midnight, and simultaneous expiry/action/toggle priority. These guest-supplied samples establish arithmetic and dispatch, not an observed 24-hour wait. The altered equality and omitted midnight correction complete their fixtures but fail the independent oracle. Twenty-four falsified records must also be rejected by the retained checks.
The source record and its adjacent configuration bind source, tool and firmware hashes. ROMs are not redistributed. This is named-configuration emulator evidence, not original-hardware verification or proof of every ROM, region or interrupt arrangement.
The untimed command caller remains useful when waiting should never spend a turn. The C64 version shares the arithmetic and adapters, with a different memory layout and independent execution evidence.