Sunday 16 February 2020

C64 Snake Game in under 1K


Here is very simple game of Snake, written in 100% 6502 assembly for the C64.

In total, it comes in well under 1K of memory in size (628 bytes to be exact). The only other memory it uses when running are the two 16-bit general purpose vectors in zero page memory, some of the SID voice 3 vectors (for a random number generator) and the video memory of the C64 itself.

In fact, if you discount the snake body pieces buffer (which once the code is loaded could in theory be placed anywhere in the C64's RAM), the code in fact comes at 458 bytes in size, and so could quite happily sit on the 512 byte boot block of an old-style floppy disk with room to spare.

It's certainly not the best version of Snake in the world - it lacks a score, any music and joystick support. (Keyboard controls are P - up, L - down, Z - left and X - right). But it works, uses the entire character area of the screen and has a certain degree of challenge.

Mainly, it was an exercise in properly learning the 65xx processor instruction set, while trying to come in under 1024 bytes in size as an extra personal challenge.

Anyway, copy-paste the code into a new text file renamed to 'snake1k.asm', and assemble it using:

    dasm snake1k.asm

then drag the resulting a.out file into VICE to try it out using:

    SYS 4096

from BASIC. I've also tested it on theC64 (or Maxi as some like to call it), where it also works just fine.

Short video of it running here:

https://www.youtube.com/watch?v=vOrLySWMXFY

Enjoy!

processor 6502
org $1000

NUMBER_OF_SNAKE_PARTS_TO_ADD = 4
FOOD_OUT_OF_RANGE_POS        = $FF

SNAKE_DIRECTION_LEFT = 0
SNAKE_DIRECTION_RIGHT = 1
SNAKE_DIRECTION_UP = 2
SNAKE_DIRECTION_DOWN = 3

_start:
; RND technique using SID from:
; https://www.atarimagazines.com/compute/issue72/random_numbers.php
LDA #$FF  ; maximum frequency value
STA $D40E ; voice 3 frequency low byte
STA $D40F ; voice 3 frequency high byte
LDA #$80  ; noise waveform, gate bit off
STA $D412 ; voice 3 control register

jsr $e544 ; clear the screen

; Initialize everything
ldy #0
init:
lda default_values,y
sta snake_head_x,y
iny
cpy #[body_directions - snake_head_x + 1]
            ; +1 to include first body byte
bne init

draw_snake_head:
ldx snake_head_x
ldy snake_head_y
jsr calculate_snake_screen_address
            ; exits with y == 0

lda #87
sta ($FD),y ; y should be zero by here

; save head address for use when erasing later
lda $FD
sta $FB
lda $FE
sta $FC

draw_snake_tail:
ldx snake_tail_x
ldy snake_tail_y
jsr calculate_snake_screen_address

lda #81
sta ($FD),y ; y should be zero by here

; delay wait to make each update playable :)
ldx #$20
wait:
ldy #$FF
wait_inner_loop:
dey
bne wait_inner_loop
dex
bne wait

; erase head
lda #81
sta ($FB),y ; y should be zero by here, ptr == head position

; erase tail
lda #$20
sta ($FD),y ; y should be zero by here, ptr == tail position

; move_head
lda body_directions
and #%11000000 ; keep only head direction
bne check_head_right
dec snake_head_x
check_head_right:
cmp #$40
bne check_head_up
inc snake_head_x
check_head_up:
cmp #$80
bne check_head_down
dec snake_head_y
check_head_down:
cmp #$C0
bne move_head_done
inc snake_head_y

move_head_done:
; Add a single snake part (if required) for this game loop
lda snake_parts_to_add
beq move_tail
inc snake_length
dec snake_parts_to_add
jmp skip_tail_move

move_tail:
; Get snake tail direction, move tail
ldx snake_length

; we now need to divide by 4 to get the byte position
txa
clc
ror
clc
ror
tay ; y is now buffer bytes offset

txa
and #$03
tax
        ; x == number of 2-bit shifts in byte to get tail direction

lda body_directions,y
; a == direction value for four possible body parts

direction_shift_loop:
dex
bmi tail_direction_found
rol
rol
jmp direction_shift_loop

tail_direction_found:
and #$C0 ; get just that one direction in top two bits

; a == tail direction in two high bits
and #%11000000 ; keep only tail direction
bne check_tail_right
dec snake_tail_x
check_tail_right:
cmp #$40
bne check_tail_up
inc snake_tail_x
check_tail_up:
cmp #$80
bne check_tail_down
dec snake_tail_y
check_tail_down:
cmp #$C0
bne skip_tail_move
inc snake_tail_y

skip_tail_move:
; Update all body directions as snake moves
lda body_directions
and #$C0
tay
jsr update_body_directions
jsr update_body_directions
lda #$3F
and body_directions
sta body_directions
tya
ora body_directions
sta body_directions

; check for all collisions, with edges and snake parts
ldx snake_head_x
cpx #$FF ; gone past zero, so hit screen edge
beq game_over

cpx #40
beq game_over

ldx snake_head_y
cpx #$FF
beq game_over

cpx #25
beq game_over

ldx snake_head_x
ldy snake_head_y
jsr calculate_snake_screen_address
lda ($FD),y ; y should be zero here
tax

        ; increase snake length, clear food
cpx #83 ; food?
bne check_for_obstacle
lda #NUMBER_OF_SNAKE_PARTS_TO_ADD
sta snake_parts_to_add
lda #FOOD_OUT_OF_RANGE_POS
sta food_x
sta food_y
bne collision_checks_done

check_for_obstacle:
cpx #$20
bne game_over

collision_checks_done:
jsr read_keys

; food never appears at FOOD_OUT_OF_RANGE_POS
lda #FOOD_OUT_OF_RANGE_POS
cmp food_x
bne draw_food
cmp food_y
bne draw_food

jsr get_rnd_pos
stx food_x
sty food_y

draw_food:
ldx food_x
ldy food_y
jsr calculate_snake_screen_address
lda #83
sta ($FD),y ; y should be zero after calculation jsr

jmp draw_snake_head

; Game Over - restart the game
game_over:
ldx #0
stx 198 ; clear key buffer
jmp _start

; Entry: none
; Exit: x == xcoord, y == ycoord, a corrupt
get_rnd_pos:
ldx $D41B ; get RND from SID
get_rnd_x_loop:
cpx #40
bcc get_rnd_y
txa
clc
sbc #40
tax
bne get_rnd_x_loop

get_rnd_y:
ldy $D41B ; get RND from SID
get_rnd_y_loop:
cpy #25
bcc get_rnd_done
tya
clc
sbc #25
tay
bne get_rnd_x_loop

get_rnd_done:
rts

update_body_directions:
ldx #0
update_body_loop:
rol ; use carry from A
            ; (first time through will be garbage but that's okay)
ror body_directions,x
ror ; temp store carry in A for next time round the loop (and next byte)
inx
cpx #170
bne update_body_loop
rts

; Read Keys and modify head direction accordingly
; Exit: a,x corrupt
read_keys:
clv
ldx 197 ; read key buffer
cpx #41 ; P
bne check_down_key
ldx #[SNAKE_DIRECTION_UP << 6]
bvc key_direction_update

check_down_key:
cpx #42 ; L
bne check_left_key
ldx #[SNAKE_DIRECTION_DOWN << 6]
bvc key_direction_update

check_left_key:
cpx #12 ; Z
bne check_right_key
ldx #[SNAKE_DIRECTION_LEFT << 6]
bvc key_direction_update

check_right_key:
cpx #23 ; X
bne check_keys_done
ldx #[SNAKE_DIRECTION_RIGHT << 6]

key_direction_update:
lda body_directions
and #$3F ; Keep all bits except top two (head direction)
sta body_directions
txa ; get new direction for head
ora body_directions
sta body_directions

check_keys_done:
rts

; Calculate Snake Screen Address
; Entry: x, y - screen coordinates
; Exit: y == 0, x,a corrupt
calculate_snake_screen_address:
; initialize character screen memory pointer
lda #0
sta $FD
lda #$04
sta $FE

; calculate actual address
row_address_calc:
cpy #0
clc
beq row_address_calc_done
lda #40
adc $FD
sta $FD
lda #0
adc $FE
sta $FE
dey
bvc row_address_calc ; 2nd adc never overflows, so safe
row_address_calc_done:
txa
adc $FD
sta $FD
tya ; y guaranteed to be zero here, saves a byte over lda #0
adc $FE
sta $FE
rts

default_values:
dc.b 19,12, 20,12, 1, 4, FOOD_OUT_OF_RANGE_POS, FOOD_OUT_OF_RANGE_POS, 0

snake_head_x:
dc.b 0
snake_head_y:
dc.b 0
snake_tail_x:
dc.b 0
snake_tail_y:
dc.b 0
snake_length:
dc.b 0
snake_parts_to_add:
dc.b 0
food_x:
dc.b FOOD_OUT_OF_RANGE_POS
food_y:
dc.b FOOD_OUT_OF_RANGE_POS
body_directions:
ds.b 170, 0 ; each byte has four body part directions in it,
    ; each direction two bits,
    ; = left, %01 = right,
    ; %10 = up,   %11 = down

_end:
; label only here to measure length of program

1 comment: