Skip to content

Quest and Inventory Flags on C64

A key collected, a door unlocked, a conversation finished: eight yes/no facts fit in one byte.

A key collected, a door unlocked, a conversation finished: eight yes/no facts fit in one byte. A mask lets you change one fact without erasing its neighbours, or ask whether a group of requirements has been met.

C64 shows the working result 01 08 02 00 00 00 00 00.
The completed native caller shows hexadecimal state bytes. The field order below explains the result; the full verification packet retains every call.

Include, initialise and call

Bind FLAGS, MASK, MODE, ALL_SET as 4 contiguous private bytes in that order. Include the helper once. Call quest_flags_init at the deliberate reset boundary: it clears all named bytes, including inputs. Set your starting values afterwards. Call quest_flags_step when this service is needed.

Initialise once for a fresh world. Assign a power-of-two mask to each independent fact: 1, 2, 4, 8 and so on. Use MODE 0 to query without changing the world. A combined mask such as 12 asks for bits 4 and 8 together; the result means all, not any.

FLAGS stores eight independent bits. MASK selects any subset of them. MODE 1 sets those bits, MODE 2 clears them; other modes only query. After the optional change, ALL_SET is 1 exactly when every bit in MASK is present in FLAGS. An empty mask is satisfied. MASK and MODE survive.

Reusable initialisation and service

6502.inc
; SPDX-License-Identifier: MIT
; Quest and Inventory Flags. Bind these contiguous private bytes: FLAGS, MASK, MODE, ALL_SET.
; init clears the whole block; step uses current inputs. No I/O, waits or IRQ owner.
; A/X/flags may change; Y and decimal mode survive. Binary arithmetic requires D=0.
quest_flags_init:
    lda #0
    sta FLAGS
    sta MASK
    sta MODE
    sta ALL_SET
    rts
quest_flags_step:
    lda MODE
    cmp #1
    beq flags_set
    cmp #2
    bne flags_test
    lda MASK
    eor #255
    and FLAGS
    sta FLAGS
    jmp flags_test
flags_set:
    lda FLAGS
    ora MASK
    sta FLAGS
flags_test:
    lda #0
    sta ALL_SET
    lda FLAGS
    and MASK
    cmp MASK
    bne flags_done
    inc ALL_SET
flags_done:
    rts
component_end:

State and ownership

FLAGS persists across calls. MASK and MODE are inputs. ALL_SET is replaced on each call and describes the state after any set or clear operation.

A/X/flags may change; Y and decimal mode survive. Clear decimal mode before binary arithmetic; the standalone caller uses CLD. A direct call pushes a two-byte return address.

This implementation has fixed symbol bindings and one mutation owner. Independent users need distinct state blocks and scoped/renamed bindings; including another copy with the same global labels is not sufficient. Sequential queries may reuse a scratch block only after the previous result has been consumed. Do not let an interrupt and the main loop mutate the same block without a separately designed ownership policy.

The standalone PRG takes over screen/font/colour RAM, disables IRQ sources and does not return to BASIC. Its private diagnostic workspace is $70–$89; its packet starts at $C000.

A small caller you can adapt

The standalone program includes the helper and a separate client. Its read-only table exercises 96 valid inputs, then this client initialises once and retains state between calls. Replace the supplied input changes with your own game values; the display and packet machinery are diagnostics rather than dependencies of the component.

Buildable caller, bindings and include order

demo.asm
FLAGS=$80
MASK=$81
MODE=$82
ALL_SET=$83
STATE=$80
input_ptr=$70
cases_left=$72
ptr=$74
saved_y=$76
OUTPUT=$c000
preview=OUTPUT+1400
ready=OUTPUT+1408
CUSTOM=0
BROKEN=0
SCORE_CAP=65535
QUEUE_CAP=4
REPEAT_DELAY=4
REPEAT_RATE=2
!source "caller.inc"
!source "exercise.inc"
component_start:
!source "component.inc"
!source "cases.inc"
hex_font:
    !byte 0,0,0,0,0,0,0,0,0,56,40,40,40,56,0,0,0,16,48,16,16,56,0,0,0,56,8,56,32,56,0,0,0,56,8,56,8,56,0,0,0,40,40,56,8,8,0,0,0,56,32,56,8,56,0,0,0,56,32,56,40,56,0,0,0,56,8,8,8,8,0,0,0,56,40,56,40,56,0,0,0,56,40,56,8,56,0,0,0,16,40,56,40,40,0,0,0,48,40,48,40,48,0,0,0,56,32,32,32,56,0,0,0,48,40,40,40,48,0,0,0,56,32,48,32,56,0,0,0,56,32,48,32,32,0,0
code_end:

A separate client retaining state between calls

exercise.inc, excerpt
preview_rules:
!if CUSTOM {
    lda #1
    sta FLAGS
    lda #16
    sta MASK
    lda #1
    sta MODE
!if BROKEN {
    lda #3
    sta MASK
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
    lda #0
    sta MODE
!if BROKEN {
    lda #3
    sta MASK
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
    lda #2
    sta MODE
!if BROKEN {
    lda #3
    sta MASK
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
} else {
    lda #1
    sta FLAGS
    lda #8
    sta MASK
    lda #1
    sta MODE
!if BROKEN {
    lda #3
    sta MASK
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
    lda #0
    sta MODE
!if BROKEN {
    lda #3
    sta MASK
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
    lda #2
    sta MODE
!if BROKEN {
    lda #3
    sta MASK
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
}
    rts

The eight display fields follow FLAGS, MASK, MODE, ALL_SET; unused display fields are zero. Each field is a hexadecimal byte. The working row is 01 08 02 00 00 00 00 00. The output is a completed diagnostic state, not a recording of movement, input or sound hardware.

Make it your own

The custom caller moves its example fact from mask 8 to mask 16. It leaves the unrelated mask-1 fact intact. Changing the bit assignment needs matching definitions in every user of that save/world data.

Maintained custom configuration or caller

custom.asm
FLAGS=$80
MASK=$81
MODE=$82
ALL_SET=$83
STATE=$80
input_ptr=$70
cases_left=$72
ptr=$74
saved_y=$76
OUTPUT=$c000
preview=OUTPUT+1400
ready=OUTPUT+1408
CUSTOM=1
BROKEN=0
SCORE_CAP=999
QUEUE_CAP=2
REPEAT_DELAY=6
REPEAT_RATE=3
!source "caller.inc"
!source "exercise.inc"
component_start:
!source "component.inc"
!source "cases.inc"
hex_font:
    !byte 0,0,0,0,0,0,0,0,0,56,40,40,40,56,0,0,0,16,48,16,16,56,0,0,0,56,8,56,32,56,0,0,0,56,8,56,8,56,0,0,0,40,40,56,8,8,0,0,0,56,32,56,8,56,0,0,0,56,32,56,40,56,0,0,0,56,8,8,8,8,0,0,0,56,40,56,40,56,0,0,0,56,40,56,8,56,0,0,0,16,40,56,40,40,0,0,0,48,40,48,40,48,0,0,0,56,32,32,32,56,0,0,0,48,40,40,40,48,0,0,0,56,32,48,32,56,0,0,0,56,32,48,32,32,0,0
code_end:

The observed custom row is 01 10 02 00 00 00 00 00. The retained records qualify these named configurations; a new input domain or larger layout needs its own bounds and fresh execution checks.

Common mistake and repair

The wrong client passes 3, the intended bit number, where the API expects mask 8. The binary value 3 selects the two lowest bits. Setting and later clearing it destroys the unrelated mask-1 fact. Pass a mask, not an ordinal bit number.

Deliberately wrong client selection

wrong.asm
FLAGS=$80
MASK=$81
MODE=$82
ALL_SET=$83
STATE=$80
input_ptr=$70
cases_left=$72
ptr=$74
saved_y=$76
OUTPUT=$c000
preview=OUTPUT+1400
ready=OUTPUT+1408
CUSTOM=0
BROKEN=1
SCORE_CAP=65535
QUEUE_CAP=4
REPEAT_DELAY=4
REPEAT_RATE=2
!source "caller.inc"
!source "exercise.inc"
component_start:
!source "component.inc"
!source "cases.inc"
hex_font:
    !byte 0,0,0,0,0,0,0,0,0,56,40,40,40,56,0,0,0,16,48,16,16,56,0,0,0,56,8,56,32,56,0,0,0,56,8,56,8,56,0,0,0,40,40,56,8,8,0,0,0,56,32,56,8,56,0,0,0,56,32,56,40,56,0,0,0,56,8,8,8,8,0,0,0,56,40,56,40,56,0,0,0,56,40,56,8,56,0,0,0,16,40,56,40,40,0,0,0,48,40,48,40,48,0,0,0,56,32,32,32,56,0,0,0,48,40,40,40,48,0,0,0,56,32,48,32,56,0,0,0,56,32,48,32,32,0,0
code_end:
C64 shows the deliberate fault result 00 03 02 00 00 00 00 00.
The wrong client completes and records 00 03 02 00 00 00 00 00. Compare its state with the working row and the explained failure.

The wrong branch is in the displayed client above, selected by BROKEN. repaired.asm selects the original client again. Its executable matches demo exactly; Spectrum tape filenames can differ while loaded CODE is identical. The bad caller is a labelled experiment, not an alternative recommended implementation.

Cost and verification

The measured helper occupies 53 code bytes plus 4 named state bytes in this layout. Return addresses, alignment, tables and display diagnostics are additional. The sizes cover init and step together, not the whole standalone program.

At most one byte of world state changes. Mask operations take bounded paths and no scan. A byte per fact costs more RAM but may make an early project easier to inspect.

More than eight facts need a larger byte array plus an index/mask lookup. Counters belong in counters: a flag cannot hold three arrows. Shared interrupt/main-loop updates need an ownership or critical-section policy because these read/modify/write sequences are not atomic transactions.

Four native runs use PAL C64 with stock BASIC/KERNAL PRG startup and RUN; Asm198x 0.0.72 and Emu198x 0.44.2. Every run checks 96 table results, dirty initialisation and every state in a separate sequential trace against an independent integer model. This is boundary/varied-input qualification, not exhaustive coverage of every byte combination. Source/tool/firmware hashes, native startup and completion, preserved-register sentinels, immutable loaded code, actual helper extent, capture bytes and pixels, and exact repair are checked. Deliberately changed observations must fail. Firmware is hashed, never distributed.

Related reusable technique · Maths for Games · The Craft.

Source and native evidence.