Game End Detection
Board full? Someone wins. Compare scores, declare the victor, restart on keypress.
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

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:
- Wait for release - If a key is already held from claiming the last cell, wait until it’s released
- 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:
- Start → Initialise and draw
- Play → Input, validate, claim, update, check end
- End → Show results, celebrate, wait for key
- 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
| 1 | 1 | ; ============================================================================ | |
| 2 | - | ; INK WAR - Unit 5: Move Validation | |
| 2 | + | ; INK WAR - Unit 6: Game End Detection | |
| 3 | 3 | ; ============================================================================ | |
| 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. | |
| 6 | 6 | ; | |
| 7 | 7 | ; Controls: Q=Up, A=Down, O=Left, P=Right, SPACE=Claim | |
| 8 | 8 | ; ============================================================================ | |
| ... | |||
| 27 | 27 | P2_SCORE_COL equ 18 ; "P2: nn" column | |
| 28 | 28 | TURN_ROW equ 4 ; Turn indicator row | |
| 29 | 29 | 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 | |
| 30 | 35 | | |
| 31 | 36 | ; Customised colours (from Unit 3) | |
| 32 | 37 | EMPTY_ATTR equ %01101000 ; Cyan paper + BRIGHT | |
| ... | |||
| 776 | 781 | ld (current_player), a | |
| 777 | 782 | | |
| 778 | 783 | 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: | |
| 779 | 797 | call update_border | |
| 780 | 798 | call draw_cursor | |
| 781 | 799 | | |
| ... | |||
| 941 | 959 | | |
| 942 | 960 | pop bc | |
| 943 | 961 | 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 | |
| 944 | 1151 | | |
| 945 | 1152 | 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 | |
| 946 | 1161 | | |
| 947 | 1162 | ; ---------------------------------------------------------------------------- | |
| 948 | 1163 | ; Variables |