Sound Effects
Every action has audio feedback. Menu select, claim, error, victory, draw.
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

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
| Event | Sound Character | Purpose |
|---|---|---|
| Menu select | Quick high beep | Confirms input |
| Cell claim | Rising tone | Positive feedback |
| Invalid move | Low buzz | Negative feedback |
| Victory | Ascending fanfare | Celebration |
| Draw | Neutral two-tone | Neither win nor loss |
Menu Selection Sound
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_selectbefore 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_soundbefore 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
| 1 | 1 | ; ============================================================================ | |
| 2 | - | ; INK WAR - Unit 13: AI Difficulty Levels | |
| 2 | + | ; INK WAR - Unit 14: Sound Effects | |
| 3 | 3 | ; ============================================================================ | |
| 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. | |
| 6 | 6 | ; | |
| 7 | 7 | ; Controls: Q=Up, A=Down, O=Left, P=Right, SPACE=Claim | |
| 8 | 8 | ; 1=Two Player, 2=AI Easy, 3=AI Medium, 4=AI Hard | |
| ... | |||
| 160 | 160 | jp main_loop ; No valid key - keep waiting | |
| 161 | 161 | | |
| 162 | 162 | .st_two_player: | |
| 163 | + | call sound_select | |
| 163 | 164 | xor a ; GM_TWO_PLAYER = 0 | |
| 164 | 165 | ld (game_mode), a | |
| 165 | 166 | call start_game | |
| 166 | 167 | jp main_loop | |
| 167 | 168 | | |
| 168 | 169 | .st_ai_easy: | |
| 170 | + | call sound_select | |
| 169 | 171 | ld a, GM_VS_AI | |
| 170 | 172 | ld (game_mode), a | |
| 171 | 173 | ld a, AI_EASY | |
| ... | |||
| 174 | 176 | jp main_loop | |
| 175 | 177 | | |
| 176 | 178 | .st_ai_medium: | |
| 179 | + | call sound_select | |
| 177 | 180 | ld a, GM_VS_AI | |
| 178 | 181 | ld (game_mode), a | |
| 179 | 182 | ld a, AI_MEDIUM | |
| ... | |||
| 182 | 185 | jp main_loop | |
| 183 | 186 | | |
| 184 | 187 | .st_ai_hard: | |
| 188 | + | call sound_select | |
| 185 | 189 | ld a, GM_VS_AI | |
| 186 | 190 | ld (game_mode), a | |
| 187 | 191 | ld a, AI_HARD | |
| ... | |||
| 992 | 996 | or a | |
| 993 | 997 | jr z, .tc_continue | |
| 994 | 998 | | |
| 995 | - | ; Game over - show results and return to title | |
| 999 | + | ; Game over - play appropriate sound and show results | |
| 1000 | + | call play_result_sound | |
| 996 | 1001 | call show_results | |
| 997 | 1002 | call victory_celebration | |
| 998 | 1003 | call wait_for_key | |
| ... | |||
| 1131 | 1136 | | |
| 1132 | 1137 | pop bc | |
| 1133 | 1138 | 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 | |
| 1134 | 1312 | | |
| 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 | |
| 1135 | 1325 | ret | |
| 1136 | 1326 | | |
| 1137 | 1327 | ; ---------------------------------------------------------------------------- | |
| ... | |||
| 1487 | 1677 | or a | |
| 1488 | 1678 | jr z, .aim_continue | |
| 1489 | 1679 | | |
| 1490 | - | ; Game over - show results and return to title | |
| 1680 | + | ; Game over - play appropriate sound and show results | |
| 1681 | + | call play_result_sound | |
| 1491 | 1682 | call show_results | |
| 1492 | 1683 | call victory_celebration | |
| 1493 | 1684 | call wait_for_key |