;;;
;; Pseudo-random number implementation by a linear feedback shift register.
;;
;; This is one of the most common techniques developed on NES/Famicom games, and
;; it's based on the algorithm from french mathematician Évariste Galois:
;; https://en.wikipedia.org/wiki/Linear-feedback_shift_register#Galois_LFSRs.
;; That is, we have a wider-than-8-bit register which keeps on shifting and
;; "taps" on some bits whenever the carry flag is set. This sounds complicated
;; but it really is not, and it can be further optimized as Brad Smith proved:
;; https://github.com/bbbradsmith/prng_6502.
;;
;; All in all, the implementation here is based on the "basic" implementation
;; from NesHacker: https://github.com/NesHacker/NES-RNG/blob/main/nes-rng.s; as
;; it's easier to reason about. Other than that, refer to the NESDev wiki for
;; more information: https://www.nesdev.org/wiki/Random_number_generator.
;; Producing random numbers with a linear feedback shift register require at
;; least 16-bit. In this case, we have the low byte which will be the end result
;; upon each call to `linear_feedback_shift_register`; and the high byte which
;; will be messed up to feed the low byte on each iteration. If we wanted more
;; random numbers (i.e. how many random numbers can be generated before they
;; start repeating all over again), we would need to add more bytes. For a
;; simple 8-bit computer like the NES/Famicom, a 16-bit register for this is
;; more than enough.
.scope Linear
zp_register_lo = $40
zp_register_hi = $41
.endscope
;; Updates the 'a' register with a new random number as extracted from the
;; linear feedback shift register referenced in the `Linear` scope.
;;
;; See: https://github.com/NesHacker/NES-RNG/blob/main/nes-rng.s
.proc linear_feedback_shift_register
lda Linear::zp_register_hi
ldy #8
@loop:
;; Shift the least significant bit from the high byte to the low byte of the
;; 16-bit register.
lsr
ror Linear::zp_register_lo
;; Following the Galois algorithm, if the carry flag was set, then we need
;; to tap on some specific bits of the low byte with an xor. Hence, if the
;; carry flag is clear, skip the `eor` instruction.
bcc @skip_eor
eor #$B4
@skip_eor:
;; Save the current state of the high register and decrement the loop index.
sta Linear::zp_register_hi
dey
bne @loop
;; The low byte now contains the shifted values from the loop. That's our
;; final "random" number!
lda Linear::zp_register_lo
rts
.endproc