Skip to content

Patrol Between Two Endpoints on Spectrum

A guard walks to the end of a corridor, turns and comes back.

A guard walks to the end of a corridor, turns and comes back. The turn should happen at the endpoint, not after a step through the wall. This routine keeps that small behaviour separate from sprite drawing and collision.

Spectrum shows the working result 02 00 04 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 POSITION, LOWER, UPPER, DIRECTION as 4 contiguous private bytes in that order. Include the helper once. Call endpoint_patrol_init at the deliberate reset boundary: it clears all named bytes, including inputs. Set your starting values afterwards. Call endpoint_patrol_step when this service is needed.

Initialise, then supply valid bounds and a starting position before the first step. Service the helper at your chosen movement cadence. A countdown can make a guard step less often; this helper itself does not own a clock. Pause simply omits service.

Use unsigned byte LOWER, UPPER and POSITION with LOWER ≤ POSITION ≤ UPPER. DIRECTION zero means increasing, nonzero means decreasing. Step turns inward at either endpoint, takes one unit step, and normalises DIRECTION to 0 or 1. A one-point interval stays there with direction zero. There is no dwell or duplicated endpoint call.

Reusable initialisation and service

component.inc
; SPDX-License-Identifier: MIT
; Patrol Between Two Endpoints. Bind these contiguous private bytes: POSITION, LOWER, UPPER, DIRECTION.
; init clears the whole block; step uses current inputs. No I/O, waits or IRQ owner.
; AF/BC/DE/HL may change; IX/IY survive.
endpoint_patrol_init:
    xor a
    ld (POSITION),a
    ld (LOWER),a
    ld (UPPER),a
    ld (DIRECTION),a
    ret
endpoint_patrol_step:
    ld a,(LOWER)
    ld b,a
    ld a,(UPPER)
    cp b
    jr nz,patrol_range
    ld (POSITION),a
    xor a
    ld (DIRECTION),a
    ret
patrol_range:
    ld c,a
    ld a,(POSITION)
    cp b
    jr z,patrol_positive
    cp c
    jr z,patrol_negative
    ld a,(DIRECTION)
    or a
    jr z,patrol_positive
patrol_negative:
    ld a,(POSITION)
    dec a
    ld (POSITION),a
    ld a,1
    ld (DIRECTION),a
    ret
patrol_positive:
    ld a,(POSITION)
    inc a
    ld (POSITION),a
    xor a
    ld (DIRECTION),a
    ret
component_end:

State and ownership

POSITION and DIRECTION persist. LOWER and UPPER are caller-owned bounds. Altering the bounds requires moving the position into the new interval before the next call; this component does not repair arbitrary corrupt state.

AF/BC/DE/HL may change; IX/IY survive. A direct CALL uses two return-address stack bytes.

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 tape program starts at $8000, disables interrupts and owns bitmap/attribute RAM. It does not return to BASIC. The helper itself never disables interrupts or touches the display.

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
    org $8000
CUSTOM equ 0
BROKEN equ 0
SCORE_CAP equ 65535
QUEUE_CAP equ 4
REPEAT_DELAY equ 4
REPEAT_RATE equ 2
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "component.inc"
    include "cases.inc"
    include "../../physics/rectangle-overlap/display.inc"
code_end:
STATE:
POSITION: defb 0
LOWER: defb 0
UPPER: defb 0
DIRECTION: defb 0
input_ptr: defw 0
output_ptr: defw 0
cases_left: defb 0
ready: defb 0
preview: defs 8,0
OUTPUT: defs 1300,0
    end start

A separate client retaining state between calls

exercise.inc, excerpt
preview_rules:
    if CUSTOM
    ld a,4
    ld (POSITION),a
    ld a,0
    ld (LOWER),a
    ld a,6
    ld (UPPER),a
    ld a,0
    ld (DIRECTION),a
    if BROKEN
    ld a,(DIRECTION)
    or a
    ld a,(POSITION)
    jr z,bad_patrol_up_0_custom
    dec a
    jr bad_patrol_store_0_custom
bad_patrol_up_0_custom:
    inc a
bad_patrol_store_0_custom:
    ld (POSITION),a
    ld b,a
    ld a,(LOWER)
    cp b
    jr c,bad_patrol_hi_0_custom
    jr z,bad_patrol_hi_0_custom
    jr bad_patrol_flip_0_custom
bad_patrol_hi_0_custom:
    ld a,(UPPER)
    cp b
    jr nc,bad_patrol_end_0_custom
bad_patrol_flip_0_custom:
    ld a,(DIRECTION)
    xor 1
    ld (DIRECTION),a
bad_patrol_end_0_custom:
    else
    call checked_step
    endif
    call emit_state
    if BROKEN
    ld a,(DIRECTION)
    or a
    ld a,(POSITION)
    jr z,bad_patrol_up_1_custom
    dec a
    jr bad_patrol_store_1_custom
bad_patrol_up_1_custom:
    inc a
bad_patrol_store_1_custom:
    ld (POSITION),a
    ld b,a
    ld a,(LOWER)
    cp b
    jr c,bad_patrol_hi_1_custom
    jr z,bad_patrol_hi_1_custom
    jr bad_patrol_flip_1_custom
bad_patrol_hi_1_custom:
    ld a,(UPPER)
    cp b
    jr nc,bad_patrol_end_1_custom
bad_patrol_flip_1_custom:
    ld a,(DIRECTION)
    xor 1
    ld (DIRECTION),a
bad_patrol_end_1_custom:
    else
    call checked_step
    endif
    call emit_state
    if BROKEN
    ld a,(DIRECTION)
    or a
    ld a,(POSITION)
    jr z,bad_patrol_up_2_custom
    dec a
    jr bad_patrol_store_2_custom
bad_patrol_up_2_custom:
    inc a
bad_patrol_store_2_custom:
    ld (POSITION),a
    ld b,a
    ld a,(LOWER)
    cp b
    jr c,bad_patrol_hi_2_custom
    jr z,bad_patrol_hi_2_custom
    jr bad_patrol_flip_2_custom
bad_patrol_hi_2_custom:
    ld a,(UPPER)
    cp b
    jr nc,bad_patrol_end_2_custom
bad_patrol_flip_2_custom:
    ld a,(DIRECTION)
    xor 1
    ld (DIRECTION),a
bad_patrol_end_2_custom:
    else
    call checked_step
    endif
    call emit_state
    if BROKEN
    ld a,(DIRECTION)
    or a
    ld a,(POSITION)
    jr z,bad_patrol_up_3_custom
    dec a
    jr bad_patrol_store_3_custom
bad_patrol_up_3_custom:
    inc a
bad_patrol_store_3_custom:
    ld (POSITION),a
    ld b,a
    ld a,(LOWER)
    cp b
    jr c,bad_patrol_hi_3_custom
    jr z,bad_patrol_hi_3_custom
    jr bad_patrol_flip_3_custom
bad_patrol_hi_3_custom:
    ld a,(UPPER)
    cp b
    jr nc,bad_patrol_end_3_custom
bad_patrol_flip_3_custom:
    ld a,(DIRECTION)
    xor 1
    ld (DIRECTION),a
bad_patrol_end_3_custom:
    else
    call checked_step
    endif
    call emit_state
    if BROKEN
    ld a,(DIRECTION)
    or a
    ld a,(POSITION)
    jr z,bad_patrol_up_4_custom
    dec a
    jr bad_patrol_store_4_custom
bad_patrol_up_4_custom:
    inc a
bad_patrol_store_4_custom:
    ld (POSITION),a
    ld b,a
    ld a,(LOWER)
    cp b
    jr c,bad_patrol_hi_4_custom
    jr z,bad_patrol_hi_4_custom
    jr bad_patrol_flip_4_custom
bad_patrol_hi_4_custom:
    ld a,(UPPER)
    cp b
    jr nc,bad_patrol_end_4_custom
bad_patrol_flip_4_custom:
    ld a,(DIRECTION)
    xor 1
    ld (DIRECTION),a
bad_patrol_end_4_custom:
    else
    call checked_step
    endif
    call emit_state
    if BROKEN
    ld a,(DIRECTION)
    or a
    ld a,(POSITION)
    jr z,bad_patrol_up_5_custom
    dec a
    jr bad_patrol_store_5_custom
bad_patrol_up_5_custom:
    inc a
bad_patrol_store_5_custom:
    ld (POSITION),a
    ld b,a
    ld a,(LOWER)
    cp b
    jr c,bad_patrol_hi_5_custom
    jr z,bad_patrol_hi_5_custom
    jr bad_patrol_flip_5_custom
bad_patrol_hi_5_custom:
    ld a,(UPPER)
    cp b
    jr nc,bad_patrol_end_5_custom
bad_patrol_flip_5_custom:
    ld a,(DIRECTION)
    xor 1
    ld (DIRECTION),a
bad_patrol_end_5_custom:
    else
    call checked_step
    endif
    call emit_state
    else
    ld a,4
    ld (POSITION),a
    ld a,0
    ld (LOWER),a
    ld a,4
    ld (UPPER),a
    ld a,0
    ld (DIRECTION),a
    if BROKEN
    ld a,(DIRECTION)
    or a
    ld a,(POSITION)
    jr z,bad_patrol_up_0
    dec a
    jr bad_patrol_store_0
bad_patrol_up_0:
    inc a
bad_patrol_store_0:
    ld (POSITION),a
    ld b,a
    ld a,(LOWER)
    cp b
    jr c,bad_patrol_hi_0
    jr z,bad_patrol_hi_0
    jr bad_patrol_flip_0
bad_patrol_hi_0:
    ld a,(UPPER)
    cp b
    jr nc,bad_patrol_end_0
bad_patrol_flip_0:
    ld a,(DIRECTION)
    xor 1
    ld (DIRECTION),a
bad_patrol_end_0:
    else
    call checked_step
    endif
    call emit_state
    if BROKEN
    ld a,(DIRECTION)
    or a
    ld a,(POSITION)
    jr z,bad_patrol_up_1
    dec a
    jr bad_patrol_store_1
bad_patrol_up_1:
    inc a
bad_patrol_store_1:
    ld (POSITION),a
    ld b,a
    ld a,(LOWER)
    cp b
    jr c,bad_patrol_hi_1
    jr z,bad_patrol_hi_1
    jr bad_patrol_flip_1
bad_patrol_hi_1:
    ld a,(UPPER)
    cp b
    jr nc,bad_patrol_end_1
bad_patrol_flip_1:
    ld a,(DIRECTION)
    xor 1
    ld (DIRECTION),a
bad_patrol_end_1:
    else
    call checked_step
    endif
    call emit_state
    if BROKEN
    ld a,(DIRECTION)
    or a
    ld a,(POSITION)
    jr z,bad_patrol_up_2
    dec a
    jr bad_patrol_store_2
bad_patrol_up_2:
    inc a
bad_patrol_store_2:
    ld (POSITION),a
    ld b,a
    ld a,(LOWER)
    cp b
    jr c,bad_patrol_hi_2
    jr z,bad_patrol_hi_2
    jr bad_patrol_flip_2
bad_patrol_hi_2:
    ld a,(UPPER)
    cp b
    jr nc,bad_patrol_end_2
bad_patrol_flip_2:
    ld a,(DIRECTION)
    xor 1
    ld (DIRECTION),a
bad_patrol_end_2:
    else
    call checked_step
    endif
    call emit_state
    if BROKEN
    ld a,(DIRECTION)
    or a
    ld a,(POSITION)
    jr z,bad_patrol_up_3
    dec a
    jr bad_patrol_store_3
bad_patrol_up_3:
    inc a
bad_patrol_store_3:
    ld (POSITION),a
    ld b,a
    ld a,(LOWER)
    cp b
    jr c,bad_patrol_hi_3
    jr z,bad_patrol_hi_3
    jr bad_patrol_flip_3
bad_patrol_hi_3:
    ld a,(UPPER)
    cp b
    jr nc,bad_patrol_end_3
bad_patrol_flip_3:
    ld a,(DIRECTION)
    xor 1
    ld (DIRECTION),a
bad_patrol_end_3:
    else
    call checked_step
    endif
    call emit_state
    if BROKEN
    ld a,(DIRECTION)
    or a
    ld a,(POSITION)
    jr z,bad_patrol_up_4
    dec a
    jr bad_patrol_store_4
bad_patrol_up_4:
    inc a
bad_patrol_store_4:
    ld (POSITION),a
    ld b,a
    ld a,(LOWER)
    cp b
    jr c,bad_patrol_hi_4
    jr z,bad_patrol_hi_4
    jr bad_patrol_flip_4
bad_patrol_hi_4:
    ld a,(UPPER)
    cp b
    jr nc,bad_patrol_end_4
bad_patrol_flip_4:
    ld a,(DIRECTION)
    xor 1
    ld (DIRECTION),a
bad_patrol_end_4:
    else
    call checked_step
    endif
    call emit_state
    if BROKEN
    ld a,(DIRECTION)
    or a
    ld a,(POSITION)
    jr z,bad_patrol_up_5
    dec a
    jr bad_patrol_store_5
bad_patrol_up_5:
    inc a
bad_patrol_store_5:
    ld (POSITION),a
    ld b,a
    ld a,(LOWER)
    cp b
    jr c,bad_patrol_hi_5
    jr z,bad_patrol_hi_5
    jr bad_patrol_flip_5
bad_patrol_hi_5:
    ld a,(UPPER)
    cp b
    jr nc,bad_patrol_end_5
bad_patrol_flip_5:
    ld a,(DIRECTION)
    xor 1
    ld (DIRECTION),a
bad_patrol_end_5:
    else
    call checked_step
    endif
    call emit_state
    endif
    ret

The eight display fields follow POSITION, LOWER, UPPER, DIRECTION; unused display fields are zero. Each field is a hexadecimal byte. The working row is 02 00 04 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 extends the upper endpoint from four to six. The same initial position now moves forward before turning. The lower bound remains zero and the movement amount remains one.

Maintained custom configuration or caller

custom.asm
    org $8000
CUSTOM equ 1
BROKEN equ 0
SCORE_CAP equ 999
QUEUE_CAP equ 2
REPEAT_DELAY equ 6
REPEAT_RATE equ 3
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "component.inc"
    include "cases.inc"
    include "../../physics/rectangle-overlap/display.inc"
code_end:
STATE:
POSITION: defb 0
LOWER: defb 0
UPPER: defb 0
DIRECTION: defb 0
input_ptr: defw 0
output_ptr: defw 0
cases_left: defb 0
ready: defb 0
preview: defs 8,0
OUTPUT: defs 1300,0
    end start

The observed custom row is 02 00 06 01 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 steps first and only reverses after leaving the interval. Starting at the upper endpoint four, it visibly records position five before turning back. Turn before taking the next step. The wrong trace is a separate naive implementation; the valid boundary table still exercises the maintained helper.

Deliberately wrong client selection

wrong.asm
    org $8000
CUSTOM equ 0
BROKEN equ 1
SCORE_CAP equ 65535
QUEUE_CAP equ 4
REPEAT_DELAY equ 4
REPEAT_RATE equ 2
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "component.inc"
    include "cases.inc"
    include "../../physics/rectangle-overlap/display.inc"
code_end:
STATE:
POSITION: defb 0
LOWER: defb 0
UPPER: defb 0
DIRECTION: defb 0
input_ptr: defw 0
output_ptr: defw 0
cases_left: defb 0
ready: defb 0
preview: defs 8,0
OUTPUT: defs 1300,0
    end start
Spectrum shows the deliberate fault result 00 00 04 01 00 00 00 00.
The wrong client completes and records 00 00 04 01 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 73 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.

There are a few byte comparisons and one increment/decrement per service, with no route scan or rendering. Keeping movement cadence outside allows several guards to share a clock without sharing their positions.

A multi-point route needs a waypoint index and route data. Variable-speed reflection must decide how to use leftover distance after hitting an endpoint; this one-unit routine cannot be made faster by replacing its increment with an unchecked larger addition.

Four native runs use Spectrum 48K with stock ROM tape loading and USR entry; 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.