Keeping Score
Track hits with points and streaks. The numbers on screen finally mean something.
What You’re Building
You can hit notes. You can miss them. But the score stays frozen at zero, the streak never moves. Your performance isn’t being measured.
By the end of this unit, you’ll have:
- Points awarded for each hit (10 points per note)
- A streak counter that tracks consecutive hits
- Streak reset on misses
- Live score and streak display that updates as you play
The numbers mean something now. Every hit counts.

Storing the Score
A score can get big. 10 points per hit, maybe 100 notes in a song — that’s 1000 points minimum. We need more than a single byte (max 255).
Two bytes give us 0-65535:
score_lo: !byte $00 ; Low byte of score
score_hi: !byte $00 ; High byte of score
streak: !byte $00 ; Current consecutive hits (0-255)
best_streak: !byte $00 ; Best streak this game
Why track best_streak? For the end-of-game screen (Unit 8). Players like seeing their best performance, even if they fumbled at the end.
Adding Points
Adding to a 16-bit number requires handling the carry:
POINTS_PER_HIT = 10
add_score:
clc ; Clear carry before addition
lda score_lo
adc #POINTS_PER_HIT ; Add to low byte
sta score_lo
bcc as_done ; No overflow? Done
inc score_hi ; Overflow: increment high byte
as_done:
rts
When score_lo overflows past 255, the carry flag is set. We add that carry to score_hi by incrementing it. This is how all multi-byte arithmetic works on the 6502.
Managing Streaks
Streaks are simpler — just one byte. On a hit:
inc streak ; One more in a row
lda streak
cmp best_streak ; New record?
bcc skip_best ; No, skip
sta best_streak ; Yes, save it
skip_best:
On a miss:
lda #$00
sta streak ; Back to zero
The streak resets completely. No partial credit for “almost” keeping it going.
The Display Problem
We have a 16-bit binary number. The screen wants decimal digits. The 6502 doesn’t have a divide instruction. How do we convert?
Repeated subtraction. To find how many 10000s are in a number, subtract 10000 until you can’t anymore. Count the subtractions. That’s your digit.
convert_score:
; Copy score to working area
lda score_lo
sta work_lo
lda score_hi
sta work_hi
; 10000s digit
ldx #$00 ; Digit counter
cs_10000:
; Can we subtract 10000?
lda work_hi
cmp #>10000 ; Compare high bytes
bcc cs_10000_done ; Definitely can't
bne cs_10000_sub ; Definitely can
lda work_lo
cmp #<10000 ; High bytes equal, check low
bcc cs_10000_done ; Can't subtract
cs_10000_sub:
; Subtract 10000
lda work_lo
sec
sbc #<10000
sta work_lo
lda work_hi
sbc #>10000
sta work_hi
inx ; Count this subtraction
jmp cs_10000
cs_10000_done:
stx score_digits ; Store 10000s digit
Then repeat for 1000s, 100s, 10s, and 1s. Each iteration is simpler because the numbers get smaller.
The < and > operators extract the low and high bytes of a 16-bit constant. <10000 is $10, >10000 is $27.
Drawing Digits
Once we have digits (0-9), we convert to screen codes and write them:
SCORE_SCREEN_POS = SCREEN + (ROW_SCORE * 40) + 7
draw_score:
ldx #$00
ds_loop:
lda score_digits,x
clc
adc #$30 ; Convert 0-9 to screen code '0'-'9'
sta SCORE_SCREEN_POS,x
inx
cpx #$06 ; 6 digits
bne ds_loop
rts
Screen codes for digits are $30-$39 (same as PETSCII). Add $30 to a digit value, and you get the character.
Streak Display
The streak is simpler — just two digits (0-99 is plenty):
convert_streak:
lda streak
; 10s digit
ldx #$00
cst_10:
cmp #10
bcc cst_10_done
sec
sbc #10
inx
jmp cst_10
cst_10_done:
stx streak_digits ; Tens
; 1s digit (remainder in A)
sta streak_digits + 1 ; Ones
rts
This is the same algorithm, just for a single byte. Subtract 10 until you can’t, count the subtractions, the remainder is the ones digit.
Integration
In check_hit, after removing the note:
; Award points
jsr add_score
; Increment streak
inc streak
lda streak
cmp best_streak
bcc ch_skip_best
sta best_streak
ch_skip_best:
; Trigger green flash
lda #FLASH_DURATION
sta hit_flash
In move_notes, when a note despawns:
despawn_note:
; Note missed! Reset streak
lda #$00
sta streak
; Trigger red flash
lda #FLASH_DURATION
sta miss_flash
Update Every Frame
The main loop calls update_display each frame:
; Update score display
jsr update_display
update_display:
jsr convert_score
jsr draw_score
jsr convert_streak
jsr draw_streak
rts
Is this wasteful? Slightly — we’re converting and drawing even when nothing changed. But it’s simple and fast enough. Optimisation can wait.
What You’ve Built
Run it. Hit a note — watch the score jump to 10, the streak to 1. Hit another — 20, streak 2. Miss one — score stays, streak drops to 0.
You now have:
- Multi-byte arithmetic — 16-bit addition with carry
- Binary-to-decimal conversion — Repeated subtraction method
- Live display updates — Screen reflects game state
- Streak mechanics — Rewarding consistency
What You’ve Learnt
- 16-bit numbers — Use two bytes, handle carry between them
- Division without divide — Repeated subtraction gives quotient and remainder
- Screen code conversion — Add
$30to convert digit values to displayable characters - State integration — Connecting game events to visible feedback
Next Unit
Score goes up. Streak goes up and down. But there’s no consequence for missing. You can’t lose. The crowd meter sits empty, doing nothing.
In Unit 5, we add the crowd. Hit notes to keep them happy. Miss too many and they leave. Now there’s something to lose.