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

AI Framework

Play against the computer. Random but legal moves. The AI takes its turn automatically.

14% of Ink War

Two-player games are fun, but sometimes you want to play solo. This unit adds an AI opponent — a computer player that takes Player 2’s turns automatically.

The AI starts simple: it picks a random empty cell. No strategy, no intelligence, just legal moves. But this framework is the foundation for smarter opponents in later units.

Run It

pasmonext --sna inkwar.asm inkwar.sna

Unit 9 Screenshot

The title screen now offers two options: “1 - TWO PLAYER” or “2 - VS COMPUTER”. Select vs Computer and watch the AI claim cells automatically after each of your moves.

Game Mode Selection

The title screen needs to distinguish between two modes:

; ----------------------------------------------------------------------------
; Game Mode Constants
; ----------------------------------------------------------------------------

; Game modes
GM_TWO_PLAYER equ   0
GM_VS_AI    equ     1

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

; Variables needed:
; game_mode:      defb    0         ; 0=two player, 1=vs AI
; ai_timer:       defb    0         ; Countdown before AI moves

We add a game_mode variable and an ai_timer for the delay before AI moves. The delay makes the game feel more natural — instant AI moves would be jarring.

Reading the Mode Selection

The ZX Spectrum keyboard is read in half-rows. Keys 1-5 share row $F7:

; ----------------------------------------------------------------------------
; Mode Selection (in state_title)
; ----------------------------------------------------------------------------
; Waits for 1 (Two Player) or 2 (vs Computer)

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

state_title:
            ; Check for key 1 (Two Player)
            ld      a, ROW_12345
            in      a, (KEY_PORT)
            bit     0, a                ; Key 1
            jr      z, .st_two_player

            ; Check for key 2 (vs Computer)
            bit     1, a                ; Key 2
            jr      z, .st_vs_ai

            jp      main_loop           ; No valid key - keep waiting

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

.st_vs_ai:
            ld      a, GM_VS_AI
            ld      (game_mode), a
            call    start_game
            jp      main_loop

The BIT instruction tests individual bits. Bit 0 is key 1, bit 1 is key 2. When pressed, the bit reads as 0 (active low).

AI Turn Handling

The game loop checks if it’s the AI’s turn:

; ----------------------------------------------------------------------------
; AI Turn Handling (in 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

Three conditions for AI to move:

  1. Game mode is VS_AI
  2. Current player is Player 2
  3. AI timer has expired

The timer creates a half-second pause before each AI move, giving the human time to see what’s happening.

Random Number Generation

The Z80 has a built-in source of randomness — the R register:

; ----------------------------------------------------------------------------
; Random Number Generation
; ----------------------------------------------------------------------------
; Uses Z80's R register which increments with each instruction

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

random_seed:    defb    $5a         ; Seed value

The R register increments with every instruction fetch. We mix it with a seed value using addition, rotation, and XOR to produce varied results. Not cryptographically secure, but fine for a game.

Finding an Empty Cell

The AI needs to find a valid move. We start at a random position and scan until we find an empty cell:

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

Starting at a random position adds variety — without it, the AI would always claim the first available cell. The wrap-around ensures we check every cell even if we start near the end.

Why Random AI First?

Random AI is the simplest possible opponent:

  • Always makes legal moves
  • Requires minimal code
  • Easy to test and debug
  • Provides a baseline to improve upon

In upcoming units, we’ll make the AI smarter by prioritising adjacent cells, defending against the human, and targeting strategic positions.

The Complete Code

; ============================================================================
; INK WAR - Unit 9: AI Framework
; ============================================================================
; Adds AI opponent. Title screen offers mode selection. AI plays Player 2
; using random but legal moves.
;
; Controls: Q=Up, A=Down, O=Left, P=Right, SPACE=Claim
;           1=Two Player, 2=vs Computer (on title screen)
; ============================================================================

            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 timing
AI_DELAY    equ     25              ; Frames before AI moves (~0.5 sec)

; Title screen positions
TITLE_ROW   equ     8
TITLE_COL   equ     12              ; "INK WAR" (7 chars) centred: (32-7)/2=12.5
MODE1_ROW   equ     14              ; "1 - TWO PLAYER"
MODE1_COL   equ     9               ; (32-14)/2 = 9
MODE2_ROW   equ     16              ; "2 - VS COMPUTER"
MODE2_COL   equ     8               ; (32-15)/2 = 8.5

; Results screen positions
CONTINUE_ROW equ    22              ; "PRESS ANY KEY" after results
CONTINUE_COL equ    9               ; (32-13)/2 = 9.5

; Input timing
KEY_DELAY   equ     8               ; Frames between key repeats

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

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

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

main_loop:
            halt

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

            jp      main_loop

; ----------------------------------------------------------------------------
; State: Title
; ----------------------------------------------------------------------------
; Waits for 1 (Two Player) or 2 (vs Computer)

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

state_title:
            ; Check for key 1 (Two Player)
            ld      a, ROW_12345
            in      a, (KEY_PORT)
            bit     0, a                ; Key 1
            jr      z, .st_two_player

            ; Check for key 2 (vs Computer)
            bit     1, a                ; Key 2
            jr      z, .st_vs_ai

            jp      main_loop           ; No valid key - keep waiting

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

.st_vs_ai:
            ld      a, GM_VS_AI
            ld      (game_mode), 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 - show results and return to title
            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

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

flash_border_error:
            ; Flash red 3 times
            ld      b, 3

.fbe_loop:
            push    bc

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

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

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

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

            pop     bc
            djnz    .fbe_loop

            ret

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

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

; ----------------------------------------------------------------------------
; Show Results
; ----------------------------------------------------------------------------
; Displays winner message based on scores

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

            ; Determine winner
            ld      a, (p1_count)
            ld      b, a
            ld      a, (p2_count)
            cp      b
            jr      c, .sr_p1_wins      ; p2 < p1, so p1 wins
            jr      z, .sr_draw         ; p1 == p2, draw
            ; p2 > p1, p2 wins
            jr      .sr_p2_wins

.sr_p1_wins:
            ; Display "P1 WINS!"
            ld      b, RESULT_ROW
            ld      c, RESULT_COL
            ld      hl, msg_p1_wins
            ld      e, P1_TEXT
            call    print_message
            jr      .sr_continue

.sr_p2_wins:
            ; Display "P2 WINS!"
            ld      b, RESULT_ROW
            ld      c, RESULT_COL
            ld      hl, msg_p2_wins
            ld      e, P2_TEXT
            call    print_message
            jr      .sr_continue

.sr_draw:
            ; Display "DRAW!"
            ld      b, RESULT_ROW
            ld      c, RESULT_COL + 2   ; Centre "DRAW!" better
            ld      hl, msg_draw
            ld      e, TEXT_ATTR
            call    print_message

.sr_continue:
            ; Display "PRESS ANY KEY" prompt
            ld      b, CONTINUE_ROW
            ld      c, CONTINUE_COL
            ld      hl, msg_continue
            ld      e, TEXT_ATTR
            call    print_message
            ret

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

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

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

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

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

            ret

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

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

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

.vc_loop:
            push    bc

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

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

            ; Black
            xor     a
            out     (KEY_PORT), a

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

            pop     bc
            djnz    .vc_loop

            ret

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

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

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

            ; Draw "2 - VS COMPUTER"
            ld      b, MODE2_ROW
            ld      c, MODE2_COL
            ld      hl, msg_mode2
            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 random empty cell and claims it

ai_make_move:
            ; Find a random empty cell
            call    find_random_empty_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 - show results and return to title
            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 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 - VS COMPUTER", 0
msg_continue:   defb    "PRESS ANY KEY", 0

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

game_state:     defb    0               ; 0=title, 1=playing, 2=results
game_mode:      defb    0               ; 0=two player, 1=vs AI
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: Faster AI

AI_DELAY    equ     10              ; Shorter delay (~0.2 sec)

Try This: Instant AI

AI_DELAY    equ     1               ; Nearly instant

Be warned — instant AI feels unnatural and can be disorienting.

What You’ve Learnt

  • Game modes — Single variable controls two-player vs AI behaviour
  • Keyboard row reading — Individual key detection with BIT instruction
  • Random numbers — Using R register for pseudo-randomness
  • AI framework — Turn detection, delay timer, move execution

What’s Next

In Unit 10, we’ll make the AI smarter by having it prioritise cells adjacent to its existing territory.

What Changed

Unit 8 → Unit 9
+203-16
11 ; ============================================================================
2-; INK WAR - Unit 8: Complete Two-Player Game
2+; INK WAR - Unit 9: AI Framework
33 ; ============================================================================
4-; Polish pass for smooth two-player experience. Adds key repeat delay for
5-; controlled cursor movement and "PRESS ANY KEY" prompt after results.
4+; Adds AI opponent. Title screen offers mode selection. AI plays Player 2
5+; using random but legal moves.
66 ;
77 ; Controls: Q=Up, A=Down, O=Left, P=Right, SPACE=Claim
8+; 1=Two Player, 2=vs Computer (on title screen)
89 ; ============================================================================
910
1011 org 32768
...
6869 GS_TITLE equ 0
6970 GS_PLAYING equ 1
7071 GS_RESULTS equ 2
72+
73+; Game modes
74+GM_TWO_PLAYER equ 0
75+GM_VS_AI equ 1
76+
77+; AI timing
78+AI_DELAY equ 25 ; Frames before AI moves (~0.5 sec)
7179
7280 ; Title screen positions
7381 TITLE_ROW equ 8
7482 TITLE_COL equ 12 ; "INK WAR" (7 chars) centred: (32-7)/2=12.5
75-PROMPT_ROW equ 16
76-PROMPT_COL equ 5 ; "PRESS ANY KEY TO START" (22 chars): (32-22)/2=5
83+MODE1_ROW equ 14 ; "1 - TWO PLAYER"
84+MODE1_COL equ 9 ; (32-14)/2 = 9
85+MODE2_ROW equ 16 ; "2 - VS COMPUTER"
86+MODE2_COL equ 8 ; (32-15)/2 = 8.5
7787
7888 ; Results screen positions
7989 CONTINUE_ROW equ 22 ; "PRESS ANY KEY" after results
...
113123 ; ----------------------------------------------------------------------------
114124 ; State: Title
115125 ; ----------------------------------------------------------------------------
126+; Waits for 1 (Two Player) or 2 (vs Computer)
127+
128+ROW_12345 equ $f7 ; Keyboard row for 1,2,3,4,5
116129
117130 state_title:
118- ; Wait for any key press
119- xor a
131+ ; Check for key 1 (Two Player)
132+ ld a, ROW_12345
120133 in a, (KEY_PORT)
121- cpl
122- and %00011111
123- jr z, main_loop ; No key - keep waiting
134+ bit 0, a ; Key 1
135+ jr z, .st_two_player
124136
125- ; Key pressed - start game
137+ ; Check for key 2 (vs Computer)
138+ bit 1, a ; Key 2
139+ jr z, .st_vs_ai
140+
141+ jp main_loop ; No valid key - keep waiting
142+
143+.st_two_player:
144+ xor a ; GM_TWO_PLAYER = 0
145+ ld (game_mode), a
146+ call start_game
147+ jp main_loop
148+
149+.st_vs_ai:
150+ ld a, GM_VS_AI
151+ ld (game_mode), a
126152 call start_game
127153 jp main_loop
128154
...
131157 ; ----------------------------------------------------------------------------
132158
133159 state_playing:
160+ ; Check if AI's turn (Player 2 in vs AI mode)
161+ ld a, (game_mode)
162+ or a
163+ jr z, .sp_human ; Two player mode - human controls
164+
165+ ; vs AI mode - check if Player 2's turn
166+ ld a, (current_player)
167+ cp 2
168+ jr z, .sp_ai_turn
169+
170+.sp_human:
171+ ; Human player's turn
134172 call read_keyboard
135173 call handle_input
174+ jp main_loop
175+
176+.sp_ai_turn:
177+ ; AI's turn - use delay counter
178+ ld a, (ai_timer)
179+ or a
180+ jr z, .sp_ai_move ; Timer expired, make move
181+
182+ ; Still waiting
183+ dec a
184+ ld (ai_timer), a
185+ jp main_loop
186+
187+.sp_ai_move:
188+ ; Reset timer for next AI turn
189+ ld a, AI_DELAY
190+ ld (ai_timer), a
191+
192+ ; AI makes a move
193+ call ai_make_move
136194 jp main_loop
137195
138196 ; ----------------------------------------------------------------------------
...
484542 ld (p2_count), a
485543 ld (last_key), a ; No previous key
486544 ld (key_timer), a ; No delay active
545+
546+ ; Initialize AI timer
547+ ld a, AI_DELAY
548+ ld (ai_timer), a
487549
488550 ret
489551
...
12601322 ld e, TEXT_ATTR
12611323 call print_message
12621324
1263- ; Draw "PRESS ANY KEY TO START" prompt
1264- ld b, PROMPT_ROW
1265- ld c, PROMPT_COL
1266- ld hl, msg_prompt
1325+ ; Draw "1 - TWO PLAYER"
1326+ ld b, MODE1_ROW
1327+ ld c, MODE1_COL
1328+ ld hl, msg_mode1
1329+ ld e, TEXT_ATTR
1330+ call print_message
1331+
1332+ ; Draw "2 - VS COMPUTER"
1333+ ld b, MODE2_ROW
1334+ ld c, MODE2_COL
1335+ ld hl, msg_mode2
12671336 ld e, TEXT_ATTR
12681337 call print_message
12691338
...
13051374 cpl
13061375 and %00011111
13071376 jr z, .wfk_wait
1377+
1378+ ret
1379+
1380+; ----------------------------------------------------------------------------
1381+; AI Make Move
1382+; ----------------------------------------------------------------------------
1383+; AI picks a random empty cell and claims it
1384+
1385+ai_make_move:
1386+ ; Find a random empty cell
1387+ call find_random_empty_cell
1388+ ; A = cell index (0-63), or $FF if board full
1389+
1390+ cp $ff
1391+ ret z ; No empty cells (shouldn't happen)
1392+
1393+ ; Convert index to row/col and set cursor
1394+ ld b, a
1395+ and %00000111 ; Column = index AND 7
1396+ ld (cursor_col), a
1397+ ld a, b
1398+ rrca
1399+ rrca
1400+ rrca
1401+ and %00000111 ; Row = index >> 3
1402+ ld (cursor_row), a
1403+
1404+ ; Claim the cell (reuse existing code)
1405+ call claim_cell
1406+ call sound_claim
1407+
1408+ ; Switch player
1409+ ld a, (current_player)
1410+ xor 3
1411+ ld (current_player), a
1412+
1413+ call draw_ui
1414+
1415+ ; Check if game is over
1416+ call check_game_over
1417+ or a
1418+ jr z, .aim_continue
1419+
1420+ ; Game over - show results and return to title
1421+ call show_results
1422+ call victory_celebration
1423+ call wait_for_key
1424+
1425+ ; Return to title screen
1426+ ld a, GS_TITLE
1427+ ld (game_state), a
1428+ call init_screen
1429+ call draw_title_screen
1430+ xor a
1431+ out (KEY_PORT), a ; Black border for title
1432+ ret
1433+
1434+.aim_continue:
1435+ call update_border
1436+ call draw_cursor
1437+ ret
1438+
1439+; ----------------------------------------------------------------------------
1440+; Find Random Empty Cell
1441+; ----------------------------------------------------------------------------
1442+; Returns A = index of a random empty cell (0-63), or $FF if none
1443+
1444+find_random_empty_cell:
1445+ ; Get random starting position
1446+ call get_random
1447+ and %00111111 ; Limit to 0-63
1448+
1449+ ld c, a ; C = start index
1450+ ld b, 64 ; B = cells to check
1451+
1452+.frec_loop:
1453+ ; Check if cell at index C is empty
1454+ ld hl, board_state
1455+ ld d, 0
1456+ ld e, c
1457+ add hl, de
1458+ ld a, (hl)
1459+ or a
1460+ jr z, .frec_found ; Found empty cell
1461+
1462+ ; Try next cell (wrap around)
1463+ inc c
1464+ ld a, c
1465+ and %00111111 ; Wrap at 64
1466+ ld c, a
1467+
1468+ djnz .frec_loop
1469+
1470+ ; No empty cells found
1471+ ld a, $ff
1472+ ret
1473+
1474+.frec_found:
1475+ ld a, c ; Return cell index
1476+ ret
1477+
1478+; ----------------------------------------------------------------------------
1479+; Get Random
1480+; ----------------------------------------------------------------------------
1481+; Returns A = pseudo-random number using R register
13081482
1483+get_random:
1484+ ld a, r ; R register changes every instruction
1485+ ld b, a
1486+ ld a, (random_seed)
1487+ add a, b
1488+ rlca
1489+ xor b
1490+ ld (random_seed), a
13091491 ret
1492+
1493+random_seed: defb $5a ; Seed value
13101494
13111495 ; ----------------------------------------------------------------------------
13121496 ; Messages
...
13161500 msg_p2_wins: defb "P2 WINS!", 0
13171501 msg_draw: defb "DRAW!", 0
13181502 msg_title: defb "INK WAR", 0
1319-msg_prompt: defb "PRESS ANY KEY TO START", 0
1503+msg_mode1: defb "1 - TWO PLAYER", 0
1504+msg_mode2: defb "2 - VS COMPUTER", 0
13201505 msg_continue: defb "PRESS ANY KEY", 0
13211506
13221507 ; ----------------------------------------------------------------------------
...
13241509 ; ----------------------------------------------------------------------------
13251510
13261511 game_state: defb 0 ; 0=title, 1=playing, 2=results
1512+game_mode: defb 0 ; 0=two player, 1=vs AI
13271513 cursor_row: defb 0
13281514 cursor_col: defb 0
13291515 key_pressed: defb 0
13301516 last_key: defb 0 ; Previous frame's key for repeat detection
13311517 key_timer: defb 0 ; Countdown for key repeat delay
1518+ai_timer: defb 0 ; Countdown before AI moves
13321519 current_player: defb 1
13331520 p1_count: defb 0
13341521 p2_count: defb 0