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

Move Validation

Error feedback when you try to claim an occupied cell. The game pushes back.

8% of Ink War

Try claiming a cell that’s already taken. In the previous unit, nothing happened. That’s confusing - did the game register your keypress? Did something break?

This unit adds clear feedback: when you try to claim an occupied cell, you hear an error buzz and the border flashes red. The turn doesn’t change. You know exactly what went wrong.

Run It

pasmonext --sna inkwar.asm inkwar.sna

Unit 5 Screenshot

Claim a few cells. Then try to claim one that’s already taken. You’ll hear a harsh buzz and see the border flash red. Your turn doesn’t switch - you get another chance.

Validation Logic

The updated try_claim routine now branches on the cell state:

; ----------------------------------------------------------------------------
; Try Claim Cell
; ----------------------------------------------------------------------------
; Attempts to claim the current cell. If already claimed, shows error feedback.

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
            call    update_border
            call    draw_cursor

            ret

The key change is replacing ret nz with a jump to error handling. If the cell is empty (state = 0), we proceed with the claim. If it’s occupied (state ≠ 0), we provide feedback and return without changing turns.

The Error Sound

A harsh, low-frequency buzz signals “that’s wrong”:

; ----------------------------------------------------------------------------
; 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

Compared to the pleasant rising tone for valid claims, this sound is deliberately unpleasant:

  • Longer delay (80 cycles) creates a lower pitch
  • Shorter duration (30 loops) keeps it brief
  • The result is a quick “BZZT” that says “nope”

The Border Flash

Visual feedback reinforces the audio:

; ----------------------------------------------------------------------------
; 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

The border flashes red three times, with black gaps between flashes. Then update_border restores the correct player colour. The flashing is fast enough to feel urgent but not so fast it’s imperceptible.

Why Both Audio and Visual?

Good feedback uses multiple channels:

  • Audio grabs attention even when you’re not looking at the screen
  • Visual confirms what happened and draws your eye to the relevant area
  • Together, they create unmistakable feedback

In 1980s arcades, this pattern was everywhere. Error sounds and screen flashes told players “try again” without breaking the flow.

Delay Timing

The delays use simple countdown loops:

            ld      bc, 8000
.delay:
            dec     bc
            ld      a, b
            or      c
            jr      nz, .delay

At 3.5 MHz, this gives roughly 8000 × 30 cycles ≈ 70ms per flash. We check b or c because DEC BC doesn’t set the zero flag - we need to explicitly test if BC has reached zero.

The Complete Code

; ============================================================================
; INK WAR - Unit 5: Move Validation
; ============================================================================
; Adds error feedback when trying to claim an already-occupied cell.
; Invalid moves: error buzz + red border flash. Turn doesn't change.
;
; Controls: Q=Up, A=Down, O=Left, P=Right, SPACE=Claim
; ============================================================================

            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

; 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

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

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

start:
            call    init_screen
            call    init_game
            call    draw_board_border
            call    draw_board
            call    draw_ui             ; Draw score and turn display
            call    draw_cursor
            call    update_border

main_loop:
            halt

            call    read_keyboard
            call    handle_input

            jp      main_loop

; ----------------------------------------------------------------------------
; 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

            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
; ----------------------------------------------------------------------------

handle_input:
            ld      a, (key_pressed)
            or      a
            ret     z

            cp      5
            jr      z, try_claim

            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
            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

; ----------------------------------------------------------------------------
; 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

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

cursor_row:     defb    0
cursor_col:     defb    0
key_pressed:    defb    0
current_player: defb    1
p1_count:       defb    0
p2_count:       defb    0
board_state:    defs    64, 0

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

            end     start

Try This: Different Error Sound

; Higher pitch, longer duration (more annoying!)
sound_error:
            ld      b, 50               ; Longer
.se_loop:
            ; ... same inner loop ...
            ld      c, 30               ; Shorter delay = higher pitch

Try This: Screen Flash Instead

Instead of flashing the border, flash the whole screen by temporarily setting all attributes to red:

; Warning: This is more disruptive!
flash_screen_error:
            ld      hl, $5800
            ld      de, $5801
            ld      bc, 767
            ld      (hl), %01010000     ; Red paper
            ldir
            ; ... delay ...
            ; Then redraw everything

What You’ve Learnt

  • Input validation - Check before acting, provide feedback for invalid input
  • Multi-channel feedback - Combine audio and visual cues for clarity
  • Error sounds - Low frequency + short duration = unpleasant buzz
  • Border control - OUT ($FE), A with colour in bits 0-2
  • Delay loops - BC countdown for timing (DEC BC doesn’t set flags, need OR test)

What’s Next

In Unit 6, we’ll detect when the board is full and declare a winner. The game gets a proper ending.

What Changed

Unit 4 → Unit 5
+85-4
11 ; ============================================================================
2-; INK WAR - Unit 4: Score and Turn Display
2+; INK WAR - Unit 5: Move Validation
33 ; ============================================================================
4-; Adds score display and turn indicator to the game.
5-; Shows "P1: nn P2: nn" and indicates whose turn it is.
4+; Adds error feedback when trying to claim an already-occupied cell.
5+; Invalid moves: error buzz + red border flash. Turn doesn't change.
66 ;
77 ; Controls: Q=Up, A=Down, O=Left, P=Right, SPACE=Claim
88 ; ============================================================================
...
4545
4646 P1_BORDER equ 2
4747 P2_BORDER equ 1
48+ERROR_BORDER equ 2 ; Red border for errors
4849
4950 ; Keyboard ports
5051 KEY_PORT equ $fe
...
757758 try_claim:
758759 call get_cell_state
759760 or a
760- ret nz
761+ jr z, .tc_valid
762+
763+ ; Cell already claimed - error feedback
764+ call sound_error
765+ call flash_border_error
766+ call update_border ; Restore correct border colour
767+ ret
761768
769+.tc_valid:
770+ ; Valid move - claim the cell
762771 call claim_cell
763772 call sound_claim
764773
...
860869 ld b, 5
861870 .scl_delay_loop:
862871 djnz .scl_delay_loop
872+ pop bc
873+ ret
874+
875+; ----------------------------------------------------------------------------
876+; Sound - Error
877+; ----------------------------------------------------------------------------
878+; Harsh buzz for invalid move
879+
880+sound_error:
881+ ld b, 30 ; Duration
882+
883+.se_loop:
884+ push bc
885+
886+ ; Low frequency buzz (longer delay = lower pitch)
887+ ld a, $10
888+ out (KEY_PORT), a
889+ ld c, 80 ; Longer delay for low pitch
890+.se_delay1:
891+ dec c
892+ jr nz, .se_delay1
893+
894+ xor a
895+ out (KEY_PORT), a
896+ ld c, 80
897+.se_delay2:
898+ dec c
899+ jr nz, .se_delay2
900+
901+ pop bc
902+ djnz .se_loop
903+
904+ ret
905+
906+; ----------------------------------------------------------------------------
907+; Flash Border Error
908+; ----------------------------------------------------------------------------
909+; Flash border red briefly to indicate error
910+
911+flash_border_error:
912+ ; Flash red 3 times
913+ ld b, 3
914+
915+.fbe_loop:
916+ push bc
917+
918+ ; Red border
919+ ld a, ERROR_BORDER
920+ out (KEY_PORT), a
921+
922+ ; Short delay (about 3 frames)
923+ ld bc, 8000
924+.fbe_delay1:
925+ dec bc
926+ ld a, b
927+ or c
928+ jr nz, .fbe_delay1
929+
930+ ; Black border (brief off)
931+ xor a
932+ out (KEY_PORT), a
933+
934+ ; Short delay
935+ ld bc, 4000
936+.fbe_delay2:
937+ dec bc
938+ ld a, b
939+ or c
940+ jr nz, .fbe_delay2
941+
863942 pop bc
943+ djnz .fbe_loop
944+
864945 ret
865946
866947 ; ----------------------------------------------------------------------------