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