Making It Yours
Customise the board colours, position, and cursor style. Learn the internals by changing them.
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

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:
| Bits | Name | Values |
|---|---|---|
| 7 | FLASH | 0=steady, 1=flashing |
| 6 | BRIGHT | 0=normal, 1=bright |
| 5-3 | PAPER | 0-7 (background colour) |
| 2-0 | INK | 0-7 (foreground colour) |
Building Attribute Values
To create an attribute byte, work backwards:
- Choose your PAPER colour (0-7)
- Choose your INK colour (0-7)
- Decide on BRIGHT (add 64 if yes)
- 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:
| Value | Normal | Bright |
|---|---|---|
| 0 | Black | Black |
| 1 | Blue | Bright Blue |
| 2 | Red | Bright Red |
| 3 | Magenta | Bright Magenta |
| 4 | Green | Bright Green |
| 5 | Cyan | Bright Cyan |
| 6 | Yellow | Bright Yellow |
| 7 | Grey | White |
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:
- Choose a board colour that looks good
- Choose a contrasting border colour
- Pick screen border colours that clearly indicate whose turn it is
- Decide between flash and bright for the cursor
- 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
| 1 | 1 | ; ============================================================================ | |
| 2 | - | ; INK WAR - Unit 2: Claiming Cells | |
| 2 | + | ; INK WAR - Unit 3: Making It Yours | |
| 3 | 3 | ; ============================================================================ | |
| 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) | |
| 6 | 9 | ; | |
| 7 | 10 | ; Controls: Q=Up, A=Down, O=Left, P=Right, SPACE=Claim | |
| 8 | 11 | ; ============================================================================ | |
| ... | |||
| 18 | 21 | BOARD_COL equ 12 ; Board starts at column 12 | |
| 19 | 22 | BOARD_SIZE equ 8 ; 8x8 playing field | |
| 20 | 23 | | |
| 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 | + | ; ============================================================================ | |
| 29 | 64 | | |
| 30 | 65 | ; Keyboard ports (active low) | |
| 31 | 66 | 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 | |
| 36 | 71 | | |
| 37 | 72 | ; Game states | |
| 38 | 73 | STATE_EMPTY equ 0 | |
| ... | |||
| 44 | 79 | ; ---------------------------------------------------------------------------- | |
| 45 | 80 | | |
| 46 | 81 | 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 | |
| 51 | 88 | | |
| 52 | 89 | main_loop: | |
| 53 | - | halt ; Wait for frame (50Hz timing) | |
| 90 | + | halt | |
| 54 | 91 | | |
| 55 | - | call read_keyboard ; Check for input | |
| 56 | - | call handle_input ; Process movement or claim | |
| 92 | + | call read_keyboard | |
| 93 | + | call handle_input | |
| 57 | 94 | | |
| 58 | - | jp main_loop ; Repeat forever | |
| 95 | + | jp main_loop | |
| 59 | 96 | | |
| 60 | 97 | ; ---------------------------------------------------------------------------- | |
| 61 | 98 | ; Initialise Screen | |
| ... | |||
| 63 | 100 | | |
| 64 | 101 | init_screen: | |
| 65 | 102 | xor a | |
| 66 | - | out (KEY_PORT), a ; Black border | |
| 103 | + | out (KEY_PORT), a | |
| 67 | 104 | | |
| 68 | 105 | ld hl, ATTR_BASE | |
| 69 | 106 | ld de, ATTR_BASE+1 | |
| 70 | 107 | ld bc, 767 | |
| 71 | 108 | ld (hl), 0 | |
| 72 | 109 | ldir | |
| 110 | + | | |
| 111 | + | ret | |
| 112 | + | | |
| 113 | + | ; ---------------------------------------------------------------------------- | |
| 114 | + | ; Update Screen Border | |
| 115 | + | ; ---------------------------------------------------------------------------- | |
| 116 | + | ; Sets border colour based on current player | |
| 73 | 117 | | |
| 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 | |
| 74 | 128 | ret | |
| 75 | 129 | | |
| 76 | 130 | ; ---------------------------------------------------------------------------- | |
| ... | |||
| 78 | 132 | ; ---------------------------------------------------------------------------- | |
| 79 | 133 | | |
| 80 | 134 | init_game: | |
| 81 | - | ; Clear board state (all empty) | |
| 82 | 135 | ld hl, board_state | |
| 83 | 136 | ld b, 64 | |
| 84 | 137 | xor a | |
| ... | |||
| 87 | 140 | inc hl | |
| 88 | 141 | djnz .clear_loop | |
| 89 | 142 | | |
| 90 | - | ; Player 1 starts | |
| 91 | 143 | ld a, 1 | |
| 92 | 144 | ld (current_player), a | |
| 93 | 145 | | |
| 94 | - | ; Cursor at top-left | |
| 95 | 146 | xor a | |
| 96 | 147 | ld (cursor_row), a | |
| 97 | 148 | 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 | |
| 98 | 240 | | |
| 241 | + | pop de | |
| 242 | + | pop bc | |
| 243 | + | inc c | |
| 244 | + | djnz .col_loop | |
| 245 | + | pop bc | |
| 99 | 246 | ret | |
| 100 | 247 | | |
| 101 | 248 | ; ---------------------------------------------------------------------------- | |
| ... | |||
| 106 | 253 | ld b, BOARD_SIZE | |
| 107 | 254 | ld c, BOARD_ROW | |
| 108 | 255 | | |
| 109 | - | .row_loop: | |
| 256 | + | .db_row: | |
| 110 | 257 | push bc | |
| 111 | 258 | | |
| 112 | 259 | ld b, BOARD_SIZE | |
| 113 | 260 | ld d, BOARD_COL | |
| 114 | 261 | | |
| 115 | - | .col_loop: | |
| 262 | + | .db_col: | |
| 116 | 263 | push bc | |
| 117 | 264 | | |
| 118 | - | ; Calculate attribute address | |
| 119 | 265 | ld a, c | |
| 120 | 266 | ld l, a | |
| 121 | 267 | ld h, 0 | |
| ... | |||
| 134 | 280 | | |
| 135 | 281 | pop bc | |
| 136 | 282 | inc d | |
| 137 | - | djnz .col_loop | |
| 283 | + | djnz .db_col | |
| 138 | 284 | | |
| 139 | 285 | pop bc | |
| 140 | 286 | inc c | |
| 141 | - | djnz .row_loop | |
| 287 | + | djnz .db_row | |
| 142 | 288 | | |
| 143 | 289 | ret | |
| 144 | 290 | | |
| 145 | 291 | ; ---------------------------------------------------------------------------- | |
| 146 | 292 | ; Draw Cursor | |
| 147 | 293 | ; ---------------------------------------------------------------------------- | |
| 148 | - | ; Shows cursor with appropriate colour based on cell state | |
| 149 | 294 | | |
| 150 | 295 | draw_cursor: | |
| 151 | - | ; Get cell state at cursor position | |
| 152 | - | call get_cell_state ; A = state at cursor | |
| 296 | + | call get_cell_state | |
| 153 | 297 | | |
| 154 | - | ; Determine cursor attribute based on state | |
| 155 | 298 | cp STATE_P1 | |
| 156 | 299 | jr z, .dc_p1 | |
| 157 | 300 | cp STATE_P2 | |
| 158 | 301 | jr z, .dc_p2 | |
| 159 | 302 | | |
| 160 | - | ; Empty cell - use standard cursor | |
| 161 | 303 | ld a, CURSOR_ATTR | |
| 162 | 304 | jr .dc_set | |
| 163 | 305 | | |
| ... | |||
| 169 | 311 | ld a, P2_CURSOR | |
| 170 | 312 | | |
| 171 | 313 | .dc_set: | |
| 172 | - | push af ; Save attribute | |
| 314 | + | push af | |
| 173 | 315 | | |
| 174 | - | ; Calculate attribute address | |
| 175 | 316 | ld a, (cursor_row) | |
| 176 | 317 | add a, BOARD_ROW | |
| 177 | 318 | ld l, a | |
| ... | |||
| 188 | 329 | ld bc, ATTR_BASE | |
| 189 | 330 | add hl, bc | |
| 190 | 331 | | |
| 191 | - | pop af ; Restore attribute | |
| 332 | + | pop af | |
| 192 | 333 | ld (hl), a | |
| 193 | 334 | | |
| 194 | 335 | ret | |
| ... | |||
| 196 | 337 | ; ---------------------------------------------------------------------------- | |
| 197 | 338 | ; Clear Cursor | |
| 198 | 339 | ; ---------------------------------------------------------------------------- | |
| 199 | - | ; Restores cell to its proper colour (empty, P1, or P2) | |
| 200 | 340 | | |
| 201 | 341 | clear_cursor: | |
| 202 | - | ; Get cell state | |
| 203 | 342 | call get_cell_state | |
| 204 | 343 | | |
| 205 | - | ; Determine attribute based on state | |
| 206 | 344 | cp STATE_P1 | |
| 207 | 345 | jr z, .cc_p1 | |
| 208 | 346 | cp STATE_P2 | |
| 209 | 347 | jr z, .cc_p2 | |
| 210 | 348 | | |
| 211 | - | ; Empty cell | |
| 212 | 349 | ld a, EMPTY_ATTR | |
| 213 | 350 | jr .cc_set | |
| 214 | 351 | | |
| ... | |||
| 222 | 359 | .cc_set: | |
| 223 | 360 | push af | |
| 224 | 361 | | |
| 225 | - | ; Calculate attribute address | |
| 226 | 362 | ld a, (cursor_row) | |
| 227 | 363 | add a, BOARD_ROW | |
| 228 | 364 | ld l, a | |
| ... | |||
| 247 | 383 | ; ---------------------------------------------------------------------------- | |
| 248 | 384 | ; Get Cell State | |
| 249 | 385 | ; ---------------------------------------------------------------------------- | |
| 250 | - | ; Returns: A = state at cursor position (0=empty, 1=P1, 2=P2) | |
| 251 | 386 | | |
| 252 | 387 | get_cell_state: | |
| 253 | 388 | 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 | |
| 257 | 392 | ld hl, board_state | |
| 258 | 393 | ld b, 0 | |
| 259 | 394 | ld c, a | |
| 260 | 395 | add hl, bc | |
| 261 | 396 | ld a, (cursor_col) | |
| 262 | 397 | ld c, a | |
| 263 | - | add hl, bc ; HL = &board_state[row*8+col] | |
| 398 | + | add hl, bc | |
| 264 | 399 | ld a, (hl) | |
| 265 | 400 | ret | |
| 266 | 401 | | |
| ... | |||
| 272 | 407 | xor a | |
| 273 | 408 | ld (key_pressed), a | |
| 274 | 409 | | |
| 275 | - | ; Check Q (up) | |
| 276 | 410 | ld a, ROW_QAOP | |
| 277 | 411 | in a, (KEY_PORT) | |
| 278 | 412 | bit 0, a | |
| ... | |||
| 281 | 415 | ld (key_pressed), a | |
| 282 | 416 | ret | |
| 283 | 417 | .not_q: | |
| 284 | - | ; Check A (down) | |
| 285 | 418 | ld a, ROW_ASDF | |
| 286 | 419 | in a, (KEY_PORT) | |
| 287 | 420 | bit 0, a | |
| ... | |||
| 290 | 423 | ld (key_pressed), a | |
| 291 | 424 | ret | |
| 292 | 425 | .not_a: | |
| 293 | - | ; Check O (left) | |
| 294 | 426 | ld a, ROW_YUIOP | |
| 295 | 427 | in a, (KEY_PORT) | |
| 296 | 428 | bit 1, a | |
| ... | |||
| 299 | 431 | ld (key_pressed), a | |
| 300 | 432 | ret | |
| 301 | 433 | .not_o: | |
| 302 | - | ; Check P (right) | |
| 303 | 434 | ld a, ROW_YUIOP | |
| 304 | 435 | in a, (KEY_PORT) | |
| 305 | 436 | bit 0, a | |
| ... | |||
| 308 | 439 | ld (key_pressed), a | |
| 309 | 440 | ret | |
| 310 | 441 | .not_p: | |
| 311 | - | ; Check SPACE (claim) | |
| 312 | 442 | ld a, ROW_SPACE | |
| 313 | 443 | in a, (KEY_PORT) | |
| 314 | 444 | bit 0, a | |
| ... | |||
| 325 | 455 | handle_input: | |
| 326 | 456 | ld a, (key_pressed) | |
| 327 | 457 | or a | |
| 328 | - | ret z ; No key | |
| 458 | + | ret z | |
| 329 | 459 | | |
| 330 | - | cp 5 ; Space? | |
| 460 | + | cp 5 | |
| 331 | 461 | jr z, try_claim | |
| 332 | 462 | | |
| 333 | - | ; Movement key - use existing move logic | |
| 334 | 463 | call clear_cursor | |
| 335 | 464 | | |
| 336 | 465 | ld a, (key_pressed) | |
| 337 | 466 | | |
| 338 | - | cp 1 ; Up? | |
| 467 | + | cp 1 | |
| 339 | 468 | jr nz, .not_up | |
| 340 | 469 | ld a, (cursor_row) | |
| 341 | 470 | or a | |
| ... | |||
| 344 | 473 | ld (cursor_row), a | |
| 345 | 474 | jr .done | |
| 346 | 475 | .not_up: | |
| 347 | - | cp 2 ; Down? | |
| 476 | + | cp 2 | |
| 348 | 477 | jr nz, .not_down | |
| 349 | 478 | ld a, (cursor_row) | |
| 350 | 479 | cp BOARD_SIZE-1 | |
| ... | |||
| 353 | 482 | ld (cursor_row), a | |
| 354 | 483 | jr .done | |
| 355 | 484 | .not_down: | |
| 356 | - | cp 3 ; Left? | |
| 485 | + | cp 3 | |
| 357 | 486 | jr nz, .not_left | |
| 358 | 487 | ld a, (cursor_col) | |
| 359 | 488 | or a | |
| ... | |||
| 362 | 491 | ld (cursor_col), a | |
| 363 | 492 | jr .done | |
| 364 | 493 | .not_left: | |
| 365 | - | cp 4 ; Right? | |
| 494 | + | cp 4 | |
| 366 | 495 | jr nz, .done | |
| 367 | 496 | ld a, (cursor_col) | |
| 368 | 497 | cp BOARD_SIZE-1 | |
| ... | |||
| 379 | 508 | ; ---------------------------------------------------------------------------- | |
| 380 | 509 | | |
| 381 | 510 | try_claim: | |
| 382 | - | ; Check if cell is empty | |
| 383 | 511 | call get_cell_state | |
| 384 | 512 | or a | |
| 385 | - | ret nz ; Not empty - can't claim | |
| 513 | + | ret nz | |
| 386 | 514 | | |
| 387 | - | ; Claim the cell | |
| 388 | 515 | call claim_cell | |
| 389 | - | | |
| 390 | - | ; Play success sound | |
| 391 | 516 | call sound_claim | |
| 392 | 517 | | |
| 393 | - | ; Switch player | |
| 394 | 518 | ld a, (current_player) | |
| 395 | - | xor 3 ; Toggle between 1 and 2 | |
| 519 | + | xor 3 | |
| 396 | 520 | ld (current_player), a | |
| 397 | 521 | | |
| 398 | - | ; Redraw cursor with new state | |
| 522 | + | call update_border ; Update border for new player | |
| 399 | 523 | call draw_cursor | |
| 400 | 524 | | |
| 401 | 525 | ret | |
| ... | |||
| 403 | 527 | ; ---------------------------------------------------------------------------- | |
| 404 | 528 | ; Claim Cell | |
| 405 | 529 | ; ---------------------------------------------------------------------------- | |
| 406 | - | ; Claims current cell for current player | |
| 407 | 530 | | |
| 408 | 531 | claim_cell: | |
| 409 | - | ; Calculate board state index | |
| 410 | 532 | ld a, (cursor_row) | |
| 411 | 533 | add a, a | |
| 412 | 534 | add a, a | |
| ... | |||
| 417 | 539 | add hl, bc | |
| 418 | 540 | ld a, (cursor_col) | |
| 419 | 541 | ld c, a | |
| 420 | - | add hl, bc ; HL = &board_state[row*8+col] | |
| 542 | + | add hl, bc | |
| 421 | 543 | | |
| 422 | - | ; Set to current player | |
| 423 | 544 | ld a, (current_player) | |
| 424 | 545 | ld (hl), a | |
| 425 | 546 | | |
| 426 | - | ; Update attribute colour | |
| 427 | 547 | push af | |
| 428 | 548 | | |
| 429 | 549 | ld a, (cursor_row) | |
| ... | |||
| 444 | 564 | | |
| 445 | 565 | pop af | |
| 446 | 566 | cp 1 | |
| 447 | - | jr z, .player1 | |
| 567 | + | jr z, .cc_is_p1 | |
| 448 | 568 | ld (hl), P2_ATTR | |
| 449 | 569 | ret | |
| 450 | - | .player1: | |
| 570 | + | .cc_is_p1: | |
| 451 | 571 | ld (hl), P1_ATTR | |
| 452 | 572 | ret | |
| 453 | 573 | | |
| 454 | 574 | ; ---------------------------------------------------------------------------- | |
| 455 | 575 | ; Sound - Claim | |
| 456 | 576 | ; ---------------------------------------------------------------------------- | |
| 457 | - | ; Short rising tone for successful claim | |
| 458 | 577 | | |
| 459 | 578 | sound_claim: | |
| 460 | - | ld hl, 400 ; Starting pitch | |
| 461 | - | ld b, 20 ; Duration | |
| 579 | + | ld hl, 400 | |
| 580 | + | ld b, 20 | |
| 462 | 581 | | |
| 463 | 582 | .loop: | |
| 464 | 583 | push bc | |
| 465 | 584 | push hl | |
| 466 | 585 | | |
| 467 | - | ; Generate tone | |
| 468 | 586 | ld b, h | |
| 469 | 587 | ld c, l | |
| 470 | 588 | .tone_loop: | |
| 471 | - | ld a, $10 ; Speaker bit on | |
| 589 | + | ld a, $10 | |
| 472 | 590 | out (KEY_PORT), a | |
| 473 | 591 | call .delay | |
| 474 | - | xor a ; Speaker bit off | |
| 592 | + | xor a | |
| 475 | 593 | out (KEY_PORT), a | |
| 476 | 594 | call .delay | |
| 477 | 595 | dec bc | |
| ... | |||
| 482 | 600 | pop hl | |
| 483 | 601 | pop bc | |
| 484 | 602 | | |
| 485 | - | ; Increase pitch (lower delay = higher frequency) | |
| 486 | 603 | ld de, 20 | |
| 487 | 604 | or a | |
| 488 | 605 | sbc hl, de | |
| ... | |||
| 505 | 622 | cursor_row: defb 0 | |
| 506 | 623 | cursor_col: defb 0 | |
| 507 | 624 | 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 | |
| 510 | 627 | | |
| 511 | 628 | ; ---------------------------------------------------------------------------- | |
| 512 | 629 | ; End |