Skip to content
Game 2 Unit 14 of 16 1 hr learning time

The Keep's Gold

Give the keep a goal. Scatter gold through the chambers as walkable map cells, lift it on contact — map cell to floor, a chime, a counter down — and declare the keep won when the last coin is gone. The world becomes a game.

88% of Shadowkeep

You can walk the keep, light it, hear it — but you can't win it, because there's nothing to win. A place becomes a game the moment it has a goal. This unit gives the keep one, and it costs almost no new engine: gold scattered through the rooms, lifted by the move you already make, and a win when the last coin is gone.

What you'll see by the end

The Hall with two coins of gold resting on the floor — one high on the left, one low on the right — round and yellow, plainly not torch-flame; the red-cloaked thief stands in the central light, the chambers waiting to be cleared.
Gold resting on the stone — two coins here in the Hall, more through the other chambers. Walk onto one and it's yours: a bright chime, and the counter ticks down. Clear all six and the keep is won.

Gold, resting on the stone — round yellow coins, two here in the Hall and more through the other chambers. Walk onto one and it's yours: it vanishes with a bright little chime and a counter ticks down. Clear all six and the keep is won — a victory flourish, and the thief stands still in a keep emptied of its treasure. It's a small goal, but it turns wandering into playing.

Gold is just a tile that isn't a wall

The whole trick is that gold lives in the room map like everything else — a G cell — and its attribute has no BRIGHT bit, so the collision test we've used since Unit 4 already treats it as floor you can walk onto:

GOLD_ATTR   equ     %00000110         ; yellow ink, no BRIGHT -> walkable
            defb    'G'
            defw    gold_tile
            defb    GOLD_ATTR

Drop Gs into the maps and they draw as coins and let you stand on them. No new collision rule, no separate list of pickups — the map is the level, gold included.

Lifting it

When the thief steps onto a new cell, we ask one question of the map: is it gold? If so, we turn that map cell into floor — permanently, so the gold is gone for good — repaint the cell, chime, and tick the counter down. At zero, the keep is won:

collect_gold:
            call    cell_state_addr  ; the map cell we just stepped onto
            ld      a, (hl)
            cp      'G'
            ret     nz               ; not gold — nothing to do
            ld      (hl), '.'         ; taken: floor from now on
            call    pos_bc
            call    draw_floor_cell  ; repaint, so what's "under" us is floor
            call    sfx_pickup       ; the bright chime
            ld      hl, gold_remaining
            dec     (hl)
            ld      a, (hl)
            or      a
            call    z, win           ; last coin? the keep is won
            ret

It slots into the move between updating the thief's position and saving what's under him — so the "under" we save is the new floor, not the coin, and the gold never comes back when we walk away:

            ld      a, (trow)
            ld      (thief_row), a
            call    collect_gold     ; <- lift it before saving "under"
            call    save_under
            call    draw_thief

Because we edit the room's state (the working copy), a collected coin stays collected even after you leave the room and return. The keep remembers.

Winning

win is almost nothing — a flag the main loop watches, plus a flourish:

win:
            ld      a, 1
            ld      (won), a
            call    sfx_win          ; the ascending victory run
            ret
.loop:
            halt
            ld      a, (won)
            or      a
            jr      nz, .loop        ; won: freeze, the keep cleared
            call    player_step
            call    mark_step
            jr      .loop

A win condition doesn't need a screen or a state machine yet — just "stop responding, you're done." (Unit 16 turns this into a proper title-and-win loop.) The point this unit makes is smaller and more important: a goal is a counter and a check. Everything else is dressing.

Milestone — give the keep a goal

Gold is a walkable G tile (no BRIGHT bit), so collision already lets the thief stand on it. collect_gold runs between the move and the save: if the new cell is gold, it rewrites that state cell to floor (so the coin is gone for good), repaints it, chimes, and drops gold_remaining. At zero it calls win, which sets a flag the main loop watches and plays the victory run. A goal is a counter and a check.

Step 1: gold tiles, lift-on-contact, and a win at zero
+104-20
11 ; Shadowkeep — Unit 14: The Keep's Gold
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-00 = Unit 13's end: the keep walkable, lit, voiced — but no goal.
3+; step-01 scatters gold, lifts it on contact with a chime, and wins when the last coin is gone.
44
55 org 32768
66
...
1111 BANNER_ATTR equ %01001011
1212 RUBBLE_ATTR equ %00001000
1313 MARK_ATTR equ %00001111
14+GOLD_ATTR equ %00000110 ; yellow ink, no BRIGHT -> walkable gold
1415 THIEF equ %01001010
1516 WALL_BIT equ 6
1617
...
1920 NO_EXIT equ $FF
2021 MAX_SHADE equ 4
2122 MAX_TORCHES equ 4
23+TOTAL_GOLD equ 6
2224
2325 KEYS_OP equ $DFFE
2426 KEYS_Q equ $FBFE
...
5759 ei
5860 .loop:
5961 halt
62+ ld a, (won)
63+ or a
64+ jr nz, .loop ; keep won: freeze on the last coin
6065 call player_step
6166 call mark_step
6267 jr .loop
...
245250 ld a, (hl)
246251 cp '.'
247252 jr nz, .not_floor
248- call shade_for_cell
249- add a, a
250- ld e, a
251- ld d, 0
252- ld hl, shade_tiles
253- add hl, de
254- ld e, (hl)
255- inc hl
256- ld d, (hl)
257- ld (tile_ptr), de
258- ld a, FLOOR_ATTR
259- ld (tile_attr), a
260- call draw_tile
253+ call draw_floor_cell
261254 jr .cell_done
262255 .not_floor:
263256 call lookup_tile
...
363356 ld (thief_col), a
364357 ld a, (trow)
365358 ld (thief_row), a
359+ call collect_gold ; new cell might be gold — lift it first
366360 call save_under
367361 call draw_thief
368362 call sfx_step
...
486480 ld a, c
487481 cp 130 ; until it has groaned down low
488482 jr c, .sd_sweep
483+ ret
484+
485+; sfx_pickup — a bright two-blip chime: short, high, rising. The sound of gold.
486+sfx_pickup:
487+ ld b, 8
488+ ld c, 20
489+ call beep
490+ ld b, 8
491+ ld c, 14 ; a touch higher — a little ting
492+ call beep
493+ ret
494+
495+; sfx_win — an ascending flourish: a low note climbing in steps to a high one.
496+sfx_win:
497+ ld c, 60 ; low-ish
498+.sw_loop:
499+ ld b, 12
500+ push bc
501+ call beep
502+ pop bc
503+ ld a, c
504+ sub 8 ; smaller delay -> higher pitch (rising)
505+ ld c, a
506+ cp 16
507+ jr nc, .sw_loop ; until we've climbed high
508+ ret
509+
510+; ----------------------------------------------------------------------------
511+; draw_floor_cell — paint the floor at (row B, col C), shaded for the light.
512+; Extracted from draw_room so collection can repaint a cell as floor too.
513+; ----------------------------------------------------------------------------
514+draw_floor_cell:
515+ call shade_for_cell
516+ add a, a
517+ ld e, a
518+ ld d, 0
519+ ld hl, shade_tiles
520+ add hl, de
521+ ld e, (hl)
522+ inc hl
523+ ld d, (hl)
524+ ld (tile_ptr), de
525+ ld a, FLOOR_ATTR
526+ ld (tile_attr), a
527+ call draw_tile
528+ ret
529+
530+; ----------------------------------------------------------------------------
531+; collect_gold — if the cell the thief just moved onto is gold, lift it: turn
532+; the map cell to floor (gone for good), repaint it, chime, and tick the
533+; counter down. At zero, the keep is won.
534+; ----------------------------------------------------------------------------
535+collect_gold:
536+ call cell_state_addr ; HL -> map cell at the thief's position
537+ ld a, (hl)
538+ cp 'G'
539+ ret nz
540+ ld (hl), '.' ; the gold is taken — floor from now on
541+ call pos_bc ; B = row, C = col
542+ call draw_floor_cell ; so save_under grabs floor, not gold
543+ call sfx_pickup
544+ ld hl, gold_remaining
545+ dec (hl)
546+ ld a, (hl)
547+ or a
548+ call z, win
549+ ret
550+
551+; win — the last coin is gone. Flourish, and raise the flag the loop watches.
552+win:
553+ ld a, 1
554+ ld (won), a
555+ call sfx_win
489556 ret
490557
491558 pos_bc:
...
598665 defb '+'
599666 defw mark_tile
600667 defb MARK_ATTR
668+ defb 'G'
669+ defw gold_tile
670+ defb GOLD_ATTR
601671
602672 shade_tiles:
603673 defw shade0_tile
...
623693 ; lit; banners flank a statue with rubble at its feet.
624694 room0_template:
625695 defb "###############T################"
626- defb "#..............................#"
627696 defb "#..............................#"
697+ defb "#.......G......................#"
628698 defb "#..............................#"
629699 defb "#..............................#"
630700 defb "B..............S...............B"
...
638708 defb "#..............................#"
639709 defb "#........##..........##........#"
640710 defb "#........##..........##........#"
641- defb "#..............................#"
642711 defb "#..............................#"
643712 defb "#..............................#"
644713 defb "#..............................#"
645714 defb "#..............................#"
715+ defb "#.....................G........#"
646716 defb "#..............................#"
647717 defb "#..............................#"
648718 defb "################################"
...
650720 ; The Gallery — two torches: low on the south wall, and one on the east wall.
651721 room1_template:
652722 defb "###############.################"
653- defb "#..............................#"
654723 defb "#..............................#"
724+ defb "#.....G........................#"
655725 defb "#..............................#"
656726 defb "#..............................T"
657727 defb "#..............................#"
...
668738 defb "#..............................#"
669739 defb "#..............................#"
670740 defb "#........................ooo...#"
671- defb "#..............................#"
672741 defb "#..............................#"
742+ defb "#.......................G......#"
673743 defb "#..............................#"
674744 defb "#..............................#"
675745 defb "###############T################"
...
677747 ; The Vault — one flame, on the altar. Its character is the dark.
678748 room2_template:
679749 defb "################################"
680- defb "#..............................#"
681750 defb "#..............................#"
751+ defb "#.........G....................#"
682752 defb "#..............................#"
683753 defb "#..............................#"
684754 defb "#..............................#"
...
690760 defb "#.............####.............#"
691761 defb "#.............####.............#"
692762 defb "#.............####.............#"
693- defb "#..............................#"
694763 defb "#..............................#"
695764 defb "#..............................#"
696765 defb "#..............................#"
...
698767 defb "#..............................#"
699768 defb "#..............................#"
700769 defb "#..............................#"
770+ defb "#...................G..........#"
701771 defb "#..............................#"
702772 defb "###############.################"
703773
...
805875 defb %01111110
806876 defb %00011000
807877 defb %00011000
878+ defb %00000000
879+
880+gold_tile:
881+ defb %00000000
882+ defb %00111100
883+ defb %01111110
884+ defb %01111110
885+ defb %01111110
886+ defb %01111110
887+ defb %00111100
808888 defb %00000000
809889
810890 thief:
...
826906 cell_col:
827907 defb 0
828908 min_dist:
909+ defb 0
910+gold_remaining:
911+ defb TOTAL_GOLD
912+won:
829913 defb 0
830914 thief_col:
831915 defb START_COL
The complete program
; Shadowkeep — Unit 14: The Keep's Gold
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 scatters gold, lifts it on contact with a chime, and wins when the last coin is gone.

            org     32768

WALL_ATTR   equ     %01001000
FLOOR_ATTR  equ     %00001000
TORCH_ATTR  equ     %01001110
STATUE_ATTR equ     %01001111
BANNER_ATTR equ     %01001011
RUBBLE_ATTR equ     %00001000
MARK_ATTR   equ     %00001111
GOLD_ATTR   equ     %00000110         ; yellow ink, no BRIGHT -> walkable gold
THIEF       equ     %01001010
WALL_BIT    equ     6

START_COL   equ     15
START_ROW   equ     11
NO_EXIT     equ     $FF
MAX_SHADE   equ     4
MAX_TORCHES equ     4
TOTAL_GOLD  equ     6

KEYS_OP     equ     $DFFE
KEYS_Q      equ     $FBFE
KEYS_A      equ     $FDFE
KEYS_SPACE  equ     $7FFE

start:
            ld      a, 0
            out     ($FE), a

            ld      hl, room0_template
            ld      de, room0_state
            ld      bc, 768
            ldir
            ld      hl, room1_template
            ld      de, room1_state
            ld      bc, 768
            ldir
            ld      hl, room2_template
            ld      de, room2_state
            ld      bc, 768
            ldir

            xor     a
            ld      (current_room), a
            ld      a, START_COL
            ld      (thief_col), a
            ld      a, START_ROW
            ld      (thief_row), a

            call    draw_room
            call    save_under
            call    draw_thief

            im      1
            ei
.loop:
            halt
            ld      a, (won)
            or      a
            jr      nz, .loop        ; keep won: freeze on the last coin
            call    player_step
            call    mark_step
            jr      .loop

mark_step:
            ld      bc, KEYS_SPACE
            in      a, (c)
            bit     0, a
            ret     nz
            call    cell_state_addr
            ld      (hl), '+'
            ld      hl, mark_tile
            ld      de, under_thief
            ld      bc, 8
            ldir
            ld      a, MARK_ATTR
            ld      (under_thief + 8), a
            ret

cell_state_addr:
            call    room_entry_addr
            ld      a, (hl)
            inc     hl
            ld      h, (hl)
            ld      l, a
            push    hl
            ld      a, (thief_row)
            ld      l, a
            ld      h, 0
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            ld      a, (thief_col)
            ld      e, a
            ld      d, 0
            add     hl, de
            pop     de
            add     hl, de
            ret

room_entry_addr:
            ld      a, (current_room)
            ld      l, a
            ld      h, 0
            ld      d, h
            ld      e, l
            add     hl, hl
            add     hl, de
            add     hl, hl
            ld      de, rooms
            add     hl, de
            ret

; ----------------------------------------------------------------------------
; find_torches — collect every 'T' in the current room (up to MAX_TORCHES) into
; torch_list as (col, row) pairs; torch_count says how many.
; ----------------------------------------------------------------------------
find_torches:
            xor     a
            ld      (torch_count), a
            call    room_entry_addr
            ld      a, (hl)
            inc     hl
            ld      h, (hl)
            ld      l, a
            ld      b, 0
.fr_row:
            ld      c, 0
.fr_col:
            ld      a, (hl)
            cp      'T'
            jr      nz, .fr_skip
            ld      a, (torch_count)
            cp      MAX_TORCHES
            jr      nc, .fr_skip
            push    hl
            add     a, a            ; count * 2
            ld      e, a
            ld      d, 0
            ld      hl, torch_list
            add     hl, de
            ld      (hl), c         ; column
            inc     hl
            ld      (hl), b         ; row
            pop     hl
            ld      a, (torch_count)
            inc     a
            ld      (torch_count), a
.fr_skip:
            inc     hl
            inc     c
            ld      a, c
            cp      32
            jr      nz, .fr_col
            inc     b
            ld      a, b
            cp      24
            jr      nz, .fr_row
            ret

; ----------------------------------------------------------------------------
; shade_for_cell — row in B, column in C. Distance to the NEAREST torch,
; shifted by the room's falloff, clamped. Preserves B and C.
; ----------------------------------------------------------------------------
shade_for_cell:
            push    bc
            ld      a, b
            ld      (cell_row), a
            ld      a, c
            ld      (cell_col), a
            ld      a, (torch_count)
            or      a
            jr      z, .sf_dark
            ld      a, 255
            ld      (min_dist), a
            ld      hl, torch_list
            ld      a, (torch_count)
            ld      b, a            ; loop count
.sf_loop:
            ld      a, (cell_col)
            sub     (hl)            ; - torch col
            jr      nc, .sf_dc
            neg
.sf_dc:
            ld      d, a            ; |dc|
            inc     hl              ; -> torch row
            ld      a, (cell_row)
            sub     (hl)
            jr      nc, .sf_dr
            neg
.sf_dr:
            inc     hl              ; -> next torch pair
            cp      d               ; A = |dr|; take the larger
            jr      nc, .sf_mx
            ld      a, d
.sf_mx:
            ld      e, a            ; this torch's distance
            ld      a, (min_dist)
            cp      e
            jr      c, .sf_keep     ; min already smaller
            ld      a, e
            ld      (min_dist), a
.sf_keep:
            djnz    .sf_loop

            ld      a, (current_room)
            ld      c, a
            ld      b, 0
            ld      hl, room_falloff
            add     hl, bc
            ld      b, (hl)
            ld      a, (min_dist)
            inc     b
.sf_shift:
            dec     b
            jr      z, .sf_clamp
            srl     a
            jr      .sf_shift
.sf_clamp:
            cp      MAX_SHADE + 1
            jr      c, .sf_done
            ld      a, MAX_SHADE
.sf_done:
            pop     bc
            ret
.sf_dark:
            ld      a, MAX_SHADE
            pop     bc
            ret

draw_room:
            call    find_torches
            call    room_entry_addr
            ld      a, (hl)
            inc     hl
            ld      h, (hl)
            ld      l, a
            ld      (map_ptr), hl
            ld      b, 0
.room_row:
            ld      c, 0
.room_col:
            ld      hl, (map_ptr)
            ld      a, (hl)
            cp      '.'
            jr      nz, .not_floor
            call    draw_floor_cell
            jr      .cell_done
.not_floor:
            call    lookup_tile
            call    draw_tile
.cell_done:
            ld      hl, (map_ptr)
            inc     hl
            ld      (map_ptr), hl
            inc     c
            ld      a, c
            cp      32
            jr      nz, .room_col
            inc     b
            ld      a, b
            cp      24
            jr      nz, .room_row
            ret

lookup_tile:
            ld      hl, palette
.scan:
            cp      (hl)
            jr      z, .found
            inc     hl
            inc     hl
            inc     hl
            inc     hl
            jr      .scan
.found:
            inc     hl
            ld      e, (hl)
            inc     hl
            ld      d, (hl)
            ld      (tile_ptr), de
            inc     hl
            ld      a, (hl)
            ld      (tile_attr), a
            ret

draw_tile:
            push    bc
            call    attr_addr_cr
            ld      a, (tile_attr)
            ld      (hl), a
            call    scr_addr_cr
            ld      de, (tile_ptr)
            ld      b, 8
.tile_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .tile_row
            pop     bc
            ret

player_step:
            ld      a, (thief_col)
            ld      (tcol), a
            ld      a, (thief_row)
            ld      (trow), a

            ld      bc, KEYS_OP
            in      a, (c)
            bit     1, a
            jr      z, .left
            bit     0, a
            jr      z, .right
            ld      bc, KEYS_Q
            in      a, (c)
            bit     0, a
            jr      z, .up
            ld      bc, KEYS_A
            in      a, (c)
            bit     0, a
            jr      z, .down
            ret
.left:
            ld      hl, tcol
            dec     (hl)
            jr      .move
.right:
            ld      hl, tcol
            inc     (hl)
            jr      .move
.up:
            ld      hl, trow
            dec     (hl)
            jr      .move
.down:
            ld      hl, trow
            inc     (hl)
.move:
            ld      a, (trow)
            ld      b, a
            ld      a, (tcol)
            ld      c, a
            call    wall_at
            ret     nz

            call    restore_under
            ld      a, (tcol)
            ld      (thief_col), a
            ld      a, (trow)
            ld      (thief_row), a
            call    collect_gold     ; new cell might be gold — lift it first
            call    save_under
            call    draw_thief
            call    sfx_step
            call    check_exit
            ret

wall_at:
            call    attr_addr_cr
            bit     WALL_BIT, (hl)
            ret

check_exit:
            ld      a, (thief_col)
            or      a
            jr      z, .west
            cp      31
            jr      z, .east
            ld      a, (thief_row)
            or      a
            jr      z, .north
            cp      23
            jr      z, .south
            ret
.east:
            call    room_entry_addr
            ld      de, 4
            add     hl, de
            ld      a, (hl)
            cp      NO_EXIT
            ret     z
            ld      (current_room), a
            ld      a, 1
            ld      (thief_col), a
            jr      .enter
.west:
            call    room_entry_addr
            ld      de, 5
            add     hl, de
            ld      a, (hl)
            cp      NO_EXIT
            ret     z
            ld      (current_room), a
            ld      a, 30
            ld      (thief_col), a
            jr      .enter
.north:
            call    room_entry_addr
            inc     hl
            inc     hl
            ld      a, (hl)
            cp      NO_EXIT
            ret     z
            ld      (current_room), a
            ld      a, 22
            ld      (thief_row), a
            jr      .enter
.south:
            call    room_entry_addr
            inc     hl
            inc     hl
            inc     hl
            ld      a, (hl)
            cp      NO_EXIT
            ret     z
            ld      (current_room), a
            ld      a, 1
            ld      (thief_row), a
.enter:
            call    sfx_door
            call    draw_room
            call    save_under
            call    draw_thief
            ret

; ----------------------------------------------------------------------------
; The sound-effects driver.
;
; beep — the one tone primitive. B = number of square-wave cycles (how long
; the tone lasts); C = the delay constant between speaker toggles (the pitch —
; a larger C waits longer between flips, so the wave is slower and the note is
; lower). Bit 4 of port $FE is the speaker; we write it high, wait C, write it
; low (and a black border with it), wait C, and repeat B times.
; ----------------------------------------------------------------------------
beep:
.bp_cycle:
            ld      a, %00010000     ; speaker high (border stays black)
            out     ($FE), a
            ld      e, c
.bp_hi:
            dec     e
            jr      nz, .bp_hi
            xor     a                ; speaker low, border black
            out     ($FE), a
            ld      e, c
.bp_lo:
            dec     e
            jr      nz, .bp_lo
            djnz    .bp_cycle
            ret

; sfx_step — a footfall: short and low. A handful of cycles of a low tone,
; gone almost before you notice it. Played under every successful move.
sfx_step:
            ld      b, 5
            ld      c, 90
            call    beep
            ret

; sfx_door — a creak: a tone that falls in pitch as it plays. We start the
; delay constant small (higher) and let it grow (lower), a few cycles at each
; step, so the note groans downward — a heavy door swinging on its hinges.
sfx_door:
            ld      c, 36            ; start higher
.sd_sweep:
            ld      b, 3
            push    bc
            call    beep
            pop     bc
            inc     c                ; lower the pitch a little
            inc     c
            ld      a, c
            cp      130              ; until it has groaned down low
            jr      c, .sd_sweep
            ret

; sfx_pickup — a bright two-blip chime: short, high, rising. The sound of gold.
sfx_pickup:
            ld      b, 8
            ld      c, 20
            call    beep
            ld      b, 8
            ld      c, 14            ; a touch higher — a little ting
            call    beep
            ret

; sfx_win — an ascending flourish: a low note climbing in steps to a high one.
sfx_win:
            ld      c, 60            ; low-ish
.sw_loop:
            ld      b, 12
            push    bc
            call    beep
            pop     bc
            ld      a, c
            sub     8                ; smaller delay -> higher pitch (rising)
            ld      c, a
            cp      16
            jr      nc, .sw_loop     ; until we've climbed high
            ret

; ----------------------------------------------------------------------------
; draw_floor_cell — paint the floor at (row B, col C), shaded for the light.
; Extracted from draw_room so collection can repaint a cell as floor too.
; ----------------------------------------------------------------------------
draw_floor_cell:
            call    shade_for_cell
            add     a, a
            ld      e, a
            ld      d, 0
            ld      hl, shade_tiles
            add     hl, de
            ld      e, (hl)
            inc     hl
            ld      d, (hl)
            ld      (tile_ptr), de
            ld      a, FLOOR_ATTR
            ld      (tile_attr), a
            call    draw_tile
            ret

; ----------------------------------------------------------------------------
; collect_gold — if the cell the thief just moved onto is gold, lift it: turn
; the map cell to floor (gone for good), repaint it, chime, and tick the
; counter down. At zero, the keep is won.
; ----------------------------------------------------------------------------
collect_gold:
            call    cell_state_addr  ; HL -> map cell at the thief's position
            ld      a, (hl)
            cp      'G'
            ret     nz
            ld      (hl), '.'        ; the gold is taken — floor from now on
            call    pos_bc           ; B = row, C = col
            call    draw_floor_cell  ; so save_under grabs floor, not gold
            call    sfx_pickup
            ld      hl, gold_remaining
            dec     (hl)
            ld      a, (hl)
            or      a
            call    z, win
            ret

; win — the last coin is gone. Flourish, and raise the flag the loop watches.
win:
            ld      a, 1
            ld      (won), a
            call    sfx_win
            ret

pos_bc:
            ld      a, (thief_row)
            ld      b, a
            ld      a, (thief_col)
            ld      c, a
            ret

save_under:
            call    pos_bc
            call    scr_addr_cr
            ld      de, under_thief
            ld      b, 8
.save_row:
            ld      a, (hl)
            ld      (de), a
            inc     de
            inc     h
            djnz    .save_row
            call    pos_bc
            call    attr_addr_cr
            ld      a, (hl)
            ld      (under_thief + 8), a
            ret

restore_under:
            call    pos_bc
            call    scr_addr_cr
            ld      de, under_thief
            ld      b, 8
.restore_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .restore_row
            call    pos_bc
            call    attr_addr_cr
            ld      a, (under_thief + 8)
            ld      (hl), a
            ret

draw_thief:
            call    pos_bc
            call    attr_addr_cr
            ld      (hl), THIEF
            call    pos_bc
            call    scr_addr_cr
            ld      de, thief
            ld      b, 8
.thief_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .thief_row
            ret

scr_addr_cr:
            ld      a, b
            and     %00011000
            or      %01000000
            ld      h, a
            ld      a, b
            and     %00000111
            rrca
            rrca
            rrca
            or      c
            ld      l, a
            ret

attr_addr_cr:
            ld      a, b
            ld      l, a
            ld      h, 0
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            ld      de, $5800
            add     hl, de
            ld      a, c
            ld      e, a
            ld      d, 0
            add     hl, de
            ret

palette:
            defb    '.'
            defw    shade2_tile
            defb    FLOOR_ATTR
            defb    '#'
            defw    wall_tile
            defb    WALL_ATTR
            defb    'T'
            defw    torch_tile
            defb    TORCH_ATTR
            defb    'S'
            defw    statue_tile
            defb    STATUE_ATTR
            defb    'B'
            defw    banner_tile
            defb    BANNER_ATTR
            defb    'o'
            defw    rubble_tile
            defb    RUBBLE_ATTR
            defb    '+'
            defw    mark_tile
            defb    MARK_ATTR
            defb    'G'
            defw    gold_tile
            defb    GOLD_ATTR

shade_tiles:
            defw    shade0_tile
            defw    shade1_tile
            defw    shade2_tile
            defw    shade3_tile
            defw    shade4_tile

room_falloff:
            defb    2                   ; Hall — broad
            defb    1                   ; Gallery — medium
            defb    0                   ; Vault — tight

rooms:
            defw    room0_state
            defb    NO_EXIT, NO_EXIT, 1, NO_EXIT
            defw    room1_state
            defb    2, NO_EXIT, NO_EXIT, 0
            defw    room2_state
            defb    NO_EXIT, 1, NO_EXIT, NO_EXIT

; The Great Hall — three sconces (top-centre and the two side walls), broadly
; lit; banners flank a statue with rubble at its feet.
room0_template:
            defb    "###############T################"
            defb    "#..............................#"
            defb    "#.......G......................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "B..............S...............B"
            defb    "#.............ooo..............#"
            defb    "#..............................#"
            defb    "T........##..........##........T"
            defb    "#........##..........##........#"
            defb    "#..............................#"
            defb    "#..............................."
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#........##..........##........#"
            defb    "#........##..........##........#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#.....................G........#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "################################"

; The Gallery — two torches: low on the south wall, and one on the east wall.
room1_template:
            defb    "###############.################"
            defb    "#..............................#"
            defb    "#.....G........................#"
            defb    "#..............................#"
            defb    "#..............................T"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "###############.################"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "...............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "B..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#........................ooo...#"
            defb    "#..............................#"
            defb    "#.......................G......#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "###############T################"

; The Vault — one flame, on the altar. Its character is the dark.
room2_template:
            defb    "################################"
            defb    "#..............................#"
            defb    "#.........G....................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#.............#T##.............#"
            defb    "#.............####.............#"
            defb    "#.............####.............#"
            defb    "#.............####.............#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#...................G..........#"
            defb    "#..............................#"
            defb    "###############.################"

shade0_tile:
            defb    %00000000
            defb    %00100010
            defb    %00000000
            defb    %00000000
            defb    %00000000
            defb    %10001000
            defb    %00000000
            defb    %00000000
shade1_tile:
            defb    %00100010
            defb    %00000000
            defb    %10001000
            defb    %00000000
            defb    %00100010
            defb    %00000000
            defb    %10001000
            defb    %00000000
shade2_tile:
            defb    %10101010
            defb    %01010101
            defb    %10101010
            defb    %01010101
            defb    %10101010
            defb    %01010101
            defb    %10101010
            defb    %01010101
shade3_tile:
            defb    %10101010
            defb    %11111111
            defb    %01010101
            defb    %11111111
            defb    %10101010
            defb    %11111111
            defb    %01010101
            defb    %11111111
shade4_tile:
            defb    %11111111
            defb    %11101110
            defb    %11111111
            defb    %10111011
            defb    %11111111
            defb    %11101110
            defb    %11111111
            defb    %10111011

wall_tile:
            defb    %00010001
            defb    %00000000
            defb    %01000100
            defb    %00000000
            defb    %00010001
            defb    %00000000
            defb    %01000100
            defb    %00000000

torch_tile:
            defb    %00010000
            defb    %00111000
            defb    %00111000
            defb    %01111100
            defb    %01111100
            defb    %01111100
            defb    %00111000
            defb    %00010000

statue_tile:
            defb    %00111100
            defb    %01111110
            defb    %00111100
            defb    %00011000
            defb    %00011000
            defb    %00111100
            defb    %01111110
            defb    %01111110

banner_tile:
            defb    %01111110
            defb    %01111110
            defb    %01011010
            defb    %01011010
            defb    %01111110
            defb    %01111110
            defb    %00111100
            defb    %00011000

rubble_tile:
            defb    %00000000
            defb    %01100000
            defb    %01100000
            defb    %00000110
            defb    %00000110
            defb    %00011000
            defb    %00011000
            defb    %00000000

mark_tile:
            defb    %00000000
            defb    %00011000
            defb    %00011000
            defb    %01111110
            defb    %01111110
            defb    %00011000
            defb    %00011000
            defb    %00000000

gold_tile:
            defb    %00000000
            defb    %00111100
            defb    %01111110
            defb    %01111110
            defb    %01111110
            defb    %01111110
            defb    %00111100
            defb    %00000000

thief:
            defb    %00011000
            defb    %00111100
            defb    %01111110
            defb    %01111110
            defb    %01111110
            defb    %01111110
            defb    %00111100
            defb    %00100100

current_room:
            defb    0
torch_count:
            defb    0
cell_row:
            defb    0
cell_col:
            defb    0
min_dist:
            defb    0
gold_remaining:
            defb    TOTAL_GOLD
won:
            defb    0
thief_col:
            defb    START_COL
thief_row:
            defb    START_ROW
tcol:
            defb    0
trow:
            defb    0
map_ptr:
            defw    0
tile_ptr:
            defw    0
tile_attr:
            defb    0
under_thief:
            defb    0, 0, 0, 0, 0, 0, 0, 0, 0
torch_list:
            defs    MAX_TORCHES * 2

room0_state:
            defs    768
room1_state:
            defs    768
room2_state:
            defs    768

            end     start

Walk onto a coin and it's lifted — it vanishes the instant you stand on it, and the counter drops:

The thief crosses to the Hall's upper-left coin and steps onto it — gone, turned to floor, the keep one coin nearer cleared. Because we edit the room's state, it stays gone when you leave and return.

The lift is a bright chime — the gold's voice, sketched in the last unit:

ZX Spectrum beeper · port $FE bit 4
Lifting a coin — footfalls, then the bright pickup chime

And when the last coin is gone, the keep is won — an ascending victory run, and the thief stands still in a keep emptied of its treasure:

ZX Spectrum beeper · port $FE bit 4
The keep cleared — the ascending win flourish

Clear the keep — two coins in the Hall, two in the Gallery, two in the Vault, each with a chime, the last one winning the game — and you've played the first complete game you built in assembly: explore, collect, win.

Try this: more gold, harder keep

Scatter more Gs through the maps and raise TOTAL_GOLD to match. The keep takes longer to clear; the goal grows without a line of new logic. (If a coin lands on a wall or a torch it won't draw as floor — keep them on open stone.)

Try this: gold behind the pillars

Tuck a coin into the dark behind the Hall's pillars or in the Vault's gloom. Now clearing the keep means searching it — the lighting you built becomes part of the challenge, hiding what the player must find. Design and atmosphere start working together.

Try this: a coin you can't reach (yet)

Place a coin in a spot walled off from the rest. The counter can never reach zero — the keep is unwinnable. A useful bug to make on purpose: it shows that your win condition trusts the level design completely. Reachability is a level-design promise the code can't check for you.

When it's wrong, see why

  • Gold is solid — you bump into it. Its attribute has the BRIGHT bit set. Gold must be %00000110-style (no bit 6), or the wall test treats it as stone.
  • The coin comes back when you re-enter the room. You edited the template, not the state copy — or collect_gold runs after save_under, so the saved "under" is still the coin. Collect before saving.
  • Collecting never wins. TOTAL_GOLD doesn't match the number of Gs in the maps. Count them; they must agree, or the counter stops above (or wraps below) zero.
  • The counter wraps and the game never ends. A coin was collected twice, or gold_remaining started wrong. Each G must decrement exactly once — which it does, because the cell becomes floor the instant it's lifted.
  • It wins the moment you start. gold_remaining initialised to zero. It must start at TOTAL_GOLD.

Before and after

You started with a keep you could wander but never finish, and ended with a game — gold to find, a chime when you lift it, and a win when the last coin is gone. It cost almost no new engine: gold is a walkable tile, collecting is rewriting that map cell to floor, and the goal is a counter and a check. Because you edit the room's working state, a cleared coin stays cleared across visits — the persistence from Unit 7 paying off again. A win condition turned out to be tiny, and trusting: the code counts, and you promise every coin can be reached. Wandering became playing.

What you've learnt

  • A goal is a counter and a check. Collectables down, a win at zero — that's the whole shape of an objective, and it's tiny.
  • The map is the level, pickups included. Gold is a walkable tile; collecting is editing the map cell to floor. No parallel list, no special case in collision.
  • Edit state, not the template, and the world remembers. Because we change the working copy, a cleared coin stays cleared across room visits — persistence for free.
  • A win condition trusts the level. The code counts; you promise every coin is reachable. Knowing where that line sits is part of designing a game.

What's next

The keep is a complete game now — but it opens in silence and ends in silence. A game announces itself. In Unit 15, "A Theme in One Voice," we go back to the beeper and write music: a composed theme for the title, a single voice carrying a melody, built on a little note-table player. One bit, one tune — the keep gets its signature.