Skip to content

Land on a One-way Platform on C64

A fast-falling character can cross a thin platform between two updates without ever having its feet exactly on the top.

A fast-falling character can cross a thin platform between two updates without ever having its feet exactly on the top. Compare the old and proposed bottom edges, then snap the feet to the surface when the downward path crosses it.

C64 shows the working diagnostic row 01 64 14 10 5A 6E 0A 40.
A native completed caller shows its state as hexadecimal bytes. The field order below explains the row; the full packet retains every call.

Include, initialise and call

Reserve LANDED, SNAP, ACTOR_X, ACTOR_W, PREV_BOTTOM, NEW_BOTTOM, PLATFORM_X, PLATFORM_W, PLATFORM_Y, DROP as 10 contiguous private bytes in that order. Include the helper once. Call one_way_platform_init at the intended reset boundary: it clears every named byte, including input fields. Set the starting values afterwards, then call one_way_platform_step for each intended service.

Initialise once, supply the old and proposed geometry, then service before committing a fall. On LANDED, place the bottom at SNAP and let the caller decide vertical velocity and grounded state. Test each candidate platform as needed; selecting among several surfaces requires the nearest valid top along the path, not an arbitrary last result.

ACTOR_X/ACTOR_W and PLATFORM_X/PLATFORM_W describe unsigned horizontal half-open intervals. Each span includes X and stops just before X + width, so touching edges alone do not overlap. Width zero rejects contact; widened endpoints may reach 510. PREV_BOTTOM and NEW_BOTTOM are unsigned old/proposed bottom coordinates, increasing downwards. Landing requires horizontal overlap and PREV_BOTTOM ≤ PLATFORM_Y ≤ NEW_BOTTOM, with DROP zero. LANDED is replaced with 1 on contact; SNAP becomes PLATFORM_Y on contact and otherwise NEW_BOTTOM. Stationary feet exactly on top count as contact. All inputs survive.

Reusable initialisation and service

6502.inc
; SPDX-License-Identifier: MIT
; Land on a One-way Platform. Contiguous private bytes: LANDED, SNAP, ACTOR_X, ACTOR_W, PREV_BOTTOM, NEW_BOTTOM, PLATFORM_X, PLATFORM_W, PLATFORM_Y, DROP.
; init clears all fields; set inputs afterwards. No I/O, waits or IRQ ownership.
; A/X/flags change; Y and decimal mode survive. D=0 for binary arithmetic.
one_way_platform_init:
    lda #0
    sta LANDED
    sta SNAP
    sta ACTOR_X
    sta ACTOR_W
    sta PREV_BOTTOM
    sta NEW_BOTTOM
    sta PLATFORM_X
    sta PLATFORM_W
    sta PLATFORM_Y
    sta DROP
    rts
one_way_platform_step:
    lda #0
    sta LANDED
    lda NEW_BOTTOM
    sta SNAP
    lda DROP
    bne platform_done
    lda ACTOR_W
    beq platform_done
    lda PLATFORM_W
    beq platform_done
    lda PREV_BOTTOM
    cmp PLATFORM_Y
    bcc platform_new
    bne platform_done
platform_new:
    lda NEW_BOTTOM
    cmp PLATFORM_Y
    bcc platform_done
    lda PLATFORM_X
    clc
    adc PLATFORM_W
    bcs platform_actor_end
    cmp ACTOR_X
    bcc platform_done
    beq platform_done
platform_actor_end:
    lda ACTOR_X
    clc
    adc ACTOR_W
    bcs platform_land
    cmp PLATFORM_X
    bcc platform_done
    beq platform_done
platform_land:
    lda #1
    sta LANDED
    lda PLATFORM_Y
    sta SNAP
platform_done:
    rts
component_end:

State, registers and ownership

Ten bytes, with LANDED and SNAP first for inspection. Both horizontal intervals use widened arithmetic. The vertical coordinates do not wrap: crossing the byte coordinate seam is outside this contract and needs a wider world representation.

A/X/flags may change; Y and decimal mode survive. Clear decimal mode before binary arithmetic; the caller uses CLD. A direct call needs two 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 standalone PRG owns screen/font/colour RAM, disables interrupt sources and does not restore BASIC. Its private scratch uses $70–$76 and the state binding starts at $80; its diagnostic output starts at $C000.

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 one_way_platform_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

demo.asm
LANDED=$80
SNAP=$81
ACTOR_X=$82
ACTOR_W=$83
PREV_BOTTOM=$84
NEW_BOTTOM=$85
PLATFORM_X=$86
PLATFORM_W=$87
PLATFORM_Y=$88
DROP=$89
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
COYOTE_STEPS=4
BUFFER_STEPS=4
TEXT_CAP=8
PROPERTY_COUNT=16
STREAM_BYTES=13
CRC_POLY=7
!source "caller.inc"
!source "exercise.inc"
component_start:
!source "component.inc"
!source "data.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 #20
    sta ACTOR_X
    lda #16
    sta ACTOR_W
    lda #90
    sta PREV_BOTTOM
    lda #110
    sta NEW_BOTTOM
    lda #10
    sta PLATFORM_X
    lda #64
    sta PLATFORM_W
    lda #100
    sta PLATFORM_Y
    lda #1
    sta DROP
!if BROKEN {
    jsr checked_step
    lda NEW_BOTTOM
    cmp PLATFORM_Y
    beq bad_platform_done_0_custom
    sta SNAP
    lda #0
    sta LANDED
bad_platform_done_0_custom:
} else {
    jsr checked_step
}
    jsr emit_state
} else {
    lda #20
    sta ACTOR_X
    lda #16
    sta ACTOR_W
    lda #90
    sta PREV_BOTTOM
    lda #110
    sta NEW_BOTTOM
    lda #10
    sta PLATFORM_X
    lda #64
    sta PLATFORM_W
    lda #100
    sta PLATFORM_Y
    lda #0
    sta DROP
!if BROKEN {
    jsr checked_step
    lda NEW_BOTTOM
    cmp PLATFORM_Y
    beq bad_platform_done_0
    sta SNAP
    lda #0
    sta LANDED
bad_platform_done_0:
} else {
    jsr checked_step
}
    jsr emit_state
}
    rts

The display fields are LANDED, SNAP, ACTOR_X, ACTOR_W, PREV_BOTTOM, NEW_BOTTOM, PLATFORM_X, PLATFORM_W; later state bytes are retained in the full packet. Each is one hexadecimal byte. The normal row is 01 64 14 10 5A 6E 0A 40. 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 custom caller sets DROP to 1. The same downward crossing now passes through the platform, returning LANDED zero and SNAP equal to the proposed bottom 110. The caller decides how long drop-through remains active.

Maintained custom configuration or caller

custom.asm
LANDED=$80
SNAP=$81
ACTOR_X=$82
ACTOR_W=$83
PREV_BOTTOM=$84
NEW_BOTTOM=$85
PLATFORM_X=$86
PLATFORM_W=$87
PLATFORM_Y=$88
DROP=$89
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
COYOTE_STEPS=2
BUFFER_STEPS=2
TEXT_CAP=4
PROPERTY_COUNT=16
STREAM_BYTES=13
CRC_POLY=49
!source "caller.inc"
!source "exercise.inc"
component_start:
!source "component.inc"
!source "data.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 custom row is 00 6E 14 10 5A 6E 0A 40. 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 retains a contact only when the proposed bottom equals the platform top. It discards the correctly detected 90→110 crossing of top 100, so a fast fall misses the surface. Compare the interval crossed, not just equality at the endpoint.

Select the deliberately wrong client

wrong.asm
LANDED=$80
SNAP=$81
ACTOR_X=$82
ACTOR_W=$83
PREV_BOTTOM=$84
NEW_BOTTOM=$85
PLATFORM_X=$86
PLATFORM_W=$87
PLATFORM_Y=$88
DROP=$89
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
COYOTE_STEPS=4
BUFFER_STEPS=4
TEXT_CAP=8
PROPERTY_COUNT=16
STREAM_BYTES=13
CRC_POLY=7
!source "caller.inc"
!source "exercise.inc"
component_start:
!source "component.inc"
!source "data.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 row 00 6E 14 10 5A 6E 0A 40.
The wrong client completes with 00 6E 14 10 5A 6E 0A 40. Its code and the explanation above identify the cause; the capture observes the result.

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 92 code bytes, 0 immutable data bytes and 10 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.

Two horizontal interval comparisons and two vertical comparisons, with no per-pixel stepping. On 6502/Z80 a carry from an endpoint addition represents an endpoint beyond 255; 68000 uses widened words. The caller’s search over platforms is additional work.

This tests a horizontal one-way top, not walls, ceilings, slopes or a full swept-AABB solver. Teleports and large coordinate jumps need an explicit policy. Moving platforms also need relative motion and carried movement. The static rectangle-overlap routines remain useful for other contact tests.

Four native runs use PAL C64 with stock BASIC/KERNAL PRG startup and RUN; Asm198x 0.0.72 and Emu198x 0.44.2. 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.

Related technique · Maths for Games · The Craft.

Source and native evidence.