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

Results Screen

Proper end-of-game display with final scores, winner announcement, and victory margin.

23% of Ink War

When a game ends, the current display simply shows “P1 WINS!” or “DRAW!” overlaid on the board. This unit creates a proper results screen with structured information: a header, final scores for both players in their colours, the winner announcement, and the victory margin.

The results screen transforms the end of game from an afterthought into a satisfying conclusion.

Run It

pasmonext --sna inkwar.asm inkwar.sna

Unit 15 Screenshot

Play a complete game to see the enhanced results screen with final scores and victory margin.

Results Screen Layout

The screen shows information in a clear hierarchy:

         GAME OVER

    P1:32          P2:32

       P1 WINS!
       BY 04 CELLS

     PRESS ANY KEY

Each element has a defined position:

; Results screen positions
GAMEOVER_ROW equ    18              ; "GAME OVER" header
GAMEOVER_COL equ    11              ; (32-9)/2 = 11.5
FINAL_ROW   equ     20              ; Final score row
FINAL_P1_COL equ    8               ; "P1: nn"
FINAL_P2_COL equ    20              ; "P2: nn"
WINNER_ROW  equ     22              ; Winner announcement
WINNER_COL  equ     10              ; Centred
MARGIN_ROW  equ     23              ; "BY nn CELLS"
MARGIN_COL  equ     11

; Messages for results screen
msg_gameover:   defb    "GAME OVER", 0
msg_final:      defb    "FINAL SCORE", 0
msg_by:         defb    "BY ", 0
msg_cells:      defb    " CELLS", 0

The layout uses the lower portion of the screen, below the game board.

Enhanced Show Results

The show_results function now builds the complete results display:

show_results:
            ; Display "GAME OVER" header
            ld      b, GAMEOVER_ROW
            ld      c, GAMEOVER_COL
            ld      hl, msg_gameover
            ld      e, TEXT_ATTR
            call    print_message

            ; Display final P1 score with colour
            ld      b, FINAL_ROW
            ld      c, FINAL_P1_COL
            ; ... print "P1:" and score ...
            ld      e, P1_TEXT
            call    set_attr_range

            ; Display final P2 score with colour
            ; ... same pattern for P2 ...

            ; Determine winner and display message
            ld      a, (p1_count)
            ld      b, a
            ld      a, (p2_count)
            cp      b
            jr      c, .sr_p1_wins
            jr      z, .sr_draw
            jr      .sr_p2_wins

.sr_p1_wins:
            ld      hl, msg_p1_wins
            ld      e, P1_TEXT
            call    print_message
            ; Calculate margin: p1 - p2
            call    show_margin
            jr      .sr_prompt

            ; ... similar for P2 wins and draw ...

.sr_prompt:
            ld      hl, msg_continue
            call    print_message
            ret

Key improvements:

  • “GAME OVER” header — Clear signal the game has ended
  • Final scores — Both players’ scores displayed prominently with their colours
  • Winner message — In the winner’s colour (P1 = red, P2 = blue)
  • Victory margin — How many cells the winner led by

Calculating Victory Margin

When someone wins, we show the score difference:

; ----------------------------------------------------------------------------
; Show Margin
; ----------------------------------------------------------------------------
; Displays "BY nn CELLS" for victory margin
; A = margin (difference in scores)

show_margin:
            push    af              ; Save margin
            ; Print "BY "
            ld      b, MARGIN_ROW
            ld      c, MARGIN_COL
            ld      hl, msg_by
            ld      e, TEXT_ATTR
            call    print_message
            ; Print margin number
            ld      c, MARGIN_COL + 3
            pop     af
            call    print_two_digits
            ; Print " CELLS"
            ld      c, MARGIN_COL + 5
            ld      hl, msg_cells
            call    print_message
            ret

The margin calculation uses neg to negate one score before adding:

ld      a, (p1_count)
ld      b, a
ld      a, (p2_count)
neg                         ; A = -p2_count
add     a, b                ; A = p1_count - p2_count

This gives the difference without needing a subtraction instruction.

Draw Handling

Draws skip the margin display entirely — there’s no meaningful difference to show:

.sr_draw:
            ; Display "DRAW!" (centred)
            ld      b, WINNER_ROW
            ld      c, WINNER_COL + 2
            ld      hl, msg_draw
            ld      e, TEXT_ATTR
            call    print_message
            ; No margin for draw - skip directly to prompt

The DRAW! message is centred differently (offset by 2) because it’s shorter than “P1 WINS!”.

Visual Consistency

The results screen maintains visual consistency with the rest of the game:

  • P1’s score uses P1_TEXT attribute (red background)
  • P2’s score uses P2_TEXT attribute (blue background)
  • Winner message uses the winner’s colour
  • Header and margin use neutral TEXT_ATTR

This colour coding reinforces the game’s visual language — players immediately recognise “their” colour.

The Complete Code

; ============================================================================
; INK WAR - Unit 15: Results Screen
; ============================================================================
; Proper end-of-game display with final scores, winner announcement,
; and score difference. Clear presentation of the game outcome.
;
; Controls: Q=Up, A=Down, O=Left, P=Right, SPACE=Claim
;           1=Two Player, 2=AI Easy, 3=AI Medium, 4=AI Hard
; ============================================================================

            org     32768

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

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

BOARD_ROW   equ     8
BOARD_COL   equ     12
BOARD_SIZE  equ     8

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

; Game constants
TOTAL_CELLS equ     64              ; 8x8 board

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

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

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

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

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

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

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

; Game modes
GM_TWO_PLAYER equ   0
GM_VS_AI    equ     1

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

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

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

; Results screen positions
GAMEOVER_ROW equ    18              ; "GAME OVER" header
GAMEOVER_COL equ    11              ; (32-9)/2 = 11.5
FINAL_ROW   equ     20              ; Final score row
FINAL_P1_COL equ    8               ; "P1: nn"
FINAL_P2_COL equ    20              ; "P2: nn"
WINNER_ROW  equ     22              ; Winner announcement
WINNER_COL  equ     10              ; Centred
MARGIN_ROW  equ     23              ; "BY nn CELLS"
MARGIN_COL  equ     11
CONTINUE_ROW equ    22              ; "PRESS ANY KEY" after results
CONTINUE_COL equ    9               ; (32-13)/2 = 9.5

; Input timing
KEY_DELAY   equ     8               ; Frames between key repeats

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

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

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

main_loop:
            halt

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

            jp      main_loop

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

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

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

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

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

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

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

            jp      main_loop           ; No valid key - keep waiting

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

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

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

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

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

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

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

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

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

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

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

            ; AI makes a move
            call    ai_make_move
            jp      main_loop

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

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

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

            ; Wait for key release before playing
            call    wait_key_release

            ret

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

init_screen:
            xor     a
            out     (KEY_PORT), a

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

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

            ret

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

draw_ui:
            call    draw_scores
            call    draw_turn_indicator
            ret

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

draw_scores:
            ; Count cells for each player
            call    count_cells

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

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

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

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

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

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

            ret

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

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

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

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

            ret

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

print_char:
            push    bc
            push    de
            push    hl
            push    af

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

            push    hl              ; Save character data address

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

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

            pop     hl              ; HL = character data

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

            pop     af
            pop     hl
            pop     de
            pop     bc
            ret

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

print_two_digits:
            push    bc

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

.ptd_print:
            push    af              ; Save units digit

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

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

            pop     bc
            ret

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

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

            ld      hl, board_state
            ld      b, 64               ; 64 cells

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

            ret

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

set_attr_range:
            push    bc
            push    de

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

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

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

            ret

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

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

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

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

            ld      a, 1
            ld      (current_player), a

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

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

            ret

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

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

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

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

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

            ret

draw_border_row:
            push    bc
.dbr_loop:
            push    bc
            push    de

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

            ld      (hl), BORDER_ATTR

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

draw_border_col:
            push    bc
.dbc_loop:
            push    bc
            push    de

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

            ld      (hl), BORDER_ATTR

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

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

draw_board:
            ld      b, BOARD_SIZE
            ld      c, BOARD_ROW

.db_row:
            push    bc

            ld      b, BOARD_SIZE
            ld      d, BOARD_COL

.db_col:
            push    bc

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

            ld      (hl), EMPTY_ATTR

            pop     bc
            inc     d
            djnz    .db_col

            pop     bc
            inc     c
            djnz    .db_row

            ret

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

draw_cursor:
            call    get_cell_state

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

            ld      a, CURSOR_ATTR
            jr      .dc_set

.dc_p1:
            ld      a, P1_CURSOR
            jr      .dc_set

.dc_p2:
            ld      a, P2_CURSOR

.dc_set:
            push    af

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

            pop     af
            ld      (hl), a

            ret

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

clear_cursor:
            call    get_cell_state

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

            ld      a, EMPTY_ATTR
            jr      .clc_set

.clc_p1:
            ld      a, P1_ATTR
            jr      .clc_set

.clc_p2:
            ld      a, P2_ATTR

.clc_set:
            push    af

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

            pop     af
            ld      (hl), a

            ret

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

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

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

read_keyboard:
            xor     a
            ld      (key_pressed), a

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

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

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

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

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

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

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

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

.hi_allow:
            ; Process movement
            call    clear_cursor

            ld      a, (key_pressed)

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

.hi_done:
            call    draw_cursor
            ret

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

try_claim:
            call    get_cell_state
            or      a
            jr      z, .tc_valid

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

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

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

            call    draw_ui             ; Update scores and turn indicator

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

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

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

.tc_continue:
            call    update_border
            call    draw_cursor

            ret

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

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

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

            push    af

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

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

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

sound_claim:
            ld      hl, 400
            ld      b, 20

.scl_loop:
            push    bc
            push    hl

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

            pop     hl
            pop     bc

            ld      de, 20
            or      a
            sbc     hl, de

            djnz    .scl_loop
            ret

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

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

sound_error:
            ld      b, 30               ; Duration

.se_loop:
            push    bc

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

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

            pop     bc
            djnz    .se_loop

            ret

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

sound_select:
            ld      b, 15               ; Short duration

.ss_loop:
            push    bc

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

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

            pop     bc
            djnz    .ss_loop

            ret

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

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

.st_loop:
            push    bc
            push    hl

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

            pop     hl
            pop     bc

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

            djnz    .st_loop
            ret

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

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

.sv_note:
            push    bc
            push    hl

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

            pop     hl
            pop     bc

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

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

            djnz    .sv_note
            ret

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

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

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

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

            ret

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

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

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

flash_border_error:
            ; Flash red 3 times
            ld      b, 3

.fbe_loop:
            push    bc

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

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

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

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

            pop     bc
            djnz    .fbe_loop

            ret

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

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

; ----------------------------------------------------------------------------
; Show Results
; ----------------------------------------------------------------------------
; Displays enhanced results screen with final scores and winner

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

            ; Display "GAME OVER" header
            ld      b, GAMEOVER_ROW
            ld      c, GAMEOVER_COL
            ld      hl, msg_gameover
            ld      e, TEXT_ATTR
            call    print_message

            ; Display final P1 score
            ld      b, FINAL_ROW
            ld      c, FINAL_P1_COL
            ld      a, 'P'
            call    print_char
            inc     c
            ld      a, '1'
            call    print_char
            inc     c
            ld      a, ':'
            call    print_char
            inc     c
            ld      a, (p1_count)
            call    print_two_digits
            ; Set P1 colour
            ld      a, FINAL_ROW
            ld      c, FINAL_P1_COL
            ld      b, 5
            ld      e, P1_TEXT
            call    set_attr_range

            ; Display final P2 score
            ld      b, FINAL_ROW
            ld      c, FINAL_P2_COL
            ld      a, 'P'
            call    print_char
            inc     c
            ld      a, '2'
            call    print_char
            inc     c
            ld      a, ':'
            call    print_char
            inc     c
            ld      a, (p2_count)
            call    print_two_digits
            ; Set P2 colour
            ld      a, FINAL_ROW
            ld      c, FINAL_P2_COL
            ld      b, 5
            ld      e, P2_TEXT
            call    set_attr_range

            ; Determine winner and display message
            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, WINNER_ROW
            ld      c, WINNER_COL
            ld      hl, msg_p1_wins
            ld      e, P1_TEXT
            call    print_message
            ; Calculate and show margin
            ld      a, (p1_count)
            ld      b, a
            ld      a, (p2_count)
            neg
            add     a, b            ; A = p1 - p2
            call    show_margin
            jr      .sr_prompt

.sr_p2_wins:
            ; Display "P2 WINS!"
            ld      b, WINNER_ROW
            ld      c, WINNER_COL
            ld      hl, msg_p2_wins
            ld      e, P2_TEXT
            call    print_message
            ; Calculate and show margin
            ld      a, (p2_count)
            ld      b, a
            ld      a, (p1_count)
            neg
            add     a, b            ; A = p2 - p1
            call    show_margin
            jr      .sr_prompt

.sr_draw:
            ; Display "DRAW!" (centred on winner row)
            ld      b, WINNER_ROW
            ld      c, WINNER_COL + 2
            ld      hl, msg_draw
            ld      e, TEXT_ATTR
            call    print_message
            ; No margin for draw - skip to prompt

.sr_prompt:
            ; Display "PRESS ANY KEY" prompt (on next row down)
            ld      b, WINNER_ROW + 2
            ld      c, CONTINUE_COL
            ld      hl, msg_continue
            ld      e, TEXT_ATTR
            call    print_message
            ret

; ----------------------------------------------------------------------------
; Show Margin
; ----------------------------------------------------------------------------
; Displays "BY nn CELLS" for victory margin
; A = margin (difference in scores)

show_margin:
            push    af              ; Save margin
            ; Print "BY "
            ld      b, MARGIN_ROW
            ld      c, MARGIN_COL
            ld      hl, msg_by
            ld      e, TEXT_ATTR
            call    print_message
            ; Print margin number
            ld      c, MARGIN_COL + 3
            pop     af
            call    print_two_digits
            ; Print " CELLS"
            ld      c, MARGIN_COL + 5
            ld      hl, msg_cells
            call    print_message
            ret

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

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

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

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

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

            ret

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

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

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

.vc_loop:
            push    bc

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

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

            ; Black
            xor     a
            out     (KEY_PORT), a

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

            pop     bc
            djnz    .vc_loop

            ret

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

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

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

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

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

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

            ret

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

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

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

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

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

            ret

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

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

.aim_easy:
            ; Random moves
            call    find_random_empty_cell
            jr      .aim_have_cell

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

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

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

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

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

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

            call    draw_ui

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

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

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

.aim_continue:
            call    update_border
            call    draw_cursor
            ret

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

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

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

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

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

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

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

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

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

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

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

            ; Return best cell
            ld      a, (best_cell)
            ret

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

best_cell:      defb    0
best_score:     defb    0

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

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

            ; Scan all 64 cells
            ld      c, 0

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

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

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

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

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

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

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

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

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

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

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

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

.gpb_corner:
            ld      a, 2                ; Corner bonus
            ret

.gpb_edge:
            ld      a, 1                ; Edge bonus
            ret

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

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

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

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

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

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

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

            ld      a, (adj_count)
            ret

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

temp_cell:      defb    0
adj_count:      defb    0

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

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

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

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

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

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

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

            ld      a, (adj_count)
            ret

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

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

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

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

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

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

            djnz    .frec_loop

            ; No empty cells found
            ld      a, $ff
            ret

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

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

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

random_seed:    defb    $5a         ; Seed value

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

msg_p1_wins:    defb    "P1 WINS!", 0
msg_p2_wins:    defb    "P2 WINS!", 0
msg_draw:       defb    "DRAW!", 0
msg_title:      defb    "INK WAR", 0
msg_mode1:      defb    "1 - TWO PLAYER", 0
msg_mode2:      defb    "2 - AI EASY", 0
msg_mode3:      defb    "3 - AI MEDIUM", 0
msg_mode4:      defb    "4 - AI HARD", 0
msg_continue:   defb    "PRESS ANY KEY", 0
msg_gameover:   defb    "GAME OVER", 0
msg_final:      defb    "FINAL SCORE", 0
msg_by:         defb    "BY ", 0
msg_cells:      defb    " CELLS", 0

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

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

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

            end     start

Try This: Percentage Display

Show victory as a percentage of the board:

; Calculate percentage: (score * 100) / 64
ld      a, (p1_count)
; ... multiply by 100, divide by 64 ...
; Display as "50%" instead of "32"

This gives players a different perspective on their performance.

Try This: Best Score Tracking

Track and display the highest winning margin:

best_margin:    defb    0

; After calculating margin:
ld      b, a                ; B = current margin
ld      a, (best_margin)
cp      b
jr      nc, .not_best
ld      a, b
ld      (best_margin), a    ; New record
; ... display "NEW RECORD!" ...

This adds replay value by giving players a target to beat.

What You’ve Learnt

  • Screen composition — Building complex displays from simple elements
  • Information hierarchy — Ordering content by importance
  • Visual consistency — Maintaining colour coding throughout
  • Calculation techniques — Using neg for subtraction

What’s Next

In Unit 16, we’ll complete Phase 1 with final polish and a complete playable game.

What Changed

Unit 14 → Unit 15
+118-17
11 ; ============================================================================
2-; INK WAR - Unit 14: Sound Effects
2+; INK WAR - Unit 15: Results Screen
33 ; ============================================================================
4-; Complete audio feedback: menu select, claim, error, turn change, victory.
5-; The beeper gives every action a distinctive sound.
4+; Proper end-of-game display with final scores, winner announcement,
5+; and score difference. Clear presentation of the game outcome.
66 ;
77 ; Controls: Q=Up, A=Down, O=Left, P=Right, SPACE=Claim
88 ; 1=Two Player, 2=AI Easy, 3=AI Medium, 4=AI Hard
...
9595 MODE4_COL equ 10
9696
9797 ; Results screen positions
98+GAMEOVER_ROW equ 18 ; "GAME OVER" header
99+GAMEOVER_COL equ 11 ; (32-9)/2 = 11.5
100+FINAL_ROW equ 20 ; Final score row
101+FINAL_P1_COL equ 8 ; "P1: nn"
102+FINAL_P2_COL equ 20 ; "P2: nn"
103+WINNER_ROW equ 22 ; Winner announcement
104+WINNER_COL equ 10 ; Centred
105+MARGIN_ROW equ 23 ; "BY nn CELLS"
106+MARGIN_COL equ 11
98107 CONTINUE_ROW equ 22 ; "PRESS ANY KEY" after results
99108 CONTINUE_COL equ 9 ; (32-13)/2 = 9.5
100109
...
13871396 ; ----------------------------------------------------------------------------
13881397 ; Show Results
13891398 ; ----------------------------------------------------------------------------
1390-; Displays winner message based on scores
1399+; Displays enhanced results screen with final scores and winner
13911400
13921401 show_results:
13931402 ; Clear turn indicator
...
13971406 ld e, TEXT_ATTR
13981407 call set_attr_range
13991408
1400- ; Determine winner
1409+ ; Display "GAME OVER" header
1410+ ld b, GAMEOVER_ROW
1411+ ld c, GAMEOVER_COL
1412+ ld hl, msg_gameover
1413+ ld e, TEXT_ATTR
1414+ call print_message
1415+
1416+ ; Display final P1 score
1417+ ld b, FINAL_ROW
1418+ ld c, FINAL_P1_COL
1419+ ld a, 'P'
1420+ call print_char
1421+ inc c
1422+ ld a, '1'
1423+ call print_char
1424+ inc c
1425+ ld a, ':'
1426+ call print_char
1427+ inc c
1428+ ld a, (p1_count)
1429+ call print_two_digits
1430+ ; Set P1 colour
1431+ ld a, FINAL_ROW
1432+ ld c, FINAL_P1_COL
1433+ ld b, 5
1434+ ld e, P1_TEXT
1435+ call set_attr_range
1436+
1437+ ; Display final P2 score
1438+ ld b, FINAL_ROW
1439+ ld c, FINAL_P2_COL
1440+ ld a, 'P'
1441+ call print_char
1442+ inc c
1443+ ld a, '2'
1444+ call print_char
1445+ inc c
1446+ ld a, ':'
1447+ call print_char
1448+ inc c
1449+ ld a, (p2_count)
1450+ call print_two_digits
1451+ ; Set P2 colour
1452+ ld a, FINAL_ROW
1453+ ld c, FINAL_P2_COL
1454+ ld b, 5
1455+ ld e, P2_TEXT
1456+ call set_attr_range
1457+
1458+ ; Determine winner and display message
14011459 ld a, (p1_count)
14021460 ld b, a
14031461 ld a, (p2_count)
...
14091467
14101468 .sr_p1_wins:
14111469 ; Display "P1 WINS!"
1412- ld b, RESULT_ROW
1413- ld c, RESULT_COL
1470+ ld b, WINNER_ROW
1471+ ld c, WINNER_COL
14141472 ld hl, msg_p1_wins
14151473 ld e, P1_TEXT
14161474 call print_message
1417- jr .sr_continue
1475+ ; Calculate and show margin
1476+ ld a, (p1_count)
1477+ ld b, a
1478+ ld a, (p2_count)
1479+ neg
1480+ add a, b ; A = p1 - p2
1481+ call show_margin
1482+ jr .sr_prompt
14181483
14191484 .sr_p2_wins:
14201485 ; Display "P2 WINS!"
1421- ld b, RESULT_ROW
1422- ld c, RESULT_COL
1486+ ld b, WINNER_ROW
1487+ ld c, WINNER_COL
14231488 ld hl, msg_p2_wins
14241489 ld e, P2_TEXT
14251490 call print_message
1426- jr .sr_continue
1491+ ; Calculate and show margin
1492+ ld a, (p2_count)
1493+ ld b, a
1494+ ld a, (p1_count)
1495+ neg
1496+ add a, b ; A = p2 - p1
1497+ call show_margin
1498+ jr .sr_prompt
14271499
14281500 .sr_draw:
1429- ; Display "DRAW!"
1430- ld b, RESULT_ROW
1431- ld c, RESULT_COL + 2 ; Centre "DRAW!" better
1501+ ; Display "DRAW!" (centred on winner row)
1502+ ld b, WINNER_ROW
1503+ ld c, WINNER_COL + 2
14321504 ld hl, msg_draw
14331505 ld e, TEXT_ATTR
14341506 call print_message
1507+ ; No margin for draw - skip to prompt
14351508
1436-.sr_continue:
1437- ; Display "PRESS ANY KEY" prompt
1438- ld b, CONTINUE_ROW
1509+.sr_prompt:
1510+ ; Display "PRESS ANY KEY" prompt (on next row down)
1511+ ld b, WINNER_ROW + 2
14391512 ld c, CONTINUE_COL
14401513 ld hl, msg_continue
1514+ ld e, TEXT_ATTR
1515+ call print_message
1516+ ret
1517+
1518+; ----------------------------------------------------------------------------
1519+; Show Margin
1520+; ----------------------------------------------------------------------------
1521+; Displays "BY nn CELLS" for victory margin
1522+; A = margin (difference in scores)
1523+
1524+show_margin:
1525+ push af ; Save margin
1526+ ; Print "BY "
1527+ ld b, MARGIN_ROW
1528+ ld c, MARGIN_COL
1529+ ld hl, msg_by
14411530 ld e, TEXT_ATTR
1531+ call print_message
1532+ ; Print margin number
1533+ ld c, MARGIN_COL + 3
1534+ pop af
1535+ call print_two_digits
1536+ ; Print " CELLS"
1537+ ld c, MARGIN_COL + 5
1538+ ld hl, msg_cells
14421539 call print_message
14431540 ret
14441541
...
21472244 msg_mode3: defb "3 - AI MEDIUM", 0
21482245 msg_mode4: defb "4 - AI HARD", 0
21492246 msg_continue: defb "PRESS ANY KEY", 0
2247+msg_gameover: defb "GAME OVER", 0
2248+msg_final: defb "FINAL SCORE", 0
2249+msg_by: defb "BY ", 0
2250+msg_cells: defb " CELLS", 0
21502251
21512252 ; ----------------------------------------------------------------------------
21522253 ; Variables