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

Making It Yours

Customise the board colours, position, and cursor style. Learn the internals by changing them.

5% of Ink War

The game works. Now make it yours.

This unit teaches the attribute system by having you change it. You won’t just read about bits and colours - you’ll modify them and see the results. By the end, you’ll understand exactly how the Spectrum’s colour system works because you’ll have bent it to your will.

Run It

Assemble and run:

pasmonext --tapbas inkwar.asm inkwar.tap

Unit 3 Screenshot

This doesn’t look like the previous versions. The board is cyan instead of white. There’s a yellow border around it. The screen border is blue (because it’s Player 2’s turn). The cursor is bright white, not flashing.

Same game, completely different feel.

The Customisation Section

All the visual changes come from one section at the top of the code:

; ============================================================================
; CUSTOMISATION SECTION - Change these values to personalise your game!
; ============================================================================

; Attribute format: %FBPPPIII
;   F = Flash (bit 7): 1 = flashing
;   B = Bright (bit 6): 1 = brighter colours
;   PPP = Paper colour (bits 5-3): background
;   III = Ink colour (bits 2-0): foreground
;
; Colour values (0-7):
;   0=Black, 1=Blue, 2=Red, 3=Magenta, 4=Green, 5=Cyan, 6=Yellow, 7=White

; Empty cells - CUSTOMISED: cyan instead of white
;   %01101000 = BRIGHT + Paper 5 (cyan) + Ink 0 (black)
EMPTY_ATTR  equ     %01101000       ; Cyan paper, black ink + BRIGHT

; Board border - CUSTOMISED: yellow border around playing area
;   %01110000 = BRIGHT + Paper 6 (yellow) + Ink 0 (black)
BORDER_ATTR equ     %01110000       ; Yellow paper, black ink + BRIGHT

; Cursor - CUSTOMISED: bright white instead of flashing
;   %01111000 = BRIGHT + Paper 7 (white) + Ink 0 (black)
CURSOR_ATTR equ     %01111000       ; White paper + BRIGHT (no flash)

This is your control panel. Change these values, reassemble, and see the difference immediately.

Understanding the Attribute Byte

Let’s decode %01101000 (the cyan cell):

%01101000
 │└┬┘└┬┘
 │ │  └── INK: 000 = black (0)
 │ └───── PAPER: 101 = cyan (5)
 └─────── BRIGHT: 1 = yes, FLASH: 0 = no

The attribute byte packs four pieces of information into 8 bits:

BitsNameValues
7FLASH0=steady, 1=flashing
6BRIGHT0=normal, 1=bright
5-3PAPER0-7 (background colour)
2-0INK0-7 (foreground colour)

Building Attribute Values

To create an attribute byte, work backwards:

  1. Choose your PAPER colour (0-7)
  2. Choose your INK colour (0-7)
  3. Decide on BRIGHT (add 64 if yes)
  4. Decide on FLASH (add 128 if yes)

Example: Bright yellow paper, blue ink

  • PAPER = 6 (yellow) → bits 5-3 = 110
  • INK = 1 (blue) → bits 2-0 = 001
  • BRIGHT = yes → bit 6 = 1
  • FLASH = no → bit 7 = 0

Result: %01110001 = 113 decimal

Example: Flashing green paper, white ink

  • PAPER = 4 (green) → bits 5-3 = 100
  • INK = 7 (white) → bits 2-0 = 111
  • BRIGHT = no → bit 6 = 0
  • FLASH = yes → bit 7 = 1

Result: %10100111 = 167 decimal

Try This: Change the Board Colour

Edit EMPTY_ATTR to try different colours:

; Bright green board
EMPTY_ATTR  equ     %01100000       ; Green paper (4), black ink

; Bright magenta board
EMPTY_ATTR  equ     %01011000       ; Magenta paper (3), black ink

; Yellow board with blue ink
EMPTY_ATTR  equ     %01110001       ; Yellow paper (6), blue ink (1)

Reassemble after each change. Watch the board transform.

The Visible Border

In Units 1 and 2, the border around the board was invisible - black on black. Now we draw it explicitly:

; ----------------------------------------------------------------------------
; Draw Board Border
; ----------------------------------------------------------------------------
; Draws a visible border around the 8x8 playing area

draw_board_border:
            ; Top border (row 7, columns 11-20)
            ld      c, BOARD_ROW-1      ; Row 7
            ld      d, BOARD_COL-1      ; Start at column 11
            ld      b, BOARD_SIZE+2     ; 10 cells wide
            call    draw_border_row

            ; Bottom border (row 16, columns 11-20)
            ld      c, BOARD_ROW+BOARD_SIZE  ; Row 16
            ld      d, BOARD_COL-1
            ld      b, BOARD_SIZE+2
            call    draw_border_row

            ; Left border (rows 8-15, column 11)
            ld      c, BOARD_ROW        ; Start at row 8
            ld      d, BOARD_COL-1      ; Column 11
            ld      b, BOARD_SIZE       ; 8 cells tall
            call    draw_border_col

            ; Right border (rows 8-15, column 20)
            ld      c, BOARD_ROW
            ld      d, BOARD_COL+BOARD_SIZE  ; Column 20
            ld      b, BOARD_SIZE
            call    draw_border_col

            ret

The border is one row above, one row below, and one column on each side of the 8×8 playing area. We draw it as a rectangle of yellow cells.

Try This: Change the Border Colour

; Red border
BORDER_ATTR equ     %01010000       ; Red paper (2)

; White border
BORDER_ATTR equ     %01111000       ; White paper (7)

; Flashing border (dramatic!)
BORDER_ATTR equ     %11110000       ; Flash + Bright + Yellow

Dynamic Screen Border

The screen border (the area outside the main display) changes colour based on whose turn it is:

; Screen border colours for each player
P1_BORDER   equ     2               ; Red border for Player 1's turn
P2_BORDER   equ     1               ; Blue border for Player 2's turn

; ----------------------------------------------------------------------------
; Update Screen Border
; ----------------------------------------------------------------------------
; Sets border colour based on current player

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

This is different from attribute memory. The screen border is set by outputting a value (0-7) to port $FE:

            ld      a, 2            ; Red
            out     ($fe), a        ; Set border

The border colour gives immediate visual feedback about game state.

Try This: Different Turn Colours

; Green for P1, Cyan for P2
P1_BORDER   equ     4               ; Green
P2_BORDER   equ     5               ; Cyan

; Yellow for P1, Magenta for P2
P1_BORDER   equ     6               ; Yellow
P2_BORDER   equ     3               ; Magenta

The Cursor: Flash vs Bright

The original cursor used FLASH (bit 7):

CURSOR_ATTR equ     %10111000       ; Flash + white paper

The FLASH attribute alternates the cell between normal and inverted colours every 16 frames (about 0.32 seconds). It’s automatic - the hardware does it.

The new cursor uses BRIGHT instead:

CURSOR_ATTR equ     %01111000       ; Bright + white paper (no flash)

BRIGHT makes colours more vivid. The cursor now stands out by being brighter than surrounding cells, rather than flashing.

Which do you prefer? Try both.

The Colour Palette

Here’s the complete Spectrum palette:

ValueNormalBright
0BlackBlack
1BlueBright Blue
2RedBright Red
3MagentaBright Magenta
4GreenBright Green
5CyanBright Cyan
6YellowBright Yellow
7GreyWhite

Note that “white” only exists as bright 7. Normal 7 is actually grey.

The Complete Code

; ============================================================================
; INK WAR - Unit 3: Making It Yours
; ============================================================================
; Customised version demonstrating attribute control:
; - Cyan board cells (not white)
; - Yellow border cells around the board
; - Screen border changes colour with current player
; - Bright cursor (not flashing)
;
; Controls: Q=Up, A=Down, O=Left, P=Right, SPACE=Claim
; ============================================================================

            org     32768

; ----------------------------------------------------------------------------
; Constants
; ----------------------------------------------------------------------------

ATTR_BASE   equ     $5800           ; Start of attribute memory
BOARD_ROW   equ     8               ; Board starts at row 8
BOARD_COL   equ     12              ; Board starts at column 12
BOARD_SIZE  equ     8               ; 8x8 playing field

; ============================================================================
; CUSTOMISATION SECTION - Change these values to personalise your game!
; ============================================================================

; Attribute format: %FBPPPIII
;   F = Flash (bit 7): 1 = flashing
;   B = Bright (bit 6): 1 = brighter colours
;   PPP = Paper colour (bits 5-3): background
;   III = Ink colour (bits 2-0): foreground
;
; Colour values (0-7):
;   0=Black, 1=Blue, 2=Red, 3=Magenta, 4=Green, 5=Cyan, 6=Yellow, 7=White

; Empty cells - CUSTOMISED: cyan instead of white
;   %01101000 = BRIGHT + Paper 5 (cyan) + Ink 0 (black)
EMPTY_ATTR  equ     %01101000       ; Cyan paper, black ink + BRIGHT

; Board border - CUSTOMISED: yellow border around playing area
;   %01110000 = BRIGHT + Paper 6 (yellow) + Ink 0 (black)
BORDER_ATTR equ     %01110000       ; Yellow paper, black ink + BRIGHT

; Cursor - CUSTOMISED: bright white instead of flashing
;   %01111000 = BRIGHT + Paper 7 (white) + Ink 0 (black)
CURSOR_ATTR equ     %01111000       ; White paper + BRIGHT (no flash)

; Player 1 - Red (unchanged)
P1_ATTR     equ     %01010000       ; Red paper + BRIGHT
P1_CURSOR   equ     %01111010       ; White paper + Red ink + BRIGHT

; Player 2 - Blue (unchanged)
P2_ATTR     equ     %01001000       ; Blue paper + BRIGHT
P2_CURSOR   equ     %01111001       ; White paper + Blue ink + BRIGHT

; Screen border colours for each player
P1_BORDER   equ     2               ; Red border for Player 1's turn
P2_BORDER   equ     1               ; Blue border for Player 2's turn

; ============================================================================
; End of customisation section
; ============================================================================

; Keyboard ports (active low)
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   ; NEW: Draw visible border
            call    draw_board
            call    draw_cursor
            call    update_border       ; Set initial border colour

main_loop:
            halt

            call    read_keyboard
            call    handle_input

            jp      main_loop

; ----------------------------------------------------------------------------
; Initialise Screen
; ----------------------------------------------------------------------------

init_screen:
            xor     a
            out     (KEY_PORT), a

            ld      hl, ATTR_BASE
            ld      de, ATTR_BASE+1
            ld      bc, 767
            ld      (hl), 0
            ldir

            ret

; ----------------------------------------------------------------------------
; Update Screen Border
; ----------------------------------------------------------------------------
; Sets border colour based on current player

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
.clear_loop:
            ld      (hl), a
            inc     hl
            djnz    .clear_loop

            ld      a, 1
            ld      (current_player), a

            xor     a
            ld      (cursor_row), a
            ld      (cursor_col), a

            ret

; ----------------------------------------------------------------------------
; Draw Board Border
; ----------------------------------------------------------------------------
; Draws a visible border around the 8x8 playing area

draw_board_border:
            ; Top border (row 7, columns 11-20)
            ld      c, BOARD_ROW-1      ; Row 7
            ld      d, BOARD_COL-1      ; Start at column 11
            ld      b, BOARD_SIZE+2     ; 10 cells wide
            call    draw_border_row

            ; Bottom border (row 16, columns 11-20)
            ld      c, BOARD_ROW+BOARD_SIZE  ; Row 16
            ld      d, BOARD_COL-1
            ld      b, BOARD_SIZE+2
            call    draw_border_row

            ; Left border (rows 8-15, column 11)
            ld      c, BOARD_ROW        ; Start at row 8
            ld      d, BOARD_COL-1      ; Column 11
            ld      b, BOARD_SIZE       ; 8 cells tall
            call    draw_border_col

            ; Right border (rows 8-15, column 20)
            ld      c, BOARD_ROW
            ld      d, BOARD_COL+BOARD_SIZE  ; Column 20
            ld      b, BOARD_SIZE
            call    draw_border_col

            ret

; Draw horizontal border row
; C = row, D = start column, B = width
draw_border_row:
            push    bc
.row_loop:
            push    bc
            push    de

            ; Calculate attribute address
            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    .row_loop
            pop     bc
            ret

; Draw vertical border column
; C = start row, D = column, B = height
draw_border_col:
            push    bc
.col_loop:
            push    bc
            push    de

            ; Calculate attribute address
            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    .col_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, .cc_p1
            cp      STATE_P2
            jr      z, .cc_p2

            ld      a, EMPTY_ATTR
            jr      .cc_set

.cc_p1:
            ld      a, P1_ATTR
            jr      .cc_set

.cc_p2:
            ld      a, P2_ATTR

.cc_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, .not_q
            ld      a, 1
            ld      (key_pressed), a
            ret
.not_q:
            ld      a, ROW_ASDF
            in      a, (KEY_PORT)
            bit     0, a
            jr      nz, .not_a
            ld      a, 2
            ld      (key_pressed), a
            ret
.not_a:
            ld      a, ROW_YUIOP
            in      a, (KEY_PORT)
            bit     1, a
            jr      nz, .not_o
            ld      a, 3
            ld      (key_pressed), a
            ret
.not_o:
            ld      a, ROW_YUIOP
            in      a, (KEY_PORT)
            bit     0, a
            jr      nz, .not_p
            ld      a, 4
            ld      (key_pressed), a
            ret
.not_p:
            ld      a, ROW_SPACE
            in      a, (KEY_PORT)
            bit     0, a
            jr      nz, .not_space
            ld      a, 5
            ld      (key_pressed), a
.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, .not_up
            ld      a, (cursor_row)
            or      a
            jr      z, .done
            dec     a
            ld      (cursor_row), a
            jr      .done
.not_up:
            cp      2
            jr      nz, .not_down
            ld      a, (cursor_row)
            cp      BOARD_SIZE-1
            jr      z, .done
            inc     a
            ld      (cursor_row), a
            jr      .done
.not_down:
            cp      3
            jr      nz, .not_left
            ld      a, (cursor_col)
            or      a
            jr      z, .done
            dec     a
            ld      (cursor_col), a
            jr      .done
.not_left:
            cp      4
            jr      nz, .done
            ld      a, (cursor_col)
            cp      BOARD_SIZE-1
            jr      z, .done
            inc     a
            ld      (cursor_col), a

.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    update_border       ; Update border for new player
            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, .cc_is_p1
            ld      (hl), P2_ATTR
            ret
.cc_is_p1:
            ld      (hl), P1_ATTR
            ret

; ----------------------------------------------------------------------------
; Sound - Claim
; ----------------------------------------------------------------------------

sound_claim:
            ld      hl, 400
            ld      b, 20

.loop:
            push    bc
            push    hl

            ld      b, h
            ld      c, l
.tone_loop:
            ld      a, $10
            out     (KEY_PORT), a
            call    .delay
            xor     a
            out     (KEY_PORT), a
            call    .delay
            dec     bc
            ld      a, b
            or      c
            jr      nz, .tone_loop

            pop     hl
            pop     bc

            ld      de, 20
            or      a
            sbc     hl, de

            djnz    .loop
            ret

.delay:
            push    bc
            ld      b, 5
.delay_loop:
            djnz    .delay_loop
            pop     bc
            ret

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

cursor_row:     defb    0
cursor_col:     defb    0
key_pressed:    defb    0
current_player: defb    1
board_state:    defs    64, 0

; ----------------------------------------------------------------------------
; End
; ----------------------------------------------------------------------------

            end     start

Challenge: Create Your Theme

Design a complete colour scheme:

  1. Choose a board colour that looks good
  2. Choose a contrasting border colour
  3. Pick screen border colours that clearly indicate whose turn it is
  4. Decide between flash and bright for the cursor
  5. Make sure player colours (red/blue) still stand out

Document your choices in comments. Share your theme.

What You’ve Learnt

  • Attribute byte format - FBPPPIII: flash, bright, paper, ink packed into 8 bits
  • Calculating attributes - Shift paper left 3, add ink, optionally add 64 (bright) or 128 (flash)
  • Screen border - OUT ($FE), A sets the border to colours 0-7
  • FLASH vs BRIGHT - Flash alternates automatically; bright intensifies colours
  • Colour palette - 8 colours × 2 brightness levels, but black stays black

What’s Next

In Unit 4, we’ll add score and turn display - text on screen showing whose turn it is and how many cells each player controls.

What Changed

Unit 2 → Unit 3
+203-86
11 ; ============================================================================
2-; INK WAR - Unit 2: Claiming Cells
2+; INK WAR - Unit 3: Making It Yours
33 ; ============================================================================
4-; Two players take turns claiming cells on the board.
5-; Press Space to claim the cell under the cursor.
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)
69 ;
710 ; Controls: Q=Up, A=Down, O=Left, P=Right, SPACE=Claim
811 ; ============================================================================
...
1821 BOARD_COL equ 12 ; Board starts at column 12
1922 BOARD_SIZE equ 8 ; 8x8 playing field
2023
21-; Attribute colours (FBPPPIII format)
22-BORDER_ATTR equ %00000000 ; Black on black (border)
23-EMPTY_ATTR equ %00111000 ; White paper, black ink (empty cell)
24-CURSOR_ATTR equ %10111000 ; White paper, black ink + FLASH
25-P1_ATTR equ %01010000 ; Red paper, black ink + BRIGHT (Player 1)
26-P2_ATTR equ %01001000 ; Blue paper, black ink + BRIGHT (Player 2)
27-P1_CURSOR equ %11010000 ; Red paper + FLASH (Player 1 cursor on own cell)
28-P2_CURSOR equ %11001000 ; Blue paper + FLASH (Player 2 cursor on own cell)
24+; ============================================================================
25+; CUSTOMISATION SECTION - Change these values to personalise your game!
26+; ============================================================================
27+
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
36+
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
40+
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
44+
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)
48+
49+; Player 1 - Red (unchanged)
50+P1_ATTR equ %01010000 ; Red paper + BRIGHT
51+P1_CURSOR equ %01111010 ; White paper + Red ink + BRIGHT
52+
53+; Player 2 - Blue (unchanged)
54+P2_ATTR equ %01001000 ; Blue paper + BRIGHT
55+P2_CURSOR equ %01111001 ; White paper + Blue ink + BRIGHT
56+
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
60+
61+; ============================================================================
62+; End of customisation section
63+; ============================================================================
2964
3065 ; Keyboard ports (active low)
3166 KEY_PORT equ $fe
32-ROW_QAOP equ $fb ; Q W E R T row
33-ROW_ASDF equ $fd ; A S D F G row
34-ROW_YUIOP equ $df ; Y U I O P row
35-ROW_SPACE equ $7f ; SPACE SYM M N B row
67+ROW_QAOP equ $fb
68+ROW_ASDF equ $fd
69+ROW_YUIOP equ $df
70+ROW_SPACE equ $7f
3671
3772 ; Game states
3873 STATE_EMPTY equ 0
...
4479 ; ----------------------------------------------------------------------------
4580
4681 start:
47- call init_screen ; Clear screen and set border
48- call init_game ; Initialise game state
49- call draw_board ; Draw the game board
50- call draw_cursor ; Show cursor at starting position
82+ call init_screen
83+ call init_game
84+ call draw_board_border ; NEW: Draw visible border
85+ call draw_board
86+ call draw_cursor
87+ call update_border ; Set initial border colour
5188
5289 main_loop:
53- halt ; Wait for frame (50Hz timing)
90+ halt
5491
55- call read_keyboard ; Check for input
56- call handle_input ; Process movement or claim
92+ call read_keyboard
93+ call handle_input
5794
58- jp main_loop ; Repeat forever
95+ jp main_loop
5996
6097 ; ----------------------------------------------------------------------------
6198 ; Initialise Screen
...
63100
64101 init_screen:
65102 xor a
66- out (KEY_PORT), a ; Black border
103+ out (KEY_PORT), a
67104
68105 ld hl, ATTR_BASE
69106 ld de, ATTR_BASE+1
70107 ld bc, 767
71108 ld (hl), 0
72109 ldir
110+
111+ ret
112+
113+; ----------------------------------------------------------------------------
114+; Update Screen Border
115+; ----------------------------------------------------------------------------
116+; Sets border colour based on current player
73117
118+update_border:
119+ ld a, (current_player)
120+ cp 1
121+ jr z, .ub_p1
122+ ld a, P2_BORDER
123+ jr .ub_set
124+.ub_p1:
125+ ld a, P1_BORDER
126+.ub_set:
127+ out (KEY_PORT), a
74128 ret
75129
76130 ; ----------------------------------------------------------------------------
...
78132 ; ----------------------------------------------------------------------------
79133
80134 init_game:
81- ; Clear board state (all empty)
82135 ld hl, board_state
83136 ld b, 64
84137 xor a
...
87140 inc hl
88141 djnz .clear_loop
89142
90- ; Player 1 starts
91143 ld a, 1
92144 ld (current_player), a
93145
94- ; Cursor at top-left
95146 xor a
96147 ld (cursor_row), a
97148 ld (cursor_col), a
149+
150+ ret
151+
152+; ----------------------------------------------------------------------------
153+; Draw Board Border
154+; ----------------------------------------------------------------------------
155+; Draws a visible border around the 8x8 playing area
156+
157+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
162+ call draw_border_row
163+
164+ ; Bottom border (row 16, columns 11-20)
165+ ld c, BOARD_ROW+BOARD_SIZE ; Row 16
166+ ld d, BOARD_COL-1
167+ ld b, BOARD_SIZE+2
168+ call draw_border_row
169+
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
174+ call draw_border_col
175+
176+ ; Right border (rows 8-15, column 20)
177+ ld c, BOARD_ROW
178+ ld d, BOARD_COL+BOARD_SIZE ; Column 20
179+ ld b, BOARD_SIZE
180+ call draw_border_col
181+
182+ ret
183+
184+; Draw horizontal border row
185+; C = row, D = start column, B = width
186+draw_border_row:
187+ push bc
188+.row_loop:
189+ push bc
190+ push de
191+
192+ ; Calculate attribute address
193+ ld a, c
194+ ld l, a
195+ ld h, 0
196+ add hl, hl
197+ add hl, hl
198+ add hl, hl
199+ add hl, hl
200+ add hl, hl
201+ ld a, d
202+ add a, l
203+ ld l, a
204+ ld bc, ATTR_BASE
205+ add hl, bc
206+
207+ ld (hl), BORDER_ATTR
208+
209+ pop de
210+ pop bc
211+ inc d
212+ djnz .row_loop
213+ pop bc
214+ ret
215+
216+; Draw vertical border column
217+; C = start row, D = column, B = height
218+draw_border_col:
219+ push bc
220+.col_loop:
221+ push bc
222+ push de
223+
224+ ; Calculate attribute address
225+ ld a, c
226+ ld l, a
227+ ld h, 0
228+ add hl, hl
229+ add hl, hl
230+ add hl, hl
231+ add hl, hl
232+ add hl, hl
233+ ld a, d
234+ add a, l
235+ ld l, a
236+ ld bc, ATTR_BASE
237+ add hl, bc
238+
239+ ld (hl), BORDER_ATTR
98240
241+ pop de
242+ pop bc
243+ inc c
244+ djnz .col_loop
245+ pop bc
99246 ret
100247
101248 ; ----------------------------------------------------------------------------
...
106253 ld b, BOARD_SIZE
107254 ld c, BOARD_ROW
108255
109-.row_loop:
256+.db_row:
110257 push bc
111258
112259 ld b, BOARD_SIZE
113260 ld d, BOARD_COL
114261
115-.col_loop:
262+.db_col:
116263 push bc
117264
118- ; Calculate attribute address
119265 ld a, c
120266 ld l, a
121267 ld h, 0
...
134280
135281 pop bc
136282 inc d
137- djnz .col_loop
283+ djnz .db_col
138284
139285 pop bc
140286 inc c
141- djnz .row_loop
287+ djnz .db_row
142288
143289 ret
144290
145291 ; ----------------------------------------------------------------------------
146292 ; Draw Cursor
147293 ; ----------------------------------------------------------------------------
148-; Shows cursor with appropriate colour based on cell state
149294
150295 draw_cursor:
151- ; Get cell state at cursor position
152- call get_cell_state ; A = state at cursor
296+ call get_cell_state
153297
154- ; Determine cursor attribute based on state
155298 cp STATE_P1
156299 jr z, .dc_p1
157300 cp STATE_P2
158301 jr z, .dc_p2
159302
160- ; Empty cell - use standard cursor
161303 ld a, CURSOR_ATTR
162304 jr .dc_set
163305
...
169311 ld a, P2_CURSOR
170312
171313 .dc_set:
172- push af ; Save attribute
314+ push af
173315
174- ; Calculate attribute address
175316 ld a, (cursor_row)
176317 add a, BOARD_ROW
177318 ld l, a
...
188329 ld bc, ATTR_BASE
189330 add hl, bc
190331
191- pop af ; Restore attribute
332+ pop af
192333 ld (hl), a
193334
194335 ret
...
196337 ; ----------------------------------------------------------------------------
197338 ; Clear Cursor
198339 ; ----------------------------------------------------------------------------
199-; Restores cell to its proper colour (empty, P1, or P2)
200340
201341 clear_cursor:
202- ; Get cell state
203342 call get_cell_state
204343
205- ; Determine attribute based on state
206344 cp STATE_P1
207345 jr z, .cc_p1
208346 cp STATE_P2
209347 jr z, .cc_p2
210348
211- ; Empty cell
212349 ld a, EMPTY_ATTR
213350 jr .cc_set
214351
...
222359 .cc_set:
223360 push af
224361
225- ; Calculate attribute address
226362 ld a, (cursor_row)
227363 add a, BOARD_ROW
228364 ld l, a
...
247383 ; ----------------------------------------------------------------------------
248384 ; Get Cell State
249385 ; ----------------------------------------------------------------------------
250-; Returns: A = state at cursor position (0=empty, 1=P1, 2=P2)
251386
252387 get_cell_state:
253388 ld a, (cursor_row)
254- add a, a ; *2
255- add a, a ; *4
256- add a, a ; *8
389+ add a, a
390+ add a, a
391+ add a, a
257392 ld hl, board_state
258393 ld b, 0
259394 ld c, a
260395 add hl, bc
261396 ld a, (cursor_col)
262397 ld c, a
263- add hl, bc ; HL = &board_state[row*8+col]
398+ add hl, bc
264399 ld a, (hl)
265400 ret
266401
...
272407 xor a
273408 ld (key_pressed), a
274409
275- ; Check Q (up)
276410 ld a, ROW_QAOP
277411 in a, (KEY_PORT)
278412 bit 0, a
...
281415 ld (key_pressed), a
282416 ret
283417 .not_q:
284- ; Check A (down)
285418 ld a, ROW_ASDF
286419 in a, (KEY_PORT)
287420 bit 0, a
...
290423 ld (key_pressed), a
291424 ret
292425 .not_a:
293- ; Check O (left)
294426 ld a, ROW_YUIOP
295427 in a, (KEY_PORT)
296428 bit 1, a
...
299431 ld (key_pressed), a
300432 ret
301433 .not_o:
302- ; Check P (right)
303434 ld a, ROW_YUIOP
304435 in a, (KEY_PORT)
305436 bit 0, a
...
308439 ld (key_pressed), a
309440 ret
310441 .not_p:
311- ; Check SPACE (claim)
312442 ld a, ROW_SPACE
313443 in a, (KEY_PORT)
314444 bit 0, a
...
325455 handle_input:
326456 ld a, (key_pressed)
327457 or a
328- ret z ; No key
458+ ret z
329459
330- cp 5 ; Space?
460+ cp 5
331461 jr z, try_claim
332462
333- ; Movement key - use existing move logic
334463 call clear_cursor
335464
336465 ld a, (key_pressed)
337466
338- cp 1 ; Up?
467+ cp 1
339468 jr nz, .not_up
340469 ld a, (cursor_row)
341470 or a
...
344473 ld (cursor_row), a
345474 jr .done
346475 .not_up:
347- cp 2 ; Down?
476+ cp 2
348477 jr nz, .not_down
349478 ld a, (cursor_row)
350479 cp BOARD_SIZE-1
...
353482 ld (cursor_row), a
354483 jr .done
355484 .not_down:
356- cp 3 ; Left?
485+ cp 3
357486 jr nz, .not_left
358487 ld a, (cursor_col)
359488 or a
...
362491 ld (cursor_col), a
363492 jr .done
364493 .not_left:
365- cp 4 ; Right?
494+ cp 4
366495 jr nz, .done
367496 ld a, (cursor_col)
368497 cp BOARD_SIZE-1
...
379508 ; ----------------------------------------------------------------------------
380509
381510 try_claim:
382- ; Check if cell is empty
383511 call get_cell_state
384512 or a
385- ret nz ; Not empty - can't claim
513+ ret nz
386514
387- ; Claim the cell
388515 call claim_cell
389-
390- ; Play success sound
391516 call sound_claim
392517
393- ; Switch player
394518 ld a, (current_player)
395- xor 3 ; Toggle between 1 and 2
519+ xor 3
396520 ld (current_player), a
397521
398- ; Redraw cursor with new state
522+ call update_border ; Update border for new player
399523 call draw_cursor
400524
401525 ret
...
403527 ; ----------------------------------------------------------------------------
404528 ; Claim Cell
405529 ; ----------------------------------------------------------------------------
406-; Claims current cell for current player
407530
408531 claim_cell:
409- ; Calculate board state index
410532 ld a, (cursor_row)
411533 add a, a
412534 add a, a
...
417539 add hl, bc
418540 ld a, (cursor_col)
419541 ld c, a
420- add hl, bc ; HL = &board_state[row*8+col]
542+ add hl, bc
421543
422- ; Set to current player
423544 ld a, (current_player)
424545 ld (hl), a
425546
426- ; Update attribute colour
427547 push af
428548
429549 ld a, (cursor_row)
...
444564
445565 pop af
446566 cp 1
447- jr z, .player1
567+ jr z, .cc_is_p1
448568 ld (hl), P2_ATTR
449569 ret
450-.player1:
570+.cc_is_p1:
451571 ld (hl), P1_ATTR
452572 ret
453573
454574 ; ----------------------------------------------------------------------------
455575 ; Sound - Claim
456576 ; ----------------------------------------------------------------------------
457-; Short rising tone for successful claim
458577
459578 sound_claim:
460- ld hl, 400 ; Starting pitch
461- ld b, 20 ; Duration
579+ ld hl, 400
580+ ld b, 20
462581
463582 .loop:
464583 push bc
465584 push hl
466585
467- ; Generate tone
468586 ld b, h
469587 ld c, l
470588 .tone_loop:
471- ld a, $10 ; Speaker bit on
589+ ld a, $10
472590 out (KEY_PORT), a
473591 call .delay
474- xor a ; Speaker bit off
592+ xor a
475593 out (KEY_PORT), a
476594 call .delay
477595 dec bc
...
482600 pop hl
483601 pop bc
484602
485- ; Increase pitch (lower delay = higher frequency)
486603 ld de, 20
487604 or a
488605 sbc hl, de
...
505622 cursor_row: defb 0
506623 cursor_col: defb 0
507624 key_pressed: defb 0
508-current_player: defb 1 ; 1 = Player 1 (Red), 2 = Player 2 (Blue)
509-board_state: defs 64, 0 ; 64 bytes, all initialised to 0
625+current_player: defb 1
626+board_state: defs 64, 0
510627
511628 ; ----------------------------------------------------------------------------
512629 ; End