The picture number in a map does not automatically tell you whether a tile is solid, harmful or climbable. A small property table lets artwork and game behaviour change independently, with a bounds check before looking anything up.

Include, initialise and call
Reserve ATTRS, MATCH, VALID, TILE_ID, MASK as 5 contiguous private bytes in that order. Include the helper once. Call tile_properties_init at the intended reset boundary: it clears every named byte, including input fields. Set the starting values afterwards, then call tile_properties_step for each intended service.
Query the map for a tile ID, then offer that ID and the property mask. Check VALID if a missing ID should be treated as a content error rather than empty space. The sample assigns independent bit values 1, 2, 4 and 8; your game gives those bits their meanings.
TILE_ID selects one entry of the caller-owned property_table, whose qualified size is 16. MASK selects properties of interest. Step replaces ATTRS/MATCH/VALID: an in-range ID returns its attribute byte and VALID 1; MATCH is 1 when any selected bit is present. MASK zero matches nothing. An invalid ID returns all three outputs zero without reading outside the table. Inputs survive.
Reusable initialisation and service
; SPDX-License-Identifier: MIT
; Look Up Tile Properties. Contiguous private bytes: ATTRS, MATCH, VALID, TILE_ID, MASK.
; init clears all fields; set inputs afterwards. No I/O, waits or IRQ ownership.
; D0-D3/A0-A1/CCR change; D4-D7/A2-A6 survive.
tile_properties_init:
clr.b ATTRS
clr.b MATCH
clr.b VALID
clr.b TILE_ID
clr.b MASK
rts
tile_properties_step:
clr.b ATTRS
clr.b MATCH
clr.b VALID
moveq #0,d0
move.b TILE_ID,d0
cmp.b #PROPERTY_COUNT,d0
bhs.s property_done
lea property_table,a0
move.b 0(a0,d0.w),d1
move.b d1,ATTRS
and.b MASK,d1
beq.s property_valid
move.b #1,MATCH
property_valid:
move.b #1,VALID
property_done:
rts
component_end:Caller-owned immutable lookup/encoded data
data_start:
property_table:
dc.b 0,1,1,2,4,1,8,0,3,6,12,9,15,2,4,8
data_end:State, registers and ownership
Five bytes of state plus a separate immutable sixteen-byte property table. A valid tile with attribute zero differs from an invalid tile because VALID is separate. This is an any-bit test; the quest-flags helper’s all-bits question has a different meaning.
D0–D3/A0–A1/CCR may change; D4–D7/A2–A6 survive. A direct BSR/JSR needs four return-address stack bytes.
The implementation binds fixed symbols and has one mutation owner. Independent users need separate state blocks and scoped/renamed labels, including routine labels; sharing the same bindings means sharing the state. Do not let an interrupt and the main loop mutate the same block without an explicit ownership policy.
The bootable caller takes over Copper/bitplane/interrupt/DMA resources and explicitly disarms all eight sprites. It does not restore Workbench. These are caller presentation choices, not dependencies of the reusable helper.
A caller you can adapt
The buildable program supplies bindings and include order. Its separate client retains state between calls. checked_step calls the public tile_properties_step entry and checks register sentinels; emit_state records diagnostics. Your game can call the public entry directly, remove the diagnostics and consume the documented outputs.
Bindings, buildable caller and include order
section code,code_c
CUSTOM = 0
BROKEN = 0
COYOTE_STEPS = 4
BUFFER_STEPS = 4
TEXT_CAP = 8
PROPERTY_COUNT = 16
STREAM_BYTES = 13
CRC_POLY = 7
include "caller.inc"
include "exercise.inc"
component_start:
include "component.inc"
include "data.inc"
include "cases.inc"
even
include "display.inc"
include "../../framework/turn-deadline/font.inc"
code_end:
STATE:
ATTRS: dc.b 0
MATCH: dc.b 0
VALID: dc.b 0
TILE_ID: dc.b 0
MASK: dc.b 0
even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1300,0
even
signature: dc.b "198XPLAYTOOLS00",0
even
copperlist:
dc.w $08e,$2c81,$090,$2cc1,$092,$0038,$094,$00d0
dc.w $100,$1200,$102,0,$104,0,$108,0,$10a,0
dc.w $180,$012,$182,$fff
copbpl:
dc.w $0e0,0,$0e2,0
dc.w $ffff,$fffe
plane: dcb.b 10240,0A separate client retaining state between calls
preview_rules:
ifne CUSTOM
move.b #1,TILE_ID
move.b #8,MASK
ifne BROKEN
move.b TILE_ID,d0
move.b d0,ATTRS
clr.b MATCH
and.b MASK,d0
beq.s bad_property_zero_0_custom
move.b #1,MATCH
bad_property_zero_0_custom:
move.b #1,VALID
else
bsr checked_step
endif
bsr emit_state
move.b #6,TILE_ID
ifne BROKEN
move.b TILE_ID,d0
move.b d0,ATTRS
clr.b MATCH
and.b MASK,d0
beq.s bad_property_zero_1_custom
move.b #1,MATCH
bad_property_zero_1_custom:
move.b #1,VALID
else
bsr checked_step
endif
bsr emit_state
else
move.b #1,TILE_ID
move.b #1,MASK
ifne BROKEN
move.b TILE_ID,d0
move.b d0,ATTRS
clr.b MATCH
and.b MASK,d0
beq.s bad_property_zero_0
move.b #1,MATCH
bad_property_zero_0:
move.b #1,VALID
else
bsr checked_step
endif
bsr emit_state
move.b #6,TILE_ID
ifne BROKEN
move.b TILE_ID,d0
move.b d0,ATTRS
clr.b MATCH
and.b MASK,d0
beq.s bad_property_zero_1
move.b #1,MATCH
bad_property_zero_1:
move.b #1,VALID
else
bsr checked_step
endif
bsr emit_state
endif
rtsThe display fields are ATTRS, MATCH, VALID, TILE_ID, MASK; remaining display fields are zero. Each is one hexadecimal byte. The normal row is 08 00 01 06 01 00 00 00. This is a completed state observation, not a recording of an actual character jumping, a camera scrolling or an asset being displayed.
Make it your own
The normal client asks about mask 1; the custom caller asks about mask 8. Tile ID six maps to attribute eight, so the latter matches and the former does not. No tile image or map layout has to change.
Maintained custom configuration or caller
section code,code_c
CUSTOM = 1
BROKEN = 0
COYOTE_STEPS = 2
BUFFER_STEPS = 2
TEXT_CAP = 4
PROPERTY_COUNT = 16
STREAM_BYTES = 13
CRC_POLY = 49
include "caller.inc"
include "exercise.inc"
component_start:
include "component.inc"
include "data.inc"
include "cases.inc"
even
include "display.inc"
include "../../framework/turn-deadline/font.inc"
code_end:
STATE:
ATTRS: dc.b 0
MATCH: dc.b 0
VALID: dc.b 0
TILE_ID: dc.b 0
MASK: dc.b 0
even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1300,0
even
signature: dc.b "198XPLAYTOOLS00",0
even
copperlist:
dc.w $08e,$2c81,$090,$2cc1,$092,$0038,$094,$00d0
dc.w $100,$1200,$102,0,$104,0,$108,0,$10a,0
dc.w $180,$012,$182,$fff
copbpl:
dc.w $0e0,0,$0e2,0
dc.w $ffff,$fffe
plane: dcb.b 10240,0The custom row is 08 01 01 06 08 00 00 00. These records qualify the named builds and inputs; another layout, format or input domain needs explicit bounds and fresh execution evidence.
Common mistake and repair
The wrong client treats the numeric tile ID as the attribute byte. Tile six then appears to carry bits two and four instead of its authored property bit eight. Look up the property data rather than encoding behaviour accidentally into artwork numbering.
Select the deliberately wrong client
section code,code_c
CUSTOM = 0
BROKEN = 1
COYOTE_STEPS = 4
BUFFER_STEPS = 4
TEXT_CAP = 8
PROPERTY_COUNT = 16
STREAM_BYTES = 13
CRC_POLY = 7
include "caller.inc"
include "exercise.inc"
component_start:
include "component.inc"
include "data.inc"
include "cases.inc"
even
include "display.inc"
include "../../framework/turn-deadline/font.inc"
code_end:
STATE:
ATTRS: dc.b 0
MATCH: dc.b 0
VALID: dc.b 0
TILE_ID: dc.b 0
MASK: dc.b 0
even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1300,0
even
signature: dc.b "198XPLAYTOOLS00",0
even
copperlist:
dc.w $08e,$2c81,$090,$2cc1,$092,$0038,$094,$00d0
dc.w $100,$1200,$102,0,$104,0,$108,0,$10a,0
dc.w $180,$012,$182,$fff
copbpl:
dc.w $0e0,0,$0e2,0
dc.w $ffff,$fffe
plane: dcb.b 10240,0
The BROKEN branch is visible in the client. repaired.asm restores the normal selection; its executable matches demo exactly. Spectrum tape filenames may differ while loaded CODE remains identical. The fault is a labelled experiment, not a suggested alternative.
Cost, limits and verification
The measured layout has 100 code bytes, 16 immutable data bytes and 5 private state bytes. Alignment, stack usage, diagnostic tables and display machinery are additional. The extents come from the loaded program’s symbols, not from counting source lines.
One bounds check, one byte lookup and a mask test. The immutable table costs sixteen bytes; no per-tile properties need to be copied into every map cell. The table transfer to an actual display is unrelated work.
A larger catalogue needs an updated count, storage and index width as appropriate. Sparse IDs may need a different lookup structure. Animated frames can share the same properties, or the animation owner can choose when behaviour changes; neither should happen accidentally because the image index changed.
Four native runs use A500 OCS PAL with Kickstart 1.3 and an OFS cold-boot disk; Asm198x 0.0.72 and Emu198x 0.44.2, with Build198x 0.2.9 for disk mastering. Each checks 64 boundary/varied/error cases, dirty initialisation and every state of a separate sequential client against an independent integer model. This is not exhaustive coverage of every possible byte combination. Source/tool/firmware hashes, immutable loaded code/data, native startup/completion, preserved-register sentinels, helper/data sizes, screenshot bytes/pixels and exact repair are checked. Deliberately altered observations must fail. Firmware is hashed, never distributed.