A coin worth 300 points should still be worth 300 when the low byte fills up. This binary score uses two bytes and keeps the carry between them. It also makes the top of the range deliberate: the score stops at its cap instead of wrapping back to zero.

Include, initialise and call
Bind SCORE_LO, SCORE_HI, ADD_LO, ADD_HI, CLIPPED as 5 contiguous private bytes in that order. Include the helper once. Call score_counter_init at the deliberate reset boundary: it clears all named bytes, including inputs. Set your starting values afterwards. Call score_counter_step when this service is needed.
Initialise at the start of a new session. Set both award bytes before each call; an award of 300 is low byte 44, high byte 1. Keep the score in binary for arithmetic and format a separate display copy. Call once for each scoring event, not once per overlapping frame unless that is the scoring rule.
SCORE_LO and SCORE_HI hold an unsigned little-endian score; ADD_LO and ADD_HI hold an unsigned award. The current score must be 0 through SCORE_CAP. Step adds the award with a wide intermediate, clamps to SCORE_CAP and sets CLIPPED to 1 only when the sum exceeds the cap. An exact hit on the cap leaves CLIPPED zero. The award bytes survive.
Reusable initialisation and service
; SPDX-License-Identifier: MIT
; A Saturating 16-bit Score. Bind these contiguous private bytes: SCORE_LO, SCORE_HI, ADD_LO, ADD_HI, CLIPPED.
; init clears the whole block; step uses current inputs. No I/O, waits or IRQ owner.
; AF/BC/DE/HL may change; IX/IY survive.
score_counter_init:
xor a
ld (SCORE_LO),a
ld (SCORE_HI),a
ld (ADD_LO),a
ld (ADD_HI),a
ld (CLIPPED),a
ret
score_counter_step:
xor a
ld (CLIPPED),a
ld hl,(SCORE_LO)
ld de,(ADD_LO)
add hl,de
jr c,score_cap
ld de,SCORE_CAP
push hl
or a
sbc hl,de
pop hl
jr c,score_store
jr z,score_store
score_cap:
ld hl,SCORE_CAP
ld a,1
ld (CLIPPED),a
score_store:
ld (SCORE_LO),hl
ret
component_end:State and ownership
The two SCORE bytes persist; the two ADD bytes are inputs; CLIPPED is a replaced overflow/cap pulse. Byte order is explicit on every CPU, including the big-endian 68000.
AF/BC/DE/HL may change; IX/IY survive. A direct CALL uses two return-address stack bytes. The cap comparison saves HL with PUSH/POP, adding two temporary bytes: allow four bytes for this helper call, excluding its caller and interrupts.
This implementation has fixed symbol bindings and one mutation owner. Independent users need distinct state blocks and scoped/renamed bindings; including another copy with the same global labels is not sufficient. Sequential queries may reuse a scratch block only after the previous result has been consumed. Do not let an interrupt and the main loop mutate the same block without a separately designed ownership policy.
The standalone tape program starts at $8000, disables interrupts and owns bitmap/attribute RAM. It does not return to BASIC. The helper itself never disables interrupts or touches the display.
A small caller you can adapt
The standalone program includes the helper and a separate client. Its read-only table exercises 96 valid inputs, then this client initialises once and retains state between calls. Replace the supplied input changes with your own game values; the display and packet machinery are diagnostics rather than dependencies of the component.
Buildable caller, bindings and include order
org $8000
CUSTOM equ 0
BROKEN equ 0
SCORE_CAP equ 65535
QUEUE_CAP equ 4
REPEAT_DELAY equ 4
REPEAT_RATE equ 2
include "caller.inc"
include "exercise.inc"
component_start:
include "component.inc"
include "cases.inc"
include "../../physics/rectangle-overlap/display.inc"
code_end:
STATE:
SCORE_LO: defb 0
SCORE_HI: defb 0
ADD_LO: defb 0
ADD_HI: defb 0
CLIPPED: defb 0
input_ptr: defw 0
output_ptr: defw 0
cases_left: defb 0
ready: defb 0
preview: defs 8,0
OUTPUT: defs 1300,0
end startA separate client retaining state between calls
preview_rules:
if CUSTOM
ld a,250
ld (SCORE_LO),a
ld a,0
ld (SCORE_HI),a
ld a,44
ld (ADD_LO),a
ld a,1
ld (ADD_HI),a
if BROKEN
ld a,0
ld (ADD_HI),a
call checked_step
else
call checked_step
endif
call emit_state
ld a,255
ld (ADD_LO),a
ld a,255
ld (ADD_HI),a
if BROKEN
ld a,0
ld (ADD_HI),a
call checked_step
else
call checked_step
endif
call emit_state
else
ld a,250
ld (SCORE_LO),a
ld a,0
ld (SCORE_HI),a
ld a,44
ld (ADD_LO),a
ld a,1
ld (ADD_HI),a
if BROKEN
ld a,0
ld (ADD_HI),a
call checked_step
else
call checked_step
endif
call emit_state
ld a,255
ld (ADD_LO),a
ld a,255
ld (ADD_HI),a
if BROKEN
ld a,0
ld (ADD_HI),a
call checked_step
else
call checked_step
endif
call emit_state
endif
retThe eight display fields follow SCORE_LO, SCORE_HI, ADD_LO, ADD_HI, CLIPPED; unused display fields are zero. Each field is a hexadecimal byte. The working row is FF FF FF FF 01 00 00 00. The output is a completed diagnostic state, not a recording of movement, input or sound hardware.
Make it your own
The custom build sets SCORE_CAP to 999 instead of 65,535. It uses the same two-byte representation and enforces the smaller cap. The native case table keeps every starting score inside the chosen cap.
Maintained custom configuration or caller
org $8000
CUSTOM equ 1
BROKEN equ 0
SCORE_CAP equ 999
QUEUE_CAP equ 2
REPEAT_DELAY equ 6
REPEAT_RATE equ 3
include "caller.inc"
include "exercise.inc"
component_start:
include "component.inc"
include "cases.inc"
include "../../physics/rectangle-overlap/display.inc"
code_end:
STATE:
SCORE_LO: defb 0
SCORE_HI: defb 0
ADD_LO: defb 0
ADD_HI: defb 0
CLIPPED: defb 0
input_ptr: defw 0
output_ptr: defw 0
cases_left: defb 0
ready: defb 0
preview: defs 8,0
OUTPUT: defs 1300,0
end startThe observed custom row is E7 03 FF FF 01 00 00 00. The retained records qualify these named configurations; a new input domain or larger layout needs its own bounds and fresh execution checks.
Common mistake and repair
The wrong client clears ADD_HI before awarding points. A 300-point award becomes 44; a 65,535-point award becomes 255. Supply both bytes together. Merely changing a display width cannot restore points that the arithmetic never received.
Deliberately wrong client selection
org $8000
CUSTOM equ 0
BROKEN equ 1
SCORE_CAP equ 65535
QUEUE_CAP equ 4
REPEAT_DELAY equ 4
REPEAT_RATE equ 2
include "caller.inc"
include "exercise.inc"
component_start:
include "component.inc"
include "cases.inc"
include "../../physics/rectangle-overlap/display.inc"
code_end:
STATE:
SCORE_LO: defb 0
SCORE_HI: defb 0
ADD_LO: defb 0
ADD_HI: defb 0
CLIPPED: defb 0
input_ptr: defw 0
output_ptr: defw 0
cases_left: defb 0
ready: defb 0
preview: defs 8,0
OUTPUT: defs 1300,0
end start
The wrong branch is in the displayed client above, selected by BROKEN. repaired.asm selects the original client again. Its executable matches demo exactly; Spectrum tape filenames can differ while loaded CODE is identical. The bad caller is a labelled experiment, not an alternative recommended implementation.
Cost and verification
The measured helper occupies 55 code bytes plus 5 named state bytes in this layout. Return addresses, alignment, tables and display diagnostics are additional. The sizes cover init and step together, not the whole standalone program.
The addition and cap comparison use bounded paths. On 6502 the carry joins two byte additions; the Z80 uses a word addition and carry; the 68000 forms a long intermediate. Rendering and decimal conversion are separate costs.
A BCD score can simplify decimal display on some CPUs, but the NES CPU does not implement decimal arithmetic. Wider binary counters or milestone rewards need their own carry, cap and event policies. Saturation deliberately loses later awards; games that need lifetime totals should choose a wider representation.
Four native runs use Spectrum 48K with stock ROM tape loading and USR entry; Asm198x 0.0.72 and Emu198x 0.44.2. Every run checks 96 table results, dirty initialisation and every state in a separate sequential trace against an independent integer model. This is boundary/varied-input qualification, not exhaustive coverage of every byte combination. Source/tool/firmware hashes, native startup and completion, preserved-register sentinels, immutable loaded code, actual helper extent, capture bytes and pixels, and exact repair are checked. Deliberately changed observations must fail. Firmware is hashed, never distributed.