Gravity should keep adding speed until the chosen terminal velocity, not turn a fast fall into an upward launch when an integer overflows. This helper adds signed acceleration and clamps the result to an explicit symmetric speed limit.

Include, initialise and call
Reserve VEL_LO, VEL_HI, ACC_LO, ACC_HI, LIMIT_LO, LIMIT_HI, CLIPPED, VALID as 8 contiguous private bytes in that order. Include the helper once. Call bounded_acceleration_init at the intended reset boundary: it clears every named byte, including input fields. Set the starting values afterwards, then call bounded_acceleration_step for each intended service.
Initialise once, set the starting velocity, acceleration and limit, then service before the position update if that is your chosen integrator order. Treat that order as part of the game’s behaviour. The normal four-call client accelerates by −0.5 per call to a limit of 2.0 and finishes at −2.0.
VEL_LO/VEL_HI and ACC_LO/ACC_HI are signed two’s-complement 8.8 words, stored low byte first. LIMIT_LO/LIMIT_HI are a nonnegative raw magnitude from 0 to 32,767. Step clears CLIPPED and VALID, rejects a limit with its high bit set, and otherwise adds velocity and acceleration in a logically wider signed range before clamping to −LIMIT…+LIMIT. VALID becomes 1; CLIPPED becomes 1 only when the wide sum was outside that interval. An invalid limit preserves velocity and leaves both flags zero. Acceleration and limit bytes survive.
Reusable initialisation and service
; SPDX-License-Identifier: MIT
; Bounded Signed Acceleration. Contiguous private bytes: VEL_LO, VEL_HI, ACC_LO, ACC_HI, LIMIT_LO, LIMIT_HI, CLIPPED, VALID.
; 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.
bounded_acceleration_init:
lda #0
sta VEL_LO
sta VEL_HI
sta ACC_LO
sta ACC_HI
sta LIMIT_LO
sta LIMIT_HI
sta CLIPPED
sta VALID
rts
bounded_acceleration_step:
lda #0
sta CLIPPED
sta VALID
lda LIMIT_HI
bpl accel_valid
rts
accel_valid:
inc VALID
lda VEL_LO
clc
adc ACC_LO
sta VEL_LO
lda VEL_HI
adc ACC_HI
sta VEL_HI
bvc accel_normal
lda ACC_HI
bmi accel_negative_cap
jmp accel_positive_cap
accel_normal:
lda VEL_HI
bmi accel_negative
cmp LIMIT_HI
bcc accel_done
bne accel_positive_cap
lda VEL_LO
cmp LIMIT_LO
bcc accel_done
beq accel_done
accel_positive_cap:
lda LIMIT_LO
sta VEL_LO
lda LIMIT_HI
sta VEL_HI
lda #1
sta CLIPPED
rts
accel_negative:
lda VEL_LO
eor #255
clc
adc #1
pha
lda VEL_HI
eor #255
adc #0
tax
pla
cpx LIMIT_HI
bcc accel_done
bne accel_negative_cap
cmp LIMIT_LO
bcc accel_done
beq accel_done
accel_negative_cap:
lda LIMIT_LO
eor #255
clc
adc #1
sta VEL_LO
lda LIMIT_HI
eor #255
adc #0
sta VEL_HI
lda #1
sta CLIPPED
accel_done:
rts
component_end:State, registers and ownership
Eight bytes: two for velocity, two for acceleration, two for a magnitude limit, and two replaced status flags. The symmetric limit cannot be 32,768 because the positive signed endpoint cannot represent it. Initial velocity may be anywhere in the signed 16-bit range; a valid call clamps the resulting sum.
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. This helper can also use one temporary stack byte, so allow three bytes for its call, excluding its caller and interrupts.
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 bounded_acceleration_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
VEL_LO=$80
VEL_HI=$81
ACC_LO=$82
ACC_HI=$83
LIMIT_LO=$84
LIMIT_HI=$85
CLIPPED=$86
VALID=$87
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
preview_rules:
!if CUSTOM {
lda #128
sta ACC_LO
lda #255
sta ACC_HI
lda #0
sta LIMIT_LO
lda #1
sta LIMIT_HI
!if BROKEN {
lda #0
sta ACC_HI
jsr checked_step
} else {
jsr checked_step
}
jsr emit_state
!if BROKEN {
lda #0
sta ACC_HI
jsr checked_step
} else {
jsr checked_step
}
jsr emit_state
!if BROKEN {
lda #0
sta ACC_HI
jsr checked_step
} else {
jsr checked_step
}
jsr emit_state
!if BROKEN {
lda #0
sta ACC_HI
jsr checked_step
} else {
jsr checked_step
}
jsr emit_state
} else {
lda #128
sta ACC_LO
lda #255
sta ACC_HI
lda #0
sta LIMIT_LO
lda #2
sta LIMIT_HI
!if BROKEN {
lda #0
sta ACC_HI
jsr checked_step
} else {
jsr checked_step
}
jsr emit_state
!if BROKEN {
lda #0
sta ACC_HI
jsr checked_step
} else {
jsr checked_step
}
jsr emit_state
!if BROKEN {
lda #0
sta ACC_HI
jsr checked_step
} else {
jsr checked_step
}
jsr emit_state
!if BROKEN {
lda #0
sta ACC_HI
jsr checked_step
} else {
jsr checked_step
}
jsr emit_state
}
rtsThe display fields are VEL_LO, VEL_HI, ACC_LO, ACC_HI, LIMIT_LO, LIMIT_HI, CLIPPED, VALID. Each is one hexadecimal byte. The normal row is 00 FE 80 FF 00 02 00 01. 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 client lowers the limit from 2.0 to 1.0. Continued acceleration now saturates at −1.0 and sets CLIPPED on the later calls. A zero limit is also a valid request to hold velocity at zero.
Maintained custom configuration or caller
VEL_LO=$80
VEL_HI=$81
ACC_LO=$82
ACC_HI=$83
LIMIT_LO=$84
LIMIT_HI=$85
CLIPPED=$86
VALID=$87
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 FF 80 FF 00 01 01 01. 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 clears ACC_HI. The intended FF80 displacement (−0.5) becomes 0080 (+0.5), so the object accelerates the wrong way. Supply the signed high byte rather than zero-extending a negative value.
Select the deliberately wrong client
VEL_LO=$80
VEL_HI=$81
ACC_LO=$82
ACC_HI=$83
LIMIT_LO=$84
LIMIT_HI=$85
CLIPPED=$86
VALID=$87
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:
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 135 code bytes, 0 immutable data bytes and 8 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.
The 6502 examines signed overflow after the high-byte ADC; the Z80 uses ADC HL,DE because ADD HL,DE does not provide the same signed-overflow result; the 68000 sign-extends words into a long intermediate. Comparisons and clamping take bounded paths. No floating-point unit is used.
This models acceleration over fixed simulation updates. A variable time step needs explicit scaling, wider intermediates and a rounding policy. Drag, collision response and different upward/downward limits are separate rules. If parameters are prevalidated, an unchecked internal variant may save work, but it is a different contract and is not the qualified implementation here.
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.