The Keep's Gold
Three rooms you can wander, and nothing to do in them. Scatter gold through the keep, let the thief lift each coin as he steps on it, and make the last one matter: collect them all and the keep stands.
You have a keep. Three rooms, joined by doorways, a hooded thief who walks them and cannot pass through walls. It is a place — but it is not yet a game, because there is nothing in it to want. You can wander the Hall, thread the Gallery’s gap, circle the Vault’s altar, and then… wander back. A place you can leave exactly as you found it is a room, not a level.
This unit gives the keep a point. Gold — coins scattered through the rooms — and a reason to move: lift every one, and the keep is yours. It is the oldest bargain in games, the one Atic Atac and a hundred others struck: here is treasure, here is somewhere to carry it from, now go. And with the gold comes the shape every finished game needs — a win: a state the keep can reach where the playing stops and the flourish plays.
What you’ll see by the end

Two coins glint in the Hall, more in the rooms beyond — six in all. Walk the thief onto one and it is gone: a bright ting, and the counter drops. Clear all six and the screen turns black to tell you so — THE KEEP STANDS — then hands you back to the title to do it again. A goal, a tally, an ending: the parts that turn walking into playing.
Gold is a glyph you can stand on
You have placed tiles in the room map before — walls, torches-to-come, the chalk you leave. Gold is one more, G, a coin drawn in plain yellow. The difference is in its attribute: where a wall is bright (and so reads as solid to wall_at), gold is dim — walkable — so the thief steps onto it, not into it:
GOLD_ATTR equ %00000110 ; yellow ink, no BRIGHT -> walkable gold
Drop a few into the room maps like any other letter:
defb "#.........................G....#" ; a coin on the right
Lift it as you step
Every successful move already ends the same way: the thief lands on a new cell, and we save what’s beneath him and draw him on top. Collecting is one question asked at that moment — is the cell I just stepped onto gold? — and if so, we lift it:
collect_gold:
call cell_state_addr ; the map cell under the thief
ld a, (hl)
cp 'G'
ret nz ; not gold — nothing to do
ld (hl), '.' ; taken — floor from now on
call draw_floor_cell ; repaint the cell empty
call sfx_pickup ; a bright two-blip chime
ld hl, gold_remaining
dec (hl) ; one fewer to find
ret
We change the coin in the map, not just on screen — G becomes . — so it stays gone when the room is redrawn, and so save_under grabs floor, not treasure, as the thief moves off. The coin is lifted once and for all.
The last coin matters
A counter you can drive to zero is a game waiting for an ending. collect_gold ticks gold_remaining down; when it reaches zero, the keep is clear, and we raise a flag — won — that the main loop is watching:
dec (hl)
ld a, (hl)
or a
call z, win ; the last coin — the keep stands
That flag is the hinge of the whole program. Until now the loop has done one thing forever: read the keys, move the thief, repeat. Now it is a small state machine — the same title → play → win → title shape you built in your first game, brought to the keep:
main_title:
call show_title ; the name, and PRESS SPACE
call new_game ; reset the rooms, place the gold
.game_loop:
halt
ld a, (won)
or a
jr nz, .won ; cleared? -> the flourish
call player_step
jr .game_loop
.won:
call show_win ; THE KEEP STANDS
jr main_title ; round again
new_game copies the room templates back into working memory — so the gold you lifted last time returns — resets the counter to six, and stands the thief at the door. The keep is replayable because every game begins by rebuilding it.
Milestone — a goal and a win
Gold is a walkable glyph you lift by stepping on it; a counter tracks how many remain; and the loop that only ever played now knows how to end — the last coin raises the win flag, the flourish plays, and the title returns for another run. Two steps: first the gold and the collecting, then the win that makes it mean something.
| 1 | 1 | ; Shadowkeep — Unit 9: The Keep's Gold | |
| 2 | 2 | ; Cumulative build; every step runs on its own. Narrative: the unit page. | |
| 3 | - | ; step-00 = Unit 8's end: three rooms you can explore, but nothing to seek. | |
| 3 | + | ; step-01 scatters gold through the rooms and lets the thief collect it. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 8 | 8 | FLOOR_ATTR equ %00001000 | |
| 9 | 9 | MARK_ATTR equ %00001111 | |
| 10 | 10 | THIEF equ %01001010 | |
| 11 | + | GOLD_ATTR equ %00000110 ; yellow ink, no BRIGHT -> walkable gold | |
| 12 | + | TOTAL_GOLD equ 6 | |
| 11 | 13 | WALL_BIT equ 6 | |
| 12 | 14 | | |
| 13 | 15 | START_COL equ 15 | |
| ... | |||
| 42 | 44 | ld (thief_col), a | |
| 43 | 45 | ld a, START_ROW | |
| 44 | 46 | ld (thief_row), a | |
| 47 | + | ld a, TOTAL_GOLD | |
| 48 | + | ld (gold_remaining), a | |
| 45 | 49 | | |
| 46 | 50 | call draw_room | |
| 47 | 51 | call save_under | |
| ... | |||
| 221 | 225 | ld (thief_col), a | |
| 222 | 226 | ld a, (trow) | |
| 223 | 227 | ld (thief_row), a | |
| 228 | + | call collect_gold ; the new cell might be gold — lift it first | |
| 224 | 229 | call save_under | |
| 225 | 230 | call draw_thief | |
| 226 | 231 | call check_exit | |
| ... | |||
| 348 | 353 | inc de | |
| 349 | 354 | inc h | |
| 350 | 355 | djnz .thief_row | |
| 356 | + | ret | |
| 357 | + | | |
| 358 | + | ; ---------------------------------------------------------------------------- | |
| 359 | + | ; The gold — the keep's goal. Step onto a coin and it lifts: the map cell turns | |
| 360 | + | ; to floor (gone for good), the cell repaints as plain floor (no lighting yet), | |
| 361 | + | ; a chime sounds, and the counter ticks down. | |
| 362 | + | ; ---------------------------------------------------------------------------- | |
| 363 | + | collect_gold: | |
| 364 | + | call cell_state_addr ; HL -> map cell at the thief's position | |
| 365 | + | ld a, (hl) | |
| 366 | + | cp 'G' | |
| 367 | + | ret nz | |
| 368 | + | ld (hl), '.' ; the gold is taken — floor from now on | |
| 369 | + | call pos_bc ; B = row, C = col | |
| 370 | + | call draw_floor_cell ; repaint the cell as plain floor | |
| 371 | + | call sfx_pickup | |
| 372 | + | ld hl, gold_remaining | |
| 373 | + | dec (hl) | |
| 374 | + | ret | |
| 375 | + | | |
| 376 | + | ; draw_floor_cell — paint the floor tile at (row B, col C). Lighting (Unit 13) | |
| 377 | + | ; will replace this with a shade chosen by distance from the nearest torch. | |
| 378 | + | draw_floor_cell: | |
| 379 | + | ld hl, floor_tile | |
| 380 | + | ld (tile_ptr), hl | |
| 381 | + | ld a, FLOOR_ATTR | |
| 382 | + | ld (tile_attr), a | |
| 383 | + | call draw_tile | |
| 384 | + | ret | |
| 385 | + | | |
| 386 | + | ; ---------------------------------------------------------------------------- | |
| 387 | + | ; beep — the one tone primitive (the tiny game's blip, reused). B = cycles, | |
| 388 | + | ; C = the delay between speaker toggles (the pitch; larger C = lower). | |
| 389 | + | ; ---------------------------------------------------------------------------- | |
| 390 | + | beep: | |
| 391 | + | .bp_cycle: | |
| 392 | + | ld a, %00010000 | |
| 393 | + | out ($FE), a | |
| 394 | + | ld e, c | |
| 395 | + | .bp_hi: | |
| 396 | + | dec e | |
| 397 | + | jr nz, .bp_hi | |
| 398 | + | xor a | |
| 399 | + | out ($FE), a | |
| 400 | + | ld e, c | |
| 401 | + | .bp_lo: | |
| 402 | + | dec e | |
| 403 | + | jr nz, .bp_lo | |
| 404 | + | djnz .bp_cycle | |
| 405 | + | ret | |
| 406 | + | | |
| 407 | + | ; sfx_pickup — a bright two-blip chime: the sound of gold. | |
| 408 | + | sfx_pickup: | |
| 409 | + | ld b, 8 | |
| 410 | + | ld c, 20 | |
| 411 | + | call beep | |
| 412 | + | ld b, 8 | |
| 413 | + | ld c, 14 | |
| 414 | + | call beep | |
| 351 | 415 | ret | |
| 416 | + | | |
| 417 | + | gold_tile: | |
| 418 | + | defb %00000000 | |
| 419 | + | defb %00111100 | |
| 420 | + | defb %01111110 | |
| 421 | + | defb %01111110 | |
| 422 | + | defb %01111110 | |
| 423 | + | defb %01111110 | |
| 424 | + | defb %00111100 | |
| 425 | + | defb %00000000 | |
| 352 | 426 | | |
| 353 | 427 | scr_addr_cr: | |
| 354 | 428 | ld a, b | |
| ... | |||
| 391 | 465 | defb '+' | |
| 392 | 466 | defw mark_tile | |
| 393 | 467 | defb MARK_ATTR | |
| 468 | + | defb 'G' | |
| 469 | + | defw gold_tile | |
| 470 | + | defb GOLD_ATTR | |
| 394 | 471 | | |
| 395 | 472 | ; ---------------------------------------------------------------------------- | |
| 396 | 473 | ; The keep: three rooms. Hall -east-> Gallery -north-> Vault, and back. | |
| ... | |||
| 407 | 484 | ; The Great Hall — four pillars, east door at row 11. | |
| 408 | 485 | room0_template: | |
| 409 | 486 | defb "################################" | |
| 410 | - | defb "#..............................#" | |
| 411 | 487 | defb "#..............................#" | |
| 412 | 488 | defb "#..............................#" | |
| 413 | 489 | defb "#..............................#" | |
| 414 | 490 | defb "#..............................#" | |
| 415 | 491 | defb "#..............................#" | |
| 492 | + | defb "#.........................G....#" | |
| 416 | 493 | defb "#..............................#" | |
| 417 | 494 | defb "#........##..........##........#" | |
| 418 | 495 | defb "#........##..........##........#" | |
| ... | |||
| 422 | 499 | defb "#..............................#" | |
| 423 | 500 | defb "#........##..........##........#" | |
| 424 | 501 | defb "#........##..........##........#" | |
| 425 | - | defb "#..............................#" | |
| 426 | 502 | defb "#..............................#" | |
| 427 | 503 | defb "#..............................#" | |
| 428 | 504 | defb "#..............................#" | |
| 429 | 505 | defb "#..............................#" | |
| 506 | + | defb "#.........................G....#" | |
| 430 | 507 | defb "#..............................#" | |
| 431 | 508 | defb "#..............................#" | |
| 432 | 509 | defb "################################" | |
| ... | |||
| 443 | 520 | defb "#..............................#" | |
| 444 | 521 | defb "#..............................#" | |
| 445 | 522 | defb "###############.################" | |
| 446 | - | defb "#..............................#" | |
| 447 | 523 | defb "#..............................#" | |
| 448 | - | defb "...............................#" | |
| 449 | 524 | defb "#..............................#" | |
| 525 | + | defb "........................G......#" | |
| 450 | 526 | defb "#..............................#" | |
| 451 | 527 | defb "#..............................#" | |
| 452 | 528 | defb "#..............................#" | |
| ... | |||
| 455 | 531 | defb "#..............................#" | |
| 456 | 532 | defb "#..............................#" | |
| 457 | 533 | defb "#..............................#" | |
| 534 | + | defb "#.......................G......#" | |
| 458 | 535 | defb "#..............................#" | |
| 459 | 536 | defb "#..............................#" | |
| 460 | 537 | defb "################################" | |
| ... | |||
| 476 | 553 | defb "#.............####.............#" | |
| 477 | 554 | defb "#.............####.............#" | |
| 478 | 555 | defb "#.............####.............#" | |
| 479 | - | defb "#..............................#" | |
| 480 | 556 | defb "#..............................#" | |
| 481 | 557 | defb "#..............................#" | |
| 482 | 558 | defb "#..............................#" | |
| 483 | 559 | defb "#..............................#" | |
| 484 | 560 | defb "#..............................#" | |
| 485 | 561 | defb "#..............................#" | |
| 562 | + | defb "#.........G.........G..........#" | |
| 486 | 563 | defb "#..............................#" | |
| 487 | 564 | defb "#..............................#" | |
| 488 | 565 | defb "###############.################" | |
| ... | |||
| 545 | 622 | defb 0 | |
| 546 | 623 | under_thief: | |
| 547 | 624 | defb 0, 0, 0, 0, 0, 0, 0, 0, 0 | |
| 625 | + | gold_remaining: | |
| 626 | + | defb TOTAL_GOLD | |
| 548 | 627 | | |
| 549 | 628 | room0_state: | |
| 550 | 629 | defs 768 |
| 1 | 1 | ; Shadowkeep — Unit 9: The Keep's Gold | |
| 2 | 2 | ; Cumulative build; every step runs on its own. Narrative: the unit page. | |
| 3 | - | ; step-01 scatters gold through the rooms and lets the thief collect it. | |
| 3 | + | ; step-02 makes the last coin win: THE KEEP STANDS, in a title -> play -> win loop. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 24 | 24 | start: | |
| 25 | 25 | ld a, 0 | |
| 26 | 26 | out ($FE), a | |
| 27 | + | im 1 | |
| 28 | + | | |
| 29 | + | ; --- TITLE -> PLAY -> WIN -> TITLE ------------------------------------------ | |
| 30 | + | main_title: | |
| 31 | + | call show_title ; returns (interrupts off) when SPACE pressed | |
| 32 | + | call new_game ; reset the keep, place the gold, draw | |
| 33 | + | ei | |
| 34 | + | .game_loop: | |
| 35 | + | halt | |
| 36 | + | ld a, (won) | |
| 37 | + | or a | |
| 38 | + | jr nz, .won | |
| 39 | + | call player_step | |
| 40 | + | call mark_step | |
| 41 | + | jr .game_loop | |
| 42 | + | .won: | |
| 43 | + | call show_win ; "THE KEEP STANDS", flourish, wait for SPACE | |
| 44 | + | jr main_title ; round again | |
| 27 | 45 | | |
| 46 | + | ; new_game — copy the room templates back into working state (so collected gold | |
| 47 | + | ; returns), reset the thief, the gold counter and the win flag, draw the keep. | |
| 48 | + | new_game: | |
| 28 | 49 | ld hl, room0_template | |
| 29 | 50 | ld de, room0_state | |
| 30 | 51 | ld bc, 768 | |
| ... | |||
| 40 | 61 | | |
| 41 | 62 | xor a | |
| 42 | 63 | ld (current_room), a | |
| 64 | + | ld (won), a | |
| 65 | + | ld a, TOTAL_GOLD | |
| 66 | + | ld (gold_remaining), a | |
| 43 | 67 | ld a, START_COL | |
| 44 | 68 | ld (thief_col), a | |
| 45 | 69 | ld a, START_ROW | |
| 46 | 70 | ld (thief_row), a | |
| 47 | - | ld a, TOTAL_GOLD | |
| 48 | - | ld (gold_remaining), a | |
| 49 | 71 | | |
| 50 | 72 | call draw_room | |
| 51 | 73 | call save_under | |
| 52 | 74 | call draw_thief | |
| 53 | - | | |
| 54 | - | im 1 | |
| 55 | - | ei | |
| 56 | - | .loop: | |
| 57 | - | halt | |
| 58 | - | call player_step | |
| 59 | - | call mark_step | |
| 60 | - | jr .loop | |
| 75 | + | ret | |
| 61 | 76 | | |
| 62 | 77 | mark_step: | |
| 63 | 78 | ld bc, KEYS_SPACE | |
| ... | |||
| 371 | 386 | call sfx_pickup | |
| 372 | 387 | ld hl, gold_remaining | |
| 373 | 388 | dec (hl) | |
| 389 | + | ld a, (hl) | |
| 390 | + | or a | |
| 391 | + | call z, win ; the last coin — the keep stands | |
| 374 | 392 | ret | |
| 375 | 393 | | |
| 376 | 394 | ; draw_floor_cell — paint the floor tile at (row B, col C). Lighting (Unit 13) | |
| ... | |||
| 423 | 441 | defb %01111110 | |
| 424 | 442 | defb %00111100 | |
| 425 | 443 | defb %00000000 | |
| 444 | + | | |
| 445 | + | ; ---------------------------------------------------------------------------- | |
| 446 | + | ; show_title — black screen, the keep's name and a prompt; wait for SPACE (and | |
| 447 | + | ; release). Silent for now — the composed theme arrives in Unit 18. Words use the | |
| 448 | + | ; ROM's own 8x8 font at $3D00. | |
| 449 | + | ; ---------------------------------------------------------------------------- | |
| 450 | + | show_title: | |
| 451 | + | di | |
| 452 | + | call clear_screen | |
| 453 | + | ld hl, str_title | |
| 454 | + | ld b, 8 | |
| 455 | + | ld c, 11 | |
| 456 | + | call print_string | |
| 457 | + | ld hl, str_prompt | |
| 458 | + | ld b, 16 | |
| 459 | + | ld c, 6 | |
| 460 | + | call print_string | |
| 461 | + | .tt_wait: | |
| 462 | + | ld bc, KEYS_SPACE | |
| 463 | + | in a, (c) | |
| 464 | + | bit 0, a | |
| 465 | + | jr nz, .tt_wait | |
| 466 | + | .tt_rel: | |
| 467 | + | ld bc, KEYS_SPACE | |
| 468 | + | in a, (c) | |
| 469 | + | bit 0, a | |
| 470 | + | jr z, .tt_rel | |
| 471 | + | ret | |
| 472 | + | | |
| 473 | + | ; show_win — the keep is cleared. Black screen, the words, the flourish, then | |
| 474 | + | ; wait for SPACE (and release) to return to the title. | |
| 475 | + | show_win: | |
| 476 | + | di | |
| 477 | + | call clear_screen | |
| 478 | + | ld hl, str_won | |
| 479 | + | ld b, 9 | |
| 480 | + | ld c, 8 | |
| 481 | + | call print_string | |
| 482 | + | ld hl, str_again | |
| 483 | + | ld b, 15 | |
| 484 | + | ld c, 5 | |
| 485 | + | call print_string | |
| 486 | + | call sfx_win | |
| 487 | + | .sw_wait: | |
| 488 | + | ld bc, KEYS_SPACE | |
| 489 | + | in a, (c) | |
| 490 | + | bit 0, a | |
| 491 | + | jr nz, .sw_wait | |
| 492 | + | .sw_rel: | |
| 493 | + | ld bc, KEYS_SPACE | |
| 494 | + | in a, (c) | |
| 495 | + | bit 0, a | |
| 496 | + | jr z, .sw_rel | |
| 497 | + | ret | |
| 498 | + | | |
| 499 | + | ; win — the last coin is gone. Flourish, and raise the flag the loop watches. | |
| 500 | + | win: | |
| 501 | + | ld a, 1 | |
| 502 | + | ld (won), a | |
| 503 | + | call sfx_win | |
| 504 | + | ret | |
| 505 | + | | |
| 506 | + | ; sfx_win — an ascending flourish: a low note climbing in steps to a high one. | |
| 507 | + | sfx_win: | |
| 508 | + | ld c, 60 | |
| 509 | + | .swf_loop: | |
| 510 | + | ld b, 12 | |
| 511 | + | push bc | |
| 512 | + | call beep | |
| 513 | + | pop bc | |
| 514 | + | ld a, c | |
| 515 | + | sub 8 | |
| 516 | + | ld c, a | |
| 517 | + | cp 16 | |
| 518 | + | jr nc, .swf_loop | |
| 519 | + | ret | |
| 520 | + | | |
| 521 | + | ; clear_screen — fill bitmap and attributes with zero: a black screen. | |
| 522 | + | clear_screen: | |
| 523 | + | ld hl, $4000 | |
| 524 | + | ld (hl), 0 | |
| 525 | + | ld de, $4001 | |
| 526 | + | ld bc, 6911 | |
| 527 | + | ldir | |
| 528 | + | ret | |
| 529 | + | | |
| 530 | + | ; print_string — HL -> zero-terminated text, B = row, C = col. | |
| 531 | + | print_string: | |
| 532 | + | .ps_loop: | |
| 533 | + | ld a, (hl) | |
| 534 | + | or a | |
| 535 | + | ret z | |
| 536 | + | push hl | |
| 537 | + | call print_char | |
| 538 | + | pop hl | |
| 539 | + | inc hl | |
| 540 | + | inc c | |
| 541 | + | jr .ps_loop | |
| 542 | + | | |
| 543 | + | ; print_char — A = character (32..127), B = row, C = col. Copies the ROM font | |
| 544 | + | ; glyph at $3D00 + (char-32)*8 to the screen, bright white. | |
| 545 | + | print_char: | |
| 546 | + | push bc | |
| 547 | + | sub 32 | |
| 548 | + | ld l, a | |
| 549 | + | ld h, 0 | |
| 550 | + | add hl, hl | |
| 551 | + | add hl, hl | |
| 552 | + | add hl, hl | |
| 553 | + | ld de, $3D00 | |
| 554 | + | add hl, de | |
| 555 | + | push hl | |
| 556 | + | call scr_addr_cr | |
| 557 | + | pop de | |
| 558 | + | ld b, 8 | |
| 559 | + | .pc_row: | |
| 560 | + | ld a, (de) | |
| 561 | + | ld (hl), a | |
| 562 | + | inc de | |
| 563 | + | inc h | |
| 564 | + | djnz .pc_row | |
| 565 | + | pop bc | |
| 566 | + | call attr_addr_cr | |
| 567 | + | ld (hl), %01000111 | |
| 568 | + | ret | |
| 569 | + | | |
| 570 | + | str_title: | |
| 571 | + | defb "SHADOWKEEP", 0 | |
| 572 | + | str_prompt: | |
| 573 | + | defb "PRESS SPACE TO ENTER", 0 | |
| 574 | + | str_won: | |
| 575 | + | defb "THE KEEP STANDS", 0 | |
| 576 | + | str_again: | |
| 577 | + | defb "PRESS SPACE TO RETURN", 0 | |
| 426 | 578 | | |
| 427 | 579 | scr_addr_cr: | |
| 428 | 580 | ld a, b | |
| ... | |||
| 624 | 776 | defb 0, 0, 0, 0, 0, 0, 0, 0, 0 | |
| 625 | 777 | gold_remaining: | |
| 626 | 778 | defb TOTAL_GOLD | |
| 779 | + | won: | |
| 780 | + | defb 0 | |
| 627 | 781 | | |
| 628 | 782 | room0_state: | |
| 629 | 783 | defs 768 |
The complete program
; Shadowkeep — Unit 9: The Keep's Gold
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-02 makes the last coin win: THE KEEP STANDS, in a title -> play -> win loop.
org 32768
WALL_ATTR equ %01001000
FLOOR_ATTR equ %00001000
MARK_ATTR equ %00001111
THIEF equ %01001010
GOLD_ATTR equ %00000110 ; yellow ink, no BRIGHT -> walkable gold
TOTAL_GOLD equ 6
WALL_BIT equ 6
START_COL equ 15
START_ROW equ 11
NO_EXIT equ $FF
KEYS_OP equ $DFFE
KEYS_Q equ $FBFE
KEYS_A equ $FDFE
KEYS_SPACE equ $7FFE
start:
ld a, 0
out ($FE), a
im 1
; --- TITLE -> PLAY -> WIN -> TITLE ------------------------------------------
main_title:
call show_title ; returns (interrupts off) when SPACE pressed
call new_game ; reset the keep, place the gold, draw
ei
.game_loop:
halt
ld a, (won)
or a
jr nz, .won
call player_step
call mark_step
jr .game_loop
.won:
call show_win ; "THE KEEP STANDS", flourish, wait for SPACE
jr main_title ; round again
; new_game — copy the room templates back into working state (so collected gold
; returns), reset the thief, the gold counter and the win flag, draw the keep.
new_game:
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 (won), a
ld a, TOTAL_GOLD
ld (gold_remaining), 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
ret
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
draw_room:
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)
call lookup_tile
call draw_tile
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 ; the new cell might be gold — lift it first
call save_under
call draw_thief
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 draw_room
call save_under
call draw_thief
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
; ----------------------------------------------------------------------------
; The gold — the keep's goal. Step onto a coin and it lifts: the map cell turns
; to floor (gone for good), the cell repaints as plain floor (no lighting yet),
; a chime sounds, and the counter ticks down.
; ----------------------------------------------------------------------------
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 ; repaint the cell as plain floor
call sfx_pickup
ld hl, gold_remaining
dec (hl)
ld a, (hl)
or a
call z, win ; the last coin — the keep stands
ret
; draw_floor_cell — paint the floor tile at (row B, col C). Lighting (Unit 13)
; will replace this with a shade chosen by distance from the nearest torch.
draw_floor_cell:
ld hl, floor_tile
ld (tile_ptr), hl
ld a, FLOOR_ATTR
ld (tile_attr), a
call draw_tile
ret
; ----------------------------------------------------------------------------
; beep — the one tone primitive (the tiny game's blip, reused). B = cycles,
; C = the delay between speaker toggles (the pitch; larger C = lower).
; ----------------------------------------------------------------------------
beep:
.bp_cycle:
ld a, %00010000
out ($FE), a
ld e, c
.bp_hi:
dec e
jr nz, .bp_hi
xor a
out ($FE), a
ld e, c
.bp_lo:
dec e
jr nz, .bp_lo
djnz .bp_cycle
ret
; sfx_pickup — a bright two-blip chime: the sound of gold.
sfx_pickup:
ld b, 8
ld c, 20
call beep
ld b, 8
ld c, 14
call beep
ret
gold_tile:
defb %00000000
defb %00111100
defb %01111110
defb %01111110
defb %01111110
defb %01111110
defb %00111100
defb %00000000
; ----------------------------------------------------------------------------
; show_title — black screen, the keep's name and a prompt; wait for SPACE (and
; release). Silent for now — the composed theme arrives in Unit 18. Words use the
; ROM's own 8x8 font at $3D00.
; ----------------------------------------------------------------------------
show_title:
di
call clear_screen
ld hl, str_title
ld b, 8
ld c, 11
call print_string
ld hl, str_prompt
ld b, 16
ld c, 6
call print_string
.tt_wait:
ld bc, KEYS_SPACE
in a, (c)
bit 0, a
jr nz, .tt_wait
.tt_rel:
ld bc, KEYS_SPACE
in a, (c)
bit 0, a
jr z, .tt_rel
ret
; show_win — the keep is cleared. Black screen, the words, the flourish, then
; wait for SPACE (and release) to return to the title.
show_win:
di
call clear_screen
ld hl, str_won
ld b, 9
ld c, 8
call print_string
ld hl, str_again
ld b, 15
ld c, 5
call print_string
call sfx_win
.sw_wait:
ld bc, KEYS_SPACE
in a, (c)
bit 0, a
jr nz, .sw_wait
.sw_rel:
ld bc, KEYS_SPACE
in a, (c)
bit 0, a
jr z, .sw_rel
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
; sfx_win — an ascending flourish: a low note climbing in steps to a high one.
sfx_win:
ld c, 60
.swf_loop:
ld b, 12
push bc
call beep
pop bc
ld a, c
sub 8
ld c, a
cp 16
jr nc, .swf_loop
ret
; clear_screen — fill bitmap and attributes with zero: a black screen.
clear_screen:
ld hl, $4000
ld (hl), 0
ld de, $4001
ld bc, 6911
ldir
ret
; print_string — HL -> zero-terminated text, B = row, C = col.
print_string:
.ps_loop:
ld a, (hl)
or a
ret z
push hl
call print_char
pop hl
inc hl
inc c
jr .ps_loop
; print_char — A = character (32..127), B = row, C = col. Copies the ROM font
; glyph at $3D00 + (char-32)*8 to the screen, bright white.
print_char:
push bc
sub 32
ld l, a
ld h, 0
add hl, hl
add hl, hl
add hl, hl
ld de, $3D00
add hl, de
push hl
call scr_addr_cr
pop de
ld b, 8
.pc_row:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .pc_row
pop bc
call attr_addr_cr
ld (hl), %01000111
ret
str_title:
defb "SHADOWKEEP", 0
str_prompt:
defb "PRESS SPACE TO ENTER", 0
str_won:
defb "THE KEEP STANDS", 0
str_again:
defb "PRESS SPACE TO RETURN", 0
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 floor_tile
defb FLOOR_ATTR
defb '#'
defw wall_tile
defb WALL_ATTR
defb '+'
defw mark_tile
defb MARK_ATTR
defb 'G'
defw gold_tile
defb GOLD_ATTR
; ----------------------------------------------------------------------------
; The keep: three rooms. Hall -east-> Gallery -north-> Vault, and back.
; entry: map ptr, North, South, East, West
; ----------------------------------------------------------------------------
rooms:
defw room0_state
defb NO_EXIT, NO_EXIT, 1, NO_EXIT ; Hall: east -> Gallery
defw room1_state
defb 2, NO_EXIT, NO_EXIT, 0 ; Gallery: north -> Vault, west -> Hall
defw room2_state
defb NO_EXIT, 1, NO_EXIT, NO_EXIT ; Vault: south -> Gallery
; The Great Hall — four pillars, east door at row 11.
room0_template:
defb "################################"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#.........................G....#"
defb "#..............................#"
defb "#........##..........##........#"
defb "#........##..........##........#"
defb "#..............................#"
defb "#..............................."
defb "#..............................#"
defb "#..............................#"
defb "#........##..........##........#"
defb "#........##..........##........#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#.........................G....#"
defb "#..............................#"
defb "#..............................#"
defb "################################"
; The Gallery — a dividing wall with one gap (column 15). West door (row 11)
; back to the Hall; north door (column 15) up to the Vault.
room1_template:
defb "###############.################"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "###############.################"
defb "#..............................#"
defb "#..............................#"
defb "........................G......#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#.......................G......#"
defb "#..............................#"
defb "#..............................#"
defb "################################"
; The Vault — a great altar of stone in the middle, south door (column 15)
; down to the Gallery.
room2_template:
defb "################################"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#.............####.............#"
defb "#.............####.............#"
defb "#.............####.............#"
defb "#.............####.............#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#.........G.........G..........#"
defb "#..............................#"
defb "#..............................#"
defb "###############.################"
floor_tile:
defb %10101010
defb %01010101
defb %10101010
defb %01010101
defb %10101010
defb %01010101
defb %10101010
defb %01010101
wall_tile:
defb %00010001
defb %00000000
defb %01000100
defb %00000000
defb %00010001
defb %00000000
defb %01000100
defb %00000000
mark_tile:
defb %00000000
defb %00011000
defb %00011000
defb %01111110
defb %01111110
defb %00011000
defb %00011000
defb %00000000
thief:
defb %00011000
defb %00111100
defb %01111110
defb %01111110
defb %01111110
defb %01111110
defb %00111100
defb %00100100
current_room:
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
gold_remaining:
defb TOTAL_GOLD
won:
defb 0
room0_state:
defs 768
room1_state:
defs 768
room2_state:
defs 768
end start
Collect the Hall’s two coins, cross to the Gallery and the Vault for the rest, and lift the sixth:
Try this: a coin behind the altar
Move a G in the Vault map to a cell hemmed in by the altar’s stone, or tuck one in a pillar’s shadow in the Hall. The keep is only cleared when every coin is lifted, so a coin you have to work to reach is a small puzzle — the beginning of designing a level, not just filling a room.
Try this: change the reward
sfx_pickup is two quick blips. Make the chime yours: a single low thunk for a heavy coin, or three rising blips for a richer prize. The sound a game makes when you succeed is half of why success feels good — spend a minute making yours feel good.
Try this: count up, not down
We count gold_remaining down to zero. Flip it: count coins collected up, and win when the total equals the number you placed. The same game, a different feeling — a score that grows, not a debt that shrinks. Which one suits a treasure hunt?
When it’s wrong, see why
- The coin vanishes but comes back. You repainted the screen but not the map — the redraw put the
Gback. Change the map cell to.as well; that is what makes it gone. - You win the instant you touch one coin. The counter is starting at 1, or
decruns before the count is set.gold_remainingmust be reset to the full count innew_game, before the loop begins. - The thief walks straight over gold without lifting it.
collect_goldisn’t called after a move, or it’s checking the old cell. It has to run once the thief has landed on the new cell, reading that cell’s contents. - Second game has no gold.
new_gameisn’t copying the templates back — the working rooms still hold the floors you left. Rebuild the rooms every game and the treasure returns. - The coin is solid — the thief stops dead at it.
GOLD_ATTRhas its BRIGHT bit set, sowall_atreads it as wall. Gold must be dim to be walkable.
Before and after
You began with three rooms and nothing to do in them, and finished with a keep that can be cleared — six coins to lift, a tally that falls, and a black screen with two words that mean you did it. Nothing exotic did the work: a walkable glyph, a question asked after each step, a counter, and a flag the loop watches. That flag is the important thing. It turned a walk that never ended into a game that can — and a game that can end is a game you can win.
What you’ve learnt
- A collectible is a walkable tile you lift on contact. Change it in the map, not just on screen, and it stays lifted.
- A goal is a counter with an ending. Track what remains; when it hits zero, the game is won.
- A win is a state, not a moment. A flag the loop watches, turning play into title → play → win → title — the shape every finished game shares.
- Replayable means rebuilt. Every new game copies the keep back from its templates, so the treasure — and the challenge — return.
What’s next
The keep can be cleared, but nothing stops you. You can take all the time in the world, walk any route, never lose a thing — and a game you cannot lose is only half a game. In Unit 10, “Something in the Dark,” the keep stops being empty: a second figure moves in it, walking a slow patrol of its own. It cannot hurt you yet — first we give it a body and a route. But the coins are about to get harder to reach.