;;; ;; 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: " 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: $" 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