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; the decimal flag is unchanged. The standalone NTSC NROM caller owns $70–$76 and a bounded CPU-RAM results buffer. It disables NMI, DMC and audio and renders a fixed background diagnostic; it is not a game loop. Your NMI must not advance this same generator concurrently. The component itself touches no PPU, APU or controller 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 40,976 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
.segment "HEADER"
.byte "NES",$1a,2,1,0,0,0,0,0,0,0,0,0,0
RNG_STATE=$70
seed_index=$71
checkpoint=$72
reg_x=$73
ptr=$74
reg_y=$76
OUTPUT=$0300
preview=OUTPUT+800
ready=OUTPUT+808
SEED=0
BROKEN=0
.include "caller.inc"
.include "../../../../../cross-platform/patterns/framework/seeded-random/exercise6502.inc"
.include "../../../../../cross-platform/patterns/framework/seeded-random/6502.inc"
code_end:
.segment "VECTORS"
.word nmi,reset,irq
.segment "CHARS"
.include "../../input/controller-reading/edges-font.inc"
.res 8192-272,0The 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
.segment "CODE"
reset:
sei
cld
ldx #$ff
txs
inx
stx $2000
stx $2001
stx $4010
stx $4015
lda #$40
sta $4017
lda #0
clear_ram:
sta $0000,x
sta $0100,x
sta $0200,x
sta $0300,x
sta $0400,x
sta $0500,x
sta $0600,x
sta $0700,x
inx
bne clear_ram
jsr wait_vblank
jsr wait_vblank
jsr exercise_rng
; Normal library use: initialize once, then draw only when the game needs it.
lda #SEED
.if BROKEN
sta RNG_STATE
.else
jsr rng_seed
.endif
ldx #0
preview_next:
jsr rng_next
sta preview,x
inx
cpx #8
bne preview_next
jsr wait_vblank
bit $2002
lda #$20
sta $2006
lda #0
sta $2006
lda #0
ldy #4
ldx #0
clear_names:
sta $2007
inx
bne clear_names
dey
bne clear_names
lda #$3f
sta $2006
lda #0
sta $2006
lda #$0f
sta $2007
lda #$30
sta $2007
lda #$20
sta $2006
lda #$22
sta $2006
ldx #0
show_byte:
lda preview,x
lsr
lsr
lsr
lsr
tay
iny
tya
sta $2007
lda preview,x
and #15
tay
iny
tya
sta $2007
lda #0
sta $2007
inx
cpx #8
bne show_byte
lda #0
sta $2005
sta $2005
lda #$0a
sta $2001
lda #165
sta ready
hold:
jmp hold
wait_vblank:
bit $2002
bpl wait_vblank
rts
nmi:
irq:
rtiMake 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
.segment "HEADER"
.byte "NES",$1a,2,1,0,0,0,0,0,0,0,0,0,0
RNG_STATE=$70
seed_index=$71
checkpoint=$72
reg_x=$73
ptr=$74
reg_y=$76
OUTPUT=$0300
preview=OUTPUT+800
ready=OUTPUT+808
SEED=167
BROKEN=0
.include "caller.inc"
.include "../../../../../cross-platform/patterns/framework/seeded-random/exercise6502.inc"
.include "../../../../../cross-platform/patterns/framework/seeded-random/6502.inc"
code_end:
.segment "VECTORS"
.word nmi,reset,irq
.segment "CHARS"
.include "../../input/controller-reading/edges-font.inc"
.res 8192-272,0Common 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
.segment "HEADER"
.byte "NES",$1a,2,1,0,0,0,0,0,0,0,0,0,0
RNG_STATE=$70
seed_index=$71
checkpoint=$72
reg_x=$73
ptr=$74
reg_y=$76
OUTPUT=$0300
preview=OUTPUT+800
ready=OUTPUT+808
SEED=0
BROKEN=1
.include "caller.inc"
.include "../../../../../cross-platform/patterns/framework/seeded-random/exercise6502.inc"
.include "../../../../../cross-platform/patterns/framework/seeded-random/6502.inc"
code_end:
.segment "VECTORS"
.word nmi,reset,irq
.segment "CHARS"
.include "../../input/controller-reading/edges-font.inc"
.res 8192-272,0Existing lessons and advanced choices
The existing NES lessons and controller/frame components remain separate. This implementation shares the small 6502 arithmetic component with C64; that does not make their display, clock or cartridge interfaces interchangeable.
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.

