Skip to content
Game 1 Unit 8 of 64 1 hr learning time

Health System

A health meter that grows on hits and shrinks on misses. The game can be lost.

13% of SID Symphony

A game without stakes is just practice. Now the game can be lost.

This unit adds a health system. Health starts at half-full. Perfect hits restore 4 points. Good hits restore 2 points. Misses cost 8 points. When health reaches zero, the game ends. Success requires sustained performance.

Run It

Assemble and run:

acme -f cbm -o symphony.prg symphony.asm

Unit 8 Screenshot

The health bar displays at the bottom. Watch it grow as you hit notes and shrink when you miss. Let too many notes through and the game ends with “GAME OVER” in red.

Health system — misses drain health, hits restore it

Health Logic

Health is a single byte ranging from 0 to 64. Changes are clamped - you can’t go below 0 or above 64:

; ----------------------------------------------------------------------------
; Health Settings
; ----------------------------------------------------------------------------

HEALTH_MAX    = 64              ; Maximum health
HEALTH_START  = 32              ; Starting health (half)
HEALTH_PERFECT = 4              ; Health gained on perfect hit
HEALTH_GOOD   = 2               ; Health gained on good hit
HEALTH_MISS   = 8               ; Health lost on miss

; ----------------------------------------------------------------------------
; Initialize Health
; ----------------------------------------------------------------------------

init_health:
            lda #HEALTH_START
            sta health
            jsr display_health
            rts

; ----------------------------------------------------------------------------
; Decrease Health - Subtract HEALTH_MISS, clamp at 0
; ----------------------------------------------------------------------------

decrease_health:
            lda health
            sec
            sbc #HEALTH_MISS
            bcc health_zero     ; Underflow - set to 0
            sta health
            jsr display_health
            jsr check_game_over
            rts

health_zero:
            lda #0
            sta health
            jsr display_health
            jsr check_game_over
            rts

; ----------------------------------------------------------------------------
; Increase Health - Add amount, clamp at HEALTH_MAX
; Input: A = amount to add
; ----------------------------------------------------------------------------

increase_health:
            clc
            adc health
            cmp #HEALTH_MAX
            bcc health_ok
            lda #HEALTH_MAX     ; Clamp to max
health_ok:
            sta health
            jsr display_health
            rts

The decrease_health routine uses the carry flag to detect underflow. After SBC, if carry is clear, the result went negative. Rather than storing a garbage value, we clamp to zero. The increase_health routine works similarly - compare against the maximum and clamp if exceeded.

The Health Bar

A visual bar makes health tangible. Eight characters represent 64 health points - each character shows 8 points:

; ----------------------------------------------------------------------------
; Display Health - 8 character bar
; ----------------------------------------------------------------------------

display_health:
            ; Health ranges 0-64, displayed as 8 characters
            ; Each character represents 8 health points

            lda health
            lsr                 ; Divide by 8
            lsr
            lsr
            sta temp_health     ; Full bars to draw

            ; Draw full bars
            ldx #0
            lda temp_health
            beq draw_empty_bars

draw_full_bars:
            lda #CHAR_BAR_FULL
            sta SCREEN + (HEALTH_ROW * 40) + 16,x
            lda #HEALTH_COL
            sta COLRAM + (HEALTH_ROW * 40) + 16,x
            inx
            cpx temp_health
            bne draw_full_bars

            ; Draw empty bars for remainder
draw_empty_bars:
            cpx #8
            beq health_done
            lda #CHAR_BAR_EMPTY
            sta SCREEN + (HEALTH_ROW * 40) + 16,x
            lda #11             ; Dark grey
            sta COLRAM + (HEALTH_ROW * 40) + 16,x
            inx
            jmp draw_empty_bars

health_done:
            rts

The LSR instructions divide health by 8 (three right shifts = divide by 2³ = divide by 8). This gives the number of full bar characters to draw. The remaining positions get empty bar characters.

Custom Bar Characters

Two new custom characters create the health bar:

; Health bar full - solid block
lda #%11111111
sta CHARSET + (CHAR_BAR_FULL * 8) + 0
; ... (all 8 rows solid)

; Health bar empty - outline
lda #%11111111
sta CHARSET + (CHAR_BAR_EMPTY * 8) + 0
lda #%10000001
sta CHARSET + (CHAR_BAR_EMPTY * 8) + 1
; ... (middle rows outline only)

The full bar is a solid block. The empty bar shows just the outline - indicating capacity without filling it. The contrast makes current health immediately clear.

Game Over

When health hits zero, the game freezes:

; ----------------------------------------------------------------------------
; Check Game Over
; ----------------------------------------------------------------------------

check_game_over:
            lda health
            bne not_game_over

            ; Game over!
            lda #1
            sta game_over

            ; Display game over message
            jsr show_game_over

not_game_over:
            rts

; ----------------------------------------------------------------------------
; Show Game Over
; ----------------------------------------------------------------------------

show_game_over:
            ; Display "GAME OVER" in centre of screen
            ldx #0
game_over_loop:
            lda game_over_text,x
            beq game_over_done
            sta SCREEN + (12 * 40) + 15,x
            lda #2              ; Red
            sta COLRAM + (12 * 40) + 15,x
            inx
            jmp game_over_loop
game_over_done:

            ; Flash border red
            lda #2
            sta BORDER

            rts

game_over_text:
            !scr "game over"
            !byte 0

The game_over variable acts as a flag. Once set, the main loop skips all game logic - notes stop spawning, keys stop responding. The player sees “GAME OVER” in red and must restart.

Hooking Into Existing Code

The health system integrates with existing routines. In award_points:

            ; Increase health for perfect hit
            lda #HEALTH_PERFECT
            jsr increase_health

In handle_miss:

            ; Decrease health
            jsr decrease_health

Small additions to existing code. The routines we wrote handle all the complexity.

The Complete Code

; ============================================================================
; SID SYMPHONY - Unit 8: Health System
; ============================================================================
; A health meter that grows on hits and shrinks on misses. The game can be
; lost. Stakes make success meaningful.
;
; Controls: Z = Track 1 (high), X = Track 2 (mid), C = Track 3 (low)
; ============================================================================

; ============================================================================
; CUSTOMISATION SECTION
; ============================================================================

; SID Voice Settings
VOICE1_WAVE = $21               ; Sawtooth
VOICE2_WAVE = $41               ; Pulse
VOICE3_WAVE = $11               ; Triangle

VOICE1_FREQ = $1C               ; High pitch
VOICE2_FREQ = $0E               ; Mid pitch
VOICE3_FREQ = $07               ; Low pitch

VOICE_AD    = $09               ; Attack=0, Decay=9
VOICE_SR    = $00               ; Sustain=0, Release=0
PULSE_WIDTH = $08               ; 50% duty cycle

; Miss sound settings
MISS_FREQ   = $08               ; Low rumble
MISS_WAVE   = $81               ; Noise waveform
MISS_AD     = $00               ; Instant attack, no decay
MISS_SR     = $90               ; High sustain, fast release

; Visual Settings
BORDER_COL  = 0                 ; Black border
BG_COL      = 0                 ; Black background

TRACK1_NOTE_COL = 10            ; Light red
TRACK2_NOTE_COL = 13            ; Light green
TRACK3_NOTE_COL = 14            ; Light blue

TRACK_LINE_COL = 11             ; Dark grey
HIT_ZONE_COL = 7                ; Yellow

FLASH1_COL  = 2                 ; Red
FLASH2_COL  = 5                 ; Green
FLASH3_COL  = 6                 ; Blue

HIT_COL     = 1                 ; White - flash on successful hit
PERFECT_COL = 1                 ; White - perfect hit border flash
GOOD_COL    = 7                 ; Yellow - good hit border flash
MISS_COL    = 2                 ; Red - miss border flash

HEALTH_COL  = 5                 ; Green for health bar

; ============================================================================
; SCORING SETTINGS
; ============================================================================

PERFECT_SCORE = 100
GOOD_SCORE    = 50

; ============================================================================
; HEALTH SETTINGS
; ============================================================================

HEALTH_MAX    = 64              ; Maximum health
HEALTH_START  = 32              ; Starting health (half)
HEALTH_PERFECT = 4              ; Health gained on perfect hit
HEALTH_GOOD   = 2               ; Health gained on good hit
HEALTH_MISS   = 8               ; Health lost on miss

; ============================================================================
; HIT DETECTION SETTINGS
; ============================================================================

HIT_ZONE_MIN = 2
HIT_ZONE_MAX = 5
HIT_ZONE_CENTRE = 3

; ============================================================================
; MEMORY MAP
; ============================================================================

SCREEN      = $0400
COLRAM      = $D800
BORDER      = $D020
BGCOL       = $D021
CHARPTR     = $D018

CHARSET     = $3000

; SID registers
SID         = $D400
SID_V1_FREQ_LO = $D400
SID_V1_FREQ_HI = $D401
SID_V1_PWHI = $D403
SID_V1_CTRL = $D404
SID_V1_AD   = $D405
SID_V1_SR   = $D406

SID_V2_FREQ_LO = $D407
SID_V2_FREQ_HI = $D408
SID_V2_PWHI = $D40A
SID_V2_CTRL = $D40B
SID_V2_AD   = $D40C
SID_V2_SR   = $D40D

SID_V3_FREQ_LO = $D40E
SID_V3_FREQ_HI = $D40F
SID_V3_PWHI = $D411
SID_V3_CTRL = $D412
SID_V3_AD   = $D413
SID_V3_SR   = $D414

SID_VOLUME  = $D418

; CIA keyboard
CIA1_PRA    = $DC00
CIA1_PRB    = $DC01

; Track positions
TRACK1_ROW  = 8
TRACK2_ROW  = 12
TRACK3_ROW  = 16

; Health bar position
HEALTH_ROW  = 23

; Hit zone
HIT_ZONE_COLUMN = 3

; Custom character codes
CHAR_NOTE   = 128
CHAR_TRACK  = 129
CHAR_HITZONE = 130
CHAR_SPACE  = 32
CHAR_BAR_FULL = 131
CHAR_BAR_EMPTY = 132

; Note settings
MAX_NOTES   = 8
NOTE_SPAWN_COL = 37

; Timing
FRAMES_PER_BEAT = 25

; Zero page
ZP_PTR      = $FB
ZP_PTR_HI   = $FC

; Variables
frame_count = $02
beat_count  = $03
song_pos    = $04
song_pos_hi = $05
temp_track  = $06
key_pressed = $07
hit_quality = $08
border_flash = $09
miss_track  = $0A
game_over   = $0B               ; Non-zero when game ended

; ----------------------------------------------------------------------------
; BASIC Stub
; ----------------------------------------------------------------------------

            * = $0801

            !byte $0C, $08
            !byte $0A, $00
            !byte $9E
            !text "2064"
            !byte $00
            !byte $00, $00

; ----------------------------------------------------------------------------
; Main Program
; ----------------------------------------------------------------------------

            * = $0810

start:
            jsr copy_charset
            jsr init_screen
            jsr init_sid
            jsr init_notes
            jsr init_score
            jsr init_health

            lda #<song_data
            sta song_pos
            lda #>song_data
            sta song_pos_hi

            lda #0
            sta frame_count
            sta beat_count
            sta border_flash
            sta game_over

main_loop:
            lda #$FF
wait_raster:
            cmp $D012
            bne wait_raster

            ; Check if game is over
            lda game_over
            bne main_loop       ; Freeze if game over

            inc frame_count
            lda frame_count
            cmp #FRAMES_PER_BEAT
            bcc no_new_beat

            lda #0
            sta frame_count
            jsr check_spawn_note
            inc beat_count

no_new_beat:
            jsr update_notes
            jsr reset_track_colours
            jsr update_border_flash
            jsr check_keys

            jmp main_loop

; ----------------------------------------------------------------------------
; Copy Character Set from ROM to RAM
; ----------------------------------------------------------------------------

copy_charset:
            sei

            lda $01
            pha
            and #$FB
            sta $01

            ldx #0
copy_loop:
            lda $D000,x
            sta CHARSET,x
            lda $D100,x
            sta CHARSET+$100,x
            lda $D200,x
            sta CHARSET+$200,x
            lda $D300,x
            sta CHARSET+$300,x
            lda $D400,x
            sta CHARSET+$400,x
            lda $D500,x
            sta CHARSET+$500,x
            lda $D600,x
            sta CHARSET+$600,x
            lda $D700,x
            sta CHARSET+$700,x
            inx
            bne copy_loop

            pla
            sta $01

            cli

            jsr define_custom_chars

            lda #$1C
            sta CHARPTR

            rts

; ----------------------------------------------------------------------------
; Define Custom Characters
; ----------------------------------------------------------------------------

define_custom_chars:
            ; Note character - diamond shape
            lda #%00000110
            sta CHARSET + (CHAR_NOTE * 8) + 0
            lda #%00011110
            sta CHARSET + (CHAR_NOTE * 8) + 1
            lda #%01111110
            sta CHARSET + (CHAR_NOTE * 8) + 2
            lda #%11111110
            sta CHARSET + (CHAR_NOTE * 8) + 3
            lda #%11111110
            sta CHARSET + (CHAR_NOTE * 8) + 4
            lda #%01111110
            sta CHARSET + (CHAR_NOTE * 8) + 5
            lda #%00011110
            sta CHARSET + (CHAR_NOTE * 8) + 6
            lda #%00000110
            sta CHARSET + (CHAR_NOTE * 8) + 7

            ; Track character - horizontal line
            lda #%00000000
            sta CHARSET + (CHAR_TRACK * 8) + 0
            lda #%00000000
            sta CHARSET + (CHAR_TRACK * 8) + 1
            lda #%00000000
            sta CHARSET + (CHAR_TRACK * 8) + 2
            lda #%11111111
            sta CHARSET + (CHAR_TRACK * 8) + 3
            lda #%11111111
            sta CHARSET + (CHAR_TRACK * 8) + 4
            lda #%00000000
            sta CHARSET + (CHAR_TRACK * 8) + 5
            lda #%00000000
            sta CHARSET + (CHAR_TRACK * 8) + 6
            lda #%00000000
            sta CHARSET + (CHAR_TRACK * 8) + 7

            ; Hit zone character - vertical bars
            lda #%01100110
            sta CHARSET + (CHAR_HITZONE * 8) + 0
            sta CHARSET + (CHAR_HITZONE * 8) + 1
            sta CHARSET + (CHAR_HITZONE * 8) + 2
            sta CHARSET + (CHAR_HITZONE * 8) + 3
            sta CHARSET + (CHAR_HITZONE * 8) + 4
            sta CHARSET + (CHAR_HITZONE * 8) + 5
            sta CHARSET + (CHAR_HITZONE * 8) + 6
            sta CHARSET + (CHAR_HITZONE * 8) + 7

            ; Health bar full - solid block
            lda #%11111111
            sta CHARSET + (CHAR_BAR_FULL * 8) + 0
            sta CHARSET + (CHAR_BAR_FULL * 8) + 1
            sta CHARSET + (CHAR_BAR_FULL * 8) + 2
            sta CHARSET + (CHAR_BAR_FULL * 8) + 3
            sta CHARSET + (CHAR_BAR_FULL * 8) + 4
            sta CHARSET + (CHAR_BAR_FULL * 8) + 5
            sta CHARSET + (CHAR_BAR_FULL * 8) + 6
            sta CHARSET + (CHAR_BAR_FULL * 8) + 7

            ; Health bar empty - outline
            lda #%11111111
            sta CHARSET + (CHAR_BAR_EMPTY * 8) + 0
            lda #%10000001
            sta CHARSET + (CHAR_BAR_EMPTY * 8) + 1
            sta CHARSET + (CHAR_BAR_EMPTY * 8) + 2
            sta CHARSET + (CHAR_BAR_EMPTY * 8) + 3
            sta CHARSET + (CHAR_BAR_EMPTY * 8) + 4
            sta CHARSET + (CHAR_BAR_EMPTY * 8) + 5
            sta CHARSET + (CHAR_BAR_EMPTY * 8) + 6
            lda #%11111111
            sta CHARSET + (CHAR_BAR_EMPTY * 8) + 7

            rts

; ----------------------------------------------------------------------------
; Initialize Health
; ----------------------------------------------------------------------------

init_health:
            lda #HEALTH_START
            sta health
            jsr display_health
            rts

; ----------------------------------------------------------------------------
; Initialize Score
; ----------------------------------------------------------------------------

init_score:
            lda #0
            sta score_lo
            sta score_hi
            sta miss_count
            jsr display_score
            jsr display_misses
            rts

; ----------------------------------------------------------------------------
; Initialize Notes
; ----------------------------------------------------------------------------

init_notes:
            ldx #MAX_NOTES-1
            lda #0
init_notes_loop:
            sta note_track,x
            sta note_col,x
            dex
            bpl init_notes_loop
            rts

; ----------------------------------------------------------------------------
; Check Spawn Note
; ----------------------------------------------------------------------------

check_spawn_note:
            ldy #0

spawn_check_loop:
            lda (song_pos),y
            cmp #$FF
            beq spawn_restart_song

            cmp beat_count
            beq spawn_match
            bcs spawn_done

            jmp spawn_advance

spawn_match:
            iny
            lda (song_pos),y
            jsr spawn_note
            dey

spawn_advance:
            lda song_pos
            clc
            adc #2
            sta song_pos
            lda song_pos_hi
            adc #0
            sta song_pos_hi
            jmp spawn_check_loop

spawn_done:
            rts

spawn_restart_song:
            lda #<song_data
            sta song_pos
            lda #>song_data
            sta song_pos_hi
            lda #0
            sta beat_count
            rts

; ----------------------------------------------------------------------------
; Spawn Note
; ----------------------------------------------------------------------------

spawn_note:
            sta temp_track

            ldx #0
spawn_find_slot:
            lda note_track,x
            beq spawn_found_slot
            inx
            cpx #MAX_NOTES
            bne spawn_find_slot
            rts

spawn_found_slot:
            lda temp_track
            sta note_track,x
            lda #NOTE_SPAWN_COL
            sta note_col,x
            jsr draw_note
            rts

; ----------------------------------------------------------------------------
; Update Notes
; ----------------------------------------------------------------------------

update_notes:
            ldx #0

update_loop:
            lda note_track,x
            beq update_next

            jsr erase_note

            dec note_col,x
            lda note_col,x
            cmp #1
            bcc update_miss

            jsr draw_note
            jmp update_next

update_miss:
            lda note_track,x
            sta miss_track
            lda #0
            sta note_track,x
            jsr handle_miss

update_next:
            inx
            cpx #MAX_NOTES
            bne update_loop
            rts

; ----------------------------------------------------------------------------
; Handle Miss
; ----------------------------------------------------------------------------

handle_miss:
            inc miss_count

            jsr play_miss_sound

            lda #MISS_COL
            sta BORDER
            lda #8
            sta border_flash

            jsr display_misses

            ; Decrease health
            jsr decrease_health

            rts

; ----------------------------------------------------------------------------
; Decrease Health - Subtract HEALTH_MISS, clamp at 0
; ----------------------------------------------------------------------------

decrease_health:
            lda health
            sec
            sbc #HEALTH_MISS
            bcc health_zero     ; Underflow - set to 0
            sta health
            jsr display_health
            jsr check_game_over
            rts

health_zero:
            lda #0
            sta health
            jsr display_health
            jsr check_game_over
            rts

; ----------------------------------------------------------------------------
; Increase Health - Add amount, clamp at HEALTH_MAX
; Input: A = amount to add
; ----------------------------------------------------------------------------

increase_health:
            clc
            adc health
            cmp #HEALTH_MAX
            bcc health_ok
            lda #HEALTH_MAX     ; Clamp to max
health_ok:
            sta health
            jsr display_health
            rts

; ----------------------------------------------------------------------------
; Check Game Over
; ----------------------------------------------------------------------------

check_game_over:
            lda health
            bne not_game_over

            ; Game over!
            lda #1
            sta game_over

            ; Display game over message
            jsr show_game_over

not_game_over:
            rts

; ----------------------------------------------------------------------------
; Show Game Over
; ----------------------------------------------------------------------------

show_game_over:
            ; Display "GAME OVER" in centre of screen
            ldx #0
game_over_loop:
            lda game_over_text,x
            beq game_over_done
            sta SCREEN + (12 * 40) + 15,x
            lda #2              ; Red
            sta COLRAM + (12 * 40) + 15,x
            inx
            jmp game_over_loop
game_over_done:

            ; Flash border red
            lda #2
            sta BORDER

            rts

game_over_text:
            !scr "game over"
            !byte 0

; ----------------------------------------------------------------------------
; Display Health - 8 character bar
; ----------------------------------------------------------------------------

display_health:
            ; Health ranges 0-64, displayed as 8 characters
            ; Each character represents 8 health points

            lda health
            lsr                 ; Divide by 8
            lsr
            lsr
            sta temp_health     ; Full bars to draw

            ; Draw full bars
            ldx #0
            lda temp_health
            beq draw_empty_bars

draw_full_bars:
            lda #CHAR_BAR_FULL
            sta SCREEN + (HEALTH_ROW * 40) + 16,x
            lda #HEALTH_COL
            sta COLRAM + (HEALTH_ROW * 40) + 16,x
            inx
            cpx temp_health
            bne draw_full_bars

            ; Draw empty bars for remainder
draw_empty_bars:
            cpx #8
            beq health_done
            lda #CHAR_BAR_EMPTY
            sta SCREEN + (HEALTH_ROW * 40) + 16,x
            lda #11             ; Dark grey
            sta COLRAM + (HEALTH_ROW * 40) + 16,x
            inx
            jmp draw_empty_bars

health_done:
            rts

temp_health: !byte 0

; ----------------------------------------------------------------------------
; Play Miss Sound
; ----------------------------------------------------------------------------

play_miss_sound:
            lda #0
            sta SID_V3_FREQ_LO
            lda #MISS_FREQ
            sta SID_V3_FREQ_HI
            lda #MISS_AD
            sta SID_V3_AD
            lda #MISS_SR
            sta SID_V3_SR
            lda #MISS_WAVE
            ora #$01
            sta SID_V3_CTRL
            rts

; ----------------------------------------------------------------------------
; Draw Note
; ----------------------------------------------------------------------------

draw_note:
            lda note_track,x
            cmp #1
            beq draw_note_t1
            cmp #2
            beq draw_note_t2
            cmp #3
            beq draw_note_t3
            rts

draw_note_t1:
            lda note_col,x
            clc
            adc #<(SCREEN + TRACK1_ROW * 40)
            sta ZP_PTR
            lda #>(SCREEN + TRACK1_ROW * 40)
            adc #0
            sta ZP_PTR_HI

            ldy #0
            lda #CHAR_NOTE
            sta (ZP_PTR),y

            lda note_col,x
            clc
            adc #<(COLRAM + TRACK1_ROW * 40)
            sta ZP_PTR
            lda #>(COLRAM + TRACK1_ROW * 40)
            adc #0
            sta ZP_PTR_HI
            lda #TRACK1_NOTE_COL
            sta (ZP_PTR),y
            rts

draw_note_t2:
            lda note_col,x
            clc
            adc #<(SCREEN + TRACK2_ROW * 40)
            sta ZP_PTR
            lda #>(SCREEN + TRACK2_ROW * 40)
            adc #0
            sta ZP_PTR_HI

            ldy #0
            lda #CHAR_NOTE
            sta (ZP_PTR),y

            lda note_col,x
            clc
            adc #<(COLRAM + TRACK2_ROW * 40)
            sta ZP_PTR
            lda #>(COLRAM + TRACK2_ROW * 40)
            adc #0
            sta ZP_PTR_HI
            lda #TRACK2_NOTE_COL
            sta (ZP_PTR),y
            rts

draw_note_t3:
            lda note_col,x
            clc
            adc #<(SCREEN + TRACK3_ROW * 40)
            sta ZP_PTR
            lda #>(SCREEN + TRACK3_ROW * 40)
            adc #0
            sta ZP_PTR_HI

            ldy #0
            lda #CHAR_NOTE
            sta (ZP_PTR),y

            lda note_col,x
            clc
            adc #<(COLRAM + TRACK3_ROW * 40)
            sta ZP_PTR
            lda #>(COLRAM + TRACK3_ROW * 40)
            adc #0
            sta ZP_PTR_HI
            lda #TRACK3_NOTE_COL
            sta (ZP_PTR),y
            rts

; ----------------------------------------------------------------------------
; Erase Note
; ----------------------------------------------------------------------------

erase_note:
            lda note_track,x
            cmp #1
            beq erase_note_t1
            cmp #2
            beq erase_note_t2
            cmp #3
            beq erase_note_t3
            rts

erase_note_t1:
            lda note_col,x
            clc
            adc #<(SCREEN + TRACK1_ROW * 40)
            sta ZP_PTR
            lda #>(SCREEN + TRACK1_ROW * 40)
            adc #0
            sta ZP_PTR_HI

            ldy #0
            lda #CHAR_TRACK
            sta (ZP_PTR),y

            lda note_col,x
            clc
            adc #<(COLRAM + TRACK1_ROW * 40)
            sta ZP_PTR
            lda #>(COLRAM + TRACK1_ROW * 40)
            adc #0
            sta ZP_PTR_HI
            lda #TRACK_LINE_COL
            sta (ZP_PTR),y
            rts

erase_note_t2:
            lda note_col,x
            clc
            adc #<(SCREEN + TRACK2_ROW * 40)
            sta ZP_PTR
            lda #>(SCREEN + TRACK2_ROW * 40)
            adc #0
            sta ZP_PTR_HI

            ldy #0
            lda #CHAR_TRACK
            sta (ZP_PTR),y

            lda note_col,x
            clc
            adc #<(COLRAM + TRACK2_ROW * 40)
            sta ZP_PTR
            lda #>(COLRAM + TRACK2_ROW * 40)
            adc #0
            sta ZP_PTR_HI
            lda #TRACK_LINE_COL
            sta (ZP_PTR),y
            rts

erase_note_t3:
            lda note_col,x
            clc
            adc #<(SCREEN + TRACK3_ROW * 40)
            sta ZP_PTR
            lda #>(SCREEN + TRACK3_ROW * 40)
            adc #0
            sta ZP_PTR_HI

            ldy #0
            lda #CHAR_TRACK
            sta (ZP_PTR),y

            lda note_col,x
            clc
            adc #<(COLRAM + TRACK3_ROW * 40)
            sta ZP_PTR
            lda #>(COLRAM + TRACK3_ROW * 40)
            adc #0
            sta ZP_PTR_HI
            lda #TRACK_LINE_COL
            sta (ZP_PTR),y
            rts

; ----------------------------------------------------------------------------
; Initialize Screen
; ----------------------------------------------------------------------------

init_screen:
            lda #BORDER_COL
            sta BORDER
            lda #BG_COL
            sta BGCOL

            ldx #0
            lda #CHAR_SPACE
clr_screen:
            sta SCREEN,x
            sta SCREEN+$100,x
            sta SCREEN+$200,x
            sta SCREEN+$2E8,x
            inx
            bne clr_screen

            ldx #0
            lda #TRACK_LINE_COL
clr_colour:
            sta COLRAM,x
            sta COLRAM+$100,x
            sta COLRAM+$200,x
            sta COLRAM+$2E8,x
            inx
            bne clr_colour

            jsr draw_tracks
            jsr draw_hit_zones
            jsr draw_labels

            rts

; ----------------------------------------------------------------------------
; Draw Tracks
; ----------------------------------------------------------------------------

draw_tracks:
            lda #CHAR_TRACK
            ldx #0
draw_t1:
            sta SCREEN + (TRACK1_ROW * 40),x
            inx
            cpx #38
            bne draw_t1

            lda #CHAR_TRACK
            ldx #0
draw_t2:
            sta SCREEN + (TRACK2_ROW * 40),x
            inx
            cpx #38
            bne draw_t2

            lda #CHAR_TRACK
            ldx #0
draw_t3:
            sta SCREEN + (TRACK3_ROW * 40),x
            inx
            cpx #38
            bne draw_t3

            rts

; ----------------------------------------------------------------------------
; Draw Hit Zones
; ----------------------------------------------------------------------------

draw_hit_zones:
            lda #CHAR_HITZONE

            sta SCREEN + ((TRACK1_ROW-2) * 40) + HIT_ZONE_COLUMN
            sta SCREEN + ((TRACK1_ROW-1) * 40) + HIT_ZONE_COLUMN
            sta SCREEN + (TRACK1_ROW * 40) + HIT_ZONE_COLUMN
            sta SCREEN + ((TRACK1_ROW+1) * 40) + HIT_ZONE_COLUMN

            sta SCREEN + ((TRACK2_ROW-1) * 40) + HIT_ZONE_COLUMN
            sta SCREEN + (TRACK2_ROW * 40) + HIT_ZONE_COLUMN
            sta SCREEN + ((TRACK2_ROW+1) * 40) + HIT_ZONE_COLUMN

            sta SCREEN + ((TRACK3_ROW-1) * 40) + HIT_ZONE_COLUMN
            sta SCREEN + (TRACK3_ROW * 40) + HIT_ZONE_COLUMN
            sta SCREEN + ((TRACK3_ROW+1) * 40) + HIT_ZONE_COLUMN
            sta SCREEN + ((TRACK3_ROW+2) * 40) + HIT_ZONE_COLUMN

            lda #HIT_ZONE_COL
            sta COLRAM + ((TRACK1_ROW-2) * 40) + HIT_ZONE_COLUMN
            sta COLRAM + ((TRACK1_ROW-1) * 40) + HIT_ZONE_COLUMN
            sta COLRAM + (TRACK1_ROW * 40) + HIT_ZONE_COLUMN
            sta COLRAM + ((TRACK1_ROW+1) * 40) + HIT_ZONE_COLUMN

            sta COLRAM + ((TRACK2_ROW-1) * 40) + HIT_ZONE_COLUMN
            sta COLRAM + (TRACK2_ROW * 40) + HIT_ZONE_COLUMN
            sta COLRAM + ((TRACK2_ROW+1) * 40) + HIT_ZONE_COLUMN

            sta COLRAM + ((TRACK3_ROW-1) * 40) + HIT_ZONE_COLUMN
            sta COLRAM + (TRACK3_ROW * 40) + HIT_ZONE_COLUMN
            sta COLRAM + ((TRACK3_ROW+1) * 40) + HIT_ZONE_COLUMN
            sta COLRAM + ((TRACK3_ROW+2) * 40) + HIT_ZONE_COLUMN

            rts

; ----------------------------------------------------------------------------
; Draw Labels
; ----------------------------------------------------------------------------

draw_labels:
            ; Draw "SCORE:" label
            ldx #0
draw_score_label:
            lda score_label,x
            beq draw_score_label_done
            sta SCREEN + 1,x
            lda #1
            sta COLRAM + 1,x
            inx
            bne draw_score_label
draw_score_label_done:

            ; Draw "MISS:" label
            ldx #0
draw_miss_label:
            lda miss_label,x
            beq draw_miss_label_done
            sta SCREEN + 15,x
            lda #2
            sta COLRAM + 15,x
            inx
            bne draw_miss_label
draw_miss_label_done:

            ; Draw title
            ldx #0
draw_title:
            lda title_text,x
            beq draw_title_done
            sta SCREEN + 27,x
            lda #1
            sta COLRAM + 27,x
            inx
            bne draw_title
draw_title_done:

            ; Draw "HEALTH:" label
            ldx #0
draw_health_label:
            lda health_label,x
            beq draw_health_label_done
            sta SCREEN + (HEALTH_ROW * 40) + 8,x
            lda #5              ; Green
            sta COLRAM + (HEALTH_ROW * 40) + 8,x
            inx
            bne draw_health_label
draw_health_label_done:

            ; Track labels
            lda #$1A            ; Z
            sta SCREEN + (TRACK1_ROW * 40)
            lda #TRACK1_NOTE_COL
            sta COLRAM + (TRACK1_ROW * 40)

            lda #$18            ; X
            sta SCREEN + (TRACK2_ROW * 40)
            lda #TRACK2_NOTE_COL
            sta COLRAM + (TRACK2_ROW * 40)

            lda #$03            ; C
            sta SCREEN + (TRACK3_ROW * 40)
            lda #TRACK3_NOTE_COL
            sta COLRAM + (TRACK3_ROW * 40)

            rts

score_label:
            !scr "score:"
            !byte 0

miss_label:
            !scr "miss:"
            !byte 0

title_text:
            !scr "sid symphony"
            !byte 0

health_label:
            !scr "health:"
            !byte 0

; ----------------------------------------------------------------------------
; Initialize SID
; ----------------------------------------------------------------------------

init_sid:
            ldx #$18
            lda #0
clear_sid:
            sta SID,x
            dex
            bpl clear_sid

            lda #$0F
            sta SID_VOLUME

            lda #$00
            sta SID_V1_FREQ_LO
            lda #VOICE1_FREQ
            sta SID_V1_FREQ_HI
            lda #PULSE_WIDTH
            sta SID_V1_PWHI
            lda #VOICE_AD
            sta SID_V1_AD
            lda #VOICE_SR
            sta SID_V1_SR

            lda #$00
            sta SID_V2_FREQ_LO
            lda #VOICE2_FREQ
            sta SID_V2_FREQ_HI
            lda #PULSE_WIDTH
            sta SID_V2_PWHI
            lda #VOICE_AD
            sta SID_V2_AD
            lda #VOICE_SR
            sta SID_V2_SR

            lda #$00
            sta SID_V3_FREQ_LO
            lda #VOICE3_FREQ
            sta SID_V3_FREQ_HI
            lda #PULSE_WIDTH
            sta SID_V3_PWHI
            lda #VOICE_AD
            sta SID_V3_AD
            lda #VOICE_SR
            sta SID_V3_SR

            rts

; ----------------------------------------------------------------------------
; Reset Track Colours
; ----------------------------------------------------------------------------

reset_track_colours:
            ldx #0
            lda #TRACK_LINE_COL
reset_t1:
            sta COLRAM + (TRACK1_ROW * 40),x
            inx
            cpx #38
            bne reset_t1

            ldx #0
reset_t2:
            sta COLRAM + (TRACK2_ROW * 40),x
            inx
            cpx #38
            bne reset_t2

            ldx #0
reset_t3:
            sta COLRAM + (TRACK3_ROW * 40),x
            inx
            cpx #38
            bne reset_t3

            lda #TRACK1_NOTE_COL
            sta COLRAM + (TRACK1_ROW * 40)
            lda #TRACK2_NOTE_COL
            sta COLRAM + (TRACK2_ROW * 40)
            lda #TRACK3_NOTE_COL
            sta COLRAM + (TRACK3_ROW * 40)

            lda #HIT_ZONE_COL
            sta COLRAM + (TRACK1_ROW * 40) + HIT_ZONE_COLUMN
            sta COLRAM + (TRACK2_ROW * 40) + HIT_ZONE_COLUMN
            sta COLRAM + (TRACK3_ROW * 40) + HIT_ZONE_COLUMN

            jsr redraw_all_notes

            rts

; ----------------------------------------------------------------------------
; Redraw All Notes
; ----------------------------------------------------------------------------

redraw_all_notes:
            ldx #0
redraw_loop:
            lda note_track,x
            beq redraw_next
            jsr draw_note
redraw_next:
            inx
            cpx #MAX_NOTES
            bne redraw_loop
            rts

; ----------------------------------------------------------------------------
; Update Border Flash
; ----------------------------------------------------------------------------

update_border_flash:
            lda border_flash
            beq flash_done
            dec border_flash
            bne flash_done
            lda #BORDER_COL
            sta BORDER
flash_done:
            rts

; ----------------------------------------------------------------------------
; Check Keys
; ----------------------------------------------------------------------------

check_keys:
            lda #$FD
            sta CIA1_PRA
            lda CIA1_PRB
            and #$10
            bne check_x_key

            lda #1
            sta key_pressed
            jsr check_hit
            bcc check_x_key
            jsr play_voice1
            jsr flash_track1_hit
            jsr award_points

check_x_key:
            lda #$FB
            sta CIA1_PRA
            lda CIA1_PRB
            and #$80
            bne check_c_key

            lda #2
            sta key_pressed
            jsr check_hit
            bcc check_c_key
            jsr play_voice2
            jsr flash_track2_hit
            jsr award_points

check_c_key:
            lda #$FB
            sta CIA1_PRA
            lda CIA1_PRB
            and #$10
            bne check_keys_done

            lda #3
            sta key_pressed
            jsr check_hit
            bcc check_keys_done
            jsr play_voice3
            jsr flash_track3_hit
            jsr award_points

check_keys_done:
            lda #$FF
            sta CIA1_PRA
            rts

; ----------------------------------------------------------------------------
; Check Hit
; ----------------------------------------------------------------------------

check_hit:
            ldx #0

check_hit_loop:
            lda note_track,x
            beq check_hit_next

            cmp key_pressed
            bne check_hit_next

            lda note_col,x
            cmp #HIT_ZONE_MIN
            bcc check_hit_next
            cmp #HIT_ZONE_MAX+1
            bcs check_hit_next

            cmp #HIT_ZONE_CENTRE
            bcc hit_good
            cmp #HIT_ZONE_CENTRE+2
            bcs hit_good

            lda #2
            sta hit_quality
            jmp hit_found

hit_good:
            lda #1
            sta hit_quality

hit_found:
            jsr erase_note
            lda #0
            sta note_track,x
            sec
            rts

check_hit_next:
            inx
            cpx #MAX_NOTES
            bne check_hit_loop

            lda #0
            sta hit_quality
            clc
            rts

; ----------------------------------------------------------------------------
; Award Points
; ----------------------------------------------------------------------------

award_points:
            lda hit_quality
            cmp #2
            beq award_perfect

            lda score_lo
            clc
            adc #GOOD_SCORE
            sta score_lo
            lda score_hi
            adc #0
            sta score_hi

            lda #GOOD_COL
            sta BORDER
            lda #4
            sta border_flash

            ; Increase health for good hit
            lda #HEALTH_GOOD
            jsr increase_health

            jmp award_done

award_perfect:
            lda score_lo
            clc
            adc #PERFECT_SCORE
            sta score_lo
            lda score_hi
            adc #0
            sta score_hi

            lda #PERFECT_COL
            sta BORDER
            lda #6
            sta border_flash

            ; Increase health for perfect hit
            lda #HEALTH_PERFECT
            jsr increase_health

award_done:
            jsr display_score
            rts

; ----------------------------------------------------------------------------
; Display Score
; ----------------------------------------------------------------------------

display_score:
            lda score_lo
            sta work_lo
            lda score_hi
            sta work_hi

            ldx #0
div_10000:
            lda work_lo
            sec
            sbc #<10000
            tay
            lda work_hi
            sbc #>10000
            bcc done_10000
            sta work_hi
            sty work_lo
            inx
            jmp div_10000
done_10000:
            txa
            ora #$30
            sta SCREEN + 8

            ldx #0
div_1000:
            lda work_lo
            sec
            sbc #<1000
            tay
            lda work_hi
            sbc #>1000
            bcc done_1000
            sta work_hi
            sty work_lo
            inx
            jmp div_1000
done_1000:
            txa
            ora #$30
            sta SCREEN + 9

            ldx #0
div_100:
            lda work_lo
            sec
            sbc #100
            bcc done_100
            sta work_lo
            inx
            jmp div_100
done_100:
            txa
            ora #$30
            sta SCREEN + 10

            ldx #0
div_10:
            lda work_lo
            sec
            sbc #10
            bcc done_10
            sta work_lo
            inx
            jmp div_10
done_10:
            txa
            ora #$30
            sta SCREEN + 11

            lda work_lo
            ora #$30
            sta SCREEN + 12

            lda #7
            sta COLRAM + 8
            sta COLRAM + 9
            sta COLRAM + 10
            sta COLRAM + 11
            sta COLRAM + 12

            rts

; ----------------------------------------------------------------------------
; Display Misses
; ----------------------------------------------------------------------------

display_misses:
            lda miss_count

            ldx #0
miss_div_10:
            cmp #10
            bcc miss_done_10
            sec
            sbc #10
            inx
            jmp miss_div_10
miss_done_10:
            pha
            txa
            ora #$30
            sta SCREEN + 21
            pla
            ora #$30
            sta SCREEN + 22

            lda #2
            sta COLRAM + 21
            sta COLRAM + 22

            rts

work_lo:    !byte 0
work_hi:    !byte 0

; ----------------------------------------------------------------------------
; Play Voices
; ----------------------------------------------------------------------------

play_voice1:
            lda #VOICE1_WAVE
            ora #$01
            sta SID_V1_CTRL
            rts

play_voice2:
            lda #VOICE2_WAVE
            ora #$01
            sta SID_V2_CTRL
            rts

play_voice3:
            lda #VOICE3_WAVE
            ora #$01
            sta SID_V3_CTRL
            rts

; ----------------------------------------------------------------------------
; Flash Tracks on Hit
; ----------------------------------------------------------------------------

flash_track1_hit:
            ldx #0
            lda #HIT_COL
flash_t1h_loop:
            sta COLRAM + (TRACK1_ROW * 40),x
            inx
            cpx #38
            bne flash_t1h_loop
            lda #1
            sta COLRAM + (TRACK1_ROW * 40)
            rts

flash_track2_hit:
            ldx #0
            lda #HIT_COL
flash_t2h_loop:
            sta COLRAM + (TRACK2_ROW * 40),x
            inx
            cpx #38
            bne flash_t2h_loop
            lda #1
            sta COLRAM + (TRACK2_ROW * 40)
            rts

flash_track3_hit:
            ldx #0
            lda #HIT_COL
flash_t3h_loop:
            sta COLRAM + (TRACK3_ROW * 40),x
            inx
            cpx #38
            bne flash_t3h_loop
            lda #1
            sta COLRAM + (TRACK3_ROW * 40)
            rts

; ----------------------------------------------------------------------------
; Song Data
; ----------------------------------------------------------------------------

song_data:
            !byte 0, 1
            !byte 2, 2
            !byte 4, 3
            !byte 6, 1

            !byte 8, 2
            !byte 10, 3
            !byte 12, 1
            !byte 14, 2

            !byte 16, 3
            !byte 18, 1
            !byte 20, 2
            !byte 22, 3

            !byte 24, 1
            !byte 25, 2
            !byte 26, 3
            !byte 28, 1
            !byte 29, 2
            !byte 30, 3

            !byte $FF

; ----------------------------------------------------------------------------
; Note Arrays
; ----------------------------------------------------------------------------

note_track:
            !fill MAX_NOTES, 0

note_col:
            !fill MAX_NOTES, 0

; ----------------------------------------------------------------------------
; Game Variables
; ----------------------------------------------------------------------------

score_lo:   !byte 0
score_hi:   !byte 0
miss_count: !byte 0
health:     !byte 0

Try This: Different Balance

Adjust the difficulty by changing health values:

HEALTH_PERFECT = 2              ; Less reward
HEALTH_GOOD   = 1
HEALTH_MISS   = 12              ; Harsher punishment

This makes the game harder - misses hurt more, hits help less. Find a balance that feels challenging but fair.

Try This: Visual Health Feedback

Change the bar colour based on health level:

display_health:
            lda health
            cmp #16
            bcc health_critical
            lda #5              ; Green
            jmp set_health_col
health_critical:
            lda #2              ; Red when low
set_health_col:
            sta health_colour
            ; ... use health_colour instead of HEALTH_COL

Red when critical makes danger obvious.

What You’ve Learnt

  • Clamped values - Preventing overflow and underflow with comparisons
  • Bit shifting for division - LSR divides by 2, three shifts divide by 8
  • Game state flags - A simple variable to track game over condition
  • Visual bars - Custom characters for progress display

What’s Next

In Unit 9, we’ll design the song data format. Real songs with multiple patterns, variable timing, and musical structure. The placeholder test pattern becomes actual music.

What Changed

Unit 7 → Unit 8
+234-27
11 ; ============================================================================
2-; SID SYMPHONY - Unit 7: Miss Handling
2+; SID SYMPHONY - Unit 8: Health System
33 ; ============================================================================
4-; Notes that scroll past unhit trigger negative feedback. A harsh noise burst
5-; and red flash punish mistakes. Miss count tracks failures.
4+; A health meter that grows on hits and shrinks on misses. The game can be
5+; lost. Stakes make success meaningful.
66 ;
77 ; Controls: Z = Track 1 (high), X = Track 2 (mid), C = Track 3 (low)
88 ; ============================================================================
...
4949 PERFECT_COL = 1 ; White - perfect hit border flash
5050 GOOD_COL = 7 ; Yellow - good hit border flash
5151 MISS_COL = 2 ; Red - miss border flash
52+
53+HEALTH_COL = 5 ; Green for health bar
5254
5355 ; ============================================================================
5456 ; SCORING SETTINGS
...
5658
5759 PERFECT_SCORE = 100
5860 GOOD_SCORE = 50
61+
62+; ============================================================================
63+; HEALTH SETTINGS
64+; ============================================================================
65+
66+HEALTH_MAX = 64 ; Maximum health
67+HEALTH_START = 32 ; Starting health (half)
68+HEALTH_PERFECT = 4 ; Health gained on perfect hit
69+HEALTH_GOOD = 2 ; Health gained on good hit
70+HEALTH_MISS = 8 ; Health lost on miss
5971
6072 ; ============================================================================
6173 ; HIT DETECTION SETTINGS
...
110122 TRACK1_ROW = 8
111123 TRACK2_ROW = 12
112124 TRACK3_ROW = 16
125+
126+; Health bar position
127+HEALTH_ROW = 23
113128
114129 ; Hit zone
115130 HIT_ZONE_COLUMN = 3
...
119134 CHAR_TRACK = 129
120135 CHAR_HITZONE = 130
121136 CHAR_SPACE = 32
137+CHAR_BAR_FULL = 131
138+CHAR_BAR_EMPTY = 132
122139
123140 ; Note settings
124141 MAX_NOTES = 8
...
140157 key_pressed = $07
141158 hit_quality = $08
142159 border_flash = $09
143-miss_track = $0A ; Track of missed note (for flash)
160+miss_track = $0A
161+game_over = $0B ; Non-zero when game ended
144162
145163 ; ----------------------------------------------------------------------------
146164 ; BASIC Stub
...
167185 jsr init_sid
168186 jsr init_notes
169187 jsr init_score
188+ jsr init_health
170189
171190 lda #<song_data
172191 sta song_pos
...
177196 sta frame_count
178197 sta beat_count
179198 sta border_flash
199+ sta game_over
180200
181201 main_loop:
182202 lda #$FF
183203 wait_raster:
184204 cmp $D012
185205 bne wait_raster
206+
207+ ; Check if game is over
208+ lda game_over
209+ bne main_loop ; Freeze if game over
186210
187211 inc frame_count
188212 lda frame_count
...
252276 ; ----------------------------------------------------------------------------
253277
254278 define_custom_chars:
279+ ; Note character - diamond shape
255280 lda #%00000110
256281 sta CHARSET + (CHAR_NOTE * 8) + 0
257282 lda #%00011110
...
269294 lda #%00000110
270295 sta CHARSET + (CHAR_NOTE * 8) + 7
271296
297+ ; Track character - horizontal line
272298 lda #%00000000
273299 sta CHARSET + (CHAR_TRACK * 8) + 0
274300 lda #%00000000
...
286312 lda #%00000000
287313 sta CHARSET + (CHAR_TRACK * 8) + 7
288314
315+ ; Hit zone character - vertical bars
289316 lda #%01100110
290317 sta CHARSET + (CHAR_HITZONE * 8) + 0
291318 sta CHARSET + (CHAR_HITZONE * 8) + 1
...
295322 sta CHARSET + (CHAR_HITZONE * 8) + 5
296323 sta CHARSET + (CHAR_HITZONE * 8) + 6
297324 sta CHARSET + (CHAR_HITZONE * 8) + 7
325+
326+ ; Health bar full - solid block
327+ lda #%11111111
328+ sta CHARSET + (CHAR_BAR_FULL * 8) + 0
329+ sta CHARSET + (CHAR_BAR_FULL * 8) + 1
330+ sta CHARSET + (CHAR_BAR_FULL * 8) + 2
331+ sta CHARSET + (CHAR_BAR_FULL * 8) + 3
332+ sta CHARSET + (CHAR_BAR_FULL * 8) + 4
333+ sta CHARSET + (CHAR_BAR_FULL * 8) + 5
334+ sta CHARSET + (CHAR_BAR_FULL * 8) + 6
335+ sta CHARSET + (CHAR_BAR_FULL * 8) + 7
336+
337+ ; Health bar empty - outline
338+ lda #%11111111
339+ sta CHARSET + (CHAR_BAR_EMPTY * 8) + 0
340+ lda #%10000001
341+ sta CHARSET + (CHAR_BAR_EMPTY * 8) + 1
342+ sta CHARSET + (CHAR_BAR_EMPTY * 8) + 2
343+ sta CHARSET + (CHAR_BAR_EMPTY * 8) + 3
344+ sta CHARSET + (CHAR_BAR_EMPTY * 8) + 4
345+ sta CHARSET + (CHAR_BAR_EMPTY * 8) + 5
346+ sta CHARSET + (CHAR_BAR_EMPTY * 8) + 6
347+ lda #%11111111
348+ sta CHARSET + (CHAR_BAR_EMPTY * 8) + 7
349+
350+ rts
351+
352+; ----------------------------------------------------------------------------
353+; Initialize Health
354+; ----------------------------------------------------------------------------
298355
356+init_health:
357+ lda #HEALTH_START
358+ sta health
359+ jsr display_health
299360 rts
300361
301362 ; ----------------------------------------------------------------------------
...
396457 rts
397458
398459 ; ----------------------------------------------------------------------------
399-; Update Notes - Now detects misses
460+; Update Notes
400461 ; ----------------------------------------------------------------------------
401462
402463 update_notes:
...
411472 dec note_col,x
412473 lda note_col,x
413474 cmp #1
414- bcc update_miss ; Note passed hit zone - MISS!
475+ bcc update_miss
415476
416477 jsr draw_note
417478 jmp update_next
418479
419480 update_miss:
420- ; Note fell through - record the miss
421481 lda note_track,x
422- sta miss_track ; Save which track for flash
482+ sta miss_track
423483 lda #0
424- sta note_track,x ; Deactivate note
425- jsr handle_miss ; Process the miss
484+ sta note_track,x
485+ jsr handle_miss
426486
427487 update_next:
428488 inx
...
431491 rts
432492
433493 ; ----------------------------------------------------------------------------
434-; Handle Miss - Negative feedback for missed notes
494+; Handle Miss
435495 ; ----------------------------------------------------------------------------
436496
437497 handle_miss:
438- ; Increment miss counter
439498 inc miss_count
440499
441- ; Play miss sound (harsh noise burst)
442500 jsr play_miss_sound
443501
444- ; Red border flash
445502 lda #MISS_COL
446503 sta BORDER
447- lda #8 ; Flash for 8 frames
504+ lda #8
448505 sta border_flash
449506
450- ; Update miss display
451507 jsr display_misses
508+
509+ ; Decrease health
510+ jsr decrease_health
452511
453512 rts
454513
455514 ; ----------------------------------------------------------------------------
456-; Play Miss Sound - Harsh noise burst
515+; Decrease Health - Subtract HEALTH_MISS, clamp at 0
516+; ----------------------------------------------------------------------------
517+
518+decrease_health:
519+ lda health
520+ sec
521+ sbc #HEALTH_MISS
522+ bcc health_zero ; Underflow - set to 0
523+ sta health
524+ jsr display_health
525+ jsr check_game_over
526+ rts
527+
528+health_zero:
529+ lda #0
530+ sta health
531+ jsr display_health
532+ jsr check_game_over
533+ rts
534+
535+; ----------------------------------------------------------------------------
536+; Increase Health - Add amount, clamp at HEALTH_MAX
537+; Input: A = amount to add
538+; ----------------------------------------------------------------------------
539+
540+increase_health:
541+ clc
542+ adc health
543+ cmp #HEALTH_MAX
544+ bcc health_ok
545+ lda #HEALTH_MAX ; Clamp to max
546+health_ok:
547+ sta health
548+ jsr display_health
549+ rts
550+
551+; ----------------------------------------------------------------------------
552+; Check Game Over
553+; ----------------------------------------------------------------------------
554+
555+check_game_over:
556+ lda health
557+ bne not_game_over
558+
559+ ; Game over!
560+ lda #1
561+ sta game_over
562+
563+ ; Display game over message
564+ jsr show_game_over
565+
566+not_game_over:
567+ rts
568+
569+; ----------------------------------------------------------------------------
570+; Show Game Over
571+; ----------------------------------------------------------------------------
572+
573+show_game_over:
574+ ; Display "GAME OVER" in centre of screen
575+ ldx #0
576+game_over_loop:
577+ lda game_over_text,x
578+ beq game_over_done
579+ sta SCREEN + (12 * 40) + 15,x
580+ lda #2 ; Red
581+ sta COLRAM + (12 * 40) + 15,x
582+ inx
583+ jmp game_over_loop
584+game_over_done:
585+
586+ ; Flash border red
587+ lda #2
588+ sta BORDER
589+
590+ rts
591+
592+game_over_text:
593+ !scr "game over"
594+ !byte 0
595+
596+; ----------------------------------------------------------------------------
597+; Display Health - 8 character bar
598+; ----------------------------------------------------------------------------
599+
600+display_health:
601+ ; Health ranges 0-64, displayed as 8 characters
602+ ; Each character represents 8 health points
603+
604+ lda health
605+ lsr ; Divide by 8
606+ lsr
607+ lsr
608+ sta temp_health ; Full bars to draw
609+
610+ ; Draw full bars
611+ ldx #0
612+ lda temp_health
613+ beq draw_empty_bars
614+
615+draw_full_bars:
616+ lda #CHAR_BAR_FULL
617+ sta SCREEN + (HEALTH_ROW * 40) + 16,x
618+ lda #HEALTH_COL
619+ sta COLRAM + (HEALTH_ROW * 40) + 16,x
620+ inx
621+ cpx temp_health
622+ bne draw_full_bars
623+
624+ ; Draw empty bars for remainder
625+draw_empty_bars:
626+ cpx #8
627+ beq health_done
628+ lda #CHAR_BAR_EMPTY
629+ sta SCREEN + (HEALTH_ROW * 40) + 16,x
630+ lda #11 ; Dark grey
631+ sta COLRAM + (HEALTH_ROW * 40) + 16,x
632+ inx
633+ jmp draw_empty_bars
634+
635+health_done:
636+ rts
637+
638+temp_health: !byte 0
639+
640+; ----------------------------------------------------------------------------
641+; Play Miss Sound
457642 ; ----------------------------------------------------------------------------
458643
459644 play_miss_sound:
460- ; Use voice 3 for miss sound (temporary override)
461645 lda #0
462646 sta SID_V3_FREQ_LO
463647 lda #MISS_FREQ
...
467651 lda #MISS_SR
468652 sta SID_V3_SR
469653 lda #MISS_WAVE
470- ora #$01 ; Gate on
654+ ora #$01
471655 sta SID_V3_CTRL
472656 rts
473657
...
771955 lda miss_label,x
772956 beq draw_miss_label_done
773957 sta SCREEN + 15,x
774- lda #2 ; Red
958+ lda #2
775959 sta COLRAM + 15,x
776960 inx
777961 bne draw_miss_label
...
788972 inx
789973 bne draw_title
790974 draw_title_done:
975+
976+ ; Draw "HEALTH:" label
977+ ldx #0
978+draw_health_label:
979+ lda health_label,x
980+ beq draw_health_label_done
981+ sta SCREEN + (HEALTH_ROW * 40) + 8,x
982+ lda #5 ; Green
983+ sta COLRAM + (HEALTH_ROW * 40) + 8,x
984+ inx
985+ bne draw_health_label
986+draw_health_label_done:
791987
792988 ; Track labels
793989 lda #$1A ; Z
...
8171013
8181014 title_text:
8191015 !scr "sid symphony"
1016+ !byte 0
1017+
1018+health_label:
1019+ !scr "health:"
8201020 !byte 0
8211021
8221022 ; ----------------------------------------------------------------------------
...
10671267 sta BORDER
10681268 lda #4
10691269 sta border_flash
1270+
1271+ ; Increase health for good hit
1272+ lda #HEALTH_GOOD
1273+ jsr increase_health
10701274
10711275 jmp award_done
10721276
...
10831287 sta BORDER
10841288 lda #6
10851289 sta border_flash
1290+
1291+ ; Increase health for perfect hit
1292+ lda #HEALTH_PERFECT
1293+ jsr increase_health
10861294
10871295 award_done:
10881296 jsr display_score
...
11761384 rts
11771385
11781386 ; ----------------------------------------------------------------------------
1179-; Display Misses - Simple 2-digit display
1387+; Display Misses
11801388 ; ----------------------------------------------------------------------------
11811389
11821390 display_misses:
11831391 lda miss_count
11841392
1185- ; Tens digit
11861393 ldx #0
11871394 miss_div_10:
11881395 cmp #10
...
11921399 inx
11931400 jmp miss_div_10
11941401 miss_done_10:
1195- ; X = tens, A = ones
1196- pha ; Save ones
1402+ pha
11971403 txa
11981404 ora #$30
1199- sta SCREEN + 21 ; Tens digit
1405+ sta SCREEN + 21
12001406 pla
12011407 ora #$30
1202- sta SCREEN + 22 ; Ones digit
1408+ sta SCREEN + 22
12031409
1204- lda #2 ; Red
1410+ lda #2
12051411 sta COLRAM + 21
12061412 sta COLRAM + 22
12071413
...
13181524 score_lo: !byte 0
13191525 score_hi: !byte 0
13201526 miss_count: !byte 0
1527+health: !byte 0
13211528