Skip to content
Game 1 Unit 20 of 20 1 hr learning time

On Real Iron

The last truth: does it run on a real Spectrum? Build the finished game to a tape with an auto-running loader, watch the loading stripes, and verify Gloaming behaves on the metal exactly as it did on the emulator.

100% of Gloaming

Gloaming is finished — a complete game, played from a title and back again. There is one truth left, and it is the one that matters most: does it run on a real Spectrum? This unit takes the game off the emulator and onto the metal. No new mechanics — just the oldest ritual in Spectrum software: building a tape, watching the stripes, and seeing your game appear on actual hardware.

Where we start

Unit 19's finished game — the whole of Gloaming, but living as a .sna snapshot, a format only an emulator loads. We package the exact same code as a tape and load it the way a Spectrum has always loaded software.

The snapshot was a convenience

We have used .sna snapshots all along, because they are instant — the emulator pours the whole of memory into place and starts. A real Spectrum has no such shortcut. It loads from tape (or, on a Spectrum Next, from storage), through the ROM's loader, byte by byte. So to run on real iron, the finished game needs to be a tape, not a snapshot.

Building a tape

pasmonext builds one for us, complete with a tiny BASIC loader that loads the code and runs it:

# the snapshot — emulators, and the Next loads these too
pasmonext --sna gloaming.asm gloaming.sna

# a real tape image, with an auto-running BASIC loader
pasmonext --tapbas gloaming.asm gloaming.tap

--tapbas writes a .tap containing two blocks: a short BASIC program — CLEAR, LOAD ""CODE, RANDOMIZE USR 32768 — and the machine code itself. The loader auto-runs, so on a real machine you type LOAD "", press play, and the game loads and starts on its own. The entry address comes straight from our end start directive — the assembler and the loader agree on where the game begins.

Load it through the ROM and the screen tells you it is working the way Spectrum screens always have — the border filling with stripes as the bytes come in:

The classic ZX Spectrum loading screen: red and cyan horizontal stripes filling the border around a blank grey screen.
Gloaming loading from its own tape image, pulse by pulse through the ROM loader. Those red-and-cyan stripes are the sound of the 1980s made visible — the border colour flicked on every edge the loader reads.

When the stripes stop, the border goes black and the game starts itself — the same title you built, now running from a tape instead of a snapshot:

The GLOAMING title screen with PRESS SPACE below it, on a black screen — the game started straight from the tape load.
Loaded, and running on its own: the auto-running BASIC loader handed control to the code, and Gloaming came up at its title — from tape, exactly as it did from a snapshot.

Loading it for real

  • 48K / 128K Spectrum — write the .tap to tape (or use a DivMMC / tape-replay device), LOAD "", and watch the stripes.
  • Spectrum Next — copy gloaming.tap (or the .sna) to the SD card and load it from the browser; the Next runs 48K software faithfully. This is the most approachable "real hardware" today, and the one we target for the final sign-off.

Why it behaves the same

Here is the quiet reward for building carefully. Gloaming makes no assumptions that hold only on an emulator:

  • It is frame-lockedIM 1 + HALT pace it to the real 50 Hz interrupt, so it runs at exactly the right speed on any Spectrum, not "however fast the host is".
  • It reads the keyboard through the documented port $FE, and makes sound through the documented beeper bit — no undocumented tricks, no exact-contention timing, no host-specific behaviour.

So what you saw on the emulator is what you get on the metal. That is not luck; it is the result of every "do it the honest way" choice across the last nineteen units. A game built on quirks surprises you on real hardware. This one does not.

The finished game, entire

This is the whole of Gloaming in one file — the title and state machine, the lamplighter and the draught, lamps and tally and lives, sound and atmosphere, win, loss, and the loop back to the start. Everything you built, from Unit 1's first attribute write to here:

; ============================================================================
; GLOAMING — Unit 19: Again
; ============================================================================
; Gloaming is a finished game — but it ends and stays ended. A real game loops:
; you finish, and it offers you another go. This unit closes that loop. Win or
; lose, a key press returns you to the title, and pressing start there builds a
; brand-new game. Title -> play -> win/lose -> title -> play, round and round.
;
; The state machine (Unit 16) makes this almost free. The WIN and LOSE states
; were dead ends that just held their screen; now they each watch for a key and
; hand control back to TITLE. The crucial part is that starting again must give
; a *fresh* game — and it does, because init_game already rebuilds everything
; from scratch: lamps unlit, three lives, the walls cold and blue, the sprites
; at their starts. "Play again" and "start" are the same act.
;
; One real-world detail: DEBOUNCE. When you win, the key you were holding to
; play is probably still down; without care it would dismiss the win screen and
; start a new game in the same instant. So on entering any screen that waits for
; a key, we set a short input_lock countdown and ignore the key until it expires
; — a beat to let go and read the screen.
; ============================================================================

            org     32768

COBBLE      equ     %00000001
WALL        equ     %00001111
LAMP_ATTR   equ     %01000111
LAMP_UNLIT  equ     %00000101
LAMP_LIT    equ     %01000110
WALL_BIT    equ     3

DRAUGHT_ATTR  equ   %01000101
DRAUGHT_SPEED equ   8

PIP_UNLIT   equ     %00101000
PIP_LIT     equ     %01110000
PIP_BASE    equ     $5800 + 12
NUM_LAMPS   equ     8

LIVES       equ     3
LIFE_PIP    equ     %01010000
LIFE_BASE   equ     $5800 + 28

MSG_ATTR    equ     %01000111
MSG_ROW     equ     11
WIN_COL     equ     7
LOSE_COL    equ     10
TITLE_COL   equ     12
PROMPT_COL  equ     10
TITLE_ROW   equ     8
PROMPT_ROW  equ     14
CONT_ROW    equ     16              ; "PRESS SPACE" under a win/lose line
FONT        equ     $3C00

SPEAKER     equ     %00010000

STATE_TITLE equ     0
STATE_PLAY  equ     1
STATE_WIN   equ     2
STATE_LOSE  equ     3
LOCK        equ     25              ; input-lock frames after entering a screen

START_COL   equ     15
START_ROW   equ     11
DRAUGHT_COL0 equ    18
DRAUGHT_ROW0 equ    3

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

; ============================================================================
; SETUP.
; ============================================================================
start:
            ld      a, 0
            out     ($FE), a
            ld      a, STATE_TITLE
            ld      (game_state), a
            call    draw_title_screen   ; no startup lock — nothing to debounce yet
            im      1
            ei

main_loop:
            halt
            ld      a, (game_state)
            cp      STATE_TITLE
            jr      z, .do_title
            cp      STATE_PLAY
            jr      z, .do_play
            call    end_step            ; WIN or LOSE — wait for a key, then title
            jr      main_loop
.do_title:
            call    title_step
            jr      main_loop
.do_play:
            call    play_step
            jr      main_loop

; ----------------------------------------------------------------------------
; title_step — after the lock, SPACE starts a fresh game.
; ----------------------------------------------------------------------------
title_step:
            ld      a, (input_lock)
            or      a
            jr      z, .tready
            dec     a
            ld      (input_lock), a
            ret
.tready:
            ld      bc, KEYS_SPACE
            in      a, (c)
            bit     0, a
            ret     nz
            call    init_game
            ld      a, STATE_PLAY
            ld      (game_state), a
            ret

; ----------------------------------------------------------------------------
; end_step — WIN/LOSE: after the lock, SPACE returns to the title.
; ----------------------------------------------------------------------------
end_step:
            ld      a, (input_lock)
            or      a
            jr      z, .eready
            dec     a
            ld      (input_lock), a
            ret
.eready:
            ld      bc, KEYS_SPACE
            in      a, (c)
            bit     0, a
            ret     nz
            call    draw_title_screen
            ld      a, LOCK
            ld      (input_lock), a
            ld      a, STATE_TITLE
            ld      (game_state), a
            ret

play_step:
            call    player_step
            ld      a, (game_state)
            cp      STATE_PLAY
            ret     nz
            call    draught_step
            ld      a, (game_state)
            cp      STATE_PLAY
            ret     nz
            ld      a, (lit_count)
            cp      NUM_LAMPS
            ret     nz
            call    draw_win_screen
            ld      a, LOCK
            ld      (input_lock), a
            ld      a, STATE_WIN
            ld      (game_state), a
            ret

init_game:
            xor     a
            ld      (lit_count), a
            ld      a, LIVES
            ld      (lives), a
            ld      a, START_COL
            ld      (lamp_col), a
            ld      a, START_ROW
            ld      (lamp_row), a
            ld      a, DRAUGHT_COL0
            ld      (draught_col), a
            ld      a, DRAUGHT_ROW0
            ld      (draught_row), a
            ld      a, 1
            ld      (draught_dx), a
            ld      (draught_dy), a
            ld      a, DRAUGHT_SPEED
            ld      (draught_timer), a

            call    clear_bitmap
            ld      hl, $5800
            ld      de, $5801
            ld      (hl), COBBLE
            ld      bc, 767
            ldir
            call    warm_walls
            call    draw_pips
            call    draw_lives
            call    draw_lamps
            call    save_under
            call    draw_lamp
            call    save_draught
            call    draw_draught
            ret

warm_walls:
            ld      a, (lit_count)
            ld      e, a
            ld      d, 0
            ld      hl, wall_ramp
            add     hl, de
            ld      c, (hl)
            ld      hl, $5820
            ld      b, 32
.wt:
            ld      (hl), c
            inc     hl
            djnz    .wt
            ld      hl, $5AE0
            ld      b, 32
.wb:
            ld      (hl), c
            inc     hl
            djnz    .wb
            ld      hl, $5820
            ld      b, 23
.ws:
            ld      (hl), c
            push    hl
            ld      de, 31
            add     hl, de
            ld      (hl), c
            pop     hl
            ld      de, 32
            add     hl, de
            djnz    .ws
            ret

beep:
            di
.bcyc:
            ld      a, SPEAKER
            out     ($FE), a
            ld      a, c
.bd1:
            dec     a
            jr      nz, .bd1
            xor     a
            out     ($FE), a
            ld      a, c
.bd2:
            dec     a
            jr      nz, .bd2
            djnz    .bcyc
            ei
            ret

blip_lit:
            ld      b, $20
            ld      c, $18
            jp      beep

blip_snuff:
            ld      b, $1A
            ld      c, $40
            jp      beep

; ----------------------------------------------------------------------------
; Screens. Win and lose now invite another go.
; ----------------------------------------------------------------------------
draw_title_screen:
            call    clear_bitmap
            ld      hl, $5800
            ld      de, $5801
            ld      (hl), %00000000
            ld      bc, 767
            ldir
            ld      hl, title_text
            ld      b, TITLE_ROW
            ld      c, TITLE_COL
            call    print_string
            ld      hl, prompt_text
            ld      b, PROMPT_ROW
            ld      c, PROMPT_COL
            call    print_string
            ret

draw_win_screen:
            call    restore_under
            ld      hl, win_text
            ld      b, MSG_ROW
            ld      c, WIN_COL
            call    print_string
            ld      hl, prompt_text
            ld      b, CONT_ROW
            ld      c, PROMPT_COL
            call    print_string
            ret

draw_lose_screen:
            ld      hl, $5800
            ld      de, $5801
            ld      (hl), %00000000
            ld      bc, 767
            ldir
            ld      hl, lose_text
            ld      b, MSG_ROW
            ld      c, LOSE_COL
            call    print_string
            ld      hl, prompt_text
            ld      b, CONT_ROW
            ld      c, PROMPT_COL
            call    print_string
            ret

clear_bitmap:
            ld      hl, $4000
            ld      de, $4001
            ld      (hl), 0
            ld      bc, 6143
            ldir
            ret

; ----------------------------------------------------------------------------
; player_step.
; ----------------------------------------------------------------------------
player_step:
            ld      a, (lamp_col)
            ld      (tcol), a
            ld      a, (lamp_row)
            ld      (trow), a

            ld      bc, KEYS_OP
            in      a, (c)
            bit     1, a
            jr      z, .pleft
            bit     0, a
            jr      z, .pright
            ld      bc, KEYS_Q
            in      a, (c)
            bit     0, a
            jr      z, .pup
            ld      bc, KEYS_A
            in      a, (c)
            bit     0, a
            jr      z, .pdown
            ret

.pleft:
            ld      hl, tcol
            dec     (hl)
            jr      .pmove
.pright:
            ld      hl, tcol
            inc     (hl)
            jr      .pmove
.pup:
            ld      hl, trow
            dec     (hl)
            jr      .pmove
.pdown:
            ld      hl, trow
            inc     (hl)
.pmove:
            ld      a, (trow)
            ld      b, a
            ld      a, (tcol)
            ld      c, a
            call    wall_at
            ret     nz

            ld      a, (tcol)
            ld      hl, draught_col
            cp      (hl)
            jr      nz, .pcommit
            ld      a, (trow)
            ld      hl, draught_row
            cp      (hl)
            jr      nz, .pcommit
            call    lose_life
            ret

.pcommit:
            call    restore_under
            ld      a, (tcol)
            ld      (lamp_col), a
            ld      a, (trow)
            ld      (lamp_row), a
            call    save_under
            ld      a, (under_lamp + 8)
            cp      LAMP_UNLIT
            jr      nz, .pdrawn
            ld      a, LAMP_LIT
            ld      (under_lamp + 8), a
            call    light_pip
            call    blip_lit
            call    warm_walls
.pdrawn:
            call    draw_lamp
            ret

; ----------------------------------------------------------------------------
; draught_step.
; ----------------------------------------------------------------------------
draught_step:
            ld      a, (draught_timer)
            dec     a
            ld      (draught_timer), a
            ret     nz
            ld      a, DRAUGHT_SPEED
            ld      (draught_timer), a

            ld      a, (draught_col)
            ld      b, a
            ld      a, (draught_dx)
            add     a, b
            ld      c, a
            ld      a, (draught_row)
            ld      b, a
            call    wall_at
            jr      z, .hok
            ld      a, (draught_dx)
            neg
            ld      (draught_dx), a
.hok:
            ld      a, (draught_row)
            ld      b, a
            ld      a, (draught_dy)
            add     a, b
            ld      b, a
            ld      a, (draught_col)
            ld      c, a
            call    wall_at
            jr      z, .vok
            ld      a, (draught_dy)
            neg
            ld      (draught_dy), a
.vok:
            ld      a, (draught_col)
            ld      b, a
            ld      a, (draught_dx)
            add     a, b
            ld      (dtcol), a
            ld      a, (draught_row)
            ld      b, a
            ld      a, (draught_dy)
            add     a, b
            ld      (dtrow), a

            ld      a, (dtcol)
            ld      hl, lamp_col
            cp      (hl)
            jr      nz, .dmove
            ld      a, (dtrow)
            ld      hl, lamp_row
            cp      (hl)
            jr      nz, .dmove
            call    lose_life
            ret

.dmove:
            call    restore_draught
            ld      a, (dtcol)
            ld      (draught_col), a
            ld      a, (dtrow)
            ld      (draught_row), a
            call    save_draught
            ld      a, (under_draught + 8)
            cp      LAMP_LIT
            jr      nz, .nosnuff
            ld      a, LAMP_UNLIT
            ld      (under_draught + 8), a
            call    unlight_pip
            call    blip_snuff
            call    warm_walls
.nosnuff:
            call    draw_draught
            ret

lose_life:
            ld      a, (lives)
            dec     a
            ld      (lives), a
            ld      e, a
            ld      d, 0
            ld      hl, LIFE_BASE
            add     hl, de
            ld      (hl), COBBLE
            ld      a, (lives)
            or      a
            jr      z, .gone
            call    restore_under
            ld      a, START_COL
            ld      (lamp_col), a
            ld      a, START_ROW
            ld      (lamp_row), a
            call    save_under
            call    draw_lamp
            ret
.gone:
            call    draw_lose_screen
            ld      a, LOCK
            ld      (input_lock), a
            ld      a, STATE_LOSE
            ld      (game_state), a
            ret

; ----------------------------------------------------------------------------
; print_string / print_char.
; ----------------------------------------------------------------------------
print_string:
.ps:
            ld      a, (hl)
            cp      $FF
            ret     z
            push    hl
            push    bc
            call    print_char
            pop     bc
            pop     hl
            inc     hl
            inc     c
            jr      .ps

print_char:
            ld      l, a
            ld      h, 0
            add     hl, hl
            add     hl, hl
            add     hl, hl
            ld      de, FONT
            add     hl, de
            ex      de, hl
            push    de
            call    attr_addr_cr
            ld      (hl), MSG_ATTR
            call    scr_addr_cr
            pop     de
            ld      b, 8
.pc:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .pc
            ret

; ----------------------------------------------------------------------------
; light_pip / unlight_pip / draw_pips / draw_lives.
; ----------------------------------------------------------------------------
light_pip:
            ld      a, (lit_count)
            ld      e, a
            ld      d, 0
            inc     a
            ld      (lit_count), a
            ld      hl, PIP_BASE
            add     hl, de
            ld      (hl), PIP_LIT
            ret

unlight_pip:
            ld      a, (lit_count)
            dec     a
            ld      (lit_count), a
            ld      e, a
            ld      d, 0
            ld      hl, PIP_BASE
            add     hl, de
            ld      (hl), PIP_UNLIT
            ret

draw_pips:
            ld      hl, PIP_BASE
            ld      b, NUM_LAMPS
            ld      a, PIP_UNLIT
.dp:
            ld      (hl), a
            inc     hl
            djnz    .dp
            ret

draw_lives:
            ld      hl, LIFE_BASE
            ld      b, LIVES
            ld      a, LIFE_PIP
.dlv:
            ld      (hl), a
            inc     hl
            djnz    .dlv
            ret

; ----------------------------------------------------------------------------
; draw_lamps / draw_lantern.
; ----------------------------------------------------------------------------
draw_lamps:
            ld      hl, lamp_data
.next:
            ld      a, (hl)
            cp      $FF
            ret     z
            ld      c, a
            inc     hl
            ld      b, (hl)
            inc     hl
            push    hl
            call    draw_lantern
            pop     hl
            jr      .next

draw_lantern:
            call    attr_addr_cr
            ld      (hl), LAMP_UNLIT
            call    scr_addr_cr
            ld      de, lantern
            ld      b, 8
.dlt:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .dlt
            ret

; ----------------------------------------------------------------------------
; scr_addr_cr / attr_addr_cr / wall_at.
; ----------------------------------------------------------------------------
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

wall_at:
            call    attr_addr_cr
            bit     WALL_BIT, (hl)
            ret

; ----------------------------------------------------------------------------
; The lamplighter's save / restore / draw.
; ----------------------------------------------------------------------------
pos_bc:
            ld      a, (lamp_row)
            ld      b, a
            ld      a, (lamp_col)
            ld      c, a
            ret

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

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

draw_lamp:
            call    pos_bc
            call    attr_addr_cr
            ld      (hl), LAMP_ATTR
            call    pos_bc
            call    scr_addr_cr
            ld      de, lamplighter
            ld      b, 8
.dl:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .dl
            ret

; ----------------------------------------------------------------------------
; The draught's save / restore / draw.
; ----------------------------------------------------------------------------
dpos_bc:
            ld      a, (draught_row)
            ld      b, a
            ld      a, (draught_col)
            ld      c, a
            ret

save_draught:
            call    dpos_bc
            call    scr_addr_cr
            ld      de, under_draught
            ld      b, 8
.sd:
            ld      a, (hl)
            ld      (de), a
            inc     de
            inc     h
            djnz    .sd
            call    dpos_bc
            call    attr_addr_cr
            ld      a, (hl)
            ld      (under_draught + 8), a
            ret

restore_draught:
            call    dpos_bc
            call    scr_addr_cr
            ld      de, under_draught
            ld      b, 8
.rd:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .rd
            call    dpos_bc
            call    attr_addr_cr
            ld      a, (under_draught + 8)
            ld      (hl), a
            ret

draw_draught:
            call    dpos_bc
            call    attr_addr_cr
            ld      (hl), DRAUGHT_ATTR
            call    dpos_bc
            call    scr_addr_cr
            ld      de, draught_glyph
            ld      b, 8
.dd:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .dd
            ret

; ----------------------------------------------------------------------------
; Data.
; ----------------------------------------------------------------------------
game_state:
            defb    STATE_TITLE
input_lock:
            defb    0

wall_ramp:
            defb    %00001111
            defb    %00001111
            defb    %01001111
            defb    %01001111
            defb    %00011111
            defb    %00011111
            defb    %01011111
            defb    %01011111
            defb    %01011111

lamp_data:
            defb    4, 3
            defb    27, 3
            defb    9, 7
            defb    22, 7
            defb    6, 15
            defb    25, 15
            defb    13, 20
            defb    18, 20
            defb    $FF

lamp_col:
            defb    START_COL
lamp_row:
            defb    START_ROW
tcol:
            defb    0
trow:
            defb    0
lit_count:
            defb    0
lives:
            defb    LIVES

draught_col:
            defb    DRAUGHT_COL0
draught_row:
            defb    DRAUGHT_ROW0
draught_dx:
            defb    1
draught_dy:
            defb    1
draught_timer:
            defb    DRAUGHT_SPEED
dtcol:
            defb    0
dtrow:
            defb    0

under_lamp:
            defb    0, 0, 0, 0, 0, 0, 0, 0, 0
under_draught:
            defb    0, 0, 0, 0, 0, 0, 0, 0, 0

lamplighter:
            defb    %00111100
            defb    %00111100
            defb    %00011000
            defb    %01111110
            defb    %00011000
            defb    %00011000
            defb    %00100100
            defb    %01000010

lantern:
            defb    %00011000
            defb    %00100100
            defb    %01111110
            defb    %01111110
            defb    %01011010
            defb    %01111110
            defb    %01111110
            defb    %00111100

draught_glyph:
            defb    %00000000
            defb    %00111100
            defb    %01111110
            defb    %11111111
            defb    %11111111
            defb    %01111110
            defb    %00111100
            defb    %00000000

title_text:
            defb    "GLOAMING"
            defb    $FF
prompt_text:
            defb    "PRESS SPACE"
            defb    $FF
win_text:
            defb    "THE NIGHT IS HELD"
            defb    $FF
lose_text:
            defb    "NIGHT FALLS"
            defb    $FF

            end     start

When it's wrong, see why

Real hardware has its own short list of failure modes:

  • R Tape loading error on a real cassette — almost always volume or azimuth on the replay side; on a Next/SD or a digital tape device it loads clean. Rebuild the .tap and try again.
  • Loads, but does not start. The BASIC loader's RANDOMIZE USR address must match the code's origin. --tapbas takes it from end start, so keep org 32768 and end start in step.
  • Runs, but feels wrong. If it ever ran differently on hardware than on the emulator, suspect a reliance on host timing — but Gloaming's frame-lock and documented I/O are exactly there to stop that. It should feel identical.

Before and after

You started this unit with a finished game that only an emulator could load and ended it with the same code on a real tape, loading through the same ROM routine a Spectrum used in 1983 — and nothing about the game had to change. That is the whole point: a snapshot was a convenience, a tape is the truth, and a game built on documented behaviour crosses from one to the other without a flinch. The loading stripes are the ROM loader doing its oldest job; the title that follows is yours.

Try this: load it on your own hardware

This is the unit only you can finish. Get gloaming.tap (or the .sna) onto a real Spectrum Next, or a 48K with a tape-replay device, and play it. Confirm every piece: the title waits, QAOP moves, lamps light with a blip, the draught snuffs and chases, the walls warm, the win and loss screens appear, and space starts you over. When it all behaves on the metal exactly as it did on screen — that is the sign-off. That is your hardware truth.

Try this: a loading screen

Real Spectrum games greeted you with a picture while they loaded. Add one: a 6912-byte SCREEN$ (the full bitmap plus attributes) as a tape block before the code, and the artwork appears under the loading stripes. Drawing it is a project in itself — but it is the final flourish that makes a tape feel like a published game.

Try this: make a label

You have made a real, loadable, period-faithful game. Print an inlay. Give it a price in 1983 pounds. It earned the £1.99 budget-classic treatment — and writing the back-of-the-box blurb is a surprisingly good test of whether you can say what your game is.

What you've learnt

  • Real hardware loads from tape or storage, not snapshots; build a .tap with an auto-running BASIC loader (pasmonext --tapbas).
  • The loading stripes are the ROM loader — the same code path on the emulator and on the metal.
  • A frame-locked game using only documented I/O behaves identically on real hardware — the payoff of building it honestly.
  • The final verification is yours to make, on your own iron.

What's next

You have finished Gloaming — a complete, winnable, losable, replayable game, built in Z80 assembly from a single attribute write and verified on real hardware. That is no small thing: most people who set out to write an assembly game never finish one. You have.

Next in the track is Shadowkeep, the first substantial game — and it does not start from scratch. The cell sprite, the save/restore, the collision, the game loop, the state machine, the HUD, the sound: every one of them was born here, in Gloaming, and Shadowkeep picks them up and builds a whole walled keep on top. You did not just make a game. You made the engine your next game runs on.