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

Phase 1 Complete

A complete, playable territory control game with AI opponents and full audio-visual polish.

25% of Ink War

Congratulations! You’ve completed Phase 1 of Ink War. Over sixteen units, you’ve built a complete, playable game from scratch. Let’s review what you’ve created and celebrate the milestone.

Run It

pasmonext --sna inkwar.asm inkwar.sna

Unit 16 Screenshot

Load the game and hear the startup jingle — that’s the sound of completion.

See It In Action

Ink War gameplay — players claim cells on an 8×8 board, building territory turn by turn

What You’ve Built

Phase 1 delivers a fully functional territory control game:

; Phase 1 Complete - Feature Summary
; ============================================================================
;
; GAME MODES:
; - Two Player (hot-seat play)
; - AI Easy (random moves)
; - AI Medium (adjacent priority)
; - AI Hard (defense + position strategy)
;
; GAMEPLAY:
; - 8x8 board of attribute cells
; - Turn-based claiming
; - Move validation with error feedback
; - Game end detection
;
; UI ELEMENTS:
; - Title screen with mode selection
; - Score display with player colours
; - Turn indicator
; - Results screen with margins
;
; AUDIO:
; - Startup jingle
; - Claim sound
; - Error buzz
; - Victory fanfare
; - Draw sound
; - Menu selection beep

Every feature works together to create a polished experience.

The Journey

Here’s how Ink War grew over Phase 1:

UnitsFocusWhat You Built
1-3FoundationBoard display, cursor movement, cell claiming
4-6MechanicsScores, move validation, game end detection
7-8FlowTitle screen, complete two-player game
9-12AIRandom, adjacent, defensive, positional strategies
13-14OptionsDifficulty levels, sound effects
15-16PolishResults screen, startup sound, final verification

Each unit built on the previous, adding one clear feature at a time.

Final Polish: Startup Sound

The startup jingle gives immediate feedback that the game has loaded:

; ----------------------------------------------------------------------------
; Sound - Startup
; ----------------------------------------------------------------------------
; Quick ascending jingle when game loads

sound_startup:
            ; Three quick ascending notes
            ld      hl, 50              ; Starting pitch
            ld      b, 3                ; Three notes

.sst_note:
            push    bc
            push    hl

            ; Play note
            ld      b, 12               ; Short duration
.sst_tone:
            push    bc
            ld      a, $10
            out     (KEY_PORT), a
            ld      b, l
.sst_d1:    djnz    .sst_d1
            xor     a
            out     (KEY_PORT), a
            ld      b, l
.sst_d2:    djnz    .sst_d2
            pop     bc
            djnz    .sst_tone

            pop     hl
            pop     bc

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

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

            djnz    .sst_note
            ret

Three quick ascending notes — a small touch that makes the game feel finished.

Verification Checklist

Before moving to Phase 2, verify everything works:

Two-Player Mode:

  • ✓ Both players can claim cells
  • ✓ Turns alternate correctly
  • ✓ Invalid moves show error feedback
  • ✓ Game ends when board is full
  • ✓ Winner displayed with margin

AI Easy:

  • ✓ Makes random valid moves
  • ✓ Easy to beat consistently

AI Medium:

  • ✓ Builds connected territory
  • ✓ Moderate challenge

AI Hard:

  • ✓ Blocks opponent expansion
  • ✓ Prioritises corners and edges
  • ✓ Provides strong competition

Audio:

  • ✓ Startup jingle plays on load
  • ✓ Claim sounds on valid moves
  • ✓ Error buzz on invalid moves
  • ✓ Victory fanfare when winning
  • ✓ Draw sound on tie

The Complete Code

; ============================================================================
; INK WAR - Unit 16: Phase 1 Complete
; ============================================================================
; Complete playable game with all Phase 1 features:
; - Two-player mode with alternating turns
; - Three AI difficulty levels (Easy, Medium, Hard)
; - Sound effects for all actions and results
; - Enhanced results screen with scores and margins
;
; 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

; Video capture mode - when 1, skips title and starts AI Easy game immediately
; To enable: change VIDEO_MODE to 1 below before building
VIDEO_MODE  equ     0

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

start:
            ; Play startup jingle
            call    sound_startup

    IF VIDEO_MODE
            ; VIDEO_MODE: Skip title, start AI Easy game immediately
            ld      a, GM_VS_AI
            ld      (game_mode), a
            ld      a, AI_EASY
            ld      (ai_difficulty), a
            call    start_game
    ELSE
            ; Normal mode: 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
    ENDIF

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 - Startup
; ----------------------------------------------------------------------------
; Quick ascending jingle when game loads

sound_startup:
            ; Three quick ascending notes
            ld      hl, 50              ; Starting pitch
            ld      b, 3                ; Three notes

.sst_note:
            push    bc
            push    hl

            ; Play note
            ld      b, 12               ; Short duration
.sst_tone:
            push    bc
            ld      a, $10
            out     (KEY_PORT), a
            ld      b, l
.sst_d1:    djnz    .sst_d1
            xor     a
            out     (KEY_PORT), a
            ld      b, l
.sst_d2:    djnz    .sst_d2
            pop     bc
            djnz    .sst_tone

            pop     hl
            pop     bc

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

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

            djnz    .sst_note
            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

Code Statistics

The complete Phase 1 game in numbers:

MetricCount
Lines of code~2,300
Subroutines~40
Sound effects6
AI strategies3
Game states3

All of this fits comfortably in the Spectrum’s memory.

What You’ve Learnt

Phase 1 covered fundamental skills:

ZX Spectrum Display:

  • Attribute memory ($5800-$5AFF)
  • FBPPPIII format (Flash, Bright, Paper, Ink)
  • Screen memory organisation
  • Border colour control

Z80 Assembly:

  • Registers and addressing modes
  • Arithmetic and logic operations
  • Control flow (jumps, calls, loops)
  • Keyboard input via ports

Game Development:

  • Game loop structure
  • State machine patterns
  • Input handling and validation
  • AI decision-making

Audio:

  • Beeper sound generation
  • Timing-based pitch control
  • Musical sequences

What’s Next

Phase 2 transforms Ink War from functional to impressive:

  • Custom character sets — Replace ROM characters with custom graphics
  • Animations — Cursor pulsing, claim effects, screen transitions
  • Title music — A proper musical introduction
  • Options menu — Sound on/off, board size selection
  • Board variations — 6×6 boards for quicker games
  • Statistics — Track wins, losses, best margins

The foundation is solid. Phase 2 adds the polish that makes a game memorable.

Phase 1 Complete

You’ve built a real game. Not a tutorial example, not a code snippet — a complete, playable territory control game with multiple modes, intelligent AI, and audio feedback.

Hand the keyboard to a friend and play Ink War. You made this.

See you in Phase 2.

What Changed

Unit 15 → Unit 16
+72-4
11 ; ============================================================================
2-; INK WAR - Unit 15: Results Screen
2+; INK WAR - Unit 16: Phase 1 Complete
33 ; ============================================================================
4-; Proper end-of-game display with final scores, winner announcement,
5-; and score difference. Clear presentation of the game outcome.
4+; Complete playable game with all Phase 1 features:
5+; - Two-player mode with alternating turns
6+; - Three AI difficulty levels (Easy, Medium, Hard)
7+; - Sound effects for all actions and results
8+; - Enhanced results screen with scores and margins
69 ;
710 ; Controls: Q=Up, A=Down, O=Left, P=Right, SPACE=Claim
811 ; 1=Two Player, 2=AI Easy, 3=AI Medium, 4=AI Hard
...
109112
110113 ; Input timing
111114 KEY_DELAY equ 8 ; Frames between key repeats
115+
116+; Video capture mode - when 1, skips title and starts AI Easy game immediately
117+; To enable: change VIDEO_MODE to 1 below before building
118+VIDEO_MODE equ 0
112119
113120 ; ----------------------------------------------------------------------------
114121 ; Entry Point
115122 ; ----------------------------------------------------------------------------
116123
117124 start:
118- ; Start at title screen
125+ ; Play startup jingle
126+ call sound_startup
127+
128+ IF VIDEO_MODE
129+ ; VIDEO_MODE: Skip title, start AI Easy game immediately
130+ ld a, GM_VS_AI
131+ ld (game_mode), a
132+ ld a, AI_EASY
133+ ld (ai_difficulty), a
134+ call start_game
135+ ELSE
136+ ; Normal mode: Start at title screen
119137 ld a, GS_TITLE
120138 ld (game_state), a
121139 call init_screen
...
124142 ; Black border for title
125143 xor a
126144 out (KEY_PORT), a
145+ ENDIF
127146
128147 main_loop:
129148 halt
...
11751194
11761195 pop bc
11771196 djnz .ss_loop
1197+
1198+ ret
1199+
1200+; ----------------------------------------------------------------------------
1201+; Sound - Startup
1202+; ----------------------------------------------------------------------------
1203+; Quick ascending jingle when game loads
1204+
1205+sound_startup:
1206+ ; Three quick ascending notes
1207+ ld hl, 50 ; Starting pitch
1208+ ld b, 3 ; Three notes
1209+
1210+.sst_note:
1211+ push bc
1212+ push hl
1213+
1214+ ; Play note
1215+ ld b, 12 ; Short duration
1216+.sst_tone:
1217+ push bc
1218+ ld a, $10
1219+ out (KEY_PORT), a
1220+ ld b, l
1221+.sst_d1: djnz .sst_d1
1222+ xor a
1223+ out (KEY_PORT), a
1224+ ld b, l
1225+.sst_d2: djnz .sst_d2
1226+ pop bc
1227+ djnz .sst_tone
1228+
1229+ pop hl
1230+ pop bc
1231+
1232+ ; Decrease delay (higher pitch) for next note
1233+ ld a, l
1234+ sub 12
1235+ ld l, a
1236+
1237+ ; Brief pause between notes
1238+ push bc
1239+ ld bc, 1000
1240+.sst_pause: dec bc
1241+ ld a, b
1242+ or c
1243+ jr nz, .sst_pause
1244+ pop bc
11781245
1246+ djnz .sst_note
11791247 ret
11801248
11811249 ; ----------------------------------------------------------------------------