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.
; A/X/flags may change; Y and decimal mode survive. Binary arithmetic requires D=0.
score_counter_init:
lda #0
sta SCORE_LO
sta SCORE_HI
sta ADD_LO
sta ADD_HI
sta CLIPPED
rts
score_counter_step:
lda #0
sta CLIPPED
lda SCORE_LO
clc
adc ADD_LO
sta SCORE_LO
lda SCORE_HI
adc ADD_HI
sta SCORE_HI
bcs score_cap
cmp #>SCORE_CAP
bcc score_done
bne score_cap
lda SCORE_LO
cmp #<SCORE_CAP
bcc score_done
beq score_done
score_cap:
lda #<SCORE_CAP
sta SCORE_LO
lda #>SCORE_CAP
sta SCORE_HI
lda #1
sta CLIPPED
score_done:
rts
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.
A/X/flags may change; Y and decimal mode survive. Clear decimal mode before binary arithmetic; the standalone caller uses CLD. A direct call pushes a two-byte return address.
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 cartridge owns its background, CHR font and RAM layout. Its diagnostic packet is bounded below the end of the NES’s 2 KiB internal RAM; the component owns no PPU or audio registers.
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
.segment "HEADER"
.byte "NES",$1a,2,1,0,0,0,0,0,0,0,0,0,0
SCORE_LO=$80
SCORE_HI=$81
ADD_LO=$82
ADD_HI=$83
CLIPPED=$84
STATE=$80
input_ptr=$70
cases_left=$72
ptr=$74
saved_y=$76
OUTPUT=$0200
preview=OUTPUT+1400
ready=OUTPUT+1408
CUSTOM=0
BROKEN=0
SCORE_CAP=65535
QUEUE_CAP=4
REPEAT_DELAY=4
REPEAT_RATE=2
.include "caller.inc"
.include "exercise.inc"
component_start:
.include "component.inc"
.include "cases.inc"
code_end:
.segment "VECTORS"
.word nmi,reset,irq
.segment "CHARS"
.include "../../input/controller-reading/edges-font.inc"
.res 8192-272,0A separate client retaining state between calls
preview_rules:
.if CUSTOM
lda #250
sta SCORE_LO
lda #0
sta SCORE_HI
lda #44
sta ADD_LO
lda #1
sta ADD_HI
.if BROKEN
lda #0
sta ADD_HI
jsr checked_step
.else
jsr checked_step
.endif
jsr emit_state
lda #255
sta ADD_LO
lda #255
sta ADD_HI
.if BROKEN
lda #0
sta ADD_HI
jsr checked_step
.else
jsr checked_step
.endif
jsr emit_state
.else
lda #250
sta SCORE_LO
lda #0
sta SCORE_HI
lda #44
sta ADD_LO
lda #1
sta ADD_HI
.if BROKEN
lda #0
sta ADD_HI
jsr checked_step
.else
jsr checked_step
.endif
jsr emit_state
lda #255
sta ADD_LO
lda #255
sta ADD_HI
.if BROKEN
lda #0
sta ADD_HI
jsr checked_step
.else
jsr checked_step
.endif
jsr emit_state
.endif
rtsThe 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
.segment "HEADER"
.byte "NES",$1a,2,1,0,0,0,0,0,0,0,0,0,0
SCORE_LO=$80
SCORE_HI=$81
ADD_LO=$82
ADD_HI=$83
CLIPPED=$84
STATE=$80
input_ptr=$70
cases_left=$72
ptr=$74
saved_y=$76
OUTPUT=$0200
preview=OUTPUT+1400
ready=OUTPUT+1408
CUSTOM=1
BROKEN=0
SCORE_CAP=999
QUEUE_CAP=2
REPEAT_DELAY=6
REPEAT_RATE=3
.include "caller.inc"
.include "exercise.inc"
component_start:
.include "component.inc"
.include "cases.inc"
code_end:
.segment "VECTORS"
.word nmi,reset,irq
.segment "CHARS"
.include "../../input/controller-reading/edges-font.inc"
.res 8192-272,0The 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
.segment "HEADER"
.byte "NES",$1a,2,1,0,0,0,0,0,0,0,0,0,0
SCORE_LO=$80
SCORE_HI=$81
ADD_LO=$82
ADD_HI=$83
CLIPPED=$84
STATE=$80
input_ptr=$70
cases_left=$72
ptr=$74
saved_y=$76
OUTPUT=$0200
preview=OUTPUT+1400
ready=OUTPUT+1408
CUSTOM=0
BROKEN=1
SCORE_CAP=65535
QUEUE_CAP=4
REPEAT_DELAY=4
REPEAT_RATE=2
.include "caller.inc"
.include "exercise.inc"
component_start:
.include "component.inc"
.include "cases.inc"
code_end:
.segment "VECTORS"
.word nmi,reset,irq
.segment "CHARS"
.include "../../input/controller-reading/edges-font.inc"
.res 8192-272,0
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 59 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 NTSC NES with a mapper-0 cartridge, CHR-ROM diagnostic font, NMI and DMC disabled; 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.