I confess i got delayed, very delayed with the simpleCPU games console series (Link), this semester has been soooooo busy, too much to do and no time to do it :). However, its Christmas and as a Christmas challenge i'm going to finish off the PS2 keyboard interface (Link) and use it in a simple snake game (Link). A second consideration that delayed this work is how to release this hardware to students. I want students to practice their assembly language programming skills by writing their own games during the module i teach, during the semester, however, the hardware optimisations i have added to the games console's simpleCPUv1d processor would also be very useful for the assessment :). However, i am now happy that i have found a way to show the what, but not the how, so releasing this design is not going to break future assessments.
PS2 Interface
Snake Games
Hardware
The good thing about the PS2 interface is that its simple. Unlike USB the PS2 protocol only needs simple hardware and simple software to workout what key has been pressed on a keyboard. This makes it ideally suited for the simpleCPUv1d, where we are limited to 4KB of memory. The PS2 connector has six pins, as shown in figure 1, not sure what the two spare pins were for, but are typically left unconnected?


Figure 1 : PS2 pins
This protocol uses simple single-ended signalling (Link), open-collector drivers (Link), allowing bidirectional communications. Data transfers are typically initiated from device (keyboard) to host (PC), using a synchronous serial bus. Packet structure: start bit, B0-B7, parity and a stop bit. Note, odd parity is used. For the purposes of the game console this is a unidirectional communications, the device (keyboard) driving the CLK and DATA signals on the FPGA inputs, as shown in figure 2


Figure 2 : PS2 signals
The PS2 protocol basically uses the same format as a normal RS232 series bus (Link), however, rather than being an asynchronous bus the PS2 bus is synchronous i.e. has a separate CLK line, a falling edge signalling to the receiving host that data is valid. On a keyboard most keys are assigned an 8bit character code as shown in figure 3. There are a couple of extension codes e.g. 0xF0 when a key is released or 0xE0 for some of the less common keys. However, for this application the basic 8bit codes shown below are fine i.e. all i really need is WASD, for movement control, which are assigned the codes: 0x1D, 0x1C, 0x1B and 0x23 and maybe SPACE to start the game / fire button, which is assigned code 0x29.

Figure 3 : PS2 keyboard codes
To save some time i found a VHDL implementations of a PS2 interface on the Digikey website (Link), shown in figure 4. This interface is made of two VHDL components: ps2_keyboard.vhd and debounce.vhd, shown below. Note, i modified the ps2_keyboard file as the "ready" handshake signal was not quite what i wanted i.e. did not interface with my standard simpleCPU peripheral template. Also, i prefer not to use INTEGER types within my VHDL so replaced these with STD_LOGIC_VECTOR.


Figure 4 : Digikey PS2 keyboard interface
--------------------------------------------------------------------------------
--
-- FileName: ps2_keyboard.vhd
-- Dependencies: debounce.vhd
-- Version 1.0 11/25/2013 Scott Larson
--
--------------------------------------------------------------------------------
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use ieee.std_logic_arith.all;
use ieee.std_logic_unsigned.all;
ENTITY ps2_keyboard IS
PORT(
clk : IN STD_LOGIC; --system clock
clr : IN STD_LOGIC; --system clock
ps2_clk : IN STD_LOGIC; --clock signal from PS/2 keyboard
ps2_data : IN STD_LOGIC; --data signal from PS/2 keyboard
ps2_valid : OUT STD_LOGIC; --flag that new PS/2 code is available on ps2_code bus
ps2_code : OUT STD_LOGIC_VECTOR(7 DOWNTO 0); --rx data
ps2_error : OUT STD_LOGIC; --error flag
ps2_tp0 : OUT STD_LOGIC; --denounced clk
ps2_tp1 : OUT STD_LOGIC ); --denounced data
END ps2_keyboard;
ARCHITECTURE logic OF ps2_keyboard IS
SIGNAL sync_ffs : STD_LOGIC_VECTOR(1 DOWNTO 0); --synchronizer flip-flops for PS/2 signals
SIGNAL ps2_clk_int : STD_LOGIC; --debounced clock signal from PS/2 keyboard
SIGNAL ps2_data_int : STD_LOGIC; --debounced data signal from PS/2 keyboard
SIGNAL ps2_word : STD_LOGIC_VECTOR(10 DOWNTO 0); --stores the ps2 data word
SIGNAL error : STD_LOGIC; --validate parity, start, and stop bits
SIGNAL count_idle : STD_LOGIC_VECTOR(10 DOWNTO 0); --counter to determine PS/2 is idle
SIGNAL mode : STD_LOGIC; --mode=1 start bit detected
SIGNAL valid : STD_LOGIC; --data valid
--declare debounce component for debouncing PS2 input signals
COMPONENT debounce IS
GENERIC(
counter_size : INTEGER); --debounce period (in seconds) = 2^counter_size/(clk freq in Hz)
PORT(
clk : IN STD_LOGIC; --input clock
button : IN STD_LOGIC; --input signal to be debounced
result : OUT STD_LOGIC); --debounced signal
END COMPONENT;
BEGIN
--synchronizer flip-flops
PROCESS(clk)
BEGIN
IF(clk'EVENT AND clk = '1') THEN --rising edge of system clock
sync_ffs(0) <= ps2_clk; --synchronize PS/2 clock signal
sync_ffs(1) <= ps2_data; --synchronize PS/2 data signal
END IF;
END PROCESS;
--debounce PS2 input signals
debounce_ps2_clk: debounce
GENERIC MAP(counter_size => 8)
PORT MAP(clk => clk, button => sync_ffs(0), result => ps2_clk_int);
debounce_ps2_data: debounce
GENERIC MAP(counter_size => 8)
PORT MAP(clk => clk, button => sync_ffs(1), result => ps2_data_int);
ps2_tp0 <= ps2_clk_int;
ps2_tp1 <= ps2_data_int;
--input PS2 data
PROCESS(ps2_clk_int)
BEGIN
IF(ps2_clk_int'EVENT AND ps2_clk_int = '0') THEN --falling edge of PS2 clock
ps2_word <= ps2_data_int & ps2_word(10 DOWNTO 1); --shift in PS2 data bit
END IF;
END PROCESS;
--verify that parity, start, and stop bits are all correct
error <= NOT (NOT ps2_word(0) AND ps2_word(10) AND (ps2_word(9) XOR ps2_word(8) XOR
ps2_word(7) XOR ps2_word(6) XOR ps2_word(5) XOR ps2_word(4) XOR ps2_word(3) XOR
ps2_word(2) XOR ps2_word(1)));
ps2_error <= error;
-- CHANGES : the hardware below has be modified to match the simplecpu bus
PROCESS(clk, clr)
BEGIN
IF clr='1'
THEN
mode <= '0';
ELSIF clk='1' AND clk'event
THEN
IF mode = '0' and ps2_clk_int = '0' and ps2_data_int = '0'
THEN
mode <= '1'; --start bit detected
ELSIF valid = '1'
THEN
mode <= '0';
END IF;
END IF;
END PROCESS;
ps2_valid <= valid;
--determine if PS2 port is idle (i.e. last transaction is finished) and output result
PROCESS(clk, clr)
BEGIN
IF clr='1'
THEN
valid <= '0'; --ready flag
ps2_code <= (others=>'0'); --data
count_idle <= (others=>'0'); --idle count
ELSIF clk='1' AND clk'event
THEN
valid <= '0';
IF ps2_clk_int = '0' or mode = '0'
THEN --low PS2 clock, PS2 is active
count_idle <= (others=>'0'); --reset idle counter
ELSIF count_idle = "11111111111"
THEN
IF error = '0'
THEN --idle threshold reached and no errors detected
valid <= '1'; --set flag that new PS/2 code is available
ps2_code <= ps2_word(8 DOWNTO 1); --output new PS/2 code
count_idle <= (others=>'0'); --reset idle counter
END IF;
ELSE
count_idle <= count_idle + 1; --continue counting
END IF;
END IF;
END PROCESS;
END logic;
The ps2_keyboard component is the heart of the interface, i confess i did change this hardware a little so that it generated the correct handshaking signals, but at its heart its basically the same as the original. The PS2 input signals: CLK and DATA, are passed through the debounce component below to remove any noise i.e. switch bounce (Link). The control logic does not monitor what bits have been received e.g. start bit, or stop bit, rather it determines that data has been "received" if the CLK line has been idle i.e. high, for >55us AND there are no parity errors, which is not the most robust method of detecting that data has been received correctly. However, this approach does ensure that the receiving hardware will timeout if noise corrupts the CLK line i.e. it will prevent deadlocks etc, so for the purposes of interfacing to a keyboard this approach does seem to work quite well.
--------------------------------------------------------------------------------
--
-- FileName: debounce.vhd
-- Dependencies: none
-- Version 1.0 3/26/2012 Scott Larson
--
--------------------------------------------------------------------------------
LIBRARY ieee;
USE ieee.std_logic_1164.all;
USE ieee.std_logic_unsigned.all;
ENTITY debounce IS
GENERIC(
counter_size : INTEGER := 19); --counter size (19 bits gives 10.5ms with 50MHz clock)
PORT(
clk : IN STD_LOGIC; --input clock
button : IN STD_LOGIC; --input signal to be debounced
result : OUT STD_LOGIC); --debounced signal
END debounce;
ARCHITECTURE logic OF debounce IS
SIGNAL flipflops : STD_LOGIC_VECTOR(1 DOWNTO 0); --input flip flops
SIGNAL counter_set : STD_LOGIC; --sync reset to zero
SIGNAL counter_out : STD_LOGIC_VECTOR(counter_size DOWNTO 0) := (OTHERS => '0'); --counter output
BEGIN
counter_set <= flipflops(0) xor flipflops(1); --determine when to start/reset counter
PROCESS(clk)
BEGIN
IF(clk'EVENT and clk = '1') THEN
flipflops(0) <= button;
flipflops(1) <= flipflops(0);
IF(counter_set = '1') THEN --reset counter because input is changing
counter_out <= (OTHERS => '0');
ELSIF(counter_out(counter_size) = '0') THEN --stable input time is not yet met
counter_out <= counter_out + 1;
ELSE --stable input time is met
result <= flipflops(1);
END IF;
END IF;
END PROCESS;
END logic;
These VHDL cores were integrated into the schematic shown in figure 5. This core uses the same template as the Rotary switch controller (Link), the PS2 VHDL core captures the data from the sensor/device, its valid signal latches / sets the D-type flip-flop, wired as a SR flip-flop. The output of this flip-flop acts as the cores ready flag and can be assessed by the processor by reading the memory mapped address 0xFF8. Note, rx data is bits 0-7 and ready flag is bit8. The processor can reset this flag by writing to the same address.

Figure 5 : PS2 keyboard interface schmatic
To test this design i wrote a simple testbench, simulating the transmission of the "W" key (0x1D), as shown in figure 6. The top waveform shows the PS2 transmission i.e. ps2_clk and ps2_data, the bottom waveform shows the processor reading the received data and resetting the ready flag. The test code used to read this data is shown below, the processor reads the ps2_data, if bit8 is clear then no new data is present, processor repeats loop, when bit8 is set the processor read this data, buffers it in the variable word then resets the flag by writing to the same address.


Figure 6 : PS2 keyboard interface simulation (top), simpleCPU accessing data (bottom)
start:
move RA 0
store RA txBufferIndex
movea( RA, welcome ) # set address to welcome string
store RA srcAddr
movea( RA, txBuffer ) # set buffer address
store RA destAddr
call strcpy # copy string to TX buffer
call txString # TX string
restart:
load RA negative_sign_extend # set mask to upper byte
move RB RA
loop:
load RA PS2_DATA # read PS2 data
and RA RB # zero lower byte
jumpz loop # repeat if zero
load RA PS2_DATA # load data
store RA PS2_DATA # reset ready flag
store RA word # buffer data
call word2Hex # convert binary to hex string
call addLFCR # add LF/CR
call addNULL # end string with NULL char
call txString # TX string
jump restart # repeat
The FPGA board has a single PS2 socket, as show in figure 1. You can still buy new PS2 keyboards from your online supplier of choice, alternatively you can use an USB to PS2 adaptor, as show in figure 7. For a long time i thought there was some fancy "secret sauce" hidden in these pieces of plastic, however, i then discovered that they are just passive devices i.e. just wires. There are no differences between the green or purple adaptors, they just pass through the "USB" signals, it is the electronics within the keyboard or mouse that detects what it is plugged into and switches into either a USB or PS2 mode. However, i believe not all keyboards and mice support the PS2 mode :).

Figure 7 : USB to PS2 adptors

Figure 8 : snake game
The snake software is based on the implementation by Bro Code (Link), listing below.
# ----------------
# - Python Snake -
# ----------------
# https://www.youtube.com/watch?v=bfRwxS5d0SI
# Bro Code
from tkinter import *
import random
GAME_WIDTH = 700
GAME_HEIGHT = 700
SPEED = 50
SPACE_SIZE = 50
BODY_PARTS = 3
SNAKE_COLOR = "#00FF00"
FOOD_COLOR = "#FF0000"
BACKGROUND_COLOR = "#000000"
class Snake:
def __init__(self):
self.body_size = BODY_PARTS
self.coordinates = []
self.squares = []
for i in range(0, BODY_PARTS):
self.coordinates.append([0, 0])
for x, y in self.coordinates:
square = canvas.create_rectangle(x, y, x + SPACE_SIZE, y + SPACE_SIZE, fill=SNAKE_COLOR, tag="snake")
self.squares.append(square)
class Food:
def __init__(self):
x = random.randint(0, (GAME_WIDTH / SPACE_SIZE)-1) * SPACE_SIZE
y = random.randint(0, (GAME_HEIGHT / SPACE_SIZE) - 1) * SPACE_SIZE
self.coordinates = [x, y]
canvas.create_oval(x, y, x + SPACE_SIZE, y + SPACE_SIZE, fill=FOOD_COLOR, tag="food")
def next_turn(snake, food):
x, y = snake.coordinates[0]
if direction == "up":
y -= SPACE_SIZE
elif direction == "down":
y += SPACE_SIZE
elif direction == "left":
x -= SPACE_SIZE
elif direction == "right":
x += SPACE_SIZE
snake.coordinates.insert(0, (x, y))
square = canvas.create_rectangle(x, y, x + SPACE_SIZE, y + SPACE_SIZE, fill=SNAKE_COLOR)
snake.squares.insert(0, square)
if x == food.coordinates[0] and y == food.coordinates[1]:
global score
score += 1
label.config(text="Score:{}".format(score))
canvas.delete("food")
food = Food()
else:
del snake.coordinates[-1]
canvas.delete(snake.squares[-1])
del snake.squares[-1]
if check_collisions(snake):
game_over()
else:
window.after(SPEED, next_turn, snake, food)
def change_direction(new_direction):
global direction
if new_direction == 'left':
if direction != 'right':
direction = new_direction
elif new_direction == 'right':
if direction != 'left':
direction = new_direction
elif new_direction == 'up':
if direction != 'down':
direction = new_direction
elif new_direction == 'down':
if direction != 'up':
direction = new_direction
def check_collisions(snake):
x, y = snake.coordinates[0]
if x < 0 or x >= GAME_WIDTH:
return True
elif y < 0 or y >= GAME_HEIGHT:
return True
for body_part in snake.coordinates[1:]:
if x == body_part[0] and y == body_part[1]:
return True
return False
def game_over():
canvas.delete(ALL)
canvas.create_text( canvas.winfo_width()/2, canvas.winfo_height()/2,
font=('consolas',70), text="GAME OVER", fill="red", tag="gameover")
window = Tk()
window.title("Snake game")
window.resizable(False, False)
score = 0
direction = 'down'
label = Label(window, text="Score:{}".format(score), font=('consolas', 40))
label.pack()
canvas = Canvas(window, bg=BACKGROUND_COLOR, height=GAME_HEIGHT, width=GAME_WIDTH)
canvas.pack()
window.update()
window_width = window.winfo_width()
window_height = window.winfo_height()
screen_width = window.winfo_screenwidth()
screen_height = window.winfo_screenheight()
x = int((screen_width/2) - (window_width/2))
y = int((screen_height/2) - (window_height/2))
window.geometry(f"{window_width}x{window_height}+{x}+{y}")
window.bind('', lambda event: change_direction('left'))
window.bind('', lambda event: change_direction('right'))
window.bind('', lambda event: change_direction('up'))
window.bind('', lambda event: change_direction('down'))
snake = Snake()
food = Food()
next_turn(snake, food)
window.mainloop()
The purpose of this python code is help identify key code and data structures. The assembly code will not be a direct port of this implementation, as how code that is implemented / optimised for a high-level language will differ a lot from a low-level assembly code implementation. However, when possible its always a good idea to try out ideas using a high-level language first, to state the obvious its a lot easier to test and debug an initial proto-type using a high-level language :). From this python prototype a couple of key things were identified:
A generic "list" like data structure was implemented using the code below, i confess this code is not fully tested, but the bits i used in this game worked :).
####################
# LIST SUBROUTINES #
####################
# listIndex : index into array, 0=start
# listTailPtr : index to end of list i.e. index of array where data is written next
# listAddr : address in memory, first memory location of list
# listData : data read from list, or data to be written to list
# ------------------
# - LIST GET INDEX -
# ------------------
listGetIndex:
load RA listIndex # get index
addm RA listAddr # add base addr
load RA (RA) # read data
store RA listData # store to buffer
ret
# -----------------
# - LIST GET TAIL -
# -----------------
listGetTail:
load RA listTailPtr # get pntr address
load RA (RA) # read index
sub RA 1 # dec ptr to last written addr
addm RA listAddr # add base addr
load RA (RA) # read data
store RA listData # store to buffer
ret
# --------------------
# - LIST DELETE HEAD -
# --------------------
listDeleteHead:
load RA listTailPtr # get pntr address
load RA (RA) # load index
sub RA 0 # is list empty?
jumpz listDeleteHead_exit # yes
listDeleteHead_data:
load RA listAddr
move RC RA # RC=dst
add RA 1 # RB=src
move RB RA
listDeleteHead_loop:
load RA (RB) # load data
store RA (RC) # shuffle data up
add RB 1 # move pointers
add RC 1
load RA listTailPtr # reached end of list?
sub RA RB # if src = tail finish
jumpnz listDeleteHead_loop
load RA listTailPtr # load tail ptr
move RB RA # buffer address
load RA (RB) # load index
sub RA 1 # inc index
store RA (RB) # update index
listDeleteHead_exit:
ret
# --------------------
# - LIST DELETE TAIL -
# --------------------
listDeleteTail:
load RA listTailPtr # get pntr address
move RB RA # buffer pntr
load RA (RB) # read index
sub RA 0 # is index 0?
jumpz listDeleteTail_exit # yes, exit
sub RA 1 # no, decrement
store RA (RB) # store index
listDeleteTail_exit:
ret
# --------------------
# - LIST APPEND HEAD -
# --------------------
listAppendHead:
load RA listTailPtr # load index addr
load RA (RA) # load index
move RB RA # buffer index
addm RA listAddr # generate read address
move RC RA # RC=dst
sub RB 0 # is list empty?
jumpz listAppendHead_update # yes
moved( RA, MAX_LIST_SIZE ) # max index
sub RA RB
jumpz listAppendHead_exit # 0=full 0!=space
listAppendHead_data:
move RB RC
sub RB 1 # RB=src
listAppendHead_loop:
load RA (RB) # load data
store RA (RC) # shuffle data down
sub RB 1 # move pointers
sub RC 1
load RA listAddr # read head of list addr
sub RA RC # if dst = head finish
jumpnz listAppendHead_loop
listAppendHead_update:
load RA listData # write data to head of list
store RA (RC)
load RA listTailPtr # load tail pointer
move RB RA # buffer address
load RA (RB) # load index
add RA 1 # inc index
store RA (RB) # update index
listAppendHead_exit:
ret
# --------------------
# - LIST APPEND TAIL -
# --------------------
listAppendTail:
load RA listTailPtr # load index addr
load RA (RA) # load index
addm RA listAddr # generate read address
move RC RA # RC=dst
moved( RA, MAX_LIST_SIZE ) # max index
sub RA RB
jumpz listAppendTail_exit # 0=full 0!=space
load RA listData
store RA (RC)
load RA listTailPtr # inc tail pointer
move RB RA
load RA (RA) #
add RA 1 #
store RA (RB)
listAppendTail_exit:
ret
The random number generator is based on a look-up table of 200 "random" values. To generate this table i wrote the simple python program below:
import random
numList = []
last_num = 0
for i in range(201):
while True:
num = random.randint(1,256)
if (num not in numList) and abs(num -last_num) > 10:
numList.append(num)
print( ".data " + str(num))
break
The output of this program is then added to the assembly language program, basically 200 .data constant declarations at the end of the program.
randomNumber:
.data 50
.data 100
.data 224
.data 221
.data 40
.data 210
.data 31
.data 218
...
.data 54
.data 209
.data 245
.data 81
.data 104
.data 38
.data 227
.data 68
.data 19
.data 132
The random number subroutine uses the formula shown below to generate a random value. The base number is an 8bit value, in this use case either the display WIDTH or HEIGHT. This is multiplied by a random 8bit value from the lookup table, lookup table values vary from 1 to 255. The 16bit result is then right shifted eight times i.e. divide by 256. The multiplication has to be performed first as this is an integer only processor, no floats or fractional values here :). Note, the reason why we are not using the full screen of 160x120 pixel is that the game screen has coloured border/edge.
define(WIDTH, `148') define(HEIGHT, `112') RANDOM_NUMBER = ( BASE_NUMBER * RANDOM[RANDOM_NUMBER_INDEX] ) / 256
Therefore, if the base number is 148 i.e. the WIDTH, and the random number is 128, the resulting random number is 74. The code that implements this formula is shown below. The final step is to ensure that the random value is a multiple of 4. This is required as the snake segments and food are 4x4 pixels in size. The max value produced by this code is determined by the initial BASE_NUMBER. The minimum value is set by a threshold test, if values are less than 20 the number returned is set to 24. The means that the minimum XY position is normally 24, however, there are a small number of cases were it will be 20, in these cases the food is place hard up against the border/edge :).
# -----------------
# - RANDOM NUMBER -
# -----------------
random:
load RA num # base number (8bit)
move RC RA #
movea( RA, randomNumber ) # get base address
addm RA randomNumberIndex # add index
load RB (RA) # read random number (0-255)
mul RC RB # mul
asr RC # /256
asr RC
asr RC
asr RC
asr RC
asr RC
asr RC
asr RC
load RA random_mask # mask out lower 2 bits i.e. a multiple of 4
and RA RC
store RA num
sub RA 20 # check lower limit
jumpc random_min
jump random_update
random_min:
move RA 24 # set lower limit
store RA num
random_update:
moved( RB, MAX_RND_LIST_SIZE ) # is index > list size
load RA randomNumberIndex
sub RB RA
jumpnz random_exit
move RA 0xFF # set index -1
random_exit:
add RA 1 # inc index
store RA randomNumberIndex
ret
The random subroutine is used in the food subroutine below, generating X and Y coordinates that are used to place the snake's food. These coordinates are then checked to ensure that this XY position does not lie inside the snake, if all is well the food is drawn on the display, otherwise a new random value is calculated.
# -------------
# - DRAW FOOD -
# -------------
food:
moved( RA, WIDTH ) # generate X coordinate
store RA num
call random
load RA num
store RA food_x_pos
moved( RA, HEIGHT ) # generate Y coordinate
store RA num
call random
load RA num
store RA food_y_pos
food_check_snake:
move RA 0 # check that new position is not in snake
store RA listIndex
food_check_snake_loop:
movea( RA, snake_x_pos ) # get pixel x
store RA listAddr
call listGetIndex
load RA listData
store RA x_pos
movea( RA, snake_y_pos ) # get pixel y
store RA listAddr
call listGetIndex
load RA listData
store RA y_pos
load RA food_x_pos # check x pos
subm RA x_pos
jumpnz food_check_snake_next
load RA food_y_pos # check y pos
subm RA y_pos
jumpnz food_check_snake_next
jump food # food in snake redo
food_check_snake_next:
load RA listIndex # inc count
add RA 1
store RA listIndex
subm RA snake_x_tail_ptr # is count > tail pointer
jumpnz food_check_snake_loop
load RA food_x_pos # load X and Y
store RA x_pos
load RA food_y_pos
store RA y_pos
call drawFood # draw food
ret
The main program (below) is implemented as a basic loop. The update subroutine implements the game logic and updates the snake's position. The delayVeryShort subroutine slows down the snake's movements so that the game is playable. Next the food_eaten and finish flags (updated by the update subroutine) are checked and their associated actions performed.
########
# MAIN #
########
start:
clear_screen( 0 ) # clear screen
call init_game # setup game variable
call update_display # draw snake
call food # draw food
loop:
call update # read keyboard, update state and display
call delayVeryShort # delay
load RA food_eaten # is food eaten flag set
and RA 0xFF
jumpz test # no test finish flag
move RA 0 # clr flag
store RA food_eaten
call inc_player_score # inc score
call food # draw new food
test:
load RA finish # is finished flag set
and RA 0xFF
jumpz loop # no loop
call draw_end_screen # draw game over message
call delayLonger # wait
jump start # restart
trap:
jump trap
The init_game subroutine initialises game variables, sets the initial snake size by loading six XY coordinates into the snake segment position lists, draws the game field border/edge and finally displays the score text.
# ---------------
# -- INIT GAME --
# ---------------
init_game:
move RA 0 # zero main variables
store RA snake_x_tail_ptr
store RA snake_y_tail_ptr
store RA key
store RA num
store RA finish
store RA food_eaten
# set initial snake size to six segments
movea( RA, snake_x_tail_ptr ) # set tail pointer
store RA listTailPtr
movea( RA, snake_x_pos ) # set list address
store RA listAddr
move RA 80 # starting x pos
store RA x_pos
move RA 0
store RA count # loop count
init_loopx:
load RA x_pos
store RA listData
call listAppendHead
load RA x_pos # snake segments four pixels in size
add RA 4
store RA x_pos
load RA count
add RA 1
store RA count
sub RA SNAKE_LENGTH # repeat for snake length
jumpnz init_loopx
movea( RA, snake_y_tail_ptr ) # set index address
store RA listTailPtr
movea( RA, snake_y_pos ) # set list address
store RA listAddr
move RA 72 # starting y pos
store RA y_pos
move RA 0
store RA count
init_loopy:
load RA y_pos
store RA listData
call listAppendHead
load RA count # repeat for snake length
add RA 1
store RA count
sub RA SNAKE_LENGTH
jumpnz init_loopy
# draw screen edge
draw_box( TURQUOISE, 4, 17, 154, 19 )
draw_box( TURQUOISE, 4, 114, 154, 116 )
draw_box( TURQUOISE, 4, 17, 6, 114 )
draw_box( TURQUOISE, 152, 17, 154, 114 )
# draw score
draw_tile( LIGHT_GREY, eval( SCORE_X_POSITION + ( 7 * 0 ) ), SCORE_Y_POSITION, s_CHAR )
draw_tile( LIGHT_GREY, eval( SCORE_X_POSITION + ( 7 * 1 ) -1 ), SCORE_Y_POSITION, c_CHAR )
draw_tile( LIGHT_GREY, eval( SCORE_X_POSITION + ( 7 * 2 ) -1 ), SCORE_Y_POSITION, o_CHAR )
draw_tile( LIGHT_GREY, eval( SCORE_X_POSITION + ( 7 * 3 ) ), SCORE_Y_POSITION, r_CHAR )
draw_tile( LIGHT_GREY, eval( SCORE_X_POSITION + ( 7 * 4 ) +1 ), SCORE_Y_POSITION, e_CHAR )
draw_tile( LIGHT_GREY, eval( SCORE_X_POSITION + ( 7 * 6 ) -2 ), SCORE_Y_POSITION, zero_CHAR )
draw_tile( LIGHT_GREY, eval( SCORE_X_POSITION + ( 7 * 7 ) -1 ), SCORE_Y_POSITION, zero_CHAR )
draw_tile( LIGHT_GREY, eval( SCORE_X_POSITION + ( 7 * 8 ) ), SCORE_Y_POSITION, zero_CHAR )
ret
The final subroutine needed is the game logic, the update subroutine below. This code contains a lot of IF-THEN-ELSE tests to determine what key has been pressed and the location of the snake and food. To help explain this code i have split this subroutine into a number of section. The first part of the subroutine accesses the snake's XY head position: snake_x_pos and snake_y_pos. This XY coordinate is then updated based on what key is pressed, if no new key is pressed the last key pressed is used i.e. the snake will continue to move in the same direction.
# ----------
# - UPDATE -
# ----------
update:
load RA snake_x_pos
store RA snake_x_data # buffer current x pos
load RA snake_y_pos
store RA snake_y_data # buffer current y pos
call read_keyboard # read last key pressed
load RA key # test last key pressed
sub RA 0x1B
jumpz update_down
sub RA 1 # 0x1C
jumpz update_left
sub RA 1 # 0x1D
jumpz update_up
sub RA 6 # 0x23
jumpz update_right
jump update_exit
update_right:
load RA snake_x_data #
add RA 4
store RA snake_x_data
jump update_check_snake
update_down:
load RA snake_y_data #
add RA 4
store RA snake_y_data
jump update_check_snake
update_left:
load RA snake_x_data #
sub RA 4
store RA snake_x_data
jump update_check_snake
update_up:
load RA snake_y_data #
sub RA 4
store RA snake_y_data
The variables snake_x_data and snake_y_data contain the new snake head position. This XY coordinate is now checked to see if it is already part of the snake i.e. has the snake run into itself. If a matching XY coordinate is found in the snake_x_pos and snake_y_pos list the finish flag is set and the subroutine exits.
update_check_snake:
move RA 0 # has snake hit itself check
store RA listIndex
update_check_snake_loop:
movea( RA, snake_x_pos ) # get pixel x
store RA listAddr
call listGetIndex
load RA listData
store RA x_pos
movea( RA, snake_y_pos ) # get pixel y
store RA listAddr
call listGetIndex
load RA listData
store RA y_pos
load RA snake_x_data # check x pos
subm RA x_pos
jumpnz update_check_snake_next
load RA snake_y_data # check y pos
subm RA y_pos
jumpnz update_check_snake_next
update_check_snake_hit:
move RA 0xFF
store RA finish # set finish flag
jump update_exit # snake hit snake finish
update_check_snake_next:
load RA listIndex # inc count
add RA 1
store RA listIndex
subm RA snake_x_tail_ptr # is count > tail pointer
jumpnz update_check_snake_loop
The next section checks to see if the new head position is still within the game's playing area. If the snake has moved out of bonds the finished flag is again set and the subroutine exits.
update_check_edge:
load RA snake_x_data # has snake hit ede boundary
sub RA SNAKE_LEFT_LIMIT
jumpc update_check_edge_hit
load RA snake_x_data
moved( RB, SNAKE_RIGHT_LIMIT )
sub RB RA
jumpc update_check_edge_hit
load RA snake_y_data
sub RA SNAKE_TOP_LIMIT
jumpc update_check_edge_hit
load RA snake_y_data
moved( RB, SNAKE_BOTTOM_LIMIT )
sub RB RA
jumpc update_check_edge_hit
jump update_add_segment
update_check_edge_hit:
move RA 0xFF
store RA finish # set finish flag
jump update_exit # snake hit edge finish
If the new snake head position is in a valid position its coordinates are added to the head of the snake_x_pos and snake_y_pos lists.
update_add_segment:
movea( RA, snake_x_pos ) # move segments to next position
store RA listAddr
movea( RA, snake_x_tail_ptr )
store RA listTailPtr
load RA snake_x_data # set data
store RA listData
call listAppendHead
movea( RA, snake_y_pos ) # set list address
store RA listAddr
movea( RA, snake_y_tail_ptr ) # set tail pointer
store RA listTailPtr
load RA snake_y_data # set data
store RA listData
call listAppendHead
Next new snake head position is compare to the food position. If these positions do not match, the tail of the snake is deleted i.e. the snake does not grow, the tail's XY coordinates are removed from their associated lists and this segment's pixels are deleted from the display. If the new snake head position does match the food's position, the tail is not deleted i.e. the snake grows by one segment.
update_check_food:
load RA snake_x_data # has snake hit food, check x pos
subm RA food_x_pos
jumpnz update_blank
load RA snake_y_data # has snake hit food, check y pos
subm RA food_y_pos
jumpnz update_blank
update_hit_food:
move RA 0xFF
store RA food_eaten # set food eaten flag
jump update_display # update display
update_blank:
movea( RA, snake_x_pos ) # get tail x pos
store RA listAddr
movea( RA, snake_x_tail_ptr )
store RA listTailPtr
call listGetTail
load RA listData
store RA x_pos
movea( RA, snake_y_pos ) # get tail y pos
store RA listAddr
movea( RA, snake_y_tail_ptr )
store RA listTailPtr
call listGetTail
load RA listData
store RA y_pos
call drawBlank # clear old tail position pixel
movea( RA, snake_x_pos )
store RA listAddr
movea( RA, snake_x_tail_ptr )
store RA listTailPtr
call listDeleteTail # remove old x position from list
movea( RA, snake_y_pos )
store RA listAddr
movea( RA, snake_y_tail_ptr )
store RA listTailPtr
call listDeleteTail # remove old y position from list
The final section updates the display, drawing the snake's segments.
update_display:
move RA 0 # update pixels
store RA listIndex
update_display_loop:
movea( RA, snake_x_pos ) # get pixel x
store RA listAddr
call listGetIndex
load RA listData
store RA x_pos
movea( RA, snake_y_pos ) # get pixel y
store RA listAddr
call listGetIndex
load RA listData
store RA y_pos
call drawSegment # draw snake segment
load RA listIndex # inc count
add RA 1
store RA listIndex
subm RA snake_x_tail_ptr # is count > index
jumpnz update_display_loop
update_exit:
ret

Figure 9 : snake game
To see this master piece in action, here is a link to a video of my high score (Link)(YouTube). Note, the max score is limited to 100 i.e. the length of the snake's position list, longer than this will cause the program to over write itself, which i would assume will cause the program to self estruct, however, i do not think anyone will ever get too 100, so thats never going to be an issue :).
WORK IN PROGRESS
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.
Contact email: mike@simplecpudesign.com