Skip to content

Instantly share code, notes, and snippets.

@wkjagt
Last active March 26, 2024 00:00
Show Gist options
  • Save wkjagt/9043907 to your computer and use it in GitHub Desktop.
Save wkjagt/9043907 to your computer and use it in GitHub Desktop.
; ___ _ __ ___ __ ___
; / __|_ _ __ _| |_____ / /| __|/ \_ )
; \__ \ ' \/ _` | / / -_) _ \__ \ () / /
; |___/_||_\__,_|_\_\___\___/___/\__/___|
; An annotated version of the snake example from Nick Morgan's 6502 assembly tutorial
; on http://skilldrick.github.io/easy6502/ that I created as an exercise for myself
; to learn a little bit about assembly. I **think** I understood everything, but I may
; also be completely wrong :-)
; Change direction with keys: W A S D
; $00-01 => screen location of apple, stored as two bytes, where the first
; byte is the least significant.
; $10-11 => screen location of snake head stored as two bytes
; $12-?? => snake body (in byte pairs)
; $02 => direction ; 1 => up (bin 0001)
; 2 => right (bin 0010)
; 4 => down (bin 0100)
; 8 => left (bin 1000)
; $03 => snake length, in number of bytes, not segments
;The screens is divided in 8 strips of 8x32 "pixels". Each strip
;is stored in a page, having their own most significant byte. Each
;page has 256 bytes, starting at $00 and ending at $ff.
; ------------------------------------------------------------
;1 | $0200 - $02ff |
;2 | |
;3 | |
;4 | |
;5 | |
;6 | |
;7 | |
;8 | |
; ------------------------------------------------------------
;9 | $03 - $03ff |
;10 | |
;11 | |
;12 | |
;13 | |
;14 | |
;15 | |
;16 | |
; ------------------------------------------------------------
;17 | $04 - $03ff |
;18 | |
;19 | |
;20 | |
;21 | |
;22 | |
;23 | |
;24 | |
; ------------------------------------------------------------
;25 | $05 - $03ff |
;26 | |
;27 | |
;28 | |
;29 | |
;30 | |
;31 | |
;32 | |
; ------------------------------------------------------------
jsr init ;jump to subroutine init
jsr loop ;jump to subroutine loop
init:
jsr initSnake ;jump to subroutine initSnake
jsr generateApplePosition ;jump to subroutine generateApplePosition
rts ;return
initSnake:
;start the snake in a horizontal position in the middle of the game field
;having a total length of one head and 4 bytes for the segments, meaning a
;total length of 3: the head and two segments.
;The head is looking right, and the snaking moving to the right.
;initial snake direction (2 => right)
lda #2 ;start direction, put the dec number 2 in register A
sta $02 ;store value of register A at address $02
;initial snake length of 4
lda #4 ;start length, put the dec number 4 (the snake is 4 bytes long)
;in register A
sta $03 ;store value of register A at address $03
;Initial snake head's location's least significant byte to determine
;where in a 8x32 strip the head will start. hex $11 is just right
;of the center of the first row of a strip
lda #$11 ;put the hex number $11 (dec 17) in register A
sta $10 ;store value of register A at address hex 10
;Initial snake body, two least significant bytes set to hex $10
;and hex $0f, one and two places left of the head respectively
lda #$10 ;put the hex number $10 (dec 16) in register A
sta $12 ;store value of register A at address hex $12
lda #$0f ;put the hex number $0f (dec 15) in register A
sta $14 ;store value of register A at address hex $14
;the most significant bytes of the head and body of the snake
;are all set to hex $04, which is the third 8x32 strip.
lda #$04 ;put the hex number $04 in register A
sta $11 ;store value of register A at address hex 11
sta $13 ;store value of register A at address hex 13
sta $15 ;store value of register A at address hex 15
rts ;return
generateApplePosition:
;Th least significant byte of the apple position will determine where
;in a 8x32 strip the apple is placed. This number can be any one byte value because
;the size of one 8x32 strip fits exactly in one out of 256 bytes
lda $fe ;load a random number between 0 and 255 from address $fe into register A
sta $00 ;store value of register A at address hex 00
;load a new random number from 2 to 5 into $01 for the most significant byte of
;the apple position. This will determine in which 8x32 strip the apple is placed
lda $fe ;load a random number from address $fe into register A
;AND: logical AND with accumulator. Apply logical AND with hex $03 to value in
;register A. Hex 03 is binary 00000011, so only the two least significant bits
;are kept, resulting in a value between 0 (bin 00000000) and 3 (bin 00000011).
;Add 2 to the result, giving a random value between 2 and 5
and #$03 ;mask out lowest 2 bits
clc ;clear carry flag
adc #2 ;add to register A, using carry bit for overflow.
sta $01 ;store value of y coordinate from register A into address $01
rts ;return
loop:
;the main game loop
jsr readKeys ;jump to subroutine readKeys
jsr checkCollision ;jump to subroutine checkCollision
jsr updateSnake ;jump to subroutine updateSnake
jsr drawApple ;jump to subroutine drawApple
jsr drawSnake ;jump to subroutine drawSnake
jsr spinWheels ;jump to subroutine spinWheels
jmp loop ;jump to loop (this is what makes it loop)
readKeys:
;for getting keypresses, the last address ($ff) in the zero page contains
;the hex code of the last pressed key
lda $ff ;load the value of the latest keypress from address $ff into register A
cmp #$77 ;compare value in register A to hex $77 (W)
beq upKey ;Branch On Equal, to upKey
cmp #$64 ;compare value in register A to hex $64 (D)
beq rightKey ;Branch On Equal, to rightKey
cmp #$73 ;compare value in register A to hex $73 (S)
beq downKey ;Branch On Equal, to downKey
cmp #$61 ;compare value in register A to hex $61 (A)
beq leftKey ;Branch On Equal, to leftKey
rts ;return
upKey:
lda #4 ;load value 4 into register A, correspoding to the value for DOWN
bit $02 ;AND with value at address $02 (the current direction),
;setting the zero flag if the result of ANDing the two values
;is 0. So comparing to 4 (bin 0100) only sets zero flag if
;current direction is 4 (DOWN). So for an illegal move (current
;direction is DOWN), the result of an AND would be a non zero value
;so the zero flag would not be set. For a legal move the bit in the
;new direction should not be the same as the one set for DOWN,
;so the zero flag needs to be set
bne illegalMove ;Branch If Not Equal: meaning the zero flag is not set.
lda #1 ;Ending up here means the move is legal, load the value 1 (UP) into
;register A
sta $02 ;Store the value of A (the new direction) into register A
rts ;return
rightKey:
lda #8 ;load value 8 into register A, corresponding to the value for LEFT
bit $02 ;AND with current direction at address $02 and check if result
;is zero
bne illegalMove ;Branch If Not Equal: meaning the zero flag is not set.
lda #2 ;Ending up here means the move is legal, load the value 2 (RIGHT) into
;register A
sta $02 ;Store the value of A (the new direction) into register A
rts ;return
downKey:
lda #1 ;load value 1 into register A, correspoding to the value for UP
bit $02 ;AND with current direction at address $02 and check if result
;is zero
bne illegalMove ;Branch If Not Equal: meaning the zero flag is not set.
lda #4 ;Ending up here means the move is legal, load the value 4 (DOWN) into
;register A
sta $02 ;Store the value of A (the new direction) into register A
rts ;return
leftKey:
lda #2 ;load value 1 into register A, correspoding to the value for RIGHT
bit $02 ;AND with current direction at address $02 and check if result
;is zero
bne illegalMove ;Branch If Not Equal: meaning the zero flag is not set.
lda #8 ;Ending up here means the move is legal, load the value 8 (LEFT) into
;register A
sta $02 ;Store the value of A (the new direction) into register A
rts ;return
illegalMove:
;for an illegal move, just return, so the keypress is ignored
rts ;return
checkCollision:
jsr checkAppleCollision ;jump to subroutine checkAppleCollision
jsr checkSnakeCollision ;jump to subroutine checkSnakeCollision
rts ;return
checkAppleCollision:
;check if the snake collided with the apple by comparing the least significant
;and most significant byte of the position of the snake's head and the apple.
lda $00 ;load value at address $00 (the least significant
;byte of the apple's position) into register A
cmp $10 ;compare to the value stored at address $10
;(the least significant byte of the position of the snake's head)
bne doneCheckingAppleCollision ;if different, branch to doneCheckingAppleCollision
lda $01 ;load value of address $01 (the most significant byte
;of the apple's position) into register A
cmp $11 ;compare the value stored at address $11 (the most
;significant byte of the position of the snake's head)
bne doneCheckingAppleCollision ;if different, branch to doneCheckingAppleCollision
;Ending up here means the coordinates of the snake head are equal to that of
;the apple: eat apple
inc $03 ;increment the value held in memory $03 (snake length)
inc $03 ;twice because we're adding two bytes for one segment
;create a new apple
jsr generateApplePosition ;jump to subroutine generateApplePosition
doneCheckingAppleCollision:
;the snake head was not on the apple. Don't do anything with the apple
rts ;return
checkSnakeCollision:
ldx #2 ;Load the value 2 into the X register, so we start with the first segment
snakeCollisionLoop:
lda $10,x ;load the value stored at address $10 (the least significant byte of
;the location of the snake's head) plus the value of the x register
;(2 in the first iteration) to get the least significant byte of the
;position of the next snake segment
cmp $10 ;compare to the value at address $10 (the least significant
;byte of the position of the snake's head
bne continueCollisionLoop ;if not equals, we haven't found a collision yet,
;branch to continueCollisionLoop to continue the loop
maybeCollided:
;ending up here means we found a segment of the snake's body that
;has a least significant byte that's equal to that of the snake's head.
lda $11,x ;load the value stored at address $11 (most significant byte of
;the location of the snake's head) plus the value of the x register
;(2 in the first iteration) to get the most significant byte
;of the position of the next snake segment
cmp $11 ;compare to the value at address $11 (the most significant
;byte of the position of the snake head)
beq didCollide ;both position bytes of the compared segment of the snake body
;are equal to those of the head, so we have a collision of the
;snake's head with its own body.
continueCollisionLoop:
;increment the value in the x register twice because we use two bytes to store
;the coordinates for snake head and body segments
inx ;increment the value of the x register
inx ;increment the value of the x register
cpx $03 ;compare the value in the x register to the value stored at
;address $03 (snake length).
beq didntCollide ;if equals, we got to last section with no collision: branch
;to didntCollide
;ending up here means we haven't checked all snake body segments yet
jmp snakeCollisionLoop;jump to snakeCollisionLoop to continue the loop
didCollide:
;there was a collision
jmp gameOver ;jump to gameOver
didntCollide:
;there was no collision, continue the game
rts ;return
updateSnake:
;collision checks have been done, update the snake. Load the length of the snake
;minus one into the A register
ldx $03 ;load the value stored at address $03 (snake length) into register X
dex ;decrement the value in the X register
txa ;transfer the value stored in the X register into the A register. WHY?
updateloop:
;Example: the length of the snake is 4 bytes (two segments). In the lines above
;the X register has been set to 3. The snake coordinates are now stored as follows:
;$10,$11 : the snake head
;$12,$13,$14,$15: the snake body segments (two bytes for each of the 2 segments)
;
;The loop shifts all coordinates of the snake two places further in memory,
;calculating the offset of the origin from $10 and place it in memory offset to
;$12, effectively shifting each of the snake's segments one place further:
;
;from: x===
;to: ===
lda $10,x ;load the value stored at address $10 + x into register A
sta $12,x ;store the value of register A into address $12
;plus the value of register X
dex ;decrement X, and set negative flag if value becomes negative
bpl updateloop ;branch to updateLoop if positive (negative flag not set)
;now determine where to move the head, based on the direction of the snake
;lsr: Logical Shift Right. Shift all bits in register A one bit to the right
;the bit that "falls off" is stored in the carry flag
lda $02 ;load the value from address $02 (direction) into register A
lsr ;shift to right
bcs up ;if a 1 "fell off", we started with bin 0001, so the snakes needs to go up
lsr ;shift to right
bcs right ;if a 1 "fell off", we started with bin 0010, so the snakes needs to go right
lsr ;shift to right
bcs down ;if a 1 "fell off", we started with bin 0100, so the snakes needs to go down
lsr ;shift to right
bcs left ;if a 1 "fell off", we started with bin 1000, so the snakes needs to go left
up:
lda $10 ;put value stored at address $10 (the least significant byte, meaning the
;position in a 8x32 strip) in register A
sec ;set carry flag
sbc #$20 ;Subtract with Carry: subtract hex $20 (dec 32) together with the NOT of the
;carry bit from value in register A. If overflow occurs the carry bit is clear.
;This moves the snake up one row in its strip and checks for overflow
sta $10 ;store value of register A at address $10 (the least significant byte
;of the head's position)
bcc upup ;If the carry flag is clear, we had an overflow because of the subtraction,
;so we need to move to the strip above the current one
rts ;return
upup:
;An overflow occurred when subtracting 20 from the least significant byte
dec $11 ;decrement the most significant byte of the snake's head's position to
;move the snake's head to the next up 8x32 strip
lda #$1 ;load hex value $1 (dec 1) into register A
cmp $11 ;compare the value at address $11 (snake head's most significant
;byte, determining which strip it's in). If it's 1, we're one strip too
;(the first one has a most significant byte of $02), which means the snake
;hit the top of the screen
beq collision ;branch if equal to collision
rts ;return
right:
inc $10 ;increment the value at address $10 (snake head's least
;significant byte, determining where in the 8x32 strip the head is
;located) to move the head to the right
lda #$1f ;load value hex $1f (dec 31) into register A
bit $10 ;the value stored at address $10 (the snake head coordinate) is ANDed
;with hex $1f (bin 11111), meaning all multiples of hex $20 (dec 32)
;will be zero (because they all end with bit patterns ending in 5 zeros)
;if it's zero, it means we hit the right of the screen
beq collision ;branch to collision if zero flag is set
rts ;return
down:
lda $10 ;put value from address $10 (the least significant byte, meaning the
;position in a 8x32 strip) in register A
clc ;clear carry flag
adc #$20 ;add hex $20 (dec 32) to the value in register A and set the carry flag
;if overflow occurs
sta $10 ;store the result at address $10
bcs downdown ;if the carry flag is set, an overflow occurred when adding hex $20 to the
;least significant byte of the location of the snake's head, so we need to move
;the next 8x3 strip
rts ;return
downdown:
inc $11 ;increment the value in location hex $11, holding the most significatnt byte
;of the location of the snake's head.
lda #$6 ;load the value hex $6 into the A register
cmp $11 ;if the most significant byte of the head's location is equals to 6, we're
;one strip to far down (the last one was hex $05)
beq collision ;if equals to 6, the snake collided with the bottom of the screen
rts ;return
left:
;A collision with the left side of the screen happens if the head wraps around to
;the previous row, on the right most side of the screen, where, because the screen
;is 32 wide, the right most positions always have a least significant byte that ends
;in 11111 in binary form (hex $1f). ANDing with hex $1f in this column will always
;return hex $1f, so comparing the result of the AND with hex $1f will determine if
;the snake collided with the left side of the screen.
dec $10 ;subtract one from the value held in memory position $10 (least significant
;byte of the snake head position) to make it move left.
lda $10 ;load value held in memory position $10 (least significant byte of the
;snake head position) into register A
and #$1f ;AND the value hex $1f (bin 11111) with the value in register A
cmp #$1f ;compare the ANDed value above with bin 11111.
beq collision ;branch to collision if equals
rts ;return
collision:
jmp gameOver ;jump to gameOver
drawApple:
ldy #0 ;load the value 0 into the Y register
lda $fe ;load the value stored at address $fe (the random number generator)
;into register A
sta ($00),y ;dereference to the address stored at address $00 and $01
;(the address of the apple on the screen) and set the value to
;the value of register A and add the value of Y (0) to it. This results
;in the apple getting a random color
rts ;return
drawSnake:
ldx #0 ;set the value of the X register to 0
lda #1 ;set the value of the A register to 1
sta ($10,x) ;dereference to the memory address that's stored at address
;$10 (the two bytes for the location of the head of the snake) and
;set its value to the one stored in register A
ldx $03 ;set the value of the x register to the value stored in memory at
;location $03 (the length of the snake)
lda #0 ;set the value of the a register to 0
sta ($10,x) ;dereference to the memory address that's stored at address
;$10, add the length of the snake to it, and store the value of
;register A (0) in the resulting address. This draws a black pixel on the
;tail. Because the snake is moving, the head "draws" on the screen in
;white as it moves, and the tail works as an eraser, erasing the white trail
;using black pixels
rts ;return
spinWheels:
;slow the game down by wasting cycles
ldx #0 ;load zero in the X register
spinloop:
nop ;no operation, just skip a cycle
nop ;no operation, just skip a cycle
dex ;subtract one from the value stored in register x
bne spinloop ;if the zero flag is clear, loop. The first dex above wrapped the
;value of x to hex $ff, so the next zero value is 255 (hex $ff)
;loops later.
rts ;return
gameOver: ;game over is literally the end of the program
@hasseily
Copy link

hasseily commented Jun 13, 2018

One simple way to mitigate the slowness is inside spinWheels to load into x the length of the snake and increment x. So spinWheels slows you down less and less as the snake grows in size.
Ideally one should calculate the exact increase in cycles for each additional snake length unit, and reduce spinWheels by so many cycles.
Below is a version that reduces the spinloop by twice the length of the snake and keeps the speed relatively constant as the snake grows in length. Note that the length of the snake grows by 2 every time you eat an apple, so the loop reduces by 4 every time.
You may wonder what happens when the addition rolls over and carries. Wonder not, as the game itself will bug at that length. The snake will keep extending and the tail will stop moving.

spinWheels:
lda snakeLength
adc snakeLength
spinloop:
nop
nop
adc #1
bne spinloop
rts

@grobda
Copy link

grobda commented Feb 15, 2019

I think a better way to code the snake and detect collisions with itself would be to only store the head and tail (last segment) locations. A collision would be detected by comparing the next head position to the colour of the target location. If that colour was white then collision would be positive. The head would be drawn and tail erased as normal. This might also fix the game bugging when the snake gets to a certain length, and make the speed consistent. White would be need to be removed as a possible apple colour.

@Joker-vD
Copy link

Joker-vD commented Oct 8, 2019

@grobda How would you update the snake without storing the adjacency information? Look at these two snakes:

 ┏━━━>       ┏┓┏━>
 ┗━┓         ┃┃┃
━━━┛        ━┛┗┛

Both of them have 3x3 solid block in the middle, and their heads (and tails) look in the same directions. And yet, as they move, you would need to somehow tell them apart to correctly erase that 3x3 block of pixels.

@wkjagt
Copy link
Author

wkjagt commented Oct 8, 2019

@cjoudrey do you know the answer to this?

@yevrah
Copy link

yevrah commented Oct 14, 2019

I was thinking the same as @grobda.

Playing around with the colors I noticed that they repeat when overflowing. So black, white, ..., light grey ($0, $1, ..., $f) overflow so that black can be $10 (which comes after $f), and white can be $11, $21, and $31 . From this, we get enough whites to encode the direction of the next segment into each pixel, which allows for some memory saving shortcuts.

  • We only need to store the location of the tail segment.
  • Before we remove a tail segment we need to calculate the position of the next segment based on the white value and store this back for the next iteration.
  • Checking for collisions should be updated to see if the next segment lands on a black pixel - otherwise you would need to compare against 4 types of whites in the current implementation.
  • We don't have to worry too much about the apple as it is handled before the snake collision test.

In essence, we are creating a linked list of directions and popping the last value when removing the tail. Having said that, this is purely an exercise in minimizing the memory footprint. The original method remains as a good teaching tool and maybe more valid when creating a real game with more features.

@kjayawar
Copy link

kjayawar commented Jul 5, 2023

Hi,

I've updated the comments on the screen section. I think there are few errors in the original file. Feel free to update if you think its useful
https://gist.github.com/kjayawar/94c1341cc1409b572259e641ea4d6e79

@thgie
Copy link

thgie commented Jan 11, 2024

I do have a question of understanding. Why can't we just write sta ($0000) on line 414 in the drawApple routine (line 410). I don't understand why we need to use an indirect indexed addressing mode here.

@wkjagt
Copy link
Author

wkjagt commented Jan 11, 2024

I didn't write the code, I only annotated it. But it's very likely that Nick Morgan (who wrote the code, see comment at the top) wrote this with the instruction set of the original 6502, which didn't have a non-indexed indirect addressing mode. This was added later in the 65C02. If you need a non-indexed indirect addressing mode on an original 6502, you can simulate it by using an indirect indexed addressing mode with the register used as the index set to 0.

@thgie
Copy link

thgie commented Jan 11, 2024

I knew that, and I'm immensely grateful for your commented on version, as well as still attempting to answer my question :) That makes sense and thanks to your hint, I became aware that not all opcodes allow all addressing modes. JPM allows non-indexed indirect, whereas STA doesn't.

See https://www.masswerk.at/6502/6502_instruction_set.html#STA and https://www.masswerk.at/6502/6502_instruction_set.html#JMP.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment