Want the same enemy choices while chasing a bug? Start from the same seed and make the same calls. These routines give you a small, repeatable source of bytes; changing the seed moves to another point in its cycle.
Seed zero deliberately becomes one. The first eight draws are B4 5A 2D A2 51 9C 4E 27. The custom caller starts at A7 and shows a different sequence. The wrong caller clears the state directly and displays eight zeroes. You can see the contract before putting it into a game.
Include, initialise and call
Keep the reusable component separate from the optional display and test caller. demo.asm shows the exact include order and selected seed. Include the component once, reserve its state, call rng_seed at startup and call rng_next when a game decision needs another value. Copy the returned byte if more than one consumer should see that draw; another call advances the sequence.
A supplies the seed and returns the next byte; X/Y survive. Reserve one private zero-page byte as RNG_STATE. Flags change, but decimal mode is unchanged. Do not use the 6510 ports at $00/$01 or somebody else’s workspace. The standalone caller reserves $70–$76 for its own generator and verification transport, owns the screen/font/colour RAM, disables IRQ sources and does not return to BASIC. The component itself changes no banking, video or interrupt registers.
The helper needs 19 assembled code bytes and one state byte on this configuration. A direct call needs the processor’s return-address stack space, plus your enclosing calls and interrupts. The standalone artifact is 498 bytes, including loader/container/display/test overhead and any cartridge padding; that is not the cost of including the generator in your game. Every next call performs one shift, conditional feedback and one state write. Those operations are constant-sized here; storage counts do not promise a frame budget.
Include the component and select the external seed
RNG_STATE=$70
seed_index=$71
checkpoint=$72
reg_x=$73
ptr=$74
reg_y=$76
OUTPUT=$c000
preview=OUTPUT+800
ready=OUTPUT+808
SEED=0
BROKEN=0
!source "caller.inc"
!source "../../../../../cross-platform/patterns/framework/seeded-random/exercise6502.inc"
!source "../../../../../cross-platform/patterns/framework/seeded-random/6502.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 sequence and its limits
This is the shared article’s eight-bit, right-shifting Galois LFSR with feedback $B4. Every nonzero byte appears once in a complete 255-call cycle, then the state repeats. The sample’s independent native caller checks that full cycle and every possible input seed, not just a few appealing-looking numbers. It is predictable and correlated. Its short period makes it a useful small baseline, not a good source for every procedural world or a security-sensitive choice.
Seed once. Seeding again before each decision starts the sequence over. A frame counter or input delay can vary the initial seed, but mixing it into every draw changes the replay contract. Never describe observed player timing as guaranteed entropy or a guarantee of a different game.
Reusable seed and next routines
; SPDX-License-Identifier: MIT
; Caller defines RNG_STATE as one private writable zero-page byte.
; rng_seed: A=external byte; zero maps to one. Returns normalized seed in A.
; rng_next: A=next state; requires nonzero state. Does NOT conceal corruption.
; A/flags change, X/Y survive. Decimal flag is unchanged. Not re-entrant.
rng_seed:
cmp #0
bne rng_seed_store
lda #1
rng_seed_store:
sta RNG_STATE
rts
rng_next:
lda RNG_STATE
lsr
bcc rng_next_store
eor #$b4
rng_next_store:
sta RNG_STATE
rts
rng_code_end:Save and restore a sequence
Read the one state byte after a draw and save it without calling the generator again. Restore a valid nonzero byte through the same seed entry before continuing; the following draws repeat. The second caller saves a point inside the sequence, makes eight draws, restores it and checks that all eight repeat. Reject zero or a damaged save record explicitly: the external-seed zero policy is not validation of a save format.
This replays the generator, not an entire game. Reproducing a game also needs matching game state, inputs, update timing and random-call order. A new cosmetic random draw can otherwise change the later enemy decision. Separate state bytes can keep those consumers from consuming each other’s draws, but two phases of this tiny cycle are not statistically independent streams.
The complete optional caller
*=$0801
!byte $0b,$08,$0a,$00,$9e
!text "2064"
!byte 0,0,0
*=$0810
start:
sei
cld
lda #$7f
sta $dc0d
sta $dd0d
lda $dc0d
lda $dd0d
lda #0
sta $d01a
sta $d015
sta $d020
sta $d021
lda $dd02
ora #3
sta $dd02
lda $dd00
ora #3
sta $dd00
lda #$1b
sta $d011
lda #8
sta $d016
lda #$18
sta $d018
ldx #0
lda #0
clear_screen:
sta $0400,x
sta $0500,x
sta $0600,x
sta $0700,x
inx
bne clear_screen
lda #1
colours:
sta $d800,x
sta $d900,x
sta $da00,x
sta $db00,x
inx
bne colours
ldx #0
copy_font:
lda hex_font,x
sta $2000,x
inx
cpx #136
bne copy_font
jsr exercise_rng
; Normal library use: initialize once, then draw only when the game needs it.
lda #SEED
!if BROKEN {
sta RNG_STATE ; Deliberately bypass the zero-seed policy.
} else {
jsr rng_seed
}
ldx #0
preview_next:
jsr rng_next
sta preview,x
inx
cpx #8
bne preview_next
ldx #0
ldy #0
show_byte:
lda preview,x
lsr
lsr
lsr
lsr
clc
adc #1
sta $0400,y
iny
lda preview,x
and #15
clc
adc #1
sta $0400,y
iny
iny
inx
cpx #8
bne show_byte
lda #165
sta ready
hold:
jmp holdMake it your own
custom.asm changes the external seed from zero to $A7. Keep the component unchanged and compare its first eight output bytes with the independent sequence check. The change costs no extra component state or code on this configuration. Choose a recorded nonzero seed during debugging; add session variation only when its source and replay implications are understood.
For sixteen approximate choices, masking a raw byte with 15 is cheap, but not perfectly even: across all 255 source states, zero appears 15 times and each other nibble 16 times. Masking is not a general replacement for remainder division, and a remainder is not automatically unbiased either. Correct rejection sampling must account for the actual source domain, retry cost and legal empty/invalid cases. Bounded selection, weighted choices and larger-state generators remain separate extensions to this baseline.
Change the seed, preserve the component
RNG_STATE=$70
seed_index=$71
checkpoint=$72
reg_x=$73
ptr=$74
reg_y=$76
OUTPUT=$c000
preview=OUTPUT+800
ready=OUTPUT+808
SEED=167
BROKEN=0
!source "caller.inc"
!source "../../../../../cross-platform/patterns/framework/seeded-random/exercise6502.inc"
!source "../../../../../cross-platform/patterns/framework/seeded-random/6502.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:Common mistake: clear the state instead of seeding it
The deliberately wrong caller bypasses rng_seed and stores zero directly. Shifting zero produces no outgoing bit, so feedback never applies and every result remains zero. The program completes and renders its picture; this is a generator-state fault, not a failed loader. The repaired caller uses the normal seed entry again and produces the same artifact as the working caller.
After successful initialization, an unexpected zero means something has corrupted the state. rng_next deliberately leaves that symptom visible. Silently replacing it on every call could hide an overlapping variable, an interrupt conflict or a damaged save. Find that writer rather than hiding it.
Deliberate fault: bypass initialization with zero
RNG_STATE=$70
seed_index=$71
checkpoint=$72
reg_x=$73
ptr=$74
reg_y=$76
OUTPUT=$c000
preview=OUTPUT+800
ready=OUTPUT+808
SEED=0
BROKEN=1
!source "caller.inc"
!source "../../../../../cross-platform/patterns/framework/seeded-random/exercise6502.inc"
!source "../../../../../cross-platform/patterns/framework/seeded-random/6502.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:Existing lessons and advanced choices
C64 Starfield already uses a left-shifting $1D byte generator. This is the right-shifting $B4 form from the maintained shared article, not a drop-in promise to preserve Starfield’s sequence. The lesson is unchanged.
The shared LFSR entry compares the algorithm and its range/replay cautions. Maths for Games connects the finite state space, bit operations and outcome counts. A larger state can postpone repetition; better statistical behaviour, replay stability and distribution still need their own evidence. The Craft asks what those choices do for the player, rather than treating a random byte as a design decision.
What was verified
Four native runs on Emu198x 0.44.2 / Asm198x 0.0.72 cover normal initialization, a changed seed, the bounded zero-state fault and its exact repair. The fixture emits a guarded 795-byte packet: all 256 external seed/next pairs, a complete 255-state cycle, eight replayed draws twice, eight deliberately corrupted-zero draws and register sentinels. An independent bit-equation oracle checks the output, cycle membership and replay. The program is loaded through the named native startup path; code readback and image pixels are checked against the actual displayed bytes.
Each machine’s retained configuration pins tools and applicable firmware. No firmware or full ROM dumps are distributed. The checks must reject altered source, configuration, code, seed/cycle/replay/register observations, preview, costs and pictures. These are observations of the named emulator configuration, not physical-hardware timing measurements or proof of fairness in a finished game.

