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

Game End Detection

Board full? Someone wins. Compare scores, declare the victor, restart on keypress.

9% of Ink War

The game has been running forever. Players can claim cells until… when? There’s no ending.

This unit adds proper game end detection. When all 64 cells are claimed, the game determines the winner, displays a victory message, celebrates with a flashing border, then waits for a keypress to restart. A complete game loop.

Run It

pasmonext --sna inkwar.asm inkwar.sna

Unit 6 Screenshot

Play a full game - claim all 64 cells between two players. When the last cell is taken, you’ll see “P1 WINS!”, “P2 WINS!”, or “DRAW!” appear. The border flashes in the winner’s colour. Press any key to play again.

Detecting Game Over

The simplest check: is the board full?

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

We already have p1_count and p2_count from the score display. If their sum equals 64, every cell is claimed. The game is over.

This approach reuses existing data. No need for a separate move counter - we derive the answer from state we already track.

Determining the Winner

Three possible outcomes: P1 wins, P2 wins, or draw:

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

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

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

The Z80’s CP instruction compares by subtraction. After cp b:

  • Carry set means A < B (p2_count < p1_count → P1 wins)
  • Zero set means A = B (draw)
  • Neither means A > B (P2 wins)

Each outcome displays a different message in the appropriate colour.

Printing Messages

We need to print null-terminated strings. The print_message routine:

print_message:
            ; HL = string, B = row, C = column, E = attribute
.pm_loop:
            ld      a, (hl)         ; Get character
            or      a               ; Check for null terminator
            jr      z, .pm_done
            call    print_char
            inc     hl
            inc     c               ; Next column
            jr      .pm_loop
.pm_done:
            ; Set attributes for the message area
            ; ...

The strings are stored in memory:

msg_p1_wins:    defb    "P1 WINS!", 0
msg_p2_wins:    defb    "P2 WINS!", 0
msg_draw:       defb    "DRAW!", 0

The trailing 0 marks the end of each string.

Victory Celebration

Winners deserve fanfare:

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

The border flashes in the winner’s colour - red for P1, blue for P2, white for a draw. Five quick flashes with black gaps between them. Simple but effective.

Waiting for Input

After showing results, wait for any key:

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

Two phases:

  1. Wait for release - If a key is already held from claiming the last cell, wait until it’s released
  2. Wait for press - Then wait for a new keypress

The CPL instruction inverts A. Keyboard bits are active-low (0 = pressed), so we invert to make pressed keys show as 1s. The HALT saves power while waiting.

Restarting the Game

After wait_for_key, we jump back to start:

            jp      start           ; Restart game

This reinitialises everything: clears the board, resets scores, redraws the screen. A fresh game ready to play.

The Game Loop is Complete

The flow is now:

  1. Start → Initialise and draw
  2. Play → Input, validate, claim, update, check end
  3. End → Show results, celebrate, wait for key
  4. Restart → Jump to Start

Every game has this structure. What you’ve built is the skeleton that every game builds upon.

The Complete Code

; ============================================================================
; INK WAR - Unit 6: Game End Detection
; ============================================================================
; Detects when the board is full and declares a winner.
; Shows winner message, victory celebration, press key to restart.
;
; 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
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

; 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

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

            ; Game over - show results
            call    show_results
            call    victory_celebration
            call    wait_for_key
            jp      start               ; Restart game

.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

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

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

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

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

print_message:
            push    bc
            push    de

            ; First pass: 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:
            ; Calculate message length
            pop     de              ; E = attribute
            pop     bc              ; B = row, C = start column
            push    bc

            ; Count characters
            ld      a, c            ; Current column (after printing)
            pop     bc              ; Get start column back
            sub     c               ; A = length
            ld      b, a            ; B = count

            ; Set attributes
            ld      a, RESULT_ROW
            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

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

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

msg_p1_wins:    defb    "P1 WINS!", 0
msg_p2_wins:    defb    "P2 WINS!", 0
msg_draw:       defb    "DRAW!", 0

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

msg_p1_wins:    defb    "RED WINS!", 0
msg_p2_wins:    defb    "BLUE WINS!", 0
msg_draw:       defb    "TIE GAME!", 0

Try This: Longer Celebration

.vc_flash:
            ld      b, 10           ; 10 flashes instead of 5

What You’ve Learnt

  • End condition detection - Check when game state reaches a terminal condition
  • Score comparison - Use CP and flags to determine greater/less/equal
  • Null-terminated strings - Standard way to store variable-length text
  • Key debouncing - Wait for release before waiting for press
  • Game loop structure - Start → Play → End → Restart

What’s Next

In Unit 7, we’ll add a title screen. The game will start with “INK WAR” and “PRESS ANY KEY”, making it feel like a proper product rather than a demo.

What Changed

Unit 5 → Unit 6
+218-3
11 ; ============================================================================
2-; INK WAR - Unit 5: Move Validation
2+; INK WAR - Unit 6: Game End Detection
33 ; ============================================================================
4-; Adds error feedback when trying to claim an already-occupied cell.
5-; Invalid moves: error buzz + red border flash. Turn doesn't change.
4+; Detects when the board is full and declares a winner.
5+; Shows winner message, victory celebration, press key to restart.
66 ;
77 ; Controls: Q=Up, A=Down, O=Left, P=Right, SPACE=Claim
88 ; ============================================================================
...
2727 P2_SCORE_COL equ 18 ; "P2: nn" column
2828 TURN_ROW equ 4 ; Turn indicator row
2929 TURN_COL equ 14 ; Turn indicator column
30+RESULT_ROW equ 20 ; Winner message row
31+RESULT_COL equ 11 ; Winner message column
32+
33+; Game constants
34+TOTAL_CELLS equ 64 ; 8x8 board
3035
3136 ; Customised colours (from Unit 3)
3237 EMPTY_ATTR equ %01101000 ; Cyan paper + BRIGHT
...
776781 ld (current_player), a
777782
778783 call draw_ui ; Update scores and turn indicator
784+
785+ ; Check if game is over
786+ call check_game_over
787+ or a
788+ jr z, .tc_continue
789+
790+ ; Game over - show results
791+ call show_results
792+ call victory_celebration
793+ call wait_for_key
794+ jp start ; Restart game
795+
796+.tc_continue:
779797 call update_border
780798 call draw_cursor
781799
...
941959
942960 pop bc
943961 djnz .fbe_loop
962+
963+ ret
964+
965+; ----------------------------------------------------------------------------
966+; Check Game Over
967+; ----------------------------------------------------------------------------
968+; Returns A=1 if game is over (board full), A=0 otherwise
969+
970+check_game_over:
971+ ; Game is over when p1_count + p2_count == 64
972+ ld a, (p1_count)
973+ ld b, a
974+ ld a, (p2_count)
975+ add a, b
976+ cp TOTAL_CELLS
977+ jr z, .cgo_over
978+ xor a ; Not over
979+ ret
980+.cgo_over:
981+ ld a, 1 ; Game over
982+ ret
983+
984+; ----------------------------------------------------------------------------
985+; Show Results
986+; ----------------------------------------------------------------------------
987+; Displays winner message based on scores
988+
989+show_results:
990+ ; Clear turn indicator
991+ ld a, TURN_ROW
992+ ld c, TURN_COL
993+ ld b, 4
994+ ld e, TEXT_ATTR
995+ call set_attr_range
996+
997+ ; Determine winner
998+ ld a, (p1_count)
999+ ld b, a
1000+ ld a, (p2_count)
1001+ cp b
1002+ jr c, .sr_p1_wins ; p2 < p1, so p1 wins
1003+ jr z, .sr_draw ; p1 == p2, draw
1004+ ; p2 > p1, p2 wins
1005+ jr .sr_p2_wins
1006+
1007+.sr_p1_wins:
1008+ ; Display "P1 WINS!"
1009+ ld b, RESULT_ROW
1010+ ld c, RESULT_COL
1011+ ld hl, msg_p1_wins
1012+ ld e, P1_TEXT
1013+ call print_message
1014+ ret
1015+
1016+.sr_p2_wins:
1017+ ; Display "P2 WINS!"
1018+ ld b, RESULT_ROW
1019+ ld c, RESULT_COL
1020+ ld hl, msg_p2_wins
1021+ ld e, P2_TEXT
1022+ call print_message
1023+ ret
1024+
1025+.sr_draw:
1026+ ; Display "DRAW!"
1027+ ld b, RESULT_ROW
1028+ ld c, RESULT_COL + 2 ; Centre "DRAW!" better
1029+ ld hl, msg_draw
1030+ ld e, TEXT_ATTR
1031+ call print_message
1032+ ret
1033+
1034+; ----------------------------------------------------------------------------
1035+; Print Message
1036+; ----------------------------------------------------------------------------
1037+; HL = pointer to null-terminated string
1038+; B = row, C = starting column, E = attribute for message area
1039+
1040+print_message:
1041+ push bc
1042+ push de
1043+
1044+ ; First pass: print characters
1045+.pm_loop:
1046+ ld a, (hl)
1047+ or a
1048+ jr z, .pm_done
1049+ call print_char
1050+ inc hl
1051+ inc c
1052+ jr .pm_loop
1053+
1054+.pm_done:
1055+ ; Calculate message length
1056+ pop de ; E = attribute
1057+ pop bc ; B = row, C = start column
1058+ push bc
1059+
1060+ ; Count characters
1061+ ld a, c ; Current column (after printing)
1062+ pop bc ; Get start column back
1063+ sub c ; A = length
1064+ ld b, a ; B = count
1065+
1066+ ; Set attributes
1067+ ld a, RESULT_ROW
1068+ call set_attr_range
1069+
1070+ ret
1071+
1072+; ----------------------------------------------------------------------------
1073+; Victory Celebration
1074+; ----------------------------------------------------------------------------
1075+; Flashes border in winner's colour
1076+
1077+victory_celebration:
1078+ ; Determine winner's border colour
1079+ ld a, (p1_count)
1080+ ld b, a
1081+ ld a, (p2_count)
1082+ cp b
1083+ jr c, .vc_p1 ; p2 < p1
1084+ jr z, .vc_draw ; draw - use white
1085+ ld d, P2_BORDER ; p2 wins
1086+ jr .vc_flash
1087+.vc_p1:
1088+ ld d, P1_BORDER
1089+ jr .vc_flash
1090+.vc_draw:
1091+ ld d, 7 ; White for draw
1092+
1093+.vc_flash:
1094+ ; Flash border 5 times
1095+ ld b, 5
1096+
1097+.vc_loop:
1098+ push bc
1099+
1100+ ; Winner's colour
1101+ ld a, d
1102+ out (KEY_PORT), a
1103+
1104+ ; Delay
1105+ ld bc, 15000
1106+.vc_delay1:
1107+ dec bc
1108+ ld a, b
1109+ or c
1110+ jr nz, .vc_delay1
1111+
1112+ ; Black
1113+ xor a
1114+ out (KEY_PORT), a
1115+
1116+ ; Delay
1117+ ld bc, 10000
1118+.vc_delay2:
1119+ dec bc
1120+ ld a, b
1121+ or c
1122+ jr nz, .vc_delay2
1123+
1124+ pop bc
1125+ djnz .vc_loop
1126+
1127+ ret
1128+
1129+; ----------------------------------------------------------------------------
1130+; Wait For Key
1131+; ----------------------------------------------------------------------------
1132+; Waits until any key is pressed
1133+
1134+wait_for_key:
1135+ ; First wait for all keys to be released
1136+.wfk_release:
1137+ xor a
1138+ in a, (KEY_PORT)
1139+ cpl ; Invert (keys are active low)
1140+ and %00011111 ; Mask to key bits
1141+ jr nz, .wfk_release
1142+
1143+ ; Now wait for a key press
1144+.wfk_wait:
1145+ halt ; Wait for interrupt
1146+ xor a
1147+ in a, (KEY_PORT)
1148+ cpl
1149+ and %00011111
1150+ jr z, .wfk_wait
9441151
9451152 ret
1153+
1154+; ----------------------------------------------------------------------------
1155+; Messages
1156+; ----------------------------------------------------------------------------
1157+
1158+msg_p1_wins: defb "P1 WINS!", 0
1159+msg_p2_wins: defb "P2 WINS!", 0
1160+msg_draw: defb "DRAW!", 0
9461161
9471162 ; ----------------------------------------------------------------------------
9481163 ; Variables