; configuration {{{
.asciitable
MAP "-" = 1
MAP "|" = 2
MAP ":" = 3
MAP "." = 4
MAP "," = 5
MAP "'" = 6
.enda
.struct point
x db
y db
.endst
; }}}
; memory layout {{{
; rom {{{
.ROMBANKMAP
BANKSTOTAL 2
BANKSIZE $4000
BANKS 1
BANKSIZE $2000
BANKS 1
.ENDRO
; }}}
; ram {{{
.MEMORYMAP
DEFAULTSLOT 0
SLOTSIZE $4000
SLOT 0 $C000
SLOTSIZE $2000
SLOT 1 $0000 ; location doesn't matter, CHR data isn't in main memory
.ENDME
.ENUM $00
buttons_pressed DB
prev_buttons DB
sleeping DB
game_state DB ; 0: menu, 1: playing, 2: redrawing, 3: paused
head_x DW
head_y DW
length DB
direction DB ; 0: up, 1: down, 2: left, 3: right
frame_skip DB
frame_count DB
rand_state DB
rand_out DB
apple INSTANCEOF point
vram_addr_low DB
vram_addr_high DB
body_test_x DB
body_test_y DB
num_draws DB
.ENDE
; }}}
; }}}
; prg {{{
.bank 0
.org $0000
; main codepath {{{
; initialization {{{
; the ppu takes two frames to initialize, so we have some time to do whatever
; initialization of our own that we want to while we wait. we choose here to
; set up cpu flags in the first frame and clear out system ram in the second
; frame (clearing out ram isn't at all necessary, but we can't do anything
; useful at this point anyway, so we may as well in order to make things more
; predictable).
RESET:
SEI ; disable IRQs
CLD ; disable decimal mode
LDX #$40
STX $4017.w ; disable APU frame IRQ
LDX #$FF
TXS ; Set up stack (grows down from $FF to $00, at $0100-$01FF)
INX ; now X = 0
STX $2000.w ; disable NMI (we'll enable it later once the ppu is ready)
STX $2001.w ; disable rendering (same)
STX $4010.w ; disable DMC IRQs
; First wait for vblank to make sure PPU is ready
- BIT $2002 ; bit 7 of $2002 is reset once vblank ends
BPL - ; and bit 7 is what is checked by BPL
; set everything in ram ($0000-$07FF) to $00, except for $0200-$02FF which
; is conventionally used to hold sprite attribute data. we set that range
; to $FE, since that value as a position moves the sprites offscreen, and
; when the sprites are offscreen, it doesn't matter which sprites are
; selected or what their attributes are
clrmem:
LDA #$00
STA $0000, x
STA $0100, x
STA $0500, x
STA $0600, x
STA $0700, x
LDA #$FE
STA $0200, x
LDA #$80
STA $0300, x
LDA #$7D
STA $0400, x
INX
BNE clrmem
; initialize variables in ram
LDA #$03
LDX #$01
STA head_x, x
LDA #$04
LDX #$01
STA head_y, x
LDA #$07
STA $0201
LDA #$00
STA $0202
LDA #$00
STA $0206
STA $020A
STA $020E
; Second wait for vblank, PPU is ready after this
- BIT $2002
BPL -
; now that the ppu is ready, we can start initializing it
LoadPalettes:
LDA $2002 ; read PPU status to reset the high/low latch
LDA #$3F
STA $2006 ; write the high byte of $3F00 address
LDA #$00
STA $2006 ; write the low byte of $3F00 address
LDX #$00
LoadPalettesLoop:
LDA palette.w, x ;load palette byte
STA $2007 ;write to PPU
INX ;set index to next byte
CPX #$20
BNE LoadPalettesLoop ;if x = $20, 32 bytes copied, all done
LDA #%10000000 ; enable NMI interrupts
STA $2000
JSR end_game
; }}}
; main loop {{{
loop:
INC sleeping
- LDA sleeping
BNE -
JSR read_controller1
LDA game_state
BNE +
JSR start_screen_loop
JMP loop
+ JSR game_loop
JMP loop
; }}}
; }}}
; nmi interrupt {{{
NMI:
PHA
TXA
PHA
TYA
PHA
LDA game_state
CMP #$02
BEQ end_nmi
draw_game:
LDA #$00
STA $2003
LDA #$02
STA $4014
- LDX num_draws
BEQ done_drawing
DEX
STX num_draws
LDA #$00
CLC
ADC num_draws
ADC num_draws
ADC num_draws
TAX
LDA $0700, x
STA $2006
INX
LDA $0700, x
STA $2006
INX
LDA $0700, x
STA $2007
JMP -
done_drawing:
LDA #$20
STA $2006
LDA #$00
STA $2006
end_nmi:
LDA #$00
STA sleeping
PLA
TAY
PLA
TAX
PLA
RTI
; }}}
; subroutines {{{
start_screen_loop: ; {{{
LDX rand_state
- INX
BEQ - ; lfsr prngs have 0 as a fixed point
STX rand_state
handle_start:
LDA buttons_pressed
AND #%00010000
CMP #$00
BEQ end_start_screen_loop
JSR start_game
end_start_screen_loop:
RTS ; }}}
game_loop: ; {{{
handle_up:
LDA buttons_pressed
AND #%00001000
CMP #$00
BEQ handle_down
LDA #$00
STA direction
handle_down:
LDA buttons_pressed
AND #%00000100
CMP #$00
BEQ handle_left
LDA #$01
STA direction
handle_left:
LDA buttons_pressed
AND #%00000010
CMP #$00
BEQ handle_right
LDA #$02
STA direction
handle_right:
LDA buttons_pressed
AND #%00000001
CMP #$00
BEQ handle_pause
LDA #$03
STA direction
handle_pause:
LDA buttons_pressed
EOR prev_buttons
AND #%00010000
CMP #$00
BEQ check_pause
LDA buttons_pressed
AND #%00010000
CMP #$00
BEQ check_pause
LDA game_state
CMP #$01
BEQ +
LDA #$01
STA game_state
JMP check_pause
+ LDA #$03
STA game_state
check_pause:
LDA game_state
CMP #$03
BNE handle_frame
JMP end_game_loop
handle_frame:
LDX frame_count
INX
STX frame_count
CPX frame_skip
BPL +
JMP draw_sprites
+ LDA #$00
STA frame_count
set_offset:
LDX #$F8 ; i.e., -8
LDA direction
AND #%00000001 ; low bit determines negative or positive
BEQ set_axis
LDX #$08
set_axis:
LDY #$00
LDA direction
AND #%00000010 ; high bit determines which axis to change
BNE apply_direction_horiz
apply_direction_vert:
TXA
CLC
ADC (head_y), y
INC head_y
STA (head_y), y
LDA (head_x), y
INC head_x
STA (head_x), y
JMP check_collisions
apply_direction_horiz:
TXA
CLC
ADC (head_x), y
INC head_x
STA (head_x), y
LDA (head_y), y
INC head_y
STA (head_y), y
check_collisions
LDA (head_x), y
TAX
LDA (head_y), y
TAY
CPX #$40
BCC collision
CPX #$C0
BCS collision
CPY #$3D
BCC collision
CPY #$BD
BCS collision
STX body_test_x
STY body_test_y
DEC head_x
DEC head_y
JSR test_body_collision
INC head_x
INC head_y
LDX body_test_x
LDY body_test_y
CMP #$01
BEQ collision
CPX apple.x
BEQ maybe_eat_apple
JMP draw_sprites
collision:
JSR end_game
JMP end_game_loop
maybe_eat_apple:
CPY apple.y
BEQ eat_apple
JMP draw_sprites
eat_apple:
LDX length
INX
BEQ collision ; for now - this is the win condition
STX length
LDA speed.w, x
STA frame_skip
JSR new_apple
JSR draw_score
draw_sprites:
LDX #$00
JSR draw_sprite_at_head
LDA head_x
SEC
SBC length
STA head_x
LDA head_y
SEC
SBC length
STA head_y
LDX #$20
JSR draw_sprite_at_head
LDA head_x
CLC
ADC length
STA head_x
LDA head_y
CLC
ADC length
STA head_y
end_game_loop:
RTS ; }}}
read_controller1: ; {{{
LDA buttons_pressed
STA prev_buttons
; latch
LDA #$01
STA $4016
LDA #$00
STA $4016
; clock
LDX #$00
read_controller1_values:
CPX #$08
BPL end_read_controller1
LDA $4016
AND #%00000001
ASL buttons_pressed
ORA buttons_pressed
STA buttons_pressed
INX
JMP read_controller1_values
end_read_controller1:
RTS ; }}}
start_game: ; {{{
LDA #$02
STA game_state
LDA #$2D
STA $0204
STA $0208
STA $020C
LDA #$40
STA $0207
ADC #$08
STA $020B
ADC #$08
STA $020F
LDY #$00
LDA #$80
STA (head_x), y
LDA #$7D
STA (head_y), y
LDA #$01
STA length
LDA #$00
STA direction
LDA #20
STA frame_skip
JSR new_apple
JSR draw_score
- BIT $2002
BPL -
LDA #%00000000
STA $2001 ; disable rendering (since this will take longer than vblank)
LDA #$20
STA $2006
LDA #$00
STA $2006
LDA #$20
LDY #$07
-- LDX #$00
- STA $2007
INX
CPX #$20
BNE -
DEY
BNE --
LDX #$00
- LDA game_background_top.w, x
STA $2007
INX
CPX #$20
BNE -
LDY #$10
-- LDX #$00
- LDA game_background_middle.w, x
STA $2007
INX
CPX #$20
BNE -
DEY
BNE --
LDX #$00
- LDA game_background_bottom.w, x
STA $2007
INX
CPX #$20
BNE -
LDA #$20
LDX #$00
- STA $2007
INX
CPX #$20
BNE -
LDA #%00011000
STA $2001 ; reenable rendering
LDA #$01
STA game_state
RTS ; }}}
end_game: ; {{{
LDA #$02
STA game_state
LDA #$FE
STA $0200
STA $0203
STA $0204
STA $0207
STA $0208
STA $020B
STA $020C
STA $020F
- BIT $2002
BPL -
LDA #%00000000
STA $2001 ; disable rendering (since this will take longer than vblank)
LDA #$20
STA $2006
LDA #$00
STA $2006
LDA #$20
LDX #$0F
-- LDY #$00
- STA $2007
INY
CPY #$20
BNE -
DEX
BNE --
LDY #$00
- LDA intro_screen, y
STA $2007
INY
CPY #$20
BNE -
LDA #$20
LDX #$0E
-- LDY #$00
- STA $2007
INY
CPY #$20
BNE -
DEX
BNE --
LDA #%00011000
STA $2001 ; reenable rendering
LDA #$00
STA game_state
RTS ; }}}
new_apple: ; {{{
JSR rand4
LDA rand_out
ASL
ASL
ASL
CLC
ADC #$40
STA apple.x
JSR rand4
LDA rand_out
ASL
ASL
ASL
CLC
ADC #$3D
STA apple.y
LDA apple.x
STA body_test_x
LDA apple.y
STA body_test_y
JSR test_body_collision
CMP #$01
BEQ new_apple
LDA apple.y
STA $0200
LDA apple.x
STA $0203
RTS ; }}}
draw_sprite_at_head: ; {{{
LDA #$20
STA vram_addr_high
LDA #$E0
STA vram_addr_low
LDY #$00
LDA (head_y), y
SEC
SBC #$35
LSR
LSR
LSR
TAY
- CLC
LDA vram_addr_low
ADC #$20
STA vram_addr_low
LDA vram_addr_high
ADC #$00
STA vram_addr_high
DEY
BNE -
LDY #$00
LDA (head_x), y
SEC
SBC #$40
LSR
LSR
LSR
CLC
ADC #$08
ADC vram_addr_low
STA vram_addr_low
LDA vram_addr_high
ADC #$00
STA vram_addr_high
TXA
TAY
LDA #$00
ADC num_draws
ADC num_draws
ADC num_draws
TAX
LDA vram_addr_high
STA $0700, x
INX
LDA vram_addr_low
STA $0700, x
INX
TYA
STA $0700, x
LDX num_draws
INX
STX num_draws
RTS ; }}}
draw_score: ; {{{
LDA #$30
STA $0205
STA $0209
STA $020D
LDA length
hundreds:
CMP #100
BMI tens
SEC
SBC #100
LDX $0205
INX
TAY
TXA
STA $0205
TYA
JMP hundreds
tens:
CMP #10
BMI ones
SEC
SBC #10
LDX $0209
INX
TAY
TXA
STA $0209
TYA
JMP tens
ones:
CMP #1
BMI end_draw_score
SEC
SBC #1
LDX $020D
INX
TAY
TXA
STA $020D
TYA
JMP ones
end_draw_score:
RTS ; }}}
test_body_collision ; {{{
LDA head_x
PHA
LDA head_y
PHA
LDA head_x
SEC
SBC length
TAX
INX
TXA
STA head_x
LDA head_y
SEC
SBC length
TAX
INX
TXA
STA head_y
LDY length
- DEY
LDA (head_x), y
CMP body_test_x
BEQ maybe_collision_found
CPY #$00
BNE -
JMP collision_not_found
maybe_collision_found:
LDA (head_y), y
CMP body_test_y
BEQ collision_found
CPY #$00
BNE -
JMP collision_not_found
collision_found:
LDX #$01
JMP test_body_collision_end
collision_not_found:
LDX #$00
test_body_collision_end:
PLA
STA head_y
PLA
STA head_x
TXA
RTS ; }}}
rand4: ; {{{
JSR rand1
LDX rand_out
JSR rand1
TXA
ASL
ORA rand_out
TAX
JSR rand1
TXA
ASL
ORA rand_out
TAX
JSR rand1
TXA
ASL
ORA rand_out
STA rand_out
RTS ; }}}
rand1: ; {{{
; galois linear feedback shift register with taps at 8, 6, 5, and 4
LDA rand_state
AND #$01
STA rand_out
LSR rand_state
LDA rand_out
BEQ +
LDA rand_state
EOR #%10111000
STA rand_state
+ RTS ; }}}
; }}}
; data {{{
palette: ; {{{
.db $0F,$31,$32,$33,$0F,$35,$36,$37,$0F,$39,$3A,$3B,$0F,$3D,$3E,$0F
.db $0F,$1C,$15,$14,$0F,$02,$38,$3C,$0F,$1C,$15,$14,$0F,$02,$38,$3C
; }}}
intro_screen: ; {{{
.asc " SNAKE "
; }}}
game_background_top: ; {{{
.asc " ,----------------. "
; }}}
game_background_middle: ; {{{
.asc " | | "
; }}}
game_background_bottom: ; {{{
.asc " '----------------: "
; }}}
speed: ; {{{
.db 20,20,20,19,19,19,19,19,19,19,18,18,18,18,18,18,18,17,17,17,17,17,17,17,
.db 17,16,16,16,16,16,16,16,16,16,15,15,15,15,15,15,15,15,15,14,14,14,14,14,
.db 14,14,14,14,13,13,13,13,13,13,13,13,13,13,13,12,12,12,12,12,12,12,12,12,
.db 12,12,11,11,11,11,11,11,11,11,11,11,11,11,10,10,10,10,10,10,10,10,10,10,
.db 10,10,10,10,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
.db 8,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
.db 6,6,6,6,6,6,6,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,4,4,
.db 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,3,3,3,3,
.db 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3
; }}}
; }}}
.orga $FFFA ;first of the three vectors starts here
; interrupt vectors {{{
.dw NMI ;when an NMI happens (once per frame if enabled) the
;processor will jump to the label NMI:
.dw RESET ;when the processor first turns on or is reset, it will jump
;to the label RESET:
.dw 0 ;external interrupt IRQ is not used in this tutorial
; }}}
; }}}
; chr {{{
.bank 1 slot 1
.org $0000
.incbin "sprites.chr"
; }}}