diff options
| author | Miquel Sabaté Solà <mikisabate@gmail.com> | 2025-05-06 16:06:20 +0200 |
|---|---|---|
| committer | Miquel Sabaté Solà <mikisabate@gmail.com> | 2025-05-06 16:22:48 +0200 |
| commit | 111be468f0c78a572731ba1a273f8166dcdf7b1c (patch) | |
| tree | fc6ebb97f580dafb9f949627d7720b0c86818922 /rand/rand.s | |
| parent | ee183ebbdfdf407044948aefa06f8a131dd23213 (diff) | |
| download | code.nes-111be468f0c78a572731ba1a273f8166dcdf7b1c.tar.gz code.nes-111be468f0c78a572731ba1a273f8166dcdf7b1c.zip | |
rand: Add an example with PRNG
Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
Diffstat (limited to 'rand/rand.s')
| -rw-r--r-- | rand/rand.s | 438 |
1 files changed, 438 insertions, 0 deletions
diff --git a/rand/rand.s b/rand/rand.s new file mode 100644 index 0000000..abf944f --- /dev/null +++ b/rand/rand.s @@ -0,0 +1,438 @@ +;;; +;; Showcase different strategies for Random Number Generation (RNG). +;; +;; Refer to the README.md file for documentation. Here I have left comments +;; whenever there's something "new" if you are coming from the `basics/` +;; directory. + +.segment "HEADER" + .byte 'N', 'E', 'S', $1A + .byte $02, $01 + .byte $00, $00 + +.segment "VECTORS" + .addr nmi, reset, irq + +.segment "CHARS" +.incbin "../assets/alphanum.chr" + +.segment "CODE" + +.include "../shared/ppu.s" +.include "../shared/clear.s" +.include "../shared/joypad.s" + +;; Different algorithms. +.include "linear.s" +.include "precalc.s" + +;; Number of algorithms for this game. +ALGORITHM_SIZE = $02 + +;; See Vars::zp_button_timer. +KEY_TIMER = 15 + +;; Variables used by this game. +.scope Vars + ;; 0: "Press start" state; 1: "number generation" state. + zp_state = $30 + + ;; Timer for key presses. A button press will only be considered if the + ;; button timer is zero. Whenever that's the case, the code will reset the + ;; timer to this new value, and decrement it whenever a new press is found. + ;; Whenever we reach back to zero, then the button press will be considered + ;; again. All of this is because whenever the player presses a button, it + ;; actually presses it for more than one frame, and things can go crazy from + ;; this fact. + zp_button_timer = $31 + + ;; Algorithm that has been selected. + zp_algorithm = $32 + + ;; The random seed for this game. As described in the README.md file, this + ;; is actually a frame counter for the "Press start" state, and whenever the + ;; player hits "Start", it will be paused. That is, our random seed is + ;; simply the number of frames that the player took to press "Start" at the + ;; beginning. + ;; + ;; NOTE: it's not going to be initialized to get a more random feeling on + ;; real hardware from unknown RAM state. + zp_seed = $33 + + ;; The number to be displayed. + zp_number = $34 +.endscope + +;; Main function, this takes care of reading input, changing the state, and +;; calling the relevant algorithm to get new numbers. +.proc main + ;; Initialize all variables (except Vars::zp_seed as explained above). + lda #0 + sta Vars::zp_state + sta Vars::zp_button_timer + sta Vars::zp_algorithm + sta Vars::zp_number + + ;; Clear both screens. Yes, over the top, but it gets the job done. + CLEAR_SCREENS $20, $28 + + ;; Initialize palettes and show the "Press start" message. + jsr init_palettes + jsr show_init_screen + + cli + lda #%10001000 + sta $2000 ; PPUCTRL + lda #%00011110 + sta $2001 ; PPUMASK + +@main_game_loop: + ;; Should we actually read the joypad? This is handled via the button timer + ;; as explained above. + lda Vars::zp_button_timer + beq @check_joypad + dec Vars::zp_button_timer + jmp @end + +@check_joypad: + ;; Yes! Then read the joypad. + READ_JOYPAD1 + + ;; What's the current game state? + lda Vars::zp_state + bne @check_change_algorithm + + ;; "Press start" state. If the player is not pressing "Start", ignore + ;; everything and go to the end. + lda Joypad::zp_buttons1 + and #Joypad::BUTTON_START + beq @end + + ;; Reset the button timer. + lda #KEY_TIMER + sta Vars::zp_button_timer + + ;; The random seed has a proper value and we can use that as a first random + ;; number. + lda Vars::zp_seed + sta Vars::zp_number + + ;; The 'linear' algorithm actually disregards any parameters and needs a + ;; 16-bit register. Let's initialize this register with the current seed. + sta Linear::zp_register_lo + sta Linear::zp_register_hi + + ;; Move into the next state. + inc Vars::zp_state + + jmp @end + +@check_change_algorithm: + ;; We are in a running state. Check if the player is asking to change the + ;; algorithm. + lda Joypad::zp_buttons1 + and #Joypad::BUTTON_SELECT + beq @check_a_button + + ;; Reset the button timer. + lda #KEY_TIMER + sta Vars::zp_button_timer + + ;; The player asked to change the algorithm. Do it now and go generate a new + ;; number with that. + ldx Vars::zp_algorithm + inx + cpx #ALGORITHM_SIZE + bne @store_algorithm + ldx #0 +@store_algorithm: + stx Vars::zp_algorithm + jmp @next_number + +@check_a_button: + ;; Is the player asking for a new number? If not go to the end. + lda Joypad::zp_buttons1 + and #Joypad::BUTTON_A + beq @end + + ;; Reset the button timer. + lda #KEY_TIMER + sta Vars::zp_button_timer + +@next_number: + ;; Setup parameters depending on the algorithm and actually call it. + ldx Vars::zp_algorithm + bne @precalc + jsr linear_feedback_shift_register + jmp @store_number +@precalc: + lda Vars::zp_number + jsr precalc +@store_number: + sta Vars::zp_number + +@end: + ;; And wait for the render to happen as it's done in any other example. + lda #%10000000 + ora $20 + sta $20 +@wait_for_render: + bit $20 + bmi @wait_for_render + + jmp @main_game_loop +.endproc + +;; NMI code is pretty standard. I have added comments for the code which is +;; specific to this game. +.proc nmi + bit $20 + bpl @next + + pha + txa + pha + tya + pha + + lda #$00 + sta $2003 ; OAMADDR + lda #$02 + sta $4014 ; OAMDMA + + ;; What's the current game state? + lda Vars::zp_state + beq @seed_inc + + ;; Running state. Print the current algorithm and number. + jsr print_algorithm + jsr print_value + + ;; The running state is displayed on the other nametable. Update the PPU + ;; control register for this (it's of course stupid to update it every time, + ;; but I didn't feel like doing the proper thing of shadowing the PPU + ;; control register and update only on changes, etc.). + lda #%10001010 + sta $2000 ; PPUCTRL + bne @after_seed + +@seed_inc: + ;; "Press start" state: just increase the frame counter which is used as a + ;; seed. + inc Vars::zp_seed + +@after_seed: + bit $2002 ; PPUSTATUS + lda #$00 + sta $2005 ; PPUSCROLL + sta $2005 ; PPUSCROLL + + lda #%01111111 + and $20 + sta $20 + + pla + tay + pla + tax + pla +@next: + rti +.endproc + +;; Show the "Alg: <algorithm>" message on screen. +.proc print_algorithm + ;; "ALG: " + WRITE_PPU_DATA $298B, $1A + WRITE_PPU_DATA $298C, $25 + WRITE_PPU_DATA $298D, $20 + WRITE_PPU_DATA $298E, $34 + WRITE_PPU_DATA $298F, $00 + + lda Vars::zp_algorithm + beq @linear + + ;; "PRECALC" + WRITE_PPU_DATA $2990, $29 + WRITE_PPU_DATA $2991, $2B + WRITE_PPU_DATA $2992, $1E + WRITE_PPU_DATA $2993, $1C + WRITE_PPU_DATA $2994, $1A + WRITE_PPU_DATA $2995, $25 + WRITE_PPU_DATA $2996, $1C + rts + + ;; "LINEAR " +@linear: + WRITE_PPU_DATA $2990, $25 + WRITE_PPU_DATA $2991, $22 + WRITE_PPU_DATA $2992, $27 + WRITE_PPU_DATA $2993, $1E + WRITE_PPU_DATA $2994, $1A + WRITE_PPU_DATA $2995, $2B + WRITE_PPU_DATA $2996, $00 + rts +.endproc + +;; Show the "Val: $<number>" message on screen. +.proc print_value + ;; "VAL: $" + WRITE_PPU_DATA $29AB, $2F + WRITE_PPU_DATA $29AC, $1A + WRITE_PPU_DATA $29AD, $25 + WRITE_PPU_DATA $29AE, $34 + WRITE_PPU_DATA $29AF, $00 + WRITE_PPU_DATA $29B0, $35 + + ;; Set the high byte on the 'y' register, and the low byte on the 'x' + ;; register. + lda #$F0 + and Vars::zp_number + lsr + lsr + lsr + lsr + clc + adc #$10 + tay + lda #$0F + and Vars::zp_number + clc + adc #$10 + tax + + ;; Display the actual number. + bit $2002 + lda #$29 + sta $2006 + lda #$B1 + sta $2006 + sty $2007 + stx $2007 + + rts +.endproc + +;; Show the "Press start" message. +.proc show_init_screen + ;; "PRESS" + WRITE_PPU_DATA $21AB, $29 + WRITE_PPU_DATA $21AC, $2B + WRITE_PPU_DATA $21AD, $1E + WRITE_PPU_DATA $21AE, $2C + WRITE_PPU_DATA $21AF, $2C + + ;; "START" + WRITE_PPU_DATA $21B1, $2C + WRITE_PPU_DATA $21B2, $2D + WRITE_PPU_DATA $21B3, $1A + WRITE_PPU_DATA $21B4, $2B + WRITE_PPU_DATA $21B5, $2D + + rts +.endproc + +;; Initialize palettes. A bit over the top since only two colors are used. +.proc init_palettes + lda #$3F + sta $2006 ; PPUADDR + lda #$00 + sta $2006 ; PPUADDR + + ldx #0 +@load_palettes_loop: + lda palettes, x + sta $2007 ; PPUDATA + inx + cpx #$20 + bne @load_palettes_loop + rts +palettes: + DEFAULT_COLOR = $0F + + ;; Background + .byte DEFAULT_COLOR, $20, $FF, $FF + .byte DEFAULT_COLOR, $20, $FF, $FF + .byte DEFAULT_COLOR, $20, $FF, $FF + .byte DEFAULT_COLOR, $20, $FF, $FF + + ;; Foreground + .byte DEFAULT_COLOR, $20, $FF, $FF + .byte DEFAULT_COLOR, $20, $FF, $FF + .byte DEFAULT_COLOR, $20, $FF, $FF + .byte DEFAULT_COLOR, $20, $FF, $FF + + rts +.endproc + +;;; +;; NOTE: down below just boilerplate. Nothing special from the `basics/` +;; examples. + +.proc reset + sei + cld + + ldx #$40 + stx $4017 ; APU Frame Counter + + ldx #$FF + txs + + inx + stx $2000 ; PPUCTRL + stx $2001 ; PPUMASK + stx $4010 ; APU DMC + + bit $2002 ; PPUSTATUS +@vblankwait1: + bit $2002 ; PPUSTATUS + bpl @vblankwait1 + + ldx #0 + lda #0 +@ram_reset_loop: + sta $000, x + sta $100, x + sta $300, x + sta $400, x + sta $500, x + sta $600, x + sta $700, x + inx + bne @ram_reset_loop ; if x overflows back to #00, then we are done. + + lda #$EF +@sprite_reset_loop: + sta $200, x + inx + bne @sprite_reset_loop + + lda #$00 + sta $2003 ; OAMADDR + lda #$02 + sta $4014 ; OAMDMA + +@vblankwait2: + bit $2002 ; PPUSTATUS + bpl @vblankwait2 + + lda #$3F + sta $2006 ; PPUADDR + lda #$00 + sta $2006 ; PPUADDR + + lda #$0F + ldx #$20 +@palettes_reset_loop: + sta $2007 ; PPUDATA + dex + bne @palettes_reset_loop + + jmp main +.endproc + +.proc irq + rti +.endproc + |
