Skip to content

Query a Byte Map on ColecoVision

Read and change map cells, look up tile properties and convert world positions with reusable Z80 code and an explicit memory contract.

A level does not need to be on screen for the game to ask what is there. Give it a byte per cell, keep those bytes in row order, and the same map can answer questions about walls, hazards, goals or scenery that has changed.

This implementation supplies four routines: checked get, checked set, property lookup and conversion from unsigned world positions to eight-unit cells. It does not draw or wait for a frame. The shared entry explains the arrangement and links the implementations across all 30 machines.

Include and initialise

Define TM as the start of a reserved 20-byte context, then include the routine. The map contains W×H consecutive bytes, with width and height each from 1 to 255. Its property table contains one byte per possible ID, through the configured highest ID. Both ranges must remain addressable without crossing an address-space end, bank window or unmapped boundary. Use writable RAM for tile_set; a ROM map needs another way to record changes.

Pointers and world-coordinate words are little endian.

Context offsets: 0–1 map pointer; 2/3 width/height; 4/5 column/row; 6 tile; 7 valid; 8–10 scratch; 11 property; 12–13 property pointer; 14 highest tile ID; 15 scratch; 16–17 X; 18–19 Y. Initialise the pointers, dimensions and highest ID before a call. Scratch bytes belong to the routine.

Initialise the diagnostic map and make its first checked query

demo.asm, excerpt
start:
    di
    ld sp,$63f0
    ld hl,TM
    ld b,20
    xor a
clear_context:
    ld (hl),a
    inc hl
    dec b
    jr nz,clear_context
    ld hl,initial_map
    ld de,MAP
    ld b,35
copy_map:
    ld a,(hl)
    ld (de),a
    inc hl
    inc de
    dec b
    jr nz,copy_map
    ld a,$a5
    ld (MAP+35),a
    ld a,240
    ld (TM+0),a
    ld a,96
    ld (TM+1),a
    ld a,7
    ld (TM+2),a
    ld a,5
    ld (TM+3),a
    ld a,3
    ld (TM+14),a
    ld hl,properties
    ld a,l
    ld (TM+12),a
    ld a,h
    ld (TM+13),a
    ld a,0
    ld (TM+4),a
    ld a,0
    ld (TM+5),a
    call tile_get
    ld a,(TM+7)
    cp 1
    jr z,valid_0
    ld a,255
    ld (TM+6),a

The complete diagnostic uses a 7×5 map. Its context starts at $6000, results at $6040 and writable map at $60F0. These locations belong to a caller taking over the machine; choose and reserve suitable memory in your own project, accounting for firmware, display, banking and interrupts.

Call the routine

Set column and row, call tile_get, then check the valid byte. A successful call returns the tile ID. Failure returns valid=0 and tile=0, so tile zero alone does not mean success. This compact variant uses unsigned byte coordinates; wider maps can use a wider-coordinate variant rather than treating this as a machine limit. Translate a signed or wider world into this representation explicitly.

For a write, set column/row/tile and call tile_set. Invalid coordinates or an ID above the configured maximum leave the map unchanged. To classify a tile, call tile_property; it returns the table byte and a separate valid flag. Your game decides whether those property bits mean blocking, damage, a goal or something else.

Reusable Z80 map/query implementation

z80.inc
; Byte map, caller-owned TM..TM+19 context. AF/BC/DE/HL clobbered.
; Only instructions shared with SM83; the GB file uses its source syntax.
tile_locate:
    xor a
    ld (TM+7),a
    ld a,(TM+2)
    ld b,a
    ld a,(TM+4)
    cp b
    ret nc
    ld a,(TM+3)
    ld b,a
    ld a,(TM+5)
    cp b
    ret nc
    ld b,a
    ld a,(TM)
    ld l,a
    ld a,(TM+1)
    ld h,a
    ld a,(TM+2)
    ld e,a
    ld d,0
    ld a,b
    or a
    jr z,tile_column
tile_rows:
    add hl,de
    dec b
    jr nz,tile_rows
tile_column:
    ld a,(TM+4)
    ld e,a
    add hl,de
    ld a,l
    ld (TM+8),a
    ld a,h
    ld (TM+9),a
    ld a,1
    ld (TM+7),a
    ret

tile_get:
    call tile_locate
    ld a,(TM+7)
    or a
    jr z,tile_get_invalid
    ld a,(TM+8)
    ld l,a
    ld a,(TM+9)
    ld h,a
    ld a,(hl)
    ld (TM+6),a
    ret
tile_get_invalid:
    xor a
    ld (TM+6),a
    ret

tile_set:
    xor a
    ld (TM+7),a
    ld a,(TM+14)
    ld b,a
    ld a,(TM+6)
    cp b
    jr c,tile_set_id_ok
    jr z,tile_set_id_ok
    ret
tile_set_id_ok:
    call tile_locate
    ld a,(TM+7)
    or a
    ret z
    ld a,(TM+8)
    ld l,a
    ld a,(TM+9)
    ld h,a
    ld a,(TM+6)
    ld (hl),a
    ret

tile_property:
    xor a
    ld (TM+7),a
    ld (TM+11),a
    ld a,(TM+14)
    ld b,a
    ld a,(TM+6)
    cp b
    jr c,tile_property_ok
    jr z,tile_property_ok
    ret
tile_property_ok:
    ld e,a
    ld d,0
    ld a,(TM+12)
    ld l,a
    ld a,(TM+13)
    ld h,a
    add hl,de
    ld a,(hl)
    ld (TM+11),a
    ld a,1
    ld (TM+7),a
    ret

tile_pixel8:
    xor a
    ld (TM+7),a
    ld (TM+6),a
    ld a,(TM+16)
    ld l,a
    ld a,(TM+17)
    ld h,a
    srl h
    rr l
    srl h
    rr l
    srl h
    rr l
    ld a,h
    or a
    ret nz
    ld a,l
    ld (TM+4),a
    ld a,(TM+18)
    ld l,a
    ld a,(TM+19)
    ld h,a
    srl h
    rr l
    srl h
    rr l
    srl h
    rr l
    ld a,h
    or a
    ret nz
    ld a,l
    ld (TM+5),a
    jp tile_get

For tile_pixel8, set unsigned 16-bit X and Y in world coordinates. It divides by eight, rejects a cell coordinate too large for a byte, then performs the checked lookup. Read cell/tile outputs only after checking valid. X/Y inputs remain unchanged. Eight-unit square cells are this routine’s deliberate fast conversion; other sizes, wrapping or a signed origin need a corresponding variant.

State, stack and cost

AF, BC, DE and HL are clobbered. The routines use the current stack and the supplied context. They do not mask or install interrupts. An interrupt caller needs its own context and must preserve the state that the interrupted code needs.

Get/set use 4 bytes of return addresses at their deepest point, including the direct caller’s call into the library. Pixel conversion uses 4; property lookup uses 2. Add the surrounding caller stack and any interrupt frames.

Map storage is W×H bytes, plus highest-ID+1 property bytes, context, code and stack. This readable version adds the row width repeatedly; later rows cost more work. Row-pointer tables or fixed-width shifts are alternatives when query speed matters. There is no whole-game frame-rate claim.

Run and adapt it

make ASM198X=/path/to/asm198x builds the complete original diagnostic. It uses a synthetic reset ROM/cartridge to execute the routines and observe their RAM results. This establishes the query code and its declared memory on this machine; it does not establish a stock BASIC/FORTH loader, video output, IRQ coexistence or a frame deadline.

The caller checks every cell of a non-square map, rejected reads/writes, a map-end guard, a valid change, properties and coordinate conversion. The same observer rejects completed wrong-stride and write-without-valid versions. The map crosses a 256-byte boundary so row/column address carries are exercised. Use the reusable include in your own program; the unrolled diagnostic queries make observations explicit and are not a proposed game loop.

For the next step, draw a native tile region using these logical IDs and a separate display adapter.