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

Sound Effects

Every action has audio feedback. Menu select, claim, error, victory, draw.

22% of Ink War

We already have two sounds: a rising tone when claiming cells, and a harsh buzz for invalid moves. But the game is missing audio feedback for other events — selecting menu options, game outcomes. This unit completes the audio experience.

The ZX Spectrum’s beeper is simple: one bit controls the speaker. But by toggling it at different rates, we can create distinct sounds for every game event.

Run It

pasmonext --sna inkwar.asm inkwar.sna

Unit 14 Screenshot

Select a menu option and hear the confirmation beep. Claim cells and hear the rising tone. Win and hear the victory fanfare.

Sound Design Philosophy

Each sound should be:

  • Distinctive — Immediately identifiable
  • Appropriate — Matches the emotional context
  • Brief — Doesn’t interrupt gameplay
EventSound CharacterPurpose
Menu selectQuick high beepConfirms input
Cell claimRising tonePositive feedback
Invalid moveLow buzzNegative feedback
VictoryAscending fanfareCelebration
DrawNeutral two-toneNeither win nor loss

A quick, high-pitched beep confirms menu choices:

; ----------------------------------------------------------------------------
; Sound - Menu Select
; ----------------------------------------------------------------------------
; Quick high beep for menu selection

sound_select:
            ld      b, 15               ; Short duration

.ss_loop:
            push    bc

            ld      a, $10
            out     (KEY_PORT), a
            ld      c, 20               ; High pitch (short delay)
.ss_delay1:
            dec     c
            jr      nz, .ss_delay1

            xor     a
            out     (KEY_PORT), a
            ld      c, 20
.ss_delay2:
            dec     c
            jr      nz, .ss_delay2

            pop     bc
            djnz    .ss_loop

            ret

Short duration (15 cycles) and high pitch (delay of 20) creates a crisp “click” feeling. We add call sound_select at the start of each menu handler.

Victory Fanfare

When someone wins, we play three ascending notes — a triumphant scale:

; ----------------------------------------------------------------------------
; Sound - Victory
; ----------------------------------------------------------------------------
; Triumphant ascending fanfare - three rising notes

sound_victory:
            ; Play three ascending notes
            ld      hl, 60              ; Starting pitch (low)
            ld      b, 3                ; Three notes

.sv_note:
            push    bc
            push    hl

            ; Play note
            ld      b, 25               ; Note duration
.sv_tone:
            push    bc
            ld      a, $10
            out     (KEY_PORT), a
            ld      b, l
.sv_d1:     djnz    .sv_d1
            xor     a
            out     (KEY_PORT), a
            ld      b, l
.sv_d2:     djnz    .sv_d2
            pop     bc
            djnz    .sv_tone

            pop     hl
            pop     bc

            ; Decrease delay (higher pitch) for next note
            ld      a, l
            sub     15
            ld      l, a

            ; Brief pause between notes
            push    bc
            ld      bc, 2000
.sv_pause:  dec     bc
            ld      a, b
            or      c
            jr      nz, .sv_pause
            pop     bc

            djnz    .sv_note
            ret

The pitch decreases with each note (delay decreases = higher frequency). A brief pause between notes creates distinct tones rather than a continuous sweep.

Draw Sound

A draw gets a neutral sound — two identical tones:

; ----------------------------------------------------------------------------
; Sound - Draw
; ----------------------------------------------------------------------------
; Neutral two-tone sound for draw - same pitch repeated

sound_draw:
            ; First tone (mid pitch)
            ld      b, 20
.sd_tone1:
            push    bc
            ld      a, $10
            out     (KEY_PORT), a
            ld      c, 40
.sd_d1:     dec     c
            jr      nz, .sd_d1
            xor     a
            out     (KEY_PORT), a
            ld      c, 40
.sd_d2:     dec     c
            jr      nz, .sd_d2
            pop     bc
            djnz    .sd_tone1

            ; Brief pause
            ld      bc, 1500
.sd_pause:  dec     bc
            ld      a, b
            or      c
            jr      nz, .sd_pause

            ; Second tone (same pitch - neutral)
            ld      b, 20
.sd_tone2:
            ; ... same as first tone ...
            djnz    .sd_tone2

            ret

Same pitch for both tones signals neither triumph nor defeat — just equilibrium.

Result Sound Dispatch

We need to play the right sound based on the game outcome:

play_result_sound:
            ; Compare scores
            ld      a, (p1_count)
            ld      b, a
            ld      a, (p2_count)
            cp      b
            jr      z, .prs_draw        ; Equal = draw
            ; Someone won - play victory
            call    sound_victory
            ret
.prs_draw:
            call    sound_draw
            ret

We check if scores are equal (draw) or different (someone won). The victory sound plays regardless of which player won — both humans and AI get the same celebration.

Beeper Fundamentals

The Spectrum beeper works by toggling bit 4 of port $FE:

ld      a, $10              ; Bit 4 = speaker
out     (KEY_PORT), a       ; Speaker on
; ... delay ...
xor     a
out     (KEY_PORT), a       ; Speaker off
; ... delay ...

The delay duration determines the pitch:

  • Short delay (20) = High pitch
  • Long delay (80) = Low pitch

The number of cycles determines the duration:

  • Few cycles (15) = Brief sound
  • Many cycles (30) = Longer sound

Sound Placement

We added sound calls at key points:

  • state_title: call sound_select before starting game
  • try_claim/.tc_valid: Already had call sound_claim
  • try_claim/.tc_invalid: Already had call sound_error
  • Game over: call play_result_sound before showing results

The Complete Code

; ============================================================================
; INK WAR - Unit 14: Sound Effects
; ============================================================================
; Complete audio feedback: menu select, claim, error, turn change, victory.
; The beeper gives every action a distinctive sound.
;
; Controls: Q=Up, A=Down, O=Left, P=Right, SPACE=Claim
;           1=Two Player, 2=AI Easy, 3=AI Medium, 4=AI Hard
; ============================================================================

            org     32768

; ----------------------------------------------------------------------------
; Constants
; ----------------------------------------------------------------------------

ATTR_BASE   equ     $5800
DISPLAY_FILE equ    $4000
CHAR_SET    equ     $3C00           ; ROM character set base address

BOARD_ROW   equ     8
BOARD_COL   equ     12
BOARD_SIZE  equ     8

; Display positions
SCORE_ROW   equ     2               ; Score display row
P1_SCORE_COL equ    10              ; "P1: nn" column
P2_SCORE_COL equ    18              ; "P2: nn" column
TURN_ROW    equ     4               ; Turn indicator row
TURN_COL    equ     14              ; Turn indicator column
RESULT_ROW  equ     20              ; Winner message row
RESULT_COL  equ     11              ; Winner message column

; Game constants
TOTAL_CELLS equ     64              ; 8x8 board

; Customised colours (from Unit 3)
EMPTY_ATTR  equ     %01101000       ; Cyan paper + BRIGHT
BORDER_ATTR equ     %01110000       ; Yellow paper + BRIGHT
CURSOR_ATTR equ     %01111000       ; White paper + BRIGHT

P1_ATTR     equ     %01010000       ; Red paper + BRIGHT
P2_ATTR     equ     %01001000       ; Blue paper + BRIGHT
P1_CURSOR   equ     %01111010       ; White paper + Red ink + BRIGHT
P2_CURSOR   equ     %01111001       ; White paper + Blue ink + BRIGHT

; Text display attributes
TEXT_ATTR   equ     %00111000       ; White paper, black ink
P1_TEXT     equ     %01010111       ; Red paper, white ink + BRIGHT
P2_TEXT     equ     %01001111       ; Blue paper, white ink + BRIGHT

P1_BORDER   equ     2
P2_BORDER   equ     1
ERROR_BORDER equ    2               ; Red border for errors

; Keyboard ports
KEY_PORT    equ     $fe
ROW_QAOP    equ     $fb
ROW_ASDF    equ     $fd
ROW_YUIOP   equ     $df
ROW_SPACE   equ     $7f

; Cell states
STATE_EMPTY equ     0
STATE_P1    equ     1
STATE_P2    equ     2

; Game states (state machine)
GS_TITLE    equ     0
GS_PLAYING  equ     1
GS_RESULTS  equ     2

; Game modes
GM_TWO_PLAYER equ   0
GM_VS_AI    equ     1

; AI difficulty levels
AI_EASY     equ     0               ; Random moves
AI_MEDIUM   equ     1               ; Adjacent priority only
AI_HARD     equ     2               ; Full strategy (defense + position)

; AI timing
AI_DELAY    equ     25              ; Frames before AI moves (~0.5 sec)

; Title screen positions
TITLE_ROW   equ     6
TITLE_COL   equ     12              ; "INK WAR" (7 chars) centred
MODE1_ROW   equ     12              ; "1 - TWO PLAYER"
MODE1_COL   equ     9
MODE2_ROW   equ     14              ; "2 - AI EASY"
MODE2_COL   equ     10
MODE3_ROW   equ     16              ; "3 - AI MEDIUM"
MODE3_COL   equ     9
MODE4_ROW   equ     18              ; "4 - AI HARD"
MODE4_COL   equ     10

; Results screen positions
CONTINUE_ROW equ    22              ; "PRESS ANY KEY" after results
CONTINUE_COL equ    9               ; (32-13)/2 = 9.5

; Input timing
KEY_DELAY   equ     8               ; Frames between key repeats

; ----------------------------------------------------------------------------
; Entry Point
; ----------------------------------------------------------------------------

start:
            ; Start at title screen
            ld      a, GS_TITLE
            ld      (game_state), a
            call    init_screen
            call    draw_title_screen

            ; Black border for title
            xor     a
            out     (KEY_PORT), a

main_loop:
            halt

            ; Dispatch based on game state
            ld      a, (game_state)
            or      a
            jr      z, state_title      ; GS_TITLE = 0
            cp      GS_PLAYING
            jr      z, state_playing
            ; Must be GS_RESULTS - handled inline after game over

            jp      main_loop

; ----------------------------------------------------------------------------
; State: Title
; ----------------------------------------------------------------------------
; Waits for 1 (Two Player), 2 (AI Easy), 3 (AI Medium), 4 (AI Hard)

ROW_12345   equ     $f7             ; Keyboard row for 1,2,3,4,5

state_title:
            ; Read keyboard row for keys 1-5
            ld      a, ROW_12345
            in      a, (KEY_PORT)

            ; Check for key 1 (Two Player)
            bit     0, a                ; Key 1
            jr      z, .st_two_player

            ; Check for key 2 (AI Easy)
            bit     1, a                ; Key 2
            jr      z, .st_ai_easy

            ; Check for key 3 (AI Medium)
            bit     2, a                ; Key 3
            jr      z, .st_ai_medium

            ; Check for key 4 (AI Hard)
            bit     3, a                ; Key 4
            jr      z, .st_ai_hard

            jp      main_loop           ; No valid key - keep waiting

.st_two_player:
            call    sound_select
            xor     a                   ; GM_TWO_PLAYER = 0
            ld      (game_mode), a
            call    start_game
            jp      main_loop

.st_ai_easy:
            call    sound_select
            ld      a, GM_VS_AI
            ld      (game_mode), a
            ld      a, AI_EASY
            ld      (ai_difficulty), a
            call    start_game
            jp      main_loop

.st_ai_medium:
            call    sound_select
            ld      a, GM_VS_AI
            ld      (game_mode), a
            ld      a, AI_MEDIUM
            ld      (ai_difficulty), a
            call    start_game
            jp      main_loop

.st_ai_hard:
            call    sound_select
            ld      a, GM_VS_AI
            ld      (game_mode), a
            ld      a, AI_HARD
            ld      (ai_difficulty), a
            call    start_game
            jp      main_loop

; ----------------------------------------------------------------------------
; State: Playing
; ----------------------------------------------------------------------------

state_playing:
            ; Check if AI's turn (Player 2 in vs AI mode)
            ld      a, (game_mode)
            or      a
            jr      z, .sp_human        ; Two player mode - human controls

            ; vs AI mode - check if Player 2's turn
            ld      a, (current_player)
            cp      2
            jr      z, .sp_ai_turn

.sp_human:
            ; Human player's turn
            call    read_keyboard
            call    handle_input
            jp      main_loop

.sp_ai_turn:
            ; AI's turn - use delay counter
            ld      a, (ai_timer)
            or      a
            jr      z, .sp_ai_move      ; Timer expired, make move

            ; Still waiting
            dec     a
            ld      (ai_timer), a
            jp      main_loop

.sp_ai_move:
            ; Reset timer for next AI turn
            ld      a, AI_DELAY
            ld      (ai_timer), a

            ; AI makes a move
            call    ai_make_move
            jp      main_loop

; ----------------------------------------------------------------------------
; Start Game
; ----------------------------------------------------------------------------
; Transitions from title to playing state

start_game:
            ld      a, GS_PLAYING
            ld      (game_state), a

            call    init_screen
            call    init_game
            call    draw_board_border
            call    draw_board
            call    draw_ui
            call    draw_cursor
            call    update_border

            ; Wait for key release before playing
            call    wait_key_release

            ret

; ----------------------------------------------------------------------------
; Initialise Screen
; ----------------------------------------------------------------------------

init_screen:
            xor     a
            out     (KEY_PORT), a

            ; Clear display file (pixels)
            ld      hl, DISPLAY_FILE
            ld      de, DISPLAY_FILE+1
            ld      bc, 6143
            ld      (hl), 0
            ldir

            ; Clear all attributes to white paper, black ink
            ld      hl, ATTR_BASE
            ld      de, ATTR_BASE+1
            ld      bc, 767
            ld      (hl), TEXT_ATTR     ; White background for text areas
            ldir

            ret

; ----------------------------------------------------------------------------
; Draw UI
; ----------------------------------------------------------------------------
; Draws score display and turn indicator

draw_ui:
            call    draw_scores
            call    draw_turn_indicator
            ret

; ----------------------------------------------------------------------------
; Draw Scores
; ----------------------------------------------------------------------------
; Displays "P1: nn  P2: nn" with player colours

draw_scores:
            ; Count cells for each player
            call    count_cells

            ; Draw P1 label "P1:"
            ld      b, SCORE_ROW
            ld      c, P1_SCORE_COL
            ld      a, 'P'
            call    print_char
            inc     c
            ld      a, '1'
            call    print_char
            inc     c
            ld      a, ':'
            call    print_char
            inc     c

            ; Print P1 score
            ld      a, (p1_count)
            call    print_two_digits

            ; Set P1 colour attribute
            ld      a, SCORE_ROW
            ld      c, P1_SCORE_COL
            ld      b, 5                ; "P1:nn" = 5 characters
            ld      e, P1_TEXT
            call    set_attr_range

            ; Draw P2 label "P2:"
            ld      b, SCORE_ROW
            ld      c, P2_SCORE_COL
            ld      a, 'P'
            call    print_char
            inc     c
            ld      a, '2'
            call    print_char
            inc     c
            ld      a, ':'
            call    print_char
            inc     c

            ; Print P2 score
            ld      a, (p2_count)
            call    print_two_digits

            ; Set P2 colour attribute
            ld      a, SCORE_ROW
            ld      c, P2_SCORE_COL
            ld      b, 5
            ld      e, P2_TEXT
            call    set_attr_range

            ret

; ----------------------------------------------------------------------------
; Draw Turn Indicator
; ----------------------------------------------------------------------------
; Shows "TURN" with current player's colour

draw_turn_indicator:
            ; Print "TURN"
            ld      b, TURN_ROW
            ld      c, TURN_COL
            ld      a, 'T'
            call    print_char
            inc     c
            ld      a, 'U'
            call    print_char
            inc     c
            ld      a, 'R'
            call    print_char
            inc     c
            ld      a, 'N'
            call    print_char

            ; Set attribute based on current player
            ld      a, (current_player)
            cp      1
            jr      z, .dti_p1
            ld      e, P2_TEXT
            jr      .dti_set
.dti_p1:
            ld      e, P1_TEXT

.dti_set:
            ld      a, TURN_ROW
            ld      c, TURN_COL
            ld      b, 4                ; "TURN" = 4 chars
            call    set_attr_range

            ret

; ----------------------------------------------------------------------------
; Print Character
; ----------------------------------------------------------------------------
; A = ASCII character (32-127), B = row (0-23), C = column (0-31)
; Writes character directly to display file using ROM character set

print_char:
            push    bc
            push    de
            push    hl
            push    af

            ; Calculate character data address: CHAR_SET + char*8
            ld      l, a
            ld      h, 0
            add     hl, hl
            add     hl, hl
            add     hl, hl          ; HL = char * 8
            ld      de, CHAR_SET
            add     hl, de          ; HL = source address

            push    hl              ; Save character data address

            ; Calculate display file address
            ; Screen address: high byte varies with row, low byte = column
            ld      a, b            ; A = row (0-23)
            and     %00011000       ; Get which third (0, 8, 16)
            add     a, $40          ; Add display file base high byte
            ld      d, a

            ld      a, b            ; A = row
            and     %00000111       ; Get line within character row
            rrca
            rrca
            rrca                    ; Shift to bits 5-7
            add     a, c            ; Add column
            ld      e, a            ; DE = screen address

            pop     hl              ; HL = character data

            ; Copy 8 bytes (8 pixel rows of character)
            ld      b, 8
.pc_loop:
            ld      a, (hl)
            ld      (de), a
            inc     hl
            inc     d               ; Next screen line (add 256)
            djnz    .pc_loop

            pop     af
            pop     hl
            pop     de
            pop     bc
            ret

; ----------------------------------------------------------------------------
; Print Two Digits
; ----------------------------------------------------------------------------
; A = number (0-99), B = row, C = column (will advance by 2)
; Prints number as two digits

print_two_digits:
            push    bc

            ; Calculate tens digit
            ld      d, 0            ; Tens counter
.ptd_tens:
            cp      10
            jr      c, .ptd_print
            sub     10
            inc     d
            jr      .ptd_tens

.ptd_print:
            push    af              ; Save units digit

            ; Print tens digit
            ld      a, d
            add     a, '0'
            call    print_char
            inc     c

            ; Print units digit
            pop     af
            add     a, '0'
            call    print_char
            inc     c

            pop     bc
            ret

; ----------------------------------------------------------------------------
; Count Cells
; ----------------------------------------------------------------------------
; Counts cells owned by each player

count_cells:
            xor     a
            ld      (p1_count), a
            ld      (p2_count), a

            ld      hl, board_state
            ld      b, 64               ; 64 cells

.cc_loop:
            ld      a, (hl)
            cp      STATE_P1
            jr      nz, .cc_not_p1
            ld      a, (p1_count)
            inc     a
            ld      (p1_count), a
            jr      .cc_next
.cc_not_p1:
            cp      STATE_P2
            jr      nz, .cc_next
            ld      a, (p2_count)
            inc     a
            ld      (p2_count), a
.cc_next:
            inc     hl
            djnz    .cc_loop

            ret

; ----------------------------------------------------------------------------
; Set Attribute Range
; ----------------------------------------------------------------------------
; A = row, C = start column, B = count, E = attribute

set_attr_range:
            push    bc
            push    de

            ; Calculate start address: ATTR_BASE + row*32 + col
            ld      l, a
            ld      h, 0
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl          ; HL = row * 32
            ld      a, c
            add     a, l
            ld      l, a
            ld      bc, ATTR_BASE
            add     hl, bc          ; HL = attribute address

            pop     de              ; E = attribute
            pop     bc              ; B = count

.sar_loop:
            ld      (hl), e
            inc     hl
            djnz    .sar_loop

            ret

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

update_border:
            ld      a, (current_player)
            cp      1
            jr      z, .ub_p1
            ld      a, P2_BORDER
            jr      .ub_set
.ub_p1:
            ld      a, P1_BORDER
.ub_set:
            out     (KEY_PORT), a
            ret

; ----------------------------------------------------------------------------
; Initialise Game State
; ----------------------------------------------------------------------------

init_game:
            ld      hl, board_state
            ld      b, 64
            xor     a
.ig_loop:
            ld      (hl), a
            inc     hl
            djnz    .ig_loop

            ld      a, 1
            ld      (current_player), a

            xor     a
            ld      (cursor_row), a
            ld      (cursor_col), a
            ld      (p1_count), a
            ld      (p2_count), a
            ld      (last_key), a           ; No previous key
            ld      (key_timer), a          ; No delay active

            ; Initialize AI timer
            ld      a, AI_DELAY
            ld      (ai_timer), a

            ret

; ----------------------------------------------------------------------------
; Draw Board Border
; ----------------------------------------------------------------------------

draw_board_border:
            ld      c, BOARD_ROW-1
            ld      d, BOARD_COL-1
            ld      b, BOARD_SIZE+2
            call    draw_border_row

            ld      c, BOARD_ROW+BOARD_SIZE
            ld      d, BOARD_COL-1
            ld      b, BOARD_SIZE+2
            call    draw_border_row

            ld      c, BOARD_ROW
            ld      d, BOARD_COL-1
            ld      b, BOARD_SIZE
            call    draw_border_col

            ld      c, BOARD_ROW
            ld      d, BOARD_COL+BOARD_SIZE
            ld      b, BOARD_SIZE
            call    draw_border_col

            ret

draw_border_row:
            push    bc
.dbr_loop:
            push    bc
            push    de

            ld      a, c
            ld      l, a
            ld      h, 0
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            ld      a, d
            add     a, l
            ld      l, a
            ld      bc, ATTR_BASE
            add     hl, bc

            ld      (hl), BORDER_ATTR

            pop     de
            pop     bc
            inc     d
            djnz    .dbr_loop
            pop     bc
            ret

draw_border_col:
            push    bc
.dbc_loop:
            push    bc
            push    de

            ld      a, c
            ld      l, a
            ld      h, 0
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            ld      a, d
            add     a, l
            ld      l, a
            ld      bc, ATTR_BASE
            add     hl, bc

            ld      (hl), BORDER_ATTR

            pop     de
            pop     bc
            inc     c
            djnz    .dbc_loop
            pop     bc
            ret

; ----------------------------------------------------------------------------
; Draw Board
; ----------------------------------------------------------------------------

draw_board:
            ld      b, BOARD_SIZE
            ld      c, BOARD_ROW

.db_row:
            push    bc

            ld      b, BOARD_SIZE
            ld      d, BOARD_COL

.db_col:
            push    bc

            ld      a, c
            ld      l, a
            ld      h, 0
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            ld      a, d
            add     a, l
            ld      l, a
            ld      bc, ATTR_BASE
            add     hl, bc

            ld      (hl), EMPTY_ATTR

            pop     bc
            inc     d
            djnz    .db_col

            pop     bc
            inc     c
            djnz    .db_row

            ret

; ----------------------------------------------------------------------------
; Draw Cursor
; ----------------------------------------------------------------------------

draw_cursor:
            call    get_cell_state

            cp      STATE_P1
            jr      z, .dc_p1
            cp      STATE_P2
            jr      z, .dc_p2

            ld      a, CURSOR_ATTR
            jr      .dc_set

.dc_p1:
            ld      a, P1_CURSOR
            jr      .dc_set

.dc_p2:
            ld      a, P2_CURSOR

.dc_set:
            push    af

            ld      a, (cursor_row)
            add     a, BOARD_ROW
            ld      l, a
            ld      h, 0
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            ld      a, (cursor_col)
            add     a, BOARD_COL
            add     a, l
            ld      l, a
            ld      bc, ATTR_BASE
            add     hl, bc

            pop     af
            ld      (hl), a

            ret

; ----------------------------------------------------------------------------
; Clear Cursor
; ----------------------------------------------------------------------------

clear_cursor:
            call    get_cell_state

            cp      STATE_P1
            jr      z, .clc_p1
            cp      STATE_P2
            jr      z, .clc_p2

            ld      a, EMPTY_ATTR
            jr      .clc_set

.clc_p1:
            ld      a, P1_ATTR
            jr      .clc_set

.clc_p2:
            ld      a, P2_ATTR

.clc_set:
            push    af

            ld      a, (cursor_row)
            add     a, BOARD_ROW
            ld      l, a
            ld      h, 0
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            ld      a, (cursor_col)
            add     a, BOARD_COL
            add     a, l
            ld      l, a
            ld      bc, ATTR_BASE
            add     hl, bc

            pop     af
            ld      (hl), a

            ret

; ----------------------------------------------------------------------------
; Get Cell State
; ----------------------------------------------------------------------------

get_cell_state:
            ld      a, (cursor_row)
            add     a, a
            add     a, a
            add     a, a
            ld      hl, board_state
            ld      b, 0
            ld      c, a
            add     hl, bc
            ld      a, (cursor_col)
            ld      c, a
            add     hl, bc
            ld      a, (hl)
            ret

; ----------------------------------------------------------------------------
; Read Keyboard
; ----------------------------------------------------------------------------

read_keyboard:
            xor     a
            ld      (key_pressed), a

            ld      a, ROW_QAOP
            in      a, (KEY_PORT)
            bit     0, a
            jr      nz, .rk_not_q
            ld      a, 1
            ld      (key_pressed), a
            ret
.rk_not_q:
            ld      a, ROW_ASDF
            in      a, (KEY_PORT)
            bit     0, a
            jr      nz, .rk_not_a
            ld      a, 2
            ld      (key_pressed), a
            ret
.rk_not_a:
            ld      a, ROW_YUIOP
            in      a, (KEY_PORT)
            bit     1, a
            jr      nz, .rk_not_o
            ld      a, 3
            ld      (key_pressed), a
            ret
.rk_not_o:
            ld      a, ROW_YUIOP
            in      a, (KEY_PORT)
            bit     0, a
            jr      nz, .rk_not_p
            ld      a, 4
            ld      (key_pressed), a
            ret
.rk_not_p:
            ld      a, ROW_SPACE
            in      a, (KEY_PORT)
            bit     0, a
            jr      nz, .rk_not_space
            ld      a, 5
            ld      (key_pressed), a
.rk_not_space:
            ret

; ----------------------------------------------------------------------------
; Handle Input
; ----------------------------------------------------------------------------
; Implements key repeat delay for smooth cursor movement

handle_input:
            ld      a, (key_pressed)
            or      a
            jr      nz, .hi_have_key

            ; No key pressed - reset tracking
            xor     a
            ld      (last_key), a
            ld      (key_timer), a
            ret

.hi_have_key:
            ; Space (claim) always works immediately
            cp      5
            jr      z, try_claim

            ; Check if same key as last frame
            ld      b, a                    ; Save current key
            ld      a, (last_key)
            cp      b
            jr      nz, .hi_new_key

            ; Same key - check timer
            ld      a, (key_timer)
            or      a
            jr      z, .hi_allow            ; Timer expired, allow repeat
            dec     a
            ld      (key_timer), a
            ret                             ; Still waiting

.hi_new_key:
            ; New key pressed - save it and reset timer
            ld      a, b
            ld      (last_key), a
            ld      a, KEY_DELAY
            ld      (key_timer), a

.hi_allow:
            ; Process movement
            call    clear_cursor

            ld      a, (key_pressed)

            cp      1
            jr      nz, .hi_not_up
            ld      a, (cursor_row)
            or      a
            jr      z, .hi_done
            dec     a
            ld      (cursor_row), a
            jr      .hi_done
.hi_not_up:
            cp      2
            jr      nz, .hi_not_down
            ld      a, (cursor_row)
            cp      BOARD_SIZE-1
            jr      z, .hi_done
            inc     a
            ld      (cursor_row), a
            jr      .hi_done
.hi_not_down:
            cp      3
            jr      nz, .hi_not_left
            ld      a, (cursor_col)
            or      a
            jr      z, .hi_done
            dec     a
            ld      (cursor_col), a
            jr      .hi_done
.hi_not_left:
            cp      4
            jr      nz, .hi_done
            ld      a, (cursor_col)
            cp      BOARD_SIZE-1
            jr      z, .hi_done
            inc     a
            ld      (cursor_col), a

.hi_done:
            call    draw_cursor
            ret

; ----------------------------------------------------------------------------
; Try Claim Cell
; ----------------------------------------------------------------------------

try_claim:
            call    get_cell_state
            or      a
            jr      z, .tc_valid

            ; Cell already claimed - error feedback
            call    sound_error
            call    flash_border_error
            call    update_border       ; Restore correct border colour
            ret

.tc_valid:
            ; Valid move - claim the cell
            call    claim_cell
            call    sound_claim

            ld      a, (current_player)
            xor     3
            ld      (current_player), a

            call    draw_ui             ; Update scores and turn indicator

            ; Check if game is over
            call    check_game_over
            or      a
            jr      z, .tc_continue

            ; Game over - play appropriate sound and show results
            call    play_result_sound
            call    show_results
            call    victory_celebration
            call    wait_for_key

            ; Return to title screen
            ld      a, GS_TITLE
            ld      (game_state), a
            call    init_screen
            call    draw_title_screen
            xor     a
            out     (KEY_PORT), a       ; Black border for title
            ret

.tc_continue:
            call    update_border
            call    draw_cursor

            ret

; ----------------------------------------------------------------------------
; Claim Cell
; ----------------------------------------------------------------------------

claim_cell:
            ld      a, (cursor_row)
            add     a, a
            add     a, a
            add     a, a
            ld      hl, board_state
            ld      b, 0
            ld      c, a
            add     hl, bc
            ld      a, (cursor_col)
            ld      c, a
            add     hl, bc

            ld      a, (current_player)
            ld      (hl), a

            push    af

            ld      a, (cursor_row)
            add     a, BOARD_ROW
            ld      l, a
            ld      h, 0
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            ld      a, (cursor_col)
            add     a, BOARD_COL
            add     a, l
            ld      l, a
            ld      bc, ATTR_BASE
            add     hl, bc

            pop     af
            cp      1
            jr      z, .clm_is_p1
            ld      (hl), P2_ATTR
            ret
.clm_is_p1:
            ld      (hl), P1_ATTR
            ret

; ----------------------------------------------------------------------------
; Sound - Claim
; ----------------------------------------------------------------------------

sound_claim:
            ld      hl, 400
            ld      b, 20

.scl_loop:
            push    bc
            push    hl

            ld      b, h
            ld      c, l
.scl_tone:
            ld      a, $10
            out     (KEY_PORT), a
            call    .scl_delay
            xor     a
            out     (KEY_PORT), a
            call    .scl_delay
            dec     bc
            ld      a, b
            or      c
            jr      nz, .scl_tone

            pop     hl
            pop     bc

            ld      de, 20
            or      a
            sbc     hl, de

            djnz    .scl_loop
            ret

.scl_delay:
            push    bc
            ld      b, 5
.scl_delay_loop:
            djnz    .scl_delay_loop
            pop     bc
            ret

; ----------------------------------------------------------------------------
; Sound - Error
; ----------------------------------------------------------------------------
; Harsh buzz for invalid move

sound_error:
            ld      b, 30               ; Duration

.se_loop:
            push    bc

            ; Low frequency buzz (longer delay = lower pitch)
            ld      a, $10
            out     (KEY_PORT), a
            ld      c, 80               ; Longer delay for low pitch
.se_delay1:
            dec     c
            jr      nz, .se_delay1

            xor     a
            out     (KEY_PORT), a
            ld      c, 80
.se_delay2:
            dec     c
            jr      nz, .se_delay2

            pop     bc
            djnz    .se_loop

            ret

; ----------------------------------------------------------------------------
; Sound - Menu Select
; ----------------------------------------------------------------------------
; Quick high beep for menu selection

sound_select:
            ld      b, 15               ; Short duration

.ss_loop:
            push    bc

            ld      a, $10
            out     (KEY_PORT), a
            ld      c, 20               ; High pitch (short delay)
.ss_delay1:
            dec     c
            jr      nz, .ss_delay1

            xor     a
            out     (KEY_PORT), a
            ld      c, 20
.ss_delay2:
            dec     c
            jr      nz, .ss_delay2

            pop     bc
            djnz    .ss_loop

            ret

; ----------------------------------------------------------------------------
; Sound - Turn Change
; ----------------------------------------------------------------------------
; Brief descending tone when turn switches

sound_turn:
            ld      hl, 30              ; Starting pitch (high)
            ld      b, 8                ; Number of steps

.st_loop:
            push    bc
            push    hl

            ; Play tone at current pitch
            ld      b, 6                ; Cycles per pitch
.st_tone:
            push    bc
            ld      a, $10
            out     (KEY_PORT), a
            ld      b, l                ; Delay = pitch
.st_d1:     djnz    .st_d1
            xor     a
            out     (KEY_PORT), a
            ld      b, l
.st_d2:     djnz    .st_d2
            pop     bc
            djnz    .st_tone

            pop     hl
            pop     bc

            ; Increase delay (lower pitch)
            ld      de, 8
            add     hl, de

            djnz    .st_loop
            ret

; ----------------------------------------------------------------------------
; Sound - Victory
; ----------------------------------------------------------------------------
; Triumphant ascending fanfare

sound_victory:
            ; Play three ascending notes
            ld      hl, 60              ; Starting pitch (low)
            ld      b, 3                ; Three notes

.sv_note:
            push    bc
            push    hl

            ; Play note
            ld      b, 25               ; Note duration
.sv_tone:
            push    bc
            ld      a, $10
            out     (KEY_PORT), a
            ld      b, l
.sv_d1:     djnz    .sv_d1
            xor     a
            out     (KEY_PORT), a
            ld      b, l
.sv_d2:     djnz    .sv_d2
            pop     bc
            djnz    .sv_tone

            pop     hl
            pop     bc

            ; Decrease delay (higher pitch) for next note
            ld      a, l
            sub     15
            ld      l, a

            ; Brief pause between notes
            push    bc
            ld      bc, 2000
.sv_pause:  dec     bc
            ld      a, b
            or      c
            jr      nz, .sv_pause
            pop     bc

            djnz    .sv_note
            ret

; ----------------------------------------------------------------------------
; Sound - Draw
; ----------------------------------------------------------------------------
; Neutral two-tone sound for draw

sound_draw:
            ; First tone (mid pitch)
            ld      b, 20
.sd_tone1:
            push    bc
            ld      a, $10
            out     (KEY_PORT), a
            ld      c, 40
.sd_d1:     dec     c
            jr      nz, .sd_d1
            xor     a
            out     (KEY_PORT), a
            ld      c, 40
.sd_d2:     dec     c
            jr      nz, .sd_d2
            pop     bc
            djnz    .sd_tone1

            ; Brief pause
            ld      bc, 1500
.sd_pause:  dec     bc
            ld      a, b
            or      c
            jr      nz, .sd_pause

            ; Second tone (same pitch - neutral)
            ld      b, 20
.sd_tone2:
            push    bc
            ld      a, $10
            out     (KEY_PORT), a
            ld      c, 40
.sd_d3:     dec     c
            jr      nz, .sd_d3
            xor     a
            out     (KEY_PORT), a
            ld      c, 40
.sd_d4:     dec     c
            jr      nz, .sd_d4
            pop     bc
            djnz    .sd_tone2

            ret

; ----------------------------------------------------------------------------
; Play Result Sound
; ----------------------------------------------------------------------------
; Plays victory or draw sound based on game result

play_result_sound:
            ; Compare scores
            ld      a, (p1_count)
            ld      b, a
            ld      a, (p2_count)
            cp      b
            jr      z, .prs_draw        ; Equal = draw
            ; Someone won - play victory
            call    sound_victory
            ret
.prs_draw:
            call    sound_draw
            ret

; ----------------------------------------------------------------------------
; Flash Border Error
; ----------------------------------------------------------------------------
; Flash border red briefly to indicate error

flash_border_error:
            ; Flash red 3 times
            ld      b, 3

.fbe_loop:
            push    bc

            ; Red border
            ld      a, ERROR_BORDER
            out     (KEY_PORT), a

            ; Short delay (about 3 frames)
            ld      bc, 8000
.fbe_delay1:
            dec     bc
            ld      a, b
            or      c
            jr      nz, .fbe_delay1

            ; Black border (brief off)
            xor     a
            out     (KEY_PORT), a

            ; Short delay
            ld      bc, 4000
.fbe_delay2:
            dec     bc
            ld      a, b
            or      c
            jr      nz, .fbe_delay2

            pop     bc
            djnz    .fbe_loop

            ret

; ----------------------------------------------------------------------------
; Check Game Over
; ----------------------------------------------------------------------------
; Returns A=1 if game is over (board full), A=0 otherwise

check_game_over:
            ; Game is over when p1_count + p2_count == 64
            ld      a, (p1_count)
            ld      b, a
            ld      a, (p2_count)
            add     a, b
            cp      TOTAL_CELLS
            jr      z, .cgo_over
            xor     a               ; Not over
            ret
.cgo_over:
            ld      a, 1            ; Game over
            ret

; ----------------------------------------------------------------------------
; Show Results
; ----------------------------------------------------------------------------
; Displays winner message based on scores

show_results:
            ; Clear turn indicator
            ld      a, TURN_ROW
            ld      c, TURN_COL
            ld      b, 4
            ld      e, TEXT_ATTR
            call    set_attr_range

            ; Determine winner
            ld      a, (p1_count)
            ld      b, a
            ld      a, (p2_count)
            cp      b
            jr      c, .sr_p1_wins      ; p2 < p1, so p1 wins
            jr      z, .sr_draw         ; p1 == p2, draw
            ; p2 > p1, p2 wins
            jr      .sr_p2_wins

.sr_p1_wins:
            ; Display "P1 WINS!"
            ld      b, RESULT_ROW
            ld      c, RESULT_COL
            ld      hl, msg_p1_wins
            ld      e, P1_TEXT
            call    print_message
            jr      .sr_continue

.sr_p2_wins:
            ; Display "P2 WINS!"
            ld      b, RESULT_ROW
            ld      c, RESULT_COL
            ld      hl, msg_p2_wins
            ld      e, P2_TEXT
            call    print_message
            jr      .sr_continue

.sr_draw:
            ; Display "DRAW!"
            ld      b, RESULT_ROW
            ld      c, RESULT_COL + 2   ; Centre "DRAW!" better
            ld      hl, msg_draw
            ld      e, TEXT_ATTR
            call    print_message

.sr_continue:
            ; Display "PRESS ANY KEY" prompt
            ld      b, CONTINUE_ROW
            ld      c, CONTINUE_COL
            ld      hl, msg_continue
            ld      e, TEXT_ATTR
            call    print_message
            ret

; ----------------------------------------------------------------------------
; Print Message
; ----------------------------------------------------------------------------
; HL = pointer to null-terminated string
; B = row, C = starting column, E = attribute for message area

print_message:
            ; Save parameters we need later
            push    de              ; Save attribute in E
            push    bc              ; Save row (B) and start column (C)

            ; Print characters
.pm_loop:
            ld      a, (hl)
            or      a
            jr      z, .pm_done
            call    print_char
            inc     hl
            inc     c
            jr      .pm_loop

.pm_done:
            ; C now has end column
            ; Calculate length: end_col - start_col
            ld      a, c            ; A = end column
            pop     bc              ; B = row, C = start column
            sub     c               ; A = length
            ld      d, a            ; D = length (save it)

            ; Set up for set_attr_range: A=row, C=start_col, B=count, E=attr
            ld      a, b            ; A = row
            ld      b, d            ; B = count (length)
            pop     de              ; E = attribute
            call    set_attr_range

            ret

; ----------------------------------------------------------------------------
; Victory Celebration
; ----------------------------------------------------------------------------
; Flashes border in winner's colour

victory_celebration:
            ; Determine winner's border colour
            ld      a, (p1_count)
            ld      b, a
            ld      a, (p2_count)
            cp      b
            jr      c, .vc_p1           ; p2 < p1
            jr      z, .vc_draw         ; draw - use white
            ld      d, P2_BORDER        ; p2 wins
            jr      .vc_flash
.vc_p1:
            ld      d, P1_BORDER
            jr      .vc_flash
.vc_draw:
            ld      d, 7                ; White for draw

.vc_flash:
            ; Flash border 5 times
            ld      b, 5

.vc_loop:
            push    bc

            ; Winner's colour
            ld      a, d
            out     (KEY_PORT), a

            ; Delay
            ld      bc, 15000
.vc_delay1:
            dec     bc
            ld      a, b
            or      c
            jr      nz, .vc_delay1

            ; Black
            xor     a
            out     (KEY_PORT), a

            ; Delay
            ld      bc, 10000
.vc_delay2:
            dec     bc
            ld      a, b
            or      c
            jr      nz, .vc_delay2

            pop     bc
            djnz    .vc_loop

            ret

; ----------------------------------------------------------------------------
; Draw Title Screen
; ----------------------------------------------------------------------------
; Displays game title and prompt

draw_title_screen:
            ; Draw "INK WAR" title
            ld      b, TITLE_ROW
            ld      c, TITLE_COL
            ld      hl, msg_title
            ld      e, TEXT_ATTR
            call    print_message

            ; Draw "1 - TWO PLAYER"
            ld      b, MODE1_ROW
            ld      c, MODE1_COL
            ld      hl, msg_mode1
            ld      e, TEXT_ATTR
            call    print_message

            ; Draw "2 - AI EASY"
            ld      b, MODE2_ROW
            ld      c, MODE2_COL
            ld      hl, msg_mode2
            ld      e, TEXT_ATTR
            call    print_message

            ; Draw "3 - AI MEDIUM"
            ld      b, MODE3_ROW
            ld      c, MODE3_COL
            ld      hl, msg_mode3
            ld      e, TEXT_ATTR
            call    print_message

            ; Draw "4 - AI HARD"
            ld      b, MODE4_ROW
            ld      c, MODE4_COL
            ld      hl, msg_mode4
            ld      e, TEXT_ATTR
            call    print_message

            ret

; ----------------------------------------------------------------------------
; Wait Key Release
; ----------------------------------------------------------------------------
; Waits until all keys are released

wait_key_release:
.wkr_loop:
            xor     a
            in      a, (KEY_PORT)
            cpl                     ; Invert (keys are active low)
            and     %00011111       ; Mask to key bits
            jr      nz, .wkr_loop   ; Still holding a key
            ret

; ----------------------------------------------------------------------------
; Wait For Key
; ----------------------------------------------------------------------------
; Waits until any key is pressed

wait_for_key:
            ; First wait for all keys to be released
.wfk_release:
            xor     a
            in      a, (KEY_PORT)
            cpl                     ; Invert (keys are active low)
            and     %00011111       ; Mask to key bits
            jr      nz, .wfk_release

            ; Now wait for a key press
.wfk_wait:
            halt                    ; Wait for interrupt
            xor     a
            in      a, (KEY_PORT)
            cpl
            and     %00011111
            jr      z, .wfk_wait

            ret

; ----------------------------------------------------------------------------
; AI Make Move
; ----------------------------------------------------------------------------
; AI picks a cell based on difficulty level

ai_make_move:
            ; Dispatch based on difficulty
            ld      a, (ai_difficulty)
            or      a
            jr      z, .aim_easy            ; AI_EASY = 0
            cp      AI_MEDIUM
            jr      z, .aim_medium
            ; AI_HARD - full strategy
            call    find_best_adjacent_cell
            jr      .aim_have_cell

.aim_easy:
            ; Random moves
            call    find_random_empty_cell
            jr      .aim_have_cell

.aim_medium:
            ; Adjacent priority only (no defense/position)
            call    find_adjacent_only
            ; Fall through to .aim_have_cell

.aim_have_cell:
            ; A = cell index (0-63), or $FF if board full

            cp      $ff
            ret     z               ; No empty cells (shouldn't happen)

            ; Convert index to row/col and set cursor
            ld      b, a
            and     %00000111       ; Column = index AND 7
            ld      (cursor_col), a
            ld      a, b
            rrca
            rrca
            rrca
            and     %00000111       ; Row = index >> 3
            ld      (cursor_row), a

            ; Claim the cell (reuse existing code)
            call    claim_cell
            call    sound_claim

            ; Switch player
            ld      a, (current_player)
            xor     3
            ld      (current_player), a

            call    draw_ui

            ; Check if game is over
            call    check_game_over
            or      a
            jr      z, .aim_continue

            ; Game over - play appropriate sound and show results
            call    play_result_sound
            call    show_results
            call    victory_celebration
            call    wait_for_key

            ; Return to title screen
            ld      a, GS_TITLE
            ld      (game_state), a
            call    init_screen
            call    draw_title_screen
            xor     a
            out     (KEY_PORT), a       ; Black border for title
            ret

.aim_continue:
            call    update_border
            call    draw_cursor
            ret

; ----------------------------------------------------------------------------
; Find Best Cell (Offense + Defense + Position)
; ----------------------------------------------------------------------------
; Returns A = index of empty cell with best combined score, or $FF if none
; Score = adjacent AI + adjacent human + position bonus (corner=2, edge=1)
; This makes the AI value strategic positions in addition to adjacency

find_best_adjacent_cell:
            ; Initialize best tracking
            ld      a, $ff
            ld      (best_cell), a      ; No best cell yet
            xor     a
            ld      (best_score), a     ; Best score = 0

            ; Scan all 64 cells
            ld      c, 0                ; C = current cell index

.fbac_loop:
            ; Check if cell is empty
            ld      hl, board_state
            ld      d, 0
            ld      e, c
            add     hl, de
            ld      a, (hl)
            or      a
            jr      nz, .fbac_next      ; Not empty, skip

            ; Empty cell - count adjacent AI cells (offense)
            push    bc
            ld      a, c
            call    count_adjacent_ai
            ld      b, a                ; B = AI adjacent count

            ; Count adjacent human cells (defense)
            ld      a, c
            call    count_adjacent_p1
            add     a, b                ; A = adjacency score
            ld      b, a                ; B = adjacency score

            ; Add position bonus (corners=2, edges=1)
            ld      a, c
            call    get_position_bonus
            add     a, b                ; A = total score
            pop     bc

            ; Compare with best score
            ld      b, a                ; B = this score
            ld      a, (best_score)
            cp      b
            jr      nc, .fbac_next      ; Current best >= this score

            ; New best found
            ld      a, b
            ld      (best_score), a
            ld      a, c
            ld      (best_cell), a

.fbac_next:
            inc     c
            ld      a, c
            cp      64
            jr      c, .fbac_loop       ; Continue if c < 64

            ; Check if we found a good cell
            ld      a, (best_score)
            or      a
            jr      z, .fbac_random     ; No scored cells, use random

            ; Return best cell
            ld      a, (best_cell)
            ret

.fbac_random:
            ; Fall back to random empty cell
            call    find_random_empty_cell
            ret

best_cell:      defb    0
best_score:     defb    0

; ----------------------------------------------------------------------------
; Find Adjacent Only (Medium Difficulty)
; ----------------------------------------------------------------------------
; Returns A = index of empty cell with most AI neighbors, or $FF if none
; Only considers AI adjacency - no defense or position bonus

find_adjacent_only:
            ; Initialize best tracking
            ld      a, $ff
            ld      (best_cell), a
            xor     a
            ld      (best_score), a

            ; Scan all 64 cells
            ld      c, 0

.fao_loop:
            ; Check if cell is empty
            ld      hl, board_state
            ld      d, 0
            ld      e, c
            add     hl, de
            ld      a, (hl)
            or      a
            jr      nz, .fao_next

            ; Empty cell - count adjacent AI cells only
            push    bc
            ld      a, c
            call    count_adjacent_ai
            pop     bc

            ; Compare with best score
            ld      b, a
            ld      a, (best_score)
            cp      b
            jr      nc, .fao_next

            ; New best found
            ld      a, b
            ld      (best_score), a
            ld      a, c
            ld      (best_cell), a

.fao_next:
            inc     c
            ld      a, c
            cp      64
            jr      c, .fao_loop

            ; Check if we found an adjacent cell
            ld      a, (best_score)
            or      a
            jr      z, .fao_random

            ; Return best adjacent cell
            ld      a, (best_cell)
            ret

.fao_random:
            ; Fall back to random empty cell
            call    find_random_empty_cell
            ret

; ----------------------------------------------------------------------------
; Get Position Bonus
; ----------------------------------------------------------------------------
; A = cell index (0-63)
; Returns A = position bonus (corner=2, edge=1, center=0)

get_position_bonus:
            ; Get row and column from index
            ld      b, a
            and     %00000111           ; Column (0-7)
            ld      c, a
            ld      a, b
            rrca
            rrca
            rrca
            and     %00000111           ; Row (0-7)
            ld      b, a                ; B = row, C = col

            ; Check for corner (row=0 or 7, col=0 or 7)
            ld      a, b
            or      a
            jr      z, .gpb_row_edge    ; Row 0
            cp      7
            jr      z, .gpb_row_edge    ; Row 7
            ; Row is not edge
            ld      a, c
            or      a
            jr      z, .gpb_edge        ; Col 0, row not edge = edge
            cp      7
            jr      z, .gpb_edge        ; Col 7, row not edge = edge
            ; Neither row nor col is edge = center
            xor     a                   ; Return 0
            ret

.gpb_row_edge:
            ; Row is 0 or 7, check column
            ld      a, c
            or      a
            jr      z, .gpb_corner      ; Col 0 = corner
            cp      7
            jr      z, .gpb_corner      ; Col 7 = corner
            ; Row edge but not corner
            jr      .gpb_edge

.gpb_corner:
            ld      a, 2                ; Corner bonus
            ret

.gpb_edge:
            ld      a, 1                ; Edge bonus
            ret

; ----------------------------------------------------------------------------
; Count Adjacent AI Cells
; ----------------------------------------------------------------------------
; A = cell index (0-63)
; Returns A = count of adjacent cells owned by AI (P2)

count_adjacent_ai:
            ld      (temp_cell), a
            xor     a
            ld      (adj_count), a

            ; Get row and column
            ld      a, (temp_cell)
            ld      b, a
            and     %00000111           ; Column
            ld      c, a
            ld      a, b
            rrca
            rrca
            rrca
            and     %00000111           ; Row
            ld      b, a                ; B = row, C = col

            ; Check up (row-1)
            ld      a, b
            or      a
            jr      z, .caa_skip_up     ; Row 0, no up neighbor
            dec     a                   ; Row - 1
            push    bc
            ld      b, a
            call    .caa_check_cell
            pop     bc
.caa_skip_up:

            ; Check down (row+1)
            ld      a, b
            cp      7
            jr      z, .caa_skip_down   ; Row 7, no down neighbor
            inc     a                   ; Row + 1
            push    bc
            ld      b, a
            call    .caa_check_cell
            pop     bc
.caa_skip_down:

            ; Check left (col-1)
            ld      a, c
            or      a
            jr      z, .caa_skip_left   ; Col 0, no left neighbor
            dec     a                   ; Col - 1
            push    bc
            ld      c, a
            call    .caa_check_cell
            pop     bc
.caa_skip_left:

            ; Check right (col+1)
            ld      a, c
            cp      7
            jr      z, .caa_skip_right  ; Col 7, no right neighbor
            inc     a                   ; Col + 1
            push    bc
            ld      c, a
            call    .caa_check_cell
            pop     bc
.caa_skip_right:

            ld      a, (adj_count)
            ret

            ; Helper: check if cell at (B,C) is AI owned
.caa_check_cell:
            ; Calculate index: row*8 + col
            ld      a, b
            rlca
            rlca
            rlca                        ; Row * 8
            add     a, c                ; + col
            ld      hl, board_state
            ld      d, 0
            ld      e, a
            add     hl, de
            ld      a, (hl)
            cp      STATE_P2            ; AI is Player 2
            ret     nz                  ; Not AI cell
            ; AI cell - increment count
            ld      a, (adj_count)
            inc     a
            ld      (adj_count), a
            ret

temp_cell:      defb    0
adj_count:      defb    0

; ----------------------------------------------------------------------------
; Count Adjacent Human Cells
; ----------------------------------------------------------------------------
; A = cell index (0-63)
; Returns A = count of adjacent cells owned by human (P1)

count_adjacent_p1:
            ld      (temp_cell), a
            xor     a
            ld      (adj_count), a

            ; Get row and column
            ld      a, (temp_cell)
            ld      b, a
            and     %00000111           ; Column
            ld      c, a
            ld      a, b
            rrca
            rrca
            rrca
            and     %00000111           ; Row
            ld      b, a                ; B = row, C = col

            ; Check up (row-1)
            ld      a, b
            or      a
            jr      z, .cap_skip_up     ; Row 0, no up neighbor
            dec     a                   ; Row - 1
            push    bc
            ld      b, a
            call    .cap_check_cell
            pop     bc
.cap_skip_up:

            ; Check down (row+1)
            ld      a, b
            cp      7
            jr      z, .cap_skip_down   ; Row 7, no down neighbor
            inc     a                   ; Row + 1
            push    bc
            ld      b, a
            call    .cap_check_cell
            pop     bc
.cap_skip_down:

            ; Check left (col-1)
            ld      a, c
            or      a
            jr      z, .cap_skip_left   ; Col 0, no left neighbor
            dec     a                   ; Col - 1
            push    bc
            ld      c, a
            call    .cap_check_cell
            pop     bc
.cap_skip_left:

            ; Check right (col+1)
            ld      a, c
            cp      7
            jr      z, .cap_skip_right  ; Col 7, no right neighbor
            inc     a                   ; Col + 1
            push    bc
            ld      c, a
            call    .cap_check_cell
            pop     bc
.cap_skip_right:

            ld      a, (adj_count)
            ret

            ; Helper: check if cell at (B,C) is human owned
.cap_check_cell:
            ; Calculate index: row*8 + col
            ld      a, b
            rlca
            rlca
            rlca                        ; Row * 8
            add     a, c                ; + col
            ld      hl, board_state
            ld      d, 0
            ld      e, a
            add     hl, de
            ld      a, (hl)
            cp      STATE_P1            ; Human is Player 1
            ret     nz                  ; Not human cell
            ; Human cell - increment count
            ld      a, (adj_count)
            inc     a
            ld      (adj_count), a
            ret

; ----------------------------------------------------------------------------
; Find Random Empty Cell
; ----------------------------------------------------------------------------
; Returns A = index of a random empty cell (0-63), or $FF if none

find_random_empty_cell:
            ; Get random starting position
            call    get_random
            and     %00111111       ; Limit to 0-63

            ld      c, a            ; C = start index
            ld      b, 64           ; B = cells to check

.frec_loop:
            ; Check if cell at index C is empty
            ld      hl, board_state
            ld      d, 0
            ld      e, c
            add     hl, de
            ld      a, (hl)
            or      a
            jr      z, .frec_found  ; Found empty cell

            ; Try next cell (wrap around)
            inc     c
            ld      a, c
            and     %00111111       ; Wrap at 64
            ld      c, a

            djnz    .frec_loop

            ; No empty cells found
            ld      a, $ff
            ret

.frec_found:
            ld      a, c            ; Return cell index
            ret

; ----------------------------------------------------------------------------
; Get Random
; ----------------------------------------------------------------------------
; Returns A = pseudo-random number using R register

get_random:
            ld      a, r            ; R register changes every instruction
            ld      b, a
            ld      a, (random_seed)
            add     a, b
            rlca
            xor     b
            ld      (random_seed), a
            ret

random_seed:    defb    $5a         ; Seed value

; ----------------------------------------------------------------------------
; Messages
; ----------------------------------------------------------------------------

msg_p1_wins:    defb    "P1 WINS!", 0
msg_p2_wins:    defb    "P2 WINS!", 0
msg_draw:       defb    "DRAW!", 0
msg_title:      defb    "INK WAR", 0
msg_mode1:      defb    "1 - TWO PLAYER", 0
msg_mode2:      defb    "2 - AI EASY", 0
msg_mode3:      defb    "3 - AI MEDIUM", 0
msg_mode4:      defb    "4 - AI HARD", 0
msg_continue:   defb    "PRESS ANY KEY", 0

; ----------------------------------------------------------------------------
; Variables
; ----------------------------------------------------------------------------

game_state:     defb    0               ; 0=title, 1=playing, 2=results
game_mode:      defb    0               ; 0=two player, 1=vs AI
ai_difficulty:  defb    0               ; 0=easy, 1=medium, 2=hard
cursor_row:     defb    0
cursor_col:     defb    0
key_pressed:    defb    0
last_key:       defb    0               ; Previous frame's key for repeat detection
key_timer:      defb    0               ; Countdown for key repeat delay
ai_timer:       defb    0               ; Countdown before AI moves
current_player: defb    1
p1_count:       defb    0
p2_count:       defb    0
board_state:    defs    64, 0

; ----------------------------------------------------------------------------
; End
; ----------------------------------------------------------------------------

            end     start

Try This: Turn Change Sound

Add a brief sound when the turn switches:

sound_turn:
            ld      b, 8
.st_loop:
            ; Quick descending chirp
            ; ... tone generation ...
            djnz    .st_loop
            ret

Call it after switching players in try_claim.

Try This: AI Thinking Sound

Add a subtle sound while the AI “thinks”:

; Before AI makes move
ld      b, 3
.think:
            ; Short quiet tick
            push    bc
            ; ... minimal beep ...
            pop     bc
            djnz    .think

This creates anticipation before the AI moves.

What You’ve Learnt

  • Beeper programming — Toggling speaker bit for sound
  • Pitch control — Delay duration determines frequency
  • Sound design — Matching sounds to events
  • Audio feedback — Every action acknowledged

What’s Next

In Unit 15, we’ll create a proper results screen with winner display and final scores.

What Changed

Unit 13 → Unit 14
+196-5
11 ; ============================================================================
2-; INK WAR - Unit 13: AI Difficulty Levels
2+; INK WAR - Unit 14: Sound Effects
33 ; ============================================================================
4-; Choose your challenge: Easy (random), Medium (adjacent), Hard (full strategy)
5-; Title screen now offers four options for different play styles.
4+; Complete audio feedback: menu select, claim, error, turn change, victory.
5+; The beeper gives every action a distinctive sound.
66 ;
77 ; Controls: Q=Up, A=Down, O=Left, P=Right, SPACE=Claim
88 ; 1=Two Player, 2=AI Easy, 3=AI Medium, 4=AI Hard
...
160160 jp main_loop ; No valid key - keep waiting
161161
162162 .st_two_player:
163+ call sound_select
163164 xor a ; GM_TWO_PLAYER = 0
164165 ld (game_mode), a
165166 call start_game
166167 jp main_loop
167168
168169 .st_ai_easy:
170+ call sound_select
169171 ld a, GM_VS_AI
170172 ld (game_mode), a
171173 ld a, AI_EASY
...
174176 jp main_loop
175177
176178 .st_ai_medium:
179+ call sound_select
177180 ld a, GM_VS_AI
178181 ld (game_mode), a
179182 ld a, AI_MEDIUM
...
182185 jp main_loop
183186
184187 .st_ai_hard:
188+ call sound_select
185189 ld a, GM_VS_AI
186190 ld (game_mode), a
187191 ld a, AI_HARD
...
992996 or a
993997 jr z, .tc_continue
994998
995- ; Game over - show results and return to title
999+ ; Game over - play appropriate sound and show results
1000+ call play_result_sound
9961001 call show_results
9971002 call victory_celebration
9981003 call wait_for_key
...
11311136
11321137 pop bc
11331138 djnz .se_loop
1139+
1140+ ret
1141+
1142+; ----------------------------------------------------------------------------
1143+; Sound - Menu Select
1144+; ----------------------------------------------------------------------------
1145+; Quick high beep for menu selection
1146+
1147+sound_select:
1148+ ld b, 15 ; Short duration
1149+
1150+.ss_loop:
1151+ push bc
1152+
1153+ ld a, $10
1154+ out (KEY_PORT), a
1155+ ld c, 20 ; High pitch (short delay)
1156+.ss_delay1:
1157+ dec c
1158+ jr nz, .ss_delay1
1159+
1160+ xor a
1161+ out (KEY_PORT), a
1162+ ld c, 20
1163+.ss_delay2:
1164+ dec c
1165+ jr nz, .ss_delay2
1166+
1167+ pop bc
1168+ djnz .ss_loop
1169+
1170+ ret
1171+
1172+; ----------------------------------------------------------------------------
1173+; Sound - Turn Change
1174+; ----------------------------------------------------------------------------
1175+; Brief descending tone when turn switches
1176+
1177+sound_turn:
1178+ ld hl, 30 ; Starting pitch (high)
1179+ ld b, 8 ; Number of steps
1180+
1181+.st_loop:
1182+ push bc
1183+ push hl
1184+
1185+ ; Play tone at current pitch
1186+ ld b, 6 ; Cycles per pitch
1187+.st_tone:
1188+ push bc
1189+ ld a, $10
1190+ out (KEY_PORT), a
1191+ ld b, l ; Delay = pitch
1192+.st_d1: djnz .st_d1
1193+ xor a
1194+ out (KEY_PORT), a
1195+ ld b, l
1196+.st_d2: djnz .st_d2
1197+ pop bc
1198+ djnz .st_tone
1199+
1200+ pop hl
1201+ pop bc
1202+
1203+ ; Increase delay (lower pitch)
1204+ ld de, 8
1205+ add hl, de
1206+
1207+ djnz .st_loop
1208+ ret
1209+
1210+; ----------------------------------------------------------------------------
1211+; Sound - Victory
1212+; ----------------------------------------------------------------------------
1213+; Triumphant ascending fanfare
1214+
1215+sound_victory:
1216+ ; Play three ascending notes
1217+ ld hl, 60 ; Starting pitch (low)
1218+ ld b, 3 ; Three notes
1219+
1220+.sv_note:
1221+ push bc
1222+ push hl
1223+
1224+ ; Play note
1225+ ld b, 25 ; Note duration
1226+.sv_tone:
1227+ push bc
1228+ ld a, $10
1229+ out (KEY_PORT), a
1230+ ld b, l
1231+.sv_d1: djnz .sv_d1
1232+ xor a
1233+ out (KEY_PORT), a
1234+ ld b, l
1235+.sv_d2: djnz .sv_d2
1236+ pop bc
1237+ djnz .sv_tone
1238+
1239+ pop hl
1240+ pop bc
1241+
1242+ ; Decrease delay (higher pitch) for next note
1243+ ld a, l
1244+ sub 15
1245+ ld l, a
1246+
1247+ ; Brief pause between notes
1248+ push bc
1249+ ld bc, 2000
1250+.sv_pause: dec bc
1251+ ld a, b
1252+ or c
1253+ jr nz, .sv_pause
1254+ pop bc
1255+
1256+ djnz .sv_note
1257+ ret
1258+
1259+; ----------------------------------------------------------------------------
1260+; Sound - Draw
1261+; ----------------------------------------------------------------------------
1262+; Neutral two-tone sound for draw
1263+
1264+sound_draw:
1265+ ; First tone (mid pitch)
1266+ ld b, 20
1267+.sd_tone1:
1268+ push bc
1269+ ld a, $10
1270+ out (KEY_PORT), a
1271+ ld c, 40
1272+.sd_d1: dec c
1273+ jr nz, .sd_d1
1274+ xor a
1275+ out (KEY_PORT), a
1276+ ld c, 40
1277+.sd_d2: dec c
1278+ jr nz, .sd_d2
1279+ pop bc
1280+ djnz .sd_tone1
1281+
1282+ ; Brief pause
1283+ ld bc, 1500
1284+.sd_pause: dec bc
1285+ ld a, b
1286+ or c
1287+ jr nz, .sd_pause
1288+
1289+ ; Second tone (same pitch - neutral)
1290+ ld b, 20
1291+.sd_tone2:
1292+ push bc
1293+ ld a, $10
1294+ out (KEY_PORT), a
1295+ ld c, 40
1296+.sd_d3: dec c
1297+ jr nz, .sd_d3
1298+ xor a
1299+ out (KEY_PORT), a
1300+ ld c, 40
1301+.sd_d4: dec c
1302+ jr nz, .sd_d4
1303+ pop bc
1304+ djnz .sd_tone2
1305+
1306+ ret
1307+
1308+; ----------------------------------------------------------------------------
1309+; Play Result Sound
1310+; ----------------------------------------------------------------------------
1311+; Plays victory or draw sound based on game result
11341312
1313+play_result_sound:
1314+ ; Compare scores
1315+ ld a, (p1_count)
1316+ ld b, a
1317+ ld a, (p2_count)
1318+ cp b
1319+ jr z, .prs_draw ; Equal = draw
1320+ ; Someone won - play victory
1321+ call sound_victory
1322+ ret
1323+.prs_draw:
1324+ call sound_draw
11351325 ret
11361326
11371327 ; ----------------------------------------------------------------------------
...
14871677 or a
14881678 jr z, .aim_continue
14891679
1490- ; Game over - show results and return to title
1680+ ; Game over - play appropriate sound and show results
1681+ call play_result_sound
14911682 call show_results
14921683 call victory_celebration
14931684 call wait_for_key