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