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

Score and Turn Display

Count cells, display scores, show whose turn it is. The game gets a proper UI.

6% of Ink War

Players need to know the score. They need to know whose turn it is.

This unit adds a proper user interface: score display showing how many cells each player owns, and a turn indicator that changes colour with each move. The game starts to feel complete.

Run It

pasmonext --sna inkwar.asm inkwar.sna

Unit 4 Screenshot

Above the board you’ll see:

  • P1:00 with a score (red background)
  • P2:00 with a score (blue background)
  • TURN indicator in the current player’s colour

Claim some cells. Watch the scores update. Watch the turn indicator change colour.

Counting Cells

To display scores, we first need to count them:

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

.count_loop:
            ld      a, (hl)
            cp      STATE_P1
            jr      nz, .not_p1
            ld      a, (p1_count)
            inc     a
            ld      (p1_count), a
            jr      .next
.not_p1:
            cp      STATE_P2
            jr      nz, .next
            ld      a, (p2_count)
            inc     a
            ld      (p2_count), a
.next:
            inc     hl
            djnz    .count_loop

            ret

This iterates through all 64 cells of board_state, counting how many belong to each player. The djnz instruction (Decrement and Jump if Not Zero) is perfect for loops - it decrements B and jumps back if B isn’t zero yet.

We store the counts in p1_count and p2_count for display.

Printing Characters

Rather than wrestling with the ROM’s print routines (which need careful system variable setup), we write our own character printing routine. This gives us complete control and teaches more about how the Spectrum works.

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

The routine:

  1. Looks up the character’s pixel data in the ROM character set at $3C00
  2. Calculates where on screen to draw it using the Spectrum’s quirky display layout
  3. Copies 8 bytes (one per pixel row) to the display file

The Spectrum’s Screen Layout

The Spectrum’s display file at $4000 has a complex layout. For each character position at row R, column C:

; High byte = $40 + (row/8)*8 + pixel_line
; Low byte = (row%8)*32 + column

The screen is divided into thirds (rows 0-7, 8-15, 16-23), with each pixel row 256 bytes apart. Our print_char routine handles this complexity.

Converting Numbers to Text

Scores are numbers (0-64), but we print characters. To convert:

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

The conversion is simple: subtract 10 repeatedly to get the tens digit, then add ASCII ‘0’ (48 decimal) to convert digits to printable characters.

For example, the number 23:

  • Subtract 10 twice: 23 → 13 → 3 (tens = 2, remainder = 3)
  • Tens digit: 2 + ‘0’ = ‘2’ (ASCII 50)
  • Units digit: 3 + ‘0’ = ‘3’ (ASCII 51)

The Turn Indicator

A simple “TURN” label that changes colour based on whose turn it is:

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

We print the text first, then set the attributes afterwards. The text stays the same - only the background colour changes.

Setting Attribute Ranges

We colour the text by setting attributes for multiple consecutive cells:

set_attr_range:
            ; A = row, C = start column, B = count, E = attribute
            ; Calculate address, then write E to B consecutive bytes

This lets us create coloured “badges” for the score labels - red background for P1, blue for P2.

UI Layout Constants

The display positions are defined as constants at the top:

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

Changing these moves the entire UI. Want scores at the bottom? Change SCORE_ROW to 20.

Updating After Claims

When a cell is claimed, we update the display:

try_claim:
            ; ... claim the cell ...

            call    draw_ui         ; Update scores and turn indicator
            call    update_border
            call    draw_cursor

The draw_ui routine calls count_cells to recalculate, then redraws the score display and turn indicator. Every claim triggers an update.

The Complete Code

; ============================================================================
; INK WAR - Unit 4: Score and Turn Display
; ============================================================================
; Adds score display and turn indicator to the game.
; Shows "P1: nn  P2: nn" and indicates whose turn it is.
;
; 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

; 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

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

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

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

start:
            call    init_screen
            call    init_game
            call    draw_board_border
            call    draw_board
            call    draw_ui             ; Draw score and turn display
            call    draw_cursor
            call    update_border

main_loop:
            halt

            call    read_keyboard
            call    handle_input

            jp      main_loop

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

            call    claim_cell
            call    sound_claim

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

            call    draw_ui             ; Update scores and turn indicator
            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

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

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: Move the UI

Change the position constants:

; Scores at the bottom
SCORE_ROW   equ     21

; Turn indicator on the left
TURN_ROW    equ     12
TURN_COL    equ     2

Try This: Different Colour Scheme

; Green for P1, Magenta for P2
P1_TEXT     equ     %01100111       ; Green paper, white ink
P2_TEXT     equ     %01011111       ; Magenta paper, white ink

What You’ve Learnt

  • Custom print routine - Direct screen memory access gives full control
  • ROM character set - Character data at $3C00, each character is 8 bytes
  • Display file layout - Screen memory has complex interleaved addressing
  • Number to ASCII - Add ‘0’ (48) to convert a digit to its character code
  • Cell counting - Iterate with DJNZ, compare and branch to count categories
  • Attribute ranges - Set consecutive attributes to create coloured text backgrounds

What’s Next

In Unit 5, we’ll add proper move validation with feedback - error sounds and visual cues when you try to claim an already-occupied cell.

What Changed

Unit 3 → Unit 4
+367-117
11 ; ============================================================================
2-; INK WAR - Unit 3: Making It Yours
2+; INK WAR - Unit 4: Score and Turn Display
33 ; ============================================================================
4-; Customised version demonstrating attribute control:
5-; - Cyan board cells (not white)
6-; - Yellow border cells around the board
7-; - Screen border changes colour with current player
8-; - Bright cursor (not flashing)
4+; Adds score display and turn indicator to the game.
5+; Shows "P1: nn P2: nn" and indicates whose turn it is.
96 ;
107 ; Controls: Q=Up, A=Down, O=Left, P=Right, SPACE=Claim
118 ; ============================================================================
...
1512 ; ----------------------------------------------------------------------------
1613 ; Constants
1714 ; ----------------------------------------------------------------------------
18-
19-ATTR_BASE equ $5800 ; Start of attribute memory
20-BOARD_ROW equ 8 ; Board starts at row 8
21-BOARD_COL equ 12 ; Board starts at column 12
22-BOARD_SIZE equ 8 ; 8x8 playing field
23-
24-; ============================================================================
25-; CUSTOMISATION SECTION - Change these values to personalise your game!
26-; ============================================================================
2715
28-; Attribute format: %FBPPPIII
29-; F = Flash (bit 7): 1 = flashing
30-; B = Bright (bit 6): 1 = brighter colours
31-; PPP = Paper colour (bits 5-3): background
32-; III = Ink colour (bits 2-0): foreground
33-;
34-; Colour values (0-7):
35-; 0=Black, 1=Blue, 2=Red, 3=Magenta, 4=Green, 5=Cyan, 6=Yellow, 7=White
16+ATTR_BASE equ $5800
17+DISPLAY_FILE equ $4000
18+CHAR_SET equ $3C00 ; ROM character set base address
3619
37-; Empty cells - CUSTOMISED: cyan instead of white
38-; %01101000 = BRIGHT + Paper 5 (cyan) + Ink 0 (black)
39-EMPTY_ATTR equ %01101000 ; Cyan paper, black ink + BRIGHT
20+BOARD_ROW equ 8
21+BOARD_COL equ 12
22+BOARD_SIZE equ 8
4023
41-; Board border - CUSTOMISED: yellow border around playing area
42-; %01110000 = BRIGHT + Paper 6 (yellow) + Ink 0 (black)
43-BORDER_ATTR equ %01110000 ; Yellow paper, black ink + BRIGHT
24+; Display positions
25+SCORE_ROW equ 2 ; Score display row
26+P1_SCORE_COL equ 10 ; "P1: nn" column
27+P2_SCORE_COL equ 18 ; "P2: nn" column
28+TURN_ROW equ 4 ; Turn indicator row
29+TURN_COL equ 14 ; Turn indicator column
4430
45-; Cursor - CUSTOMISED: bright white instead of flashing
46-; %01111000 = BRIGHT + Paper 7 (white) + Ink 0 (black)
47-CURSOR_ATTR equ %01111000 ; White paper + BRIGHT (no flash)
31+; Customised colours (from Unit 3)
32+EMPTY_ATTR equ %01101000 ; Cyan paper + BRIGHT
33+BORDER_ATTR equ %01110000 ; Yellow paper + BRIGHT
34+CURSOR_ATTR equ %01111000 ; White paper + BRIGHT
4835
49-; Player 1 - Red (unchanged)
5036 P1_ATTR equ %01010000 ; Red paper + BRIGHT
51-P1_CURSOR equ %01111010 ; White paper + Red ink + BRIGHT
52-
53-; Player 2 - Blue (unchanged)
5437 P2_ATTR equ %01001000 ; Blue paper + BRIGHT
38+P1_CURSOR equ %01111010 ; White paper + Red ink + BRIGHT
5539 P2_CURSOR equ %01111001 ; White paper + Blue ink + BRIGHT
5640
57-; Screen border colours for each player
58-P1_BORDER equ 2 ; Red border for Player 1's turn
59-P2_BORDER equ 1 ; Blue border for Player 2's turn
41+; Text display attributes
42+TEXT_ATTR equ %00111000 ; White paper, black ink
43+P1_TEXT equ %01010111 ; Red paper, white ink + BRIGHT
44+P2_TEXT equ %01001111 ; Blue paper, white ink + BRIGHT
6045
61-; ============================================================================
62-; End of customisation section
63-; ============================================================================
46+P1_BORDER equ 2
47+P2_BORDER equ 1
6448
65-; Keyboard ports (active low)
49+; Keyboard ports
6650 KEY_PORT equ $fe
6751 ROW_QAOP equ $fb
6852 ROW_ASDF equ $fd
...
8165 start:
8266 call init_screen
8367 call init_game
84- call draw_board_border ; NEW: Draw visible border
68+ call draw_board_border
8569 call draw_board
70+ call draw_ui ; Draw score and turn display
8671 call draw_cursor
87- call update_border ; Set initial border colour
72+ call update_border
8873
8974 main_loop:
9075 halt
...
10186 init_screen:
10287 xor a
10388 out (KEY_PORT), a
89+
90+ ; Clear display file (pixels)
91+ ld hl, DISPLAY_FILE
92+ ld de, DISPLAY_FILE+1
93+ ld bc, 6143
94+ ld (hl), 0
95+ ldir
10496
97+ ; Clear all attributes to white paper, black ink
10598 ld hl, ATTR_BASE
10699 ld de, ATTR_BASE+1
107100 ld bc, 767
108- ld (hl), 0
101+ ld (hl), TEXT_ATTR ; White background for text areas
109102 ldir
110103
111104 ret
112105
113106 ; ----------------------------------------------------------------------------
114-; Update Screen Border
107+; Draw UI
115108 ; ----------------------------------------------------------------------------
116-; Sets border colour based on current player
109+; Draws score display and turn indicator
110+
111+draw_ui:
112+ call draw_scores
113+ call draw_turn_indicator
114+ ret
115+
116+; ----------------------------------------------------------------------------
117+; Draw Scores
118+; ----------------------------------------------------------------------------
119+; Displays "P1: nn P2: nn" with player colours
120+
121+draw_scores:
122+ ; Count cells for each player
123+ call count_cells
124+
125+ ; Draw P1 label "P1:"
126+ ld b, SCORE_ROW
127+ ld c, P1_SCORE_COL
128+ ld a, 'P'
129+ call print_char
130+ inc c
131+ ld a, '1'
132+ call print_char
133+ inc c
134+ ld a, ':'
135+ call print_char
136+ inc c
137+
138+ ; Print P1 score
139+ ld a, (p1_count)
140+ call print_two_digits
141+
142+ ; Set P1 colour attribute
143+ ld a, SCORE_ROW
144+ ld c, P1_SCORE_COL
145+ ld b, 5 ; "P1:nn" = 5 characters
146+ ld e, P1_TEXT
147+ call set_attr_range
148+
149+ ; Draw P2 label "P2:"
150+ ld b, SCORE_ROW
151+ ld c, P2_SCORE_COL
152+ ld a, 'P'
153+ call print_char
154+ inc c
155+ ld a, '2'
156+ call print_char
157+ inc c
158+ ld a, ':'
159+ call print_char
160+ inc c
161+
162+ ; Print P2 score
163+ ld a, (p2_count)
164+ call print_two_digits
165+
166+ ; Set P2 colour attribute
167+ ld a, SCORE_ROW
168+ ld c, P2_SCORE_COL
169+ ld b, 5
170+ ld e, P2_TEXT
171+ call set_attr_range
172+
173+ ret
174+
175+; ----------------------------------------------------------------------------
176+; Draw Turn Indicator
177+; ----------------------------------------------------------------------------
178+; Shows "TURN" with current player's colour
179+
180+draw_turn_indicator:
181+ ; Print "TURN"
182+ ld b, TURN_ROW
183+ ld c, TURN_COL
184+ ld a, 'T'
185+ call print_char
186+ inc c
187+ ld a, 'U'
188+ call print_char
189+ inc c
190+ ld a, 'R'
191+ call print_char
192+ inc c
193+ ld a, 'N'
194+ call print_char
195+
196+ ; Set attribute based on current player
197+ ld a, (current_player)
198+ cp 1
199+ jr z, .dti_p1
200+ ld e, P2_TEXT
201+ jr .dti_set
202+.dti_p1:
203+ ld e, P1_TEXT
204+
205+.dti_set:
206+ ld a, TURN_ROW
207+ ld c, TURN_COL
208+ ld b, 4 ; "TURN" = 4 chars
209+ call set_attr_range
210+
211+ ret
212+
213+; ----------------------------------------------------------------------------
214+; Print Character
215+; ----------------------------------------------------------------------------
216+; A = ASCII character (32-127), B = row (0-23), C = column (0-31)
217+; Writes character directly to display file using ROM character set
218+
219+print_char:
220+ push bc
221+ push de
222+ push hl
223+ push af
224+
225+ ; Calculate character data address: CHAR_SET + char*8
226+ ld l, a
227+ ld h, 0
228+ add hl, hl
229+ add hl, hl
230+ add hl, hl ; HL = char * 8
231+ ld de, CHAR_SET
232+ add hl, de ; HL = source address
233+
234+ push hl ; Save character data address
235+
236+ ; Calculate display file address
237+ ; Screen address: high byte varies with row, low byte = column
238+ ld a, b ; A = row (0-23)
239+ and %00011000 ; Get which third (0, 8, 16)
240+ add a, $40 ; Add display file base high byte
241+ ld d, a
242+
243+ ld a, b ; A = row
244+ and %00000111 ; Get line within character row
245+ rrca
246+ rrca
247+ rrca ; Shift to bits 5-7
248+ add a, c ; Add column
249+ ld e, a ; DE = screen address
250+
251+ pop hl ; HL = character data
252+
253+ ; Copy 8 bytes (8 pixel rows of character)
254+ ld b, 8
255+.pc_loop:
256+ ld a, (hl)
257+ ld (de), a
258+ inc hl
259+ inc d ; Next screen line (add 256)
260+ djnz .pc_loop
261+
262+ pop af
263+ pop hl
264+ pop de
265+ pop bc
266+ ret
267+
268+; ----------------------------------------------------------------------------
269+; Print Two Digits
270+; ----------------------------------------------------------------------------
271+; A = number (0-99), B = row, C = column (will advance by 2)
272+; Prints number as two digits
273+
274+print_two_digits:
275+ push bc
276+
277+ ; Calculate tens digit
278+ ld d, 0 ; Tens counter
279+.ptd_tens:
280+ cp 10
281+ jr c, .ptd_print
282+ sub 10
283+ inc d
284+ jr .ptd_tens
285+
286+.ptd_print:
287+ push af ; Save units digit
288+
289+ ; Print tens digit
290+ ld a, d
291+ add a, '0'
292+ call print_char
293+ inc c
294+
295+ ; Print units digit
296+ pop af
297+ add a, '0'
298+ call print_char
299+ inc c
300+
301+ pop bc
302+ ret
303+
304+; ----------------------------------------------------------------------------
305+; Count Cells
306+; ----------------------------------------------------------------------------
307+; Counts cells owned by each player
308+
309+count_cells:
310+ xor a
311+ ld (p1_count), a
312+ ld (p2_count), a
313+
314+ ld hl, board_state
315+ ld b, 64 ; 64 cells
316+
317+.cc_loop:
318+ ld a, (hl)
319+ cp STATE_P1
320+ jr nz, .cc_not_p1
321+ ld a, (p1_count)
322+ inc a
323+ ld (p1_count), a
324+ jr .cc_next
325+.cc_not_p1:
326+ cp STATE_P2
327+ jr nz, .cc_next
328+ ld a, (p2_count)
329+ inc a
330+ ld (p2_count), a
331+.cc_next:
332+ inc hl
333+ djnz .cc_loop
334+
335+ ret
336+
337+; ----------------------------------------------------------------------------
338+; Set Attribute Range
339+; ----------------------------------------------------------------------------
340+; A = row, C = start column, B = count, E = attribute
341+
342+set_attr_range:
343+ push bc
344+ push de
345+
346+ ; Calculate start address: ATTR_BASE + row*32 + col
347+ ld l, a
348+ ld h, 0
349+ add hl, hl
350+ add hl, hl
351+ add hl, hl
352+ add hl, hl
353+ add hl, hl ; HL = row * 32
354+ ld a, c
355+ add a, l
356+ ld l, a
357+ ld bc, ATTR_BASE
358+ add hl, bc ; HL = attribute address
359+
360+ pop de ; E = attribute
361+ pop bc ; B = count
362+
363+.sar_loop:
364+ ld (hl), e
365+ inc hl
366+ djnz .sar_loop
367+
368+ ret
369+
370+; ----------------------------------------------------------------------------
371+; Update Border
372+; ----------------------------------------------------------------------------
117373
118374 update_border:
119375 ld a, (current_player)
...
135391 ld hl, board_state
136392 ld b, 64
137393 xor a
138-.clear_loop:
394+.ig_loop:
139395 ld (hl), a
140396 inc hl
141- djnz .clear_loop
397+ djnz .ig_loop
142398
143399 ld a, 1
144400 ld (current_player), a
...
146402 xor a
147403 ld (cursor_row), a
148404 ld (cursor_col), a
405+ ld (p1_count), a
406+ ld (p2_count), a
149407
150408 ret
151409
152410 ; ----------------------------------------------------------------------------
153411 ; Draw Board Border
154412 ; ----------------------------------------------------------------------------
155-; Draws a visible border around the 8x8 playing area
156413
157414 draw_board_border:
158- ; Top border (row 7, columns 11-20)
159- ld c, BOARD_ROW-1 ; Row 7
160- ld d, BOARD_COL-1 ; Start at column 11
161- ld b, BOARD_SIZE+2 ; 10 cells wide
415+ ld c, BOARD_ROW-1
416+ ld d, BOARD_COL-1
417+ ld b, BOARD_SIZE+2
162418 call draw_border_row
163419
164- ; Bottom border (row 16, columns 11-20)
165- ld c, BOARD_ROW+BOARD_SIZE ; Row 16
420+ ld c, BOARD_ROW+BOARD_SIZE
166421 ld d, BOARD_COL-1
167422 ld b, BOARD_SIZE+2
168423 call draw_border_row
169424
170- ; Left border (rows 8-15, column 11)
171- ld c, BOARD_ROW ; Start at row 8
172- ld d, BOARD_COL-1 ; Column 11
173- ld b, BOARD_SIZE ; 8 cells tall
425+ ld c, BOARD_ROW
426+ ld d, BOARD_COL-1
427+ ld b, BOARD_SIZE
174428 call draw_border_col
175429
176- ; Right border (rows 8-15, column 20)
177430 ld c, BOARD_ROW
178- ld d, BOARD_COL+BOARD_SIZE ; Column 20
431+ ld d, BOARD_COL+BOARD_SIZE
179432 ld b, BOARD_SIZE
180433 call draw_border_col
181434
182435 ret
183436
184-; Draw horizontal border row
185-; C = row, D = start column, B = width
186437 draw_border_row:
187438 push bc
188-.row_loop:
439+.dbr_loop:
189440 push bc
190441 push de
191442
192- ; Calculate attribute address
193443 ld a, c
194444 ld l, a
195445 ld h, 0
...
209459 pop de
210460 pop bc
211461 inc d
212- djnz .row_loop
462+ djnz .dbr_loop
213463 pop bc
214464 ret
215465
216-; Draw vertical border column
217-; C = start row, D = column, B = height
218466 draw_border_col:
219467 push bc
220-.col_loop:
468+.dbc_loop:
221469 push bc
222470 push de
223471
224- ; Calculate attribute address
225472 ld a, c
226473 ld l, a
227474 ld h, 0
...
241488 pop de
242489 pop bc
243490 inc c
244- djnz .col_loop
491+ djnz .dbc_loop
245492 pop bc
246493 ret
247494
...
342589 call get_cell_state
343590
344591 cp STATE_P1
345- jr z, .cc_p1
592+ jr z, .clc_p1
346593 cp STATE_P2
347- jr z, .cc_p2
594+ jr z, .clc_p2
348595
349596 ld a, EMPTY_ATTR
350- jr .cc_set
597+ jr .clc_set
351598
352-.cc_p1:
599+.clc_p1:
353600 ld a, P1_ATTR
354- jr .cc_set
601+ jr .clc_set
355602
356-.cc_p2:
603+.clc_p2:
357604 ld a, P2_ATTR
358605
359-.cc_set:
606+.clc_set:
360607 push af
361608
362609 ld a, (cursor_row)
...
410657 ld a, ROW_QAOP
411658 in a, (KEY_PORT)
412659 bit 0, a
413- jr nz, .not_q
660+ jr nz, .rk_not_q
414661 ld a, 1
415662 ld (key_pressed), a
416663 ret
417-.not_q:
664+.rk_not_q:
418665 ld a, ROW_ASDF
419666 in a, (KEY_PORT)
420667 bit 0, a
421- jr nz, .not_a
668+ jr nz, .rk_not_a
422669 ld a, 2
423670 ld (key_pressed), a
424671 ret
425-.not_a:
672+.rk_not_a:
426673 ld a, ROW_YUIOP
427674 in a, (KEY_PORT)
428675 bit 1, a
429- jr nz, .not_o
676+ jr nz, .rk_not_o
430677 ld a, 3
431678 ld (key_pressed), a
432679 ret
433-.not_o:
680+.rk_not_o:
434681 ld a, ROW_YUIOP
435682 in a, (KEY_PORT)
436683 bit 0, a
437- jr nz, .not_p
684+ jr nz, .rk_not_p
438685 ld a, 4
439686 ld (key_pressed), a
440687 ret
441-.not_p:
688+.rk_not_p:
442689 ld a, ROW_SPACE
443690 in a, (KEY_PORT)
444691 bit 0, a
445- jr nz, .not_space
692+ jr nz, .rk_not_space
446693 ld a, 5
447694 ld (key_pressed), a
448-.not_space:
695+.rk_not_space:
449696 ret
450697
451698 ; ----------------------------------------------------------------------------
...
465712 ld a, (key_pressed)
466713
467714 cp 1
468- jr nz, .not_up
715+ jr nz, .hi_not_up
469716 ld a, (cursor_row)
470717 or a
471- jr z, .done
718+ jr z, .hi_done
472719 dec a
473720 ld (cursor_row), a
474- jr .done
475-.not_up:
721+ jr .hi_done
722+.hi_not_up:
476723 cp 2
477- jr nz, .not_down
724+ jr nz, .hi_not_down
478725 ld a, (cursor_row)
479726 cp BOARD_SIZE-1
480- jr z, .done
727+ jr z, .hi_done
481728 inc a
482729 ld (cursor_row), a
483- jr .done
484-.not_down:
730+ jr .hi_done
731+.hi_not_down:
485732 cp 3
486- jr nz, .not_left
733+ jr nz, .hi_not_left
487734 ld a, (cursor_col)
488735 or a
489- jr z, .done
736+ jr z, .hi_done
490737 dec a
491738 ld (cursor_col), a
492- jr .done
493-.not_left:
739+ jr .hi_done
740+.hi_not_left:
494741 cp 4
495- jr nz, .done
742+ jr nz, .hi_done
496743 ld a, (cursor_col)
497744 cp BOARD_SIZE-1
498- jr z, .done
745+ jr z, .hi_done
499746 inc a
500747 ld (cursor_col), a
501748
502-.done:
749+.hi_done:
503750 call draw_cursor
504751 ret
505752
...
519766 xor 3
520767 ld (current_player), a
521768
522- call update_border ; Update border for new player
769+ call draw_ui ; Update scores and turn indicator
770+ call update_border
523771 call draw_cursor
524772
525773 ret
...
564812
565813 pop af
566814 cp 1
567- jr z, .cc_is_p1
815+ jr z, .clm_is_p1
568816 ld (hl), P2_ATTR
569817 ret
570-.cc_is_p1:
818+.clm_is_p1:
571819 ld (hl), P1_ATTR
572820 ret
573821
...
579827 ld hl, 400
580828 ld b, 20
581829
582-.loop:
830+.scl_loop:
583831 push bc
584832 push hl
585833
586834 ld b, h
587835 ld c, l
588-.tone_loop:
836+.scl_tone:
589837 ld a, $10
590838 out (KEY_PORT), a
591- call .delay
839+ call .scl_delay
592840 xor a
593841 out (KEY_PORT), a
594- call .delay
842+ call .scl_delay
595843 dec bc
596844 ld a, b
597845 or c
598- jr nz, .tone_loop
846+ jr nz, .scl_tone
599847
600848 pop hl
601849 pop bc
...
604852 or a
605853 sbc hl, de
606854
607- djnz .loop
855+ djnz .scl_loop
608856 ret
609857
610-.delay:
858+.scl_delay:
611859 push bc
612860 ld b, 5
613-.delay_loop:
614- djnz .delay_loop
861+.scl_delay_loop:
862+ djnz .scl_delay_loop
615863 pop bc
616864 ret
617865
...
623871 cursor_col: defb 0
624872 key_pressed: defb 0
625873 current_player: defb 1
874+p1_count: defb 0
875+p2_count: defb 0
626876 board_state: defs 64, 0
627877
628878 ; ----------------------------------------------------------------------------