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.
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
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.
| 1 | 1 | ; Shadowkeep — Unit 14: The Keep's Gold | |
| 2 | 2 | ; 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. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 11 | 11 | BANNER_ATTR equ %01001011 | |
| 12 | 12 | RUBBLE_ATTR equ %00001000 | |
| 13 | 13 | MARK_ATTR equ %00001111 | |
| 14 | + | GOLD_ATTR equ %00000110 ; yellow ink, no BRIGHT -> walkable gold | |
| 14 | 15 | THIEF equ %01001010 | |
| 15 | 16 | WALL_BIT equ 6 | |
| 16 | 17 | | |
| ... | |||
| 19 | 20 | NO_EXIT equ $FF | |
| 20 | 21 | MAX_SHADE equ 4 | |
| 21 | 22 | MAX_TORCHES equ 4 | |
| 23 | + | TOTAL_GOLD equ 6 | |
| 22 | 24 | | |
| 23 | 25 | KEYS_OP equ $DFFE | |
| 24 | 26 | KEYS_Q equ $FBFE | |
| ... | |||
| 57 | 59 | ei | |
| 58 | 60 | .loop: | |
| 59 | 61 | halt | |
| 62 | + | ld a, (won) | |
| 63 | + | or a | |
| 64 | + | jr nz, .loop ; keep won: freeze on the last coin | |
| 60 | 65 | call player_step | |
| 61 | 66 | call mark_step | |
| 62 | 67 | jr .loop | |
| ... | |||
| 245 | 250 | ld a, (hl) | |
| 246 | 251 | cp '.' | |
| 247 | 252 | 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 | |
| 261 | 254 | jr .cell_done | |
| 262 | 255 | .not_floor: | |
| 263 | 256 | call lookup_tile | |
| ... | |||
| 363 | 356 | ld (thief_col), a | |
| 364 | 357 | ld a, (trow) | |
| 365 | 358 | ld (thief_row), a | |
| 359 | + | call collect_gold ; new cell might be gold — lift it first | |
| 366 | 360 | call save_under | |
| 367 | 361 | call draw_thief | |
| 368 | 362 | call sfx_step | |
| ... | |||
| 486 | 480 | ld a, c | |
| 487 | 481 | cp 130 ; until it has groaned down low | |
| 488 | 482 | 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 | |
| 489 | 556 | ret | |
| 490 | 557 | | |
| 491 | 558 | pos_bc: | |
| ... | |||
| 598 | 665 | defb '+' | |
| 599 | 666 | defw mark_tile | |
| 600 | 667 | defb MARK_ATTR | |
| 668 | + | defb 'G' | |
| 669 | + | defw gold_tile | |
| 670 | + | defb GOLD_ATTR | |
| 601 | 671 | | |
| 602 | 672 | shade_tiles: | |
| 603 | 673 | defw shade0_tile | |
| ... | |||
| 623 | 693 | ; lit; banners flank a statue with rubble at its feet. | |
| 624 | 694 | room0_template: | |
| 625 | 695 | defb "###############T################" | |
| 626 | - | defb "#..............................#" | |
| 627 | 696 | defb "#..............................#" | |
| 697 | + | defb "#.......G......................#" | |
| 628 | 698 | defb "#..............................#" | |
| 629 | 699 | defb "#..............................#" | |
| 630 | 700 | defb "B..............S...............B" | |
| ... | |||
| 638 | 708 | defb "#..............................#" | |
| 639 | 709 | defb "#........##..........##........#" | |
| 640 | 710 | defb "#........##..........##........#" | |
| 641 | - | defb "#..............................#" | |
| 642 | 711 | defb "#..............................#" | |
| 643 | 712 | defb "#..............................#" | |
| 644 | 713 | defb "#..............................#" | |
| 645 | 714 | defb "#..............................#" | |
| 715 | + | defb "#.....................G........#" | |
| 646 | 716 | defb "#..............................#" | |
| 647 | 717 | defb "#..............................#" | |
| 648 | 718 | defb "################################" | |
| ... | |||
| 650 | 720 | ; The Gallery — two torches: low on the south wall, and one on the east wall. | |
| 651 | 721 | room1_template: | |
| 652 | 722 | defb "###############.################" | |
| 653 | - | defb "#..............................#" | |
| 654 | 723 | defb "#..............................#" | |
| 724 | + | defb "#.....G........................#" | |
| 655 | 725 | defb "#..............................#" | |
| 656 | 726 | defb "#..............................T" | |
| 657 | 727 | defb "#..............................#" | |
| ... | |||
| 668 | 738 | defb "#..............................#" | |
| 669 | 739 | defb "#..............................#" | |
| 670 | 740 | defb "#........................ooo...#" | |
| 671 | - | defb "#..............................#" | |
| 672 | 741 | defb "#..............................#" | |
| 742 | + | defb "#.......................G......#" | |
| 673 | 743 | defb "#..............................#" | |
| 674 | 744 | defb "#..............................#" | |
| 675 | 745 | defb "###############T################" | |
| ... | |||
| 677 | 747 | ; The Vault — one flame, on the altar. Its character is the dark. | |
| 678 | 748 | room2_template: | |
| 679 | 749 | defb "################################" | |
| 680 | - | defb "#..............................#" | |
| 681 | 750 | defb "#..............................#" | |
| 751 | + | defb "#.........G....................#" | |
| 682 | 752 | defb "#..............................#" | |
| 683 | 753 | defb "#..............................#" | |
| 684 | 754 | defb "#..............................#" | |
| ... | |||
| 690 | 760 | defb "#.............####.............#" | |
| 691 | 761 | defb "#.............####.............#" | |
| 692 | 762 | defb "#.............####.............#" | |
| 693 | - | defb "#..............................#" | |
| 694 | 763 | defb "#..............................#" | |
| 695 | 764 | defb "#..............................#" | |
| 696 | 765 | defb "#..............................#" | |
| ... | |||
| 698 | 767 | defb "#..............................#" | |
| 699 | 768 | defb "#..............................#" | |
| 700 | 769 | defb "#..............................#" | |
| 770 | + | defb "#...................G..........#" | |
| 701 | 771 | defb "#..............................#" | |
| 702 | 772 | defb "###############.################" | |
| 703 | 773 | | |
| ... | |||
| 805 | 875 | defb %01111110 | |
| 806 | 876 | defb %00011000 | |
| 807 | 877 | 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 | |
| 808 | 888 | defb %00000000 | |
| 809 | 889 | | |
| 810 | 890 | thief: | |
| ... | |||
| 826 | 906 | cell_col: | |
| 827 | 907 | defb 0 | |
| 828 | 908 | min_dist: | |
| 909 | + | defb 0 | |
| 910 | + | gold_remaining: | |
| 911 | + | defb TOTAL_GOLD | |
| 912 | + | won: | |
| 829 | 913 | defb 0 | |
| 830 | 914 | thief_col: | |
| 831 | 915 | 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 lift is a bright chime — the gold's voice, sketched in the last unit:
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:
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
BRIGHTbit 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_goldruns aftersave_under, so the saved "under" is still the coin. Collect before saving. - Collecting never wins.
TOTAL_GOLDdoesn't match the number ofGs 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_remainingstarted wrong. EachGmust decrement exactly once — which it does, because the cell becomes floor the instant it's lifted. - It wins the moment you start.
gold_remaininginitialised to zero. It must start atTOTAL_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.