Claiming Cells
Press Space to claim territory. Cells turn your colour. Take turns with a friend.
The board is ready. Now it’s time to claim it.
This unit adds the core gameplay: press Space to claim the cell under your cursor. The cell turns your colour. Then it’s the other player’s turn. Two players, one keyboard, taking turns until the board is full.
Run It
Assemble and run:
pasmonext --tapbas inkwar.asm inkwar.tap

The board looks the same as before - but now press Space.
The cell under your cursor turns red (bright red, actually). A short rising tone plays. Move the cursor and press Space again - now it turns blue. You’re Player 2.
Keep pressing Space. Red, blue, red, blue. The board fills up with colour.
Hand the keyboard to a friend. Now you’re playing Ink War.
Try This: Change Player Colours
Edit P1_ATTR and P2_ATTR to use different colours:
P1_ATTR equ %01100000 ; Yellow paper + BRIGHT
P2_ATTR equ %01011000 ; Cyan paper + BRIGHT
Remember the colour values:
- 0 = black, 1 = blue, 2 = red, 3 = magenta
- 4 = green, 5 = cyan, 6 = yellow, 7 = white
The PAPER bits are 5-3, so shift your colour left by 3. Yellow (6) becomes 110 in bits 5-3, plus BRIGHT (bit 6) = %01110000… wait, that’s white. Let me recalculate.
Actually: %01100000 = BRIGHT + paper 4 (green). For yellow: %01110000 = BRIGHT + paper 6. Try different combinations and see what looks good.
Try This: Change the Sound
Make the claim sound descending instead of ascending:
sound_claim:
ld hl, 100 ; Starting pitch (higher)
ld b, 20
.loop:
; ... same tone generation ...
; Decrease pitch (add to delay)
ld de, 20
add hl, de ; Changed from sbc to add
djnz .loop
ret
Or make it longer, shorter, different starting pitch. The beeper is primitive but expressive.
What You’ve Learnt
- Game state arrays -
defs 64, 0reserves 64 bytes initialised to zero - Turn-based logic - Track current player, switch after valid moves
- Input validation - Check cell state before allowing claims
- XOR toggling -
xor 3switches between 1 and 2 - Beeper sound - Toggle bit 4 of port $FE at varying speeds for pitch
What’s Next
In Unit 3, you’ll customise the game’s appearance - different colours, different cursor style, making it your own. You’ll learn exactly how attribute bytes work by changing them.
What Changed
| 1 | 1 | ; ============================================================================ | |
| 2 | - | ; INK WAR - Unit 1: Hello Spectrum | |
| 2 | + | ; INK WAR - Unit 2: Claiming Cells | |
| 3 | 3 | ; ============================================================================ | |
| 4 | - | ; A territory control game for the ZX Spectrum | |
| 5 | - | ; This scaffold provides: board display, cursor, keyboard movement | |
| 4 | + | ; Two players take turns claiming cells on the board. | |
| 5 | + | ; Press Space to claim the cell under the cursor. | |
| 6 | 6 | ; | |
| 7 | - | ; Controls: Q=Up, A=Down, O=Left, P=Right | |
| 7 | + | ; Controls: Q=Up, A=Down, O=Left, P=Right, SPACE=Claim | |
| 8 | 8 | ; ============================================================================ | |
| 9 | 9 | | |
| 10 | 10 | org 32768 | |
| ... | |||
| 22 | 22 | BORDER_ATTR equ %00000000 ; Black on black (border) | |
| 23 | 23 | EMPTY_ATTR equ %00111000 ; White paper, black ink (empty cell) | |
| 24 | 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) | |
| 25 | 29 | | |
| 26 | 30 | ; Keyboard ports (active low) | |
| 27 | 31 | KEY_PORT equ $fe | |
| 28 | - | ROW_QAOP equ $fb ; Q W E R T row (bits: T R E W Q) | |
| 29 | - | ROW_ASDF equ $fd ; A S D F G row (bits: G F D S A) | |
| 30 | - | ROW_YUIOP equ $df ; Y U I O P row (bits: P O I U Y) | |
| 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 | |
| 36 | + | | |
| 37 | + | ; Game states | |
| 38 | + | STATE_EMPTY equ 0 | |
| 39 | + | STATE_P1 equ 1 | |
| 40 | + | STATE_P2 equ 2 | |
| 31 | 41 | | |
| 32 | 42 | ; ---------------------------------------------------------------------------- | |
| 33 | 43 | ; Entry Point | |
| ... | |||
| 35 | 45 | | |
| 36 | 46 | start: | |
| 37 | 47 | call init_screen ; Clear screen and set border | |
| 48 | + | call init_game ; Initialise game state | |
| 38 | 49 | call draw_board ; Draw the game board | |
| 39 | 50 | call draw_cursor ; Show cursor at starting position | |
| 40 | 51 | | |
| ... | |||
| 42 | 53 | halt ; Wait for frame (50Hz timing) | |
| 43 | 54 | | |
| 44 | 55 | call read_keyboard ; Check for input | |
| 45 | - | call move_cursor ; Update cursor if moved | |
| 56 | + | call handle_input ; Process movement or claim | |
| 46 | 57 | | |
| 47 | 58 | jp main_loop ; Repeat forever | |
| 48 | 59 | | |
| 49 | 60 | ; ---------------------------------------------------------------------------- | |
| 50 | 61 | ; Initialise Screen | |
| 51 | 62 | ; ---------------------------------------------------------------------------- | |
| 52 | - | ; Clears the screen to black and sets border colour | |
| 53 | 63 | | |
| 54 | 64 | init_screen: | |
| 55 | - | ; Set border to black | |
| 56 | - | xor a ; A = 0 (black) | |
| 57 | - | out (KEY_PORT), a ; Set border colour | |
| 65 | + | xor a | |
| 66 | + | out (KEY_PORT), a ; Black border | |
| 58 | 67 | | |
| 59 | - | ; Clear attributes to black | |
| 60 | - | ld hl, ATTR_BASE ; Start of attributes | |
| 68 | + | ld hl, ATTR_BASE | |
| 61 | 69 | ld de, ATTR_BASE+1 | |
| 62 | - | ld bc, 767 ; 768 bytes - 1 | |
| 63 | - | ld (hl), 0 ; Black on black | |
| 64 | - | ldir ; Fill all attributes | |
| 70 | + | ld bc, 767 | |
| 71 | + | ld (hl), 0 | |
| 72 | + | ldir | |
| 73 | + | | |
| 74 | + | ret | |
| 75 | + | | |
| 76 | + | ; ---------------------------------------------------------------------------- | |
| 77 | + | ; Initialise Game State | |
| 78 | + | ; ---------------------------------------------------------------------------- | |
| 79 | + | | |
| 80 | + | init_game: | |
| 81 | + | ; Clear board state (all empty) | |
| 82 | + | ld hl, board_state | |
| 83 | + | ld b, 64 | |
| 84 | + | xor a | |
| 85 | + | .clear_loop: | |
| 86 | + | ld (hl), a | |
| 87 | + | inc hl | |
| 88 | + | djnz .clear_loop | |
| 89 | + | | |
| 90 | + | ; Player 1 starts | |
| 91 | + | ld a, 1 | |
| 92 | + | ld (current_player), a | |
| 93 | + | | |
| 94 | + | ; Cursor at top-left | |
| 95 | + | xor a | |
| 96 | + | ld (cursor_row), a | |
| 97 | + | ld (cursor_col), a | |
| 65 | 98 | | |
| 66 | 99 | ret | |
| 67 | 100 | | |
| 68 | 101 | ; ---------------------------------------------------------------------------- | |
| 69 | 102 | ; Draw Board | |
| 70 | 103 | ; ---------------------------------------------------------------------------- | |
| 71 | - | ; Draws the 8x8 game board with border | |
| 72 | 104 | | |
| 73 | 105 | draw_board: | |
| 74 | - | ; Draw border (10x10 area, black) | |
| 75 | - | ; Border is already black from init, so we just draw the cells | |
| 76 | - | | |
| 77 | - | ; Draw the 8x8 playing field (white cells) | |
| 78 | - | ld b, BOARD_SIZE ; 8 rows | |
| 79 | - | ld c, BOARD_ROW ; Start at row 8 | |
| 106 | + | ld b, BOARD_SIZE | |
| 107 | + | ld c, BOARD_ROW | |
| 80 | 108 | | |
| 81 | 109 | .row_loop: | |
| 82 | 110 | push bc | |
| 83 | 111 | | |
| 84 | - | ld b, BOARD_SIZE ; 8 columns | |
| 85 | - | ld d, BOARD_COL ; Start at column 12 | |
| 112 | + | ld b, BOARD_SIZE | |
| 113 | + | ld d, BOARD_COL | |
| 86 | 114 | | |
| 87 | 115 | .col_loop: | |
| 88 | 116 | push bc | |
| 89 | 117 | | |
| 90 | - | ; Calculate attribute address: ATTR_BASE + row*32 + col | |
| 91 | - | ld a, c ; Row | |
| 118 | + | ; Calculate attribute address | |
| 119 | + | ld a, c | |
| 92 | 120 | ld l, a | |
| 93 | 121 | ld h, 0 | |
| 94 | - | add hl, hl ; *2 | |
| 95 | - | add hl, hl ; *4 | |
| 96 | - | add hl, hl ; *8 | |
| 97 | - | add hl, hl ; *16 | |
| 98 | - | add hl, hl ; *32 | |
| 99 | - | ld a, d ; Column | |
| 122 | + | add hl, hl | |
| 123 | + | add hl, hl | |
| 124 | + | add hl, hl | |
| 125 | + | add hl, hl | |
| 126 | + | add hl, hl | |
| 127 | + | ld a, d | |
| 100 | 128 | add a, l | |
| 101 | 129 | ld l, a | |
| 102 | 130 | ld bc, ATTR_BASE | |
| 103 | - | add hl, bc ; HL = attribute address | |
| 131 | + | add hl, bc | |
| 104 | 132 | | |
| 105 | - | ld (hl), EMPTY_ATTR ; Set to white (empty cell) | |
| 133 | + | ld (hl), EMPTY_ATTR | |
| 106 | 134 | | |
| 107 | 135 | pop bc | |
| 108 | - | inc d ; Next column | |
| 136 | + | inc d | |
| 109 | 137 | djnz .col_loop | |
| 110 | 138 | | |
| 111 | 139 | pop bc | |
| 112 | - | inc c ; Next row | |
| 140 | + | inc c | |
| 113 | 141 | djnz .row_loop | |
| 114 | 142 | | |
| 115 | 143 | ret | |
| ... | |||
| 117 | 145 | ; ---------------------------------------------------------------------------- | |
| 118 | 146 | ; Draw Cursor | |
| 119 | 147 | ; ---------------------------------------------------------------------------- | |
| 120 | - | ; Shows the cursor at current position with FLASH attribute | |
| 148 | + | ; Shows cursor with appropriate colour based on cell state | |
| 121 | 149 | | |
| 122 | 150 | draw_cursor: | |
| 123 | - | ; Calculate attribute address for cursor position | |
| 151 | + | ; Get cell state at cursor position | |
| 152 | + | call get_cell_state ; A = state at cursor | |
| 153 | + | | |
| 154 | + | ; Determine cursor attribute based on state | |
| 155 | + | cp STATE_P1 | |
| 156 | + | jr z, .dc_p1 | |
| 157 | + | cp STATE_P2 | |
| 158 | + | jr z, .dc_p2 | |
| 159 | + | | |
| 160 | + | ; Empty cell - use standard cursor | |
| 161 | + | ld a, CURSOR_ATTR | |
| 162 | + | jr .dc_set | |
| 163 | + | | |
| 164 | + | .dc_p1: | |
| 165 | + | ld a, P1_CURSOR | |
| 166 | + | jr .dc_set | |
| 167 | + | | |
| 168 | + | .dc_p2: | |
| 169 | + | ld a, P2_CURSOR | |
| 170 | + | | |
| 171 | + | .dc_set: | |
| 172 | + | push af ; Save attribute | |
| 173 | + | | |
| 174 | + | ; Calculate attribute address | |
| 124 | 175 | ld a, (cursor_row) | |
| 125 | - | add a, BOARD_ROW ; Add board offset | |
| 176 | + | add a, BOARD_ROW | |
| 126 | 177 | ld l, a | |
| 127 | 178 | ld h, 0 | |
| 128 | - | add hl, hl ; *32 | |
| 179 | + | add hl, hl | |
| 129 | 180 | add hl, hl | |
| 130 | 181 | add hl, hl | |
| 131 | 182 | add hl, hl | |
| 132 | 183 | add hl, hl | |
| 133 | 184 | ld a, (cursor_col) | |
| 134 | - | add a, BOARD_COL ; Add board offset | |
| 185 | + | add a, BOARD_COL | |
| 135 | 186 | add a, l | |
| 136 | 187 | ld l, a | |
| 137 | 188 | ld bc, ATTR_BASE | |
| 138 | 189 | add hl, bc | |
| 139 | 190 | | |
| 140 | - | ld (hl), CURSOR_ATTR ; Set FLASH attribute | |
| 191 | + | pop af ; Restore attribute | |
| 192 | + | ld (hl), a | |
| 141 | 193 | | |
| 142 | 194 | ret | |
| 143 | 195 | | |
| 144 | 196 | ; ---------------------------------------------------------------------------- | |
| 145 | 197 | ; Clear Cursor | |
| 146 | 198 | ; ---------------------------------------------------------------------------- | |
| 147 | - | ; Removes cursor flash from current position | |
| 199 | + | ; Restores cell to its proper colour (empty, P1, or P2) | |
| 148 | 200 | | |
| 149 | 201 | clear_cursor: | |
| 150 | - | ; Calculate attribute address for cursor position | |
| 202 | + | ; Get cell state | |
| 203 | + | call get_cell_state | |
| 204 | + | | |
| 205 | + | ; Determine attribute based on state | |
| 206 | + | cp STATE_P1 | |
| 207 | + | jr z, .cc_p1 | |
| 208 | + | cp STATE_P2 | |
| 209 | + | jr z, .cc_p2 | |
| 210 | + | | |
| 211 | + | ; Empty cell | |
| 212 | + | ld a, EMPTY_ATTR | |
| 213 | + | jr .cc_set | |
| 214 | + | | |
| 215 | + | .cc_p1: | |
| 216 | + | ld a, P1_ATTR | |
| 217 | + | jr .cc_set | |
| 218 | + | | |
| 219 | + | .cc_p2: | |
| 220 | + | ld a, P2_ATTR | |
| 221 | + | | |
| 222 | + | .cc_set: | |
| 223 | + | push af | |
| 224 | + | | |
| 225 | + | ; Calculate attribute address | |
| 151 | 226 | ld a, (cursor_row) | |
| 152 | 227 | add a, BOARD_ROW | |
| 153 | 228 | ld l, a | |
| ... | |||
| 164 | 239 | ld bc, ATTR_BASE | |
| 165 | 240 | add hl, bc | |
| 166 | 241 | | |
| 167 | - | ld (hl), EMPTY_ATTR ; Remove FLASH, back to white | |
| 242 | + | pop af | |
| 243 | + | ld (hl), a | |
| 244 | + | | |
| 245 | + | ret | |
| 246 | + | | |
| 247 | + | ; ---------------------------------------------------------------------------- | |
| 248 | + | ; Get Cell State | |
| 249 | + | ; ---------------------------------------------------------------------------- | |
| 250 | + | ; Returns: A = state at cursor position (0=empty, 1=P1, 2=P2) | |
| 168 | 251 | | |
| 252 | + | get_cell_state: | |
| 253 | + | ld a, (cursor_row) | |
| 254 | + | add a, a ; *2 | |
| 255 | + | add a, a ; *4 | |
| 256 | + | add a, a ; *8 | |
| 257 | + | ld hl, board_state | |
| 258 | + | ld b, 0 | |
| 259 | + | ld c, a | |
| 260 | + | add hl, bc | |
| 261 | + | ld a, (cursor_col) | |
| 262 | + | ld c, a | |
| 263 | + | add hl, bc ; HL = &board_state[row*8+col] | |
| 264 | + | ld a, (hl) | |
| 169 | 265 | ret | |
| 170 | 266 | | |
| 171 | 267 | ; ---------------------------------------------------------------------------- | |
| 172 | 268 | ; Read Keyboard | |
| 173 | 269 | ; ---------------------------------------------------------------------------- | |
| 174 | - | ; Checks Q/A/O/P keys and sets direction flags | |
| 175 | 270 | | |
| 176 | 271 | read_keyboard: | |
| 177 | 272 | xor a | |
| 178 | - | ld (key_pressed), a ; Clear previous | |
| 273 | + | ld (key_pressed), a | |
| 179 | 274 | | |
| 180 | - | ; Check Q (up) - port $FBFE, bit 0 | |
| 275 | + | ; Check Q (up) | |
| 181 | 276 | ld a, ROW_QAOP | |
| 182 | 277 | in a, (KEY_PORT) | |
| 183 | - | bit 0, a ; Q is bit 0 | |
| 278 | + | bit 0, a | |
| 184 | 279 | jr nz, .not_q | |
| 185 | - | ld a, 1 ; Up | |
| 280 | + | ld a, 1 | |
| 186 | 281 | ld (key_pressed), a | |
| 187 | 282 | ret | |
| 188 | 283 | .not_q: | |
| 189 | - | ; Check A (down) - port $FDFE, bit 0 | |
| 284 | + | ; Check A (down) | |
| 190 | 285 | ld a, ROW_ASDF | |
| 191 | 286 | in a, (KEY_PORT) | |
| 192 | - | bit 0, a ; A is bit 0 | |
| 287 | + | bit 0, a | |
| 193 | 288 | jr nz, .not_a | |
| 194 | - | ld a, 2 ; Down | |
| 289 | + | ld a, 2 | |
| 195 | 290 | ld (key_pressed), a | |
| 196 | 291 | ret | |
| 197 | 292 | .not_a: | |
| 198 | - | ; Check O (left) - port $DFFE, bit 1 | |
| 293 | + | ; Check O (left) | |
| 199 | 294 | ld a, ROW_YUIOP | |
| 200 | 295 | in a, (KEY_PORT) | |
| 201 | - | bit 1, a ; O is bit 1 | |
| 296 | + | bit 1, a | |
| 202 | 297 | jr nz, .not_o | |
| 203 | - | ld a, 3 ; Left | |
| 298 | + | ld a, 3 | |
| 204 | 299 | ld (key_pressed), a | |
| 205 | 300 | ret | |
| 206 | 301 | .not_o: | |
| 207 | - | ; Check P (right) - port $DFFE, bit 0 | |
| 302 | + | ; Check P (right) | |
| 208 | 303 | ld a, ROW_YUIOP | |
| 209 | 304 | in a, (KEY_PORT) | |
| 210 | - | bit 0, a ; P is bit 0 | |
| 305 | + | bit 0, a | |
| 211 | 306 | jr nz, .not_p | |
| 212 | - | ld a, 4 ; Right | |
| 307 | + | ld a, 4 | |
| 213 | 308 | ld (key_pressed), a | |
| 309 | + | ret | |
| 214 | 310 | .not_p: | |
| 311 | + | ; Check SPACE (claim) | |
| 312 | + | ld a, ROW_SPACE | |
| 313 | + | in a, (KEY_PORT) | |
| 314 | + | bit 0, a | |
| 315 | + | jr nz, .not_space | |
| 316 | + | ld a, 5 | |
| 317 | + | ld (key_pressed), a | |
| 318 | + | .not_space: | |
| 215 | 319 | ret | |
| 216 | 320 | | |
| 217 | 321 | ; ---------------------------------------------------------------------------- | |
| 218 | - | ; Move Cursor | |
| 322 | + | ; Handle Input | |
| 219 | 323 | ; ---------------------------------------------------------------------------- | |
| 220 | - | ; Moves cursor based on key_pressed value | |
| 221 | 324 | | |
| 222 | - | move_cursor: | |
| 325 | + | handle_input: | |
| 223 | 326 | ld a, (key_pressed) | |
| 224 | 327 | or a | |
| 225 | - | ret z ; No key pressed | |
| 328 | + | ret z ; No key | |
| 226 | 329 | | |
| 227 | - | call clear_cursor ; Remove old cursor | |
| 330 | + | cp 5 ; Space? | |
| 331 | + | jr z, try_claim | |
| 332 | + | | |
| 333 | + | ; Movement key - use existing move logic | |
| 334 | + | call clear_cursor | |
| 228 | 335 | | |
| 229 | 336 | ld a, (key_pressed) | |
| 230 | 337 | | |
| ... | |||
| 232 | 339 | jr nz, .not_up | |
| 233 | 340 | ld a, (cursor_row) | |
| 234 | 341 | or a | |
| 235 | - | jr z, .done ; Already at top | |
| 342 | + | jr z, .done | |
| 236 | 343 | dec a | |
| 237 | 344 | ld (cursor_row), a | |
| 238 | 345 | jr .done | |
| ... | |||
| 241 | 348 | jr nz, .not_down | |
| 242 | 349 | ld a, (cursor_row) | |
| 243 | 350 | cp BOARD_SIZE-1 | |
| 244 | - | jr z, .done ; Already at bottom | |
| 351 | + | jr z, .done | |
| 245 | 352 | inc a | |
| 246 | 353 | ld (cursor_row), a | |
| 247 | 354 | jr .done | |
| ... | |||
| 250 | 357 | jr nz, .not_left | |
| 251 | 358 | ld a, (cursor_col) | |
| 252 | 359 | or a | |
| 253 | - | jr z, .done ; Already at left | |
| 360 | + | jr z, .done | |
| 254 | 361 | dec a | |
| 255 | 362 | ld (cursor_col), a | |
| 256 | 363 | jr .done | |
| ... | |||
| 259 | 366 | jr nz, .done | |
| 260 | 367 | ld a, (cursor_col) | |
| 261 | 368 | cp BOARD_SIZE-1 | |
| 262 | - | jr z, .done ; Already at right | |
| 369 | + | jr z, .done | |
| 263 | 370 | inc a | |
| 264 | 371 | ld (cursor_col), a | |
| 265 | 372 | | |
| 266 | 373 | .done: | |
| 267 | - | call draw_cursor ; Draw new cursor | |
| 374 | + | call draw_cursor | |
| 375 | + | ret | |
| 376 | + | | |
| 377 | + | ; ---------------------------------------------------------------------------- | |
| 378 | + | ; Try Claim Cell | |
| 379 | + | ; ---------------------------------------------------------------------------- | |
| 380 | + | | |
| 381 | + | try_claim: | |
| 382 | + | ; Check if cell is empty | |
| 383 | + | call get_cell_state | |
| 384 | + | or a | |
| 385 | + | ret nz ; Not empty - can't claim | |
| 386 | + | | |
| 387 | + | ; Claim the cell | |
| 388 | + | call claim_cell | |
| 389 | + | | |
| 390 | + | ; Play success sound | |
| 391 | + | call sound_claim | |
| 392 | + | | |
| 393 | + | ; Switch player | |
| 394 | + | ld a, (current_player) | |
| 395 | + | xor 3 ; Toggle between 1 and 2 | |
| 396 | + | ld (current_player), a | |
| 397 | + | | |
| 398 | + | ; Redraw cursor with new state | |
| 399 | + | call draw_cursor | |
| 400 | + | | |
| 401 | + | ret | |
| 402 | + | | |
| 403 | + | ; ---------------------------------------------------------------------------- | |
| 404 | + | ; Claim Cell | |
| 405 | + | ; ---------------------------------------------------------------------------- | |
| 406 | + | ; Claims current cell for current player | |
| 407 | + | | |
| 408 | + | claim_cell: | |
| 409 | + | ; Calculate board state index | |
| 410 | + | ld a, (cursor_row) | |
| 411 | + | add a, a | |
| 412 | + | add a, a | |
| 413 | + | add a, a | |
| 414 | + | ld hl, board_state | |
| 415 | + | ld b, 0 | |
| 416 | + | ld c, a | |
| 417 | + | add hl, bc | |
| 418 | + | ld a, (cursor_col) | |
| 419 | + | ld c, a | |
| 420 | + | add hl, bc ; HL = &board_state[row*8+col] | |
| 421 | + | | |
| 422 | + | ; Set to current player | |
| 423 | + | ld a, (current_player) | |
| 424 | + | ld (hl), a | |
| 425 | + | | |
| 426 | + | ; Update attribute colour | |
| 427 | + | push af | |
| 428 | + | | |
| 429 | + | ld a, (cursor_row) | |
| 430 | + | add a, BOARD_ROW | |
| 431 | + | ld l, a | |
| 432 | + | ld h, 0 | |
| 433 | + | add hl, hl | |
| 434 | + | add hl, hl | |
| 435 | + | add hl, hl | |
| 436 | + | add hl, hl | |
| 437 | + | add hl, hl | |
| 438 | + | ld a, (cursor_col) | |
| 439 | + | add a, BOARD_COL | |
| 440 | + | add a, l | |
| 441 | + | ld l, a | |
| 442 | + | ld bc, ATTR_BASE | |
| 443 | + | add hl, bc | |
| 444 | + | | |
| 445 | + | pop af | |
| 446 | + | cp 1 | |
| 447 | + | jr z, .player1 | |
| 448 | + | ld (hl), P2_ATTR | |
| 449 | + | ret | |
| 450 | + | .player1: | |
| 451 | + | ld (hl), P1_ATTR | |
| 452 | + | ret | |
| 453 | + | | |
| 454 | + | ; ---------------------------------------------------------------------------- | |
| 455 | + | ; Sound - Claim | |
| 456 | + | ; ---------------------------------------------------------------------------- | |
| 457 | + | ; Short rising tone for successful claim | |
| 458 | + | | |
| 459 | + | sound_claim: | |
| 460 | + | ld hl, 400 ; Starting pitch | |
| 461 | + | ld b, 20 ; Duration | |
| 462 | + | | |
| 463 | + | .loop: | |
| 464 | + | push bc | |
| 465 | + | push hl | |
| 466 | + | | |
| 467 | + | ; Generate tone | |
| 468 | + | ld b, h | |
| 469 | + | ld c, l | |
| 470 | + | .tone_loop: | |
| 471 | + | ld a, $10 ; Speaker bit on | |
| 472 | + | out (KEY_PORT), a | |
| 473 | + | call .delay | |
| 474 | + | xor a ; Speaker bit off | |
| 475 | + | out (KEY_PORT), a | |
| 476 | + | call .delay | |
| 477 | + | dec bc | |
| 478 | + | ld a, b | |
| 479 | + | or c | |
| 480 | + | jr nz, .tone_loop | |
| 481 | + | | |
| 482 | + | pop hl | |
| 483 | + | pop bc | |
| 484 | + | | |
| 485 | + | ; Increase pitch (lower delay = higher frequency) | |
| 486 | + | ld de, 20 | |
| 487 | + | or a | |
| 488 | + | sbc hl, de | |
| 489 | + | | |
| 490 | + | djnz .loop | |
| 491 | + | ret | |
| 492 | + | | |
| 493 | + | .delay: | |
| 494 | + | push bc | |
| 495 | + | ld b, 5 | |
| 496 | + | .delay_loop: | |
| 497 | + | djnz .delay_loop | |
| 498 | + | pop bc | |
| 268 | 499 | ret | |
| 269 | 500 | | |
| 270 | 501 | ; ---------------------------------------------------------------------------- | |
| 271 | 502 | ; Variables | |
| 272 | 503 | ; ---------------------------------------------------------------------------- | |
| 273 | 504 | | |
| 274 | - | cursor_row: defb 0 ; Cursor row (0-7) | |
| 275 | - | cursor_col: defb 0 ; Cursor column (0-7) | |
| 276 | - | key_pressed: defb 0 ; Last key: 0=none, 1=up, 2=down, 3=left, 4=right | |
| 505 | + | cursor_row: defb 0 | |
| 506 | + | cursor_col: defb 0 | |
| 507 | + | 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 | |
| 277 | 510 | | |
| 278 | 511 | ; ---------------------------------------------------------------------------- | |
| 279 | 512 | ; End |
We added three things:
- Board state - An array tracking who owns each cell
- Claiming - Press Space to take ownership
- Turn switching - Players alternate automatically
Player Colours
Each player gets a distinct colour:
; Attribute colours (FBPPPIII format)
EMPTY_ATTR equ %00111000 ; White paper, black ink (empty cell)
CURSOR_ATTR equ %10111000 ; White paper, black ink + FLASH
P1_ATTR equ %01010000 ; Red paper, black ink + BRIGHT (Player 1)
P2_ATTR equ %01001000 ; Blue paper, black ink + BRIGHT (Player 2)
P1_CURSOR equ %11010000 ; Red paper + FLASH (Player 1 cursor on own cell)
P2_CURSOR equ %11001000 ; Blue paper + FLASH (Player 2 cursor on own cell)
Player 1 is red (%01010000), Player 2 is blue (%01001000). Both use BRIGHT for vivid colours that stand out from the white empty cells.
The cursor attributes (P1_CURSOR, P2_CURSOR) combine the player colour with FLASH, so when you move onto your own cell, it still flashes.
Game State
We need to remember who owns each cell:
; Game states
STATE_EMPTY equ 0
STATE_P1 equ 1
STATE_P2 equ 2
; ----------------------------------------------------------------------------
; Variables
; ----------------------------------------------------------------------------
cursor_row: defb 0
cursor_col: defb 0
key_pressed: defb 0
current_player: defb 1 ; 1 = Player 1 (Red), 2 = Player 2 (Blue)
board_state: defs 64, 0 ; 64 bytes, all initialised to 0
The board_state array has 64 bytes - one per cell. Each byte is:
0= empty1= Player 1 (red)2= Player 2 (blue)
The current_player variable tracks whose turn it is, starting with Player 1.
Initialisation
At startup, we clear the board and set Player 1 to go first:
; ----------------------------------------------------------------------------
; Initialise Game State
; ----------------------------------------------------------------------------
init_game:
; Clear board state (all empty)
ld hl, board_state
ld b, 64
xor a
.clear_loop:
ld (hl), a
inc hl
djnz .clear_loop
; Player 1 starts
ld a, 1
ld (current_player), a
; Cursor at top-left
xor a
ld (cursor_row), a
ld (cursor_col), a
ret
The djnz loop writes zero to all 64 cells. Simple and fast.
Trying to Claim
When you press Space, we check if the claim is valid:
; ----------------------------------------------------------------------------
; Try Claim Cell
; ----------------------------------------------------------------------------
try_claim:
; Check if cell is empty
call get_cell_state
or a
ret nz ; Not empty - can't claim
; Claim the cell
call claim_cell
; Play success sound
call sound_claim
; Switch player
ld a, (current_player)
xor 3 ; Toggle between 1 and 2
ld (current_player), a
; Redraw cursor with new state
call draw_cursor
ret
If the cell isn’t empty (or a / ret nz), we ignore the press. No stealing opponent cells - not yet, anyway.
The player switch uses a clever trick: xor 3 toggles between 1 and 2. Binary 01 XOR 11 = 10, and 10 XOR 11 = 01.
The Claim Itself
; ----------------------------------------------------------------------------
; Claim Cell
; ----------------------------------------------------------------------------
; Claims current cell for current player
claim_cell:
; Calculate board state index
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 ; HL = &board_state[row*8+col]
; Set to current player
ld a, (current_player)
ld (hl), a
; Update attribute colour
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, .player1
ld (hl), P2_ATTR
ret
.player1:
ld (hl), P1_ATTR
ret
Two things happen:
- We store the player number in
board_state - We change the attribute colour on screen
The array index is row * 8 + col. We calculate it using shifts: add a, a three times multiplies by 8.
Sound Feedback
A claim should feel satisfying:
; ----------------------------------------------------------------------------
; Sound - Claim
; ----------------------------------------------------------------------------
; Short rising tone for successful claim
sound_claim:
ld hl, 400 ; Starting pitch
ld b, 20 ; Duration
.loop:
push bc
push hl
; Generate tone
ld b, h
ld c, l
.tone_loop:
ld a, $10 ; Speaker bit on
out (KEY_PORT), a
call .delay
xor a ; Speaker bit off
out (KEY_PORT), a
call .delay
dec bc
ld a, b
or c
jr nz, .tone_loop
pop hl
pop bc
; Increase pitch (lower delay = higher frequency)
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
This generates a rising tone by decreasing the delay between speaker toggles. The Spectrum’s beeper is just a single bit - toggle it fast for high pitch, slow for low pitch.
The speaker is bit 4 of port $FE (the same port as border colour). We alternate between $10 (speaker on) and $00 (speaker off).
The Complete Code
; ============================================================================
; INK WAR - Unit 2: Claiming Cells
; ============================================================================
; Two players take turns claiming cells on the board.
; Press Space to claim the cell under the cursor.
;
; 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
; Attribute colours (FBPPPIII format)
BORDER_ATTR equ %00000000 ; Black on black (border)
EMPTY_ATTR equ %00111000 ; White paper, black ink (empty cell)
CURSOR_ATTR equ %10111000 ; White paper, black ink + FLASH
P1_ATTR equ %01010000 ; Red paper, black ink + BRIGHT (Player 1)
P2_ATTR equ %01001000 ; Blue paper, black ink + BRIGHT (Player 2)
P1_CURSOR equ %11010000 ; Red paper + FLASH (Player 1 cursor on own cell)
P2_CURSOR equ %11001000 ; Blue paper + FLASH (Player 2 cursor on own cell)
; Keyboard ports (active low)
KEY_PORT equ $fe
ROW_QAOP equ $fb ; Q W E R T row
ROW_ASDF equ $fd ; A S D F G row
ROW_YUIOP equ $df ; Y U I O P row
ROW_SPACE equ $7f ; SPACE SYM M N B row
; Game states
STATE_EMPTY equ 0
STATE_P1 equ 1
STATE_P2 equ 2
; ----------------------------------------------------------------------------
; Entry Point
; ----------------------------------------------------------------------------
start:
call init_screen ; Clear screen and set border
call init_game ; Initialise game state
call draw_board ; Draw the game board
call draw_cursor ; Show cursor at starting position
main_loop:
halt ; Wait for frame (50Hz timing)
call read_keyboard ; Check for input
call handle_input ; Process movement or claim
jp main_loop ; Repeat forever
; ----------------------------------------------------------------------------
; Initialise Screen
; ----------------------------------------------------------------------------
init_screen:
xor a
out (KEY_PORT), a ; Black border
ld hl, ATTR_BASE
ld de, ATTR_BASE+1
ld bc, 767
ld (hl), 0
ldir
ret
; ----------------------------------------------------------------------------
; Initialise Game State
; ----------------------------------------------------------------------------
init_game:
; Clear board state (all empty)
ld hl, board_state
ld b, 64
xor a
.clear_loop:
ld (hl), a
inc hl
djnz .clear_loop
; Player 1 starts
ld a, 1
ld (current_player), a
; Cursor at top-left
xor a
ld (cursor_row), a
ld (cursor_col), a
ret
; ----------------------------------------------------------------------------
; Draw Board
; ----------------------------------------------------------------------------
draw_board:
ld b, BOARD_SIZE
ld c, BOARD_ROW
.row_loop:
push bc
ld b, BOARD_SIZE
ld d, BOARD_COL
.col_loop:
push bc
; 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), EMPTY_ATTR
pop bc
inc d
djnz .col_loop
pop bc
inc c
djnz .row_loop
ret
; ----------------------------------------------------------------------------
; Draw Cursor
; ----------------------------------------------------------------------------
; Shows cursor with appropriate colour based on cell state
draw_cursor:
; Get cell state at cursor position
call get_cell_state ; A = state at cursor
; Determine cursor attribute based on state
cp STATE_P1
jr z, .dc_p1
cp STATE_P2
jr z, .dc_p2
; Empty cell - use standard cursor
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 ; Save attribute
; Calculate attribute address
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 ; Restore attribute
ld (hl), a
ret
; ----------------------------------------------------------------------------
; Clear Cursor
; ----------------------------------------------------------------------------
; Restores cell to its proper colour (empty, P1, or P2)
clear_cursor:
; Get cell state
call get_cell_state
; Determine attribute based on state
cp STATE_P1
jr z, .cc_p1
cp STATE_P2
jr z, .cc_p2
; Empty cell
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
; Calculate attribute address
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
; ----------------------------------------------------------------------------
; Returns: A = state at cursor position (0=empty, 1=P1, 2=P2)
get_cell_state:
ld a, (cursor_row)
add a, a ; *2
add a, a ; *4
add a, a ; *8
ld hl, board_state
ld b, 0
ld c, a
add hl, bc
ld a, (cursor_col)
ld c, a
add hl, bc ; HL = &board_state[row*8+col]
ld a, (hl)
ret
; ----------------------------------------------------------------------------
; Read Keyboard
; ----------------------------------------------------------------------------
read_keyboard:
xor a
ld (key_pressed), a
; Check Q (up)
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:
; Check A (down)
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:
; Check O (left)
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:
; Check P (right)
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:
; Check SPACE (claim)
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 ; No key
cp 5 ; Space?
jr z, try_claim
; Movement key - use existing move logic
call clear_cursor
ld a, (key_pressed)
cp 1 ; Up?
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 ; Down?
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 ; Left?
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 ; Right?
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:
; Check if cell is empty
call get_cell_state
or a
ret nz ; Not empty - can't claim
; Claim the cell
call claim_cell
; Play success sound
call sound_claim
; Switch player
ld a, (current_player)
xor 3 ; Toggle between 1 and 2
ld (current_player), a
; Redraw cursor with new state
call draw_cursor
ret
; ----------------------------------------------------------------------------
; Claim Cell
; ----------------------------------------------------------------------------
; Claims current cell for current player
claim_cell:
; Calculate board state index
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 ; HL = &board_state[row*8+col]
; Set to current player
ld a, (current_player)
ld (hl), a
; Update attribute colour
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, .player1
ld (hl), P2_ATTR
ret
.player1:
ld (hl), P1_ATTR
ret
; ----------------------------------------------------------------------------
; Sound - Claim
; ----------------------------------------------------------------------------
; Short rising tone for successful claim
sound_claim:
ld hl, 400 ; Starting pitch
ld b, 20 ; Duration
.loop:
push bc
push hl
; Generate tone
ld b, h
ld c, l
.tone_loop:
ld a, $10 ; Speaker bit on
out (KEY_PORT), a
call .delay
xor a ; Speaker bit off
out (KEY_PORT), a
call .delay
dec bc
ld a, b
or c
jr nz, .tone_loop
pop hl
pop bc
; Increase pitch (lower delay = higher frequency)
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 ; 1 = Player 1 (Red), 2 = Player 2 (Blue)
board_state: defs 64, 0 ; 64 bytes, all initialised to 0
; ----------------------------------------------------------------------------
; End
; ----------------------------------------------------------------------------
end start