Title Screen
First impressions matter. Add a title screen with state machine architecture.
The game works. Two players can battle for territory, see who wins, and play again. But it starts abruptly — no introduction, no title, just straight into gameplay.
This unit adds a title screen. More importantly, it introduces a state machine — a pattern you’ll use in every game you build.
Run It
pasmonext --sna inkwar.asm inkwar.sna

Load the snapshot and you’ll see “INK WAR” with “PRESS ANY KEY TO START”. Press any key to begin playing. When the game ends, you’ll return to this title screen.
The State Machine Pattern
Games have distinct phases: title screen, gameplay, game over. The state machine pattern handles this cleanly:
; ----------------------------------------------------------------------------
; Game States
; ----------------------------------------------------------------------------
; State machine constants for game flow
; Game states (state machine)
GS_TITLE equ 0
GS_PLAYING equ 1
GS_RESULTS equ 2
; Title screen positions
TITLE_ROW equ 8
TITLE_COL equ 12 ; "INK WAR" (7 chars) centred: (32-7)/2=12.5
PROMPT_ROW equ 16
PROMPT_COL equ 5 ; "PRESS ANY KEY TO START" (22 chars): (32-22)/2=5
Three states, each a single byte value. The current state lives in a variable. The main loop checks this state and dispatches to the appropriate handler.
Main Loop Dispatch
The entry point and main loop:
; ----------------------------------------------------------------------------
; State Machine
; ----------------------------------------------------------------------------
; Entry point and main loop dispatch
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
jp main_loop
The flow is:
- Set initial state to
GS_TITLE - Draw the title screen
- Main loop checks state and jumps to the right handler
Using or a to check for zero is a common Z80 idiom. It’s faster than cp 0 and sets the zero flag if A is zero.
Drawing the Title Screen
The title screen is straightforward — two messages centred on screen:
; ----------------------------------------------------------------------------
; 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 "PRESS ANY KEY TO START" prompt
ld b, PROMPT_ROW
ld c, PROMPT_COL
ld hl, msg_prompt
ld e, TEXT_ATTR
call print_message
ret
; Messages
msg_title: defb "INK WAR", 0
msg_prompt: defb "PRESS ANY KEY TO START", 0
We reuse the print_message routine from Unit 6. Null-terminated strings make it easy to add new messages.
State Handlers
Each state has its own handler:
; ----------------------------------------------------------------------------
; State Handlers
; ----------------------------------------------------------------------------
state_title:
; Wait for any key press
xor a
in a, (KEY_PORT)
cpl
and %00011111
jr z, main_loop ; No key - keep waiting
; Key pressed - start game
call start_game
jp main_loop
state_playing:
call read_keyboard
call handle_input
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
Title state: Wait for any key, then transition to playing.
Playing state: Run the normal game loop (keyboard, input handling).
The start_game function handles the transition — it changes state, redraws the screen, and waits for the player to release the key they pressed to start.
Returning to Title
After game over, instead of restarting immediately, we return to the title:
; ----------------------------------------------------------------------------
; Return to Title (in try_claim after game over)
; ----------------------------------------------------------------------------
; 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
This creates a complete loop: Title → Play → Results → Title.
Why State Machines?
The state machine pattern keeps code organised as games grow complex. Consider what you’d need without it:
- Flag variables for “is game over?”, “is showing title?”, “is paused?”
- Nested conditions checking multiple flags
- Spaghetti jumps between different parts of the code
With a state machine:
- One variable tracks current state
- Each state has a dedicated handler
- Transitions are explicit and clear
- Adding new states (pause menu, options) is straightforward
The Complete Code
; ============================================================================
; INK WAR - Unit 7: Title Screen
; ============================================================================
; Adds title screen with state machine. Game starts at title, transitions
; to playing, shows results, then returns to title.
;
; Controls: Q=Up, A=Down, O=Left, P=Right, SPACE=Claim
; ============================================================================
org 32768
; ----------------------------------------------------------------------------
; Constants
; ----------------------------------------------------------------------------
ATTR_BASE equ $5800
DISPLAY_FILE equ $4000
CHAR_SET equ $3C00 ; ROM character set base address
BOARD_ROW equ 8
BOARD_COL equ 12
BOARD_SIZE equ 8
; Display positions
SCORE_ROW equ 2 ; Score display row
P1_SCORE_COL equ 10 ; "P1: nn" column
P2_SCORE_COL equ 18 ; "P2: nn" column
TURN_ROW equ 4 ; Turn indicator row
TURN_COL equ 14 ; Turn indicator column
RESULT_ROW equ 20 ; Winner message row
RESULT_COL equ 11 ; Winner message column
; Game constants
TOTAL_CELLS equ 64 ; 8x8 board
; Customised colours (from Unit 3)
EMPTY_ATTR equ %01101000 ; Cyan paper + BRIGHT
BORDER_ATTR equ %01110000 ; Yellow paper + BRIGHT
CURSOR_ATTR equ %01111000 ; White paper + BRIGHT
P1_ATTR equ %01010000 ; Red paper + BRIGHT
P2_ATTR equ %01001000 ; Blue paper + BRIGHT
P1_CURSOR equ %01111010 ; White paper + Red ink + BRIGHT
P2_CURSOR equ %01111001 ; White paper + Blue ink + BRIGHT
; Text display attributes
TEXT_ATTR equ %00111000 ; White paper, black ink
P1_TEXT equ %01010111 ; Red paper, white ink + BRIGHT
P2_TEXT equ %01001111 ; Blue paper, white ink + BRIGHT
P1_BORDER equ 2
P2_BORDER equ 1
ERROR_BORDER equ 2 ; Red border for errors
; Keyboard ports
KEY_PORT equ $fe
ROW_QAOP equ $fb
ROW_ASDF equ $fd
ROW_YUIOP equ $df
ROW_SPACE equ $7f
; 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
; Title screen positions
TITLE_ROW equ 8
TITLE_COL equ 12 ; "INK WAR" (7 chars) centred: (32-7)/2=12.5
PROMPT_ROW equ 16
PROMPT_COL equ 5 ; "PRESS ANY KEY TO START" (22 chars): (32-22)/2=5
; ----------------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------------
state_title:
; Wait for any key press
xor a
in a, (KEY_PORT)
cpl
and %00011111
jr z, main_loop ; No key - keep waiting
; Key pressed - start game
call start_game
jp main_loop
; ----------------------------------------------------------------------------
; State: Playing
; ----------------------------------------------------------------------------
state_playing:
call read_keyboard
call handle_input
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
ret
; ----------------------------------------------------------------------------
; Draw Board Border
; ----------------------------------------------------------------------------
draw_board_border:
ld c, BOARD_ROW-1
ld d, BOARD_COL-1
ld b, BOARD_SIZE+2
call draw_border_row
ld c, BOARD_ROW+BOARD_SIZE
ld d, BOARD_COL-1
ld b, BOARD_SIZE+2
call draw_border_row
ld c, BOARD_ROW
ld d, BOARD_COL-1
ld b, BOARD_SIZE
call draw_border_col
ld c, BOARD_ROW
ld d, BOARD_COL+BOARD_SIZE
ld b, BOARD_SIZE
call draw_border_col
ret
draw_border_row:
push bc
.dbr_loop:
push bc
push de
ld a, c
ld l, a
ld h, 0
add hl, hl
add hl, hl
add hl, hl
add hl, hl
add hl, hl
ld a, d
add a, l
ld l, a
ld bc, ATTR_BASE
add hl, bc
ld (hl), BORDER_ATTR
pop de
pop bc
inc d
djnz .dbr_loop
pop bc
ret
draw_border_col:
push bc
.dbc_loop:
push bc
push de
ld a, c
ld l, a
ld h, 0
add hl, hl
add hl, hl
add hl, hl
add hl, hl
add hl, hl
ld a, d
add a, l
ld l, a
ld bc, ATTR_BASE
add hl, bc
ld (hl), BORDER_ATTR
pop de
pop bc
inc c
djnz .dbc_loop
pop bc
ret
; ----------------------------------------------------------------------------
; Draw Board
; ----------------------------------------------------------------------------
draw_board:
ld b, BOARD_SIZE
ld c, BOARD_ROW
.db_row:
push bc
ld b, BOARD_SIZE
ld d, BOARD_COL
.db_col:
push bc
ld a, c
ld l, a
ld h, 0
add hl, hl
add hl, hl
add hl, hl
add hl, hl
add hl, hl
ld a, d
add a, l
ld l, a
ld bc, ATTR_BASE
add hl, bc
ld (hl), EMPTY_ATTR
pop bc
inc d
djnz .db_col
pop bc
inc c
djnz .db_row
ret
; ----------------------------------------------------------------------------
; Draw Cursor
; ----------------------------------------------------------------------------
draw_cursor:
call get_cell_state
cp STATE_P1
jr z, .dc_p1
cp STATE_P2
jr z, .dc_p2
ld a, CURSOR_ATTR
jr .dc_set
.dc_p1:
ld a, P1_CURSOR
jr .dc_set
.dc_p2:
ld a, P2_CURSOR
.dc_set:
push af
ld a, (cursor_row)
add a, BOARD_ROW
ld l, a
ld h, 0
add hl, hl
add hl, hl
add hl, hl
add hl, hl
add hl, hl
ld a, (cursor_col)
add a, BOARD_COL
add a, l
ld l, a
ld bc, ATTR_BASE
add hl, bc
pop af
ld (hl), a
ret
; ----------------------------------------------------------------------------
; Clear Cursor
; ----------------------------------------------------------------------------
clear_cursor:
call get_cell_state
cp STATE_P1
jr z, .clc_p1
cp STATE_P2
jr z, .clc_p2
ld a, EMPTY_ATTR
jr .clc_set
.clc_p1:
ld a, P1_ATTR
jr .clc_set
.clc_p2:
ld a, P2_ATTR
.clc_set:
push af
ld a, (cursor_row)
add a, BOARD_ROW
ld l, a
ld h, 0
add hl, hl
add hl, hl
add hl, hl
add hl, hl
add hl, hl
ld a, (cursor_col)
add a, BOARD_COL
add a, l
ld l, a
ld bc, ATTR_BASE
add hl, bc
pop af
ld (hl), a
ret
; ----------------------------------------------------------------------------
; Get Cell State
; ----------------------------------------------------------------------------
get_cell_state:
ld a, (cursor_row)
add a, a
add a, a
add a, a
ld hl, board_state
ld b, 0
ld c, a
add hl, bc
ld a, (cursor_col)
ld c, a
add hl, bc
ld a, (hl)
ret
; ----------------------------------------------------------------------------
; Read Keyboard
; ----------------------------------------------------------------------------
read_keyboard:
xor a
ld (key_pressed), a
ld a, ROW_QAOP
in a, (KEY_PORT)
bit 0, a
jr nz, .rk_not_q
ld a, 1
ld (key_pressed), a
ret
.rk_not_q:
ld a, ROW_ASDF
in a, (KEY_PORT)
bit 0, a
jr nz, .rk_not_a
ld a, 2
ld (key_pressed), a
ret
.rk_not_a:
ld a, ROW_YUIOP
in a, (KEY_PORT)
bit 1, a
jr nz, .rk_not_o
ld a, 3
ld (key_pressed), a
ret
.rk_not_o:
ld a, ROW_YUIOP
in a, (KEY_PORT)
bit 0, a
jr nz, .rk_not_p
ld a, 4
ld (key_pressed), a
ret
.rk_not_p:
ld a, ROW_SPACE
in a, (KEY_PORT)
bit 0, a
jr nz, .rk_not_space
ld a, 5
ld (key_pressed), a
.rk_not_space:
ret
; ----------------------------------------------------------------------------
; Handle Input
; ----------------------------------------------------------------------------
handle_input:
ld a, (key_pressed)
or a
ret z
cp 5
jr z, try_claim
call clear_cursor
ld a, (key_pressed)
cp 1
jr nz, .hi_not_up
ld a, (cursor_row)
or a
jr z, .hi_done
dec a
ld (cursor_row), a
jr .hi_done
.hi_not_up:
cp 2
jr nz, .hi_not_down
ld a, (cursor_row)
cp BOARD_SIZE-1
jr z, .hi_done
inc a
ld (cursor_row), a
jr .hi_done
.hi_not_down:
cp 3
jr nz, .hi_not_left
ld a, (cursor_col)
or a
jr z, .hi_done
dec a
ld (cursor_col), a
jr .hi_done
.hi_not_left:
cp 4
jr nz, .hi_done
ld a, (cursor_col)
cp BOARD_SIZE-1
jr z, .hi_done
inc a
ld (cursor_col), a
.hi_done:
call draw_cursor
ret
; ----------------------------------------------------------------------------
; Try Claim Cell
; ----------------------------------------------------------------------------
try_claim:
call get_cell_state
or a
jr z, .tc_valid
; Cell already claimed - error feedback
call sound_error
call flash_border_error
call update_border ; Restore correct border colour
ret
.tc_valid:
; Valid move - claim the cell
call claim_cell
call sound_claim
ld a, (current_player)
xor 3
ld (current_player), a
call draw_ui ; Update scores and turn indicator
; Check if game is over
call check_game_over
or a
jr z, .tc_continue
; Game over - show results 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
ret
.sr_p2_wins:
; Display "P2 WINS!"
ld b, RESULT_ROW
ld c, RESULT_COL
ld hl, msg_p2_wins
ld e, P2_TEXT
call print_message
ret
.sr_draw:
; Display "DRAW!"
ld b, RESULT_ROW
ld c, RESULT_COL + 2 ; Centre "DRAW!" better
ld hl, msg_draw
ld e, TEXT_ATTR
call print_message
ret
; ----------------------------------------------------------------------------
; Print Message
; ----------------------------------------------------------------------------
; HL = pointer to null-terminated string
; B = row, C = starting column, E = attribute for message area
print_message:
push bc
push de
; First pass: print characters
.pm_loop:
ld a, (hl)
or a
jr z, .pm_done
call print_char
inc hl
inc c
jr .pm_loop
.pm_done:
; Calculate message length
pop de ; E = attribute
pop bc ; B = row, C = start column
push bc
; Count characters
ld a, c ; Current column (after printing)
pop bc ; Get start column back
sub c ; A = length
ld b, a ; B = count
; Set attributes
ld a, RESULT_ROW
call set_attr_range
ret
; ----------------------------------------------------------------------------
; Victory Celebration
; ----------------------------------------------------------------------------
; Flashes border in winner's colour
victory_celebration:
; Determine winner's border colour
ld a, (p1_count)
ld b, a
ld a, (p2_count)
cp b
jr c, .vc_p1 ; p2 < p1
jr z, .vc_draw ; draw - use white
ld d, P2_BORDER ; p2 wins
jr .vc_flash
.vc_p1:
ld d, P1_BORDER
jr .vc_flash
.vc_draw:
ld d, 7 ; White for draw
.vc_flash:
; Flash border 5 times
ld b, 5
.vc_loop:
push bc
; Winner's colour
ld a, d
out (KEY_PORT), a
; Delay
ld bc, 15000
.vc_delay1:
dec bc
ld a, b
or c
jr nz, .vc_delay1
; Black
xor a
out (KEY_PORT), a
; Delay
ld bc, 10000
.vc_delay2:
dec bc
ld a, b
or c
jr nz, .vc_delay2
pop bc
djnz .vc_loop
ret
; ----------------------------------------------------------------------------
; 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 "PRESS ANY KEY TO START" prompt
ld b, PROMPT_ROW
ld c, PROMPT_COL
ld hl, msg_prompt
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
; ----------------------------------------------------------------------------
; 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_prompt: defb "PRESS ANY KEY TO START", 0
; ----------------------------------------------------------------------------
; Variables
; ----------------------------------------------------------------------------
game_state: defb 0 ; 0=title, 1=playing, 2=results
cursor_row: defb 0
cursor_col: defb 0
key_pressed: defb 0
current_player: defb 1
p1_count: defb 0
p2_count: defb 0
board_state: defs 64, 0
; ----------------------------------------------------------------------------
; End
; ----------------------------------------------------------------------------
end start
Try This: Add Credits
CREDITS_ROW equ 22
CREDITS_COL equ 8
msg_credits: defb "BY YOUR NAME HERE", 0
Add a third message to draw_title_screen.
Try This: Coloured Title
TITLE_ATTR equ %01000110 ; Yellow on red + BRIGHT
Change the title message attribute for a more striking look.
What You’ve Learnt
- State machine pattern — Organise game flow into distinct states
- Main loop dispatch — Check state and jump to appropriate handler
- State transitions — Change state variable, reinitialise as needed
- Code reuse — Leverage existing routines (print_message) for new features
What’s Next
In Unit 8, we’ll polish the two-player experience with better visual feedback and prepare the foundation for AI opponents.
What Changed
| 1 | 1 | ; ============================================================================ | |
| 2 | - | ; INK WAR - Unit 6: Game End Detection | |
| 2 | + | ; INK WAR - Unit 7: Title Screen | |
| 3 | 3 | ; ============================================================================ | |
| 4 | - | ; Detects when the board is full and declares a winner. | |
| 5 | - | ; Shows winner message, victory celebration, press key to restart. | |
| 4 | + | ; Adds title screen with state machine. Game starts at title, transitions | |
| 5 | + | ; to playing, shows results, then returns to title. | |
| 6 | 6 | ; | |
| 7 | 7 | ; Controls: Q=Up, A=Down, O=Left, P=Right, SPACE=Claim | |
| 8 | 8 | ; ============================================================================ | |
| ... | |||
| 59 | 59 | ROW_YUIOP equ $df | |
| 60 | 60 | ROW_SPACE equ $7f | |
| 61 | 61 | | |
| 62 | - | ; Game states | |
| 62 | + | ; Cell states | |
| 63 | 63 | STATE_EMPTY equ 0 | |
| 64 | 64 | STATE_P1 equ 1 | |
| 65 | 65 | STATE_P2 equ 2 | |
| 66 | + | | |
| 67 | + | ; Game states (state machine) | |
| 68 | + | GS_TITLE equ 0 | |
| 69 | + | GS_PLAYING equ 1 | |
| 70 | + | GS_RESULTS equ 2 | |
| 71 | + | | |
| 72 | + | ; Title screen positions | |
| 73 | + | TITLE_ROW equ 8 | |
| 74 | + | 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 | |
| 66 | 77 | | |
| 67 | 78 | ; ---------------------------------------------------------------------------- | |
| 68 | 79 | ; Entry Point | |
| 69 | 80 | ; ---------------------------------------------------------------------------- | |
| 70 | 81 | | |
| 71 | 82 | start: | |
| 83 | + | ; Start at title screen | |
| 84 | + | ld a, GS_TITLE | |
| 85 | + | ld (game_state), a | |
| 72 | 86 | call init_screen | |
| 73 | - | call init_game | |
| 74 | - | call draw_board_border | |
| 75 | - | call draw_board | |
| 76 | - | call draw_ui ; Draw score and turn display | |
| 77 | - | call draw_cursor | |
| 78 | - | call update_border | |
| 87 | + | call draw_title_screen | |
| 88 | + | | |
| 89 | + | ; Black border for title | |
| 90 | + | xor a | |
| 91 | + | out (KEY_PORT), a | |
| 79 | 92 | | |
| 80 | 93 | main_loop: | |
| 81 | 94 | halt | |
| 95 | + | | |
| 96 | + | ; Dispatch based on game state | |
| 97 | + | ld a, (game_state) | |
| 98 | + | or a | |
| 99 | + | jr z, state_title ; GS_TITLE = 0 | |
| 100 | + | cp GS_PLAYING | |
| 101 | + | jr z, state_playing | |
| 102 | + | ; Must be GS_RESULTS - handled inline after game over | |
| 103 | + | | |
| 104 | + | jp main_loop | |
| 105 | + | | |
| 106 | + | ; ---------------------------------------------------------------------------- | |
| 107 | + | ; State: Title | |
| 108 | + | ; ---------------------------------------------------------------------------- | |
| 109 | + | | |
| 110 | + | state_title: | |
| 111 | + | ; Wait for any key press | |
| 112 | + | xor a | |
| 113 | + | in a, (KEY_PORT) | |
| 114 | + | cpl | |
| 115 | + | and %00011111 | |
| 116 | + | jr z, main_loop ; No key - keep waiting | |
| 117 | + | | |
| 118 | + | ; Key pressed - start game | |
| 119 | + | call start_game | |
| 120 | + | jp main_loop | |
| 121 | + | | |
| 122 | + | ; ---------------------------------------------------------------------------- | |
| 123 | + | ; State: Playing | |
| 124 | + | ; ---------------------------------------------------------------------------- | |
| 82 | 125 | | |
| 126 | + | state_playing: | |
| 83 | 127 | call read_keyboard | |
| 84 | 128 | call handle_input | |
| 85 | - | | |
| 86 | 129 | jp main_loop | |
| 130 | + | | |
| 131 | + | ; ---------------------------------------------------------------------------- | |
| 132 | + | ; Start Game | |
| 133 | + | ; ---------------------------------------------------------------------------- | |
| 134 | + | ; Transitions from title to playing state | |
| 135 | + | | |
| 136 | + | start_game: | |
| 137 | + | ld a, GS_PLAYING | |
| 138 | + | ld (game_state), a | |
| 139 | + | | |
| 140 | + | call init_screen | |
| 141 | + | call init_game | |
| 142 | + | call draw_board_border | |
| 143 | + | call draw_board | |
| 144 | + | call draw_ui | |
| 145 | + | call draw_cursor | |
| 146 | + | call update_border | |
| 147 | + | | |
| 148 | + | ; Wait for key release before playing | |
| 149 | + | call wait_key_release | |
| 150 | + | | |
| 151 | + | ret | |
| 87 | 152 | | |
| 88 | 153 | ; ---------------------------------------------------------------------------- | |
| 89 | 154 | ; Initialise Screen | |
| ... | |||
| 787 | 852 | or a | |
| 788 | 853 | jr z, .tc_continue | |
| 789 | 854 | | |
| 790 | - | ; Game over - show results | |
| 855 | + | ; Game over - show results and return to title | |
| 791 | 856 | call show_results | |
| 792 | 857 | call victory_celebration | |
| 793 | 858 | call wait_for_key | |
| 794 | - | jp start ; Restart game | |
| 859 | + | | |
| 860 | + | ; Return to title screen | |
| 861 | + | ld a, GS_TITLE | |
| 862 | + | ld (game_state), a | |
| 863 | + | call init_screen | |
| 864 | + | call draw_title_screen | |
| 865 | + | xor a | |
| 866 | + | out (KEY_PORT), a ; Black border for title | |
| 867 | + | ret | |
| 795 | 868 | | |
| 796 | 869 | .tc_continue: | |
| 797 | 870 | call update_border | |
| ... | |||
| 1123 | 1196 | | |
| 1124 | 1197 | pop bc | |
| 1125 | 1198 | djnz .vc_loop | |
| 1199 | + | | |
| 1200 | + | ret | |
| 1201 | + | | |
| 1202 | + | ; ---------------------------------------------------------------------------- | |
| 1203 | + | ; Draw Title Screen | |
| 1204 | + | ; ---------------------------------------------------------------------------- | |
| 1205 | + | ; Displays game title and prompt | |
| 1206 | + | | |
| 1207 | + | draw_title_screen: | |
| 1208 | + | ; Draw "INK WAR" title | |
| 1209 | + | ld b, TITLE_ROW | |
| 1210 | + | ld c, TITLE_COL | |
| 1211 | + | ld hl, msg_title | |
| 1212 | + | ld e, TEXT_ATTR | |
| 1213 | + | call print_message | |
| 1214 | + | | |
| 1215 | + | ; Draw "PRESS ANY KEY TO START" prompt | |
| 1216 | + | ld b, PROMPT_ROW | |
| 1217 | + | ld c, PROMPT_COL | |
| 1218 | + | ld hl, msg_prompt | |
| 1219 | + | ld e, TEXT_ATTR | |
| 1220 | + | call print_message | |
| 1221 | + | | |
| 1222 | + | ret | |
| 1223 | + | | |
| 1224 | + | ; ---------------------------------------------------------------------------- | |
| 1225 | + | ; Wait Key Release | |
| 1226 | + | ; ---------------------------------------------------------------------------- | |
| 1227 | + | ; Waits until all keys are released | |
| 1126 | 1228 | | |
| 1229 | + | wait_key_release: | |
| 1230 | + | .wkr_loop: | |
| 1231 | + | xor a | |
| 1232 | + | in a, (KEY_PORT) | |
| 1233 | + | cpl ; Invert (keys are active low) | |
| 1234 | + | and %00011111 ; Mask to key bits | |
| 1235 | + | jr nz, .wkr_loop ; Still holding a key | |
| 1127 | 1236 | ret | |
| 1128 | 1237 | | |
| 1129 | 1238 | ; ---------------------------------------------------------------------------- | |
| ... | |||
| 1158 | 1267 | msg_p1_wins: defb "P1 WINS!", 0 | |
| 1159 | 1268 | msg_p2_wins: defb "P2 WINS!", 0 | |
| 1160 | 1269 | msg_draw: defb "DRAW!", 0 | |
| 1270 | + | msg_title: defb "INK WAR", 0 | |
| 1271 | + | msg_prompt: defb "PRESS ANY KEY TO START", 0 | |
| 1161 | 1272 | | |
| 1162 | 1273 | ; ---------------------------------------------------------------------------- | |
| 1163 | 1274 | ; Variables | |
| 1164 | 1275 | ; ---------------------------------------------------------------------------- | |
| 1165 | 1276 | | |
| 1277 | + | game_state: defb 0 ; 0=title, 1=playing, 2=results | |
| 1166 | 1278 | cursor_row: defb 0 | |
| 1167 | 1279 | cursor_col: defb 0 | |
| 1168 | 1280 | key_pressed: defb 0 |