PS2 keyboard and Snakes

Home

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.

Table of Contents

PS2 Interface
Snake Games
Hardware

PS2 Interface

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

Snake Game



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

Creative Commons Licence

This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

Contact email: mike@simplecpudesign.com

Back