aboutsummaryrefslogtreecommitdiff
;;;
;; 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