AI Framework
Play against the computer. Random but legal moves. The AI takes its turn automatically.
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

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:
- Game mode is VS_AI
- Current player is Player 2
- 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
| 1 | 1 | ; ============================================================================ | |
| 2 | - | ; INK WAR - Unit 8: Complete Two-Player Game | |
| 2 | + | ; INK WAR - Unit 9: AI Framework | |
| 3 | 3 | ; ============================================================================ | |
| 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. | |
| 6 | 6 | ; | |
| 7 | 7 | ; Controls: Q=Up, A=Down, O=Left, P=Right, SPACE=Claim | |
| 8 | + | ; 1=Two Player, 2=vs Computer (on title screen) | |
| 8 | 9 | ; ============================================================================ | |
| 9 | 10 | | |
| 10 | 11 | org 32768 | |
| ... | |||
| 68 | 69 | GS_TITLE equ 0 | |
| 69 | 70 | GS_PLAYING equ 1 | |
| 70 | 71 | 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) | |
| 71 | 79 | | |
| 72 | 80 | ; Title screen positions | |
| 73 | 81 | TITLE_ROW equ 8 | |
| 74 | 82 | 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 | |
| 77 | 87 | | |
| 78 | 88 | ; Results screen positions | |
| 79 | 89 | CONTINUE_ROW equ 22 ; "PRESS ANY KEY" after results | |
| ... | |||
| 113 | 123 | ; ---------------------------------------------------------------------------- | |
| 114 | 124 | ; State: Title | |
| 115 | 125 | ; ---------------------------------------------------------------------------- | |
| 126 | + | ; Waits for 1 (Two Player) or 2 (vs Computer) | |
| 127 | + | | |
| 128 | + | ROW_12345 equ $f7 ; Keyboard row for 1,2,3,4,5 | |
| 116 | 129 | | |
| 117 | 130 | state_title: | |
| 118 | - | ; Wait for any key press | |
| 119 | - | xor a | |
| 131 | + | ; Check for key 1 (Two Player) | |
| 132 | + | ld a, ROW_12345 | |
| 120 | 133 | 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 | |
| 124 | 136 | | |
| 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 | |
| 126 | 152 | call start_game | |
| 127 | 153 | jp main_loop | |
| 128 | 154 | | |
| ... | |||
| 131 | 157 | ; ---------------------------------------------------------------------------- | |
| 132 | 158 | | |
| 133 | 159 | 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 | |
| 134 | 172 | call read_keyboard | |
| 135 | 173 | 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 | |
| 136 | 194 | jp main_loop | |
| 137 | 195 | | |
| 138 | 196 | ; ---------------------------------------------------------------------------- | |
| ... | |||
| 484 | 542 | ld (p2_count), a | |
| 485 | 543 | ld (last_key), a ; No previous key | |
| 486 | 544 | ld (key_timer), a ; No delay active | |
| 545 | + | | |
| 546 | + | ; Initialize AI timer | |
| 547 | + | ld a, AI_DELAY | |
| 548 | + | ld (ai_timer), a | |
| 487 | 549 | | |
| 488 | 550 | ret | |
| 489 | 551 | | |
| ... | |||
| 1260 | 1322 | ld e, TEXT_ATTR | |
| 1261 | 1323 | call print_message | |
| 1262 | 1324 | | |
| 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 | |
| 1267 | 1336 | ld e, TEXT_ATTR | |
| 1268 | 1337 | call print_message | |
| 1269 | 1338 | | |
| ... | |||
| 1305 | 1374 | cpl | |
| 1306 | 1375 | and %00011111 | |
| 1307 | 1376 | 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 | |
| 1308 | 1482 | | |
| 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 | |
| 1309 | 1491 | ret | |
| 1492 | + | | |
| 1493 | + | random_seed: defb $5a ; Seed value | |
| 1310 | 1494 | | |
| 1311 | 1495 | ; ---------------------------------------------------------------------------- | |
| 1312 | 1496 | ; Messages | |
| ... | |||
| 1316 | 1500 | msg_p2_wins: defb "P2 WINS!", 0 | |
| 1317 | 1501 | msg_draw: defb "DRAW!", 0 | |
| 1318 | 1502 | 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 | |
| 1320 | 1505 | msg_continue: defb "PRESS ANY KEY", 0 | |
| 1321 | 1506 | | |
| 1322 | 1507 | ; ---------------------------------------------------------------------------- | |
| ... | |||
| 1324 | 1509 | ; ---------------------------------------------------------------------------- | |
| 1325 | 1510 | | |
| 1326 | 1511 | game_state: defb 0 ; 0=title, 1=playing, 2=results | |
| 1512 | + | game_mode: defb 0 ; 0=two player, 1=vs AI | |
| 1327 | 1513 | cursor_row: defb 0 | |
| 1328 | 1514 | cursor_col: defb 0 | |
| 1329 | 1515 | key_pressed: defb 0 | |
| 1330 | 1516 | last_key: defb 0 ; Previous frame's key for repeat detection | |
| 1331 | 1517 | key_timer: defb 0 ; Countdown for key repeat delay | |
| 1518 | + | ai_timer: defb 0 ; Countdown before AI moves | |
| 1332 | 1519 | current_player: defb 1 | |
| 1333 | 1520 | p1_count: defb 0 | |
| 1334 | 1521 | p2_count: defb 0 |