🐍 Build a Snake Game in TypeScript

How to build a retro snake game with HTML5 and TypeScript

Kenneth Reilly
ITNEXT

--

Screen capture of a retro snake game made with HTML5 Canvas and TypeScript

Introduction

If there’s one thing that left a huge impression on me growing up, it was the cool games of the 80’s and 90’s that were incredibly simple but also really fun to play. I also learned basic programming around this time, so naturally it wasn’t long before I started to create my own simple games.

Back in 2015, I created this old-school snake game in TypeScript, to test out the graphical features of the canvas object in HTML5, and to get a feel for how it could be used to re-create classic 2D games from the ground up.

I chose TypeScript because I had built several UIs and UI frameworks with it by that time, many of them modeled closely after my experience with C# and WPF. This made it a great choice for quickly prototyping a custom game engine from scratch, where I wanted flawless performance and virtually bug-free behavior (we always want bug-free software, but in this case I got to build the entire game from scratch using the tools of my choice, so I actually got it).

The entire project is open-source and available here, so if you feel like it, grab a copy and have fun experimenting with it and seeing what you come up with.

Project Structure

This project is pretty barebones, with no external dependencies beyond a TypeScript compiler. This makes it an ideal environment for developing a simple game engine from scratch. Here are package.json and tsconfig.json:

The package.json file in this project is more or less for reference, as there really are no dependencies or anything else going on. Within tsconfig.json there are instructions for the compiler to output files as ES6 modules and place them in the build directory for loading in the browser.

Within the project root also is the index.html file for serving the game as a static app (which has been done here) and a css folder for CSS3 styles.

The contents of src are the main entry file game.ts along with three folders for objects, types, and ux, which correspond to these key concepts:

  • objects: Objects used within the game, such as the Snake or a Coin
  • types: Basic data types such as Position, Speed, Direction, and GameKey
  • ux: Components that handle gameplay and rendering such as Canvas

In each of these folders is an index.ts file which simply groups and exports the contents of the other files into a single module for each. That covers it for the folder structure and project config files, so next we’ll take a look at the presentation and style of the game as defined in HTML5 and CSS3.

HTML and CSS Definitions

This game relies on canvas for most of the real on-screen work, so the rest of the game UX is mostly there to support that. Here is a look at index.html:

Here we can see that there’s not much to this HTML file: some basic social graph metadata, a header with a few simple buttons and div readouts, and the canvas itself where the game action will be drawn.

Next up is the CSS file, css/style.css:

In the snippet above, css/style.css is open in both tabs, showing the first half in the first tab and the second in the right. There’s not much to it, with a few color definitions, the basic page structure with centered content, and a few styles for buttons and links and things. Most of the work happens in the canvas so these few styles are just enough to get a basic game set up. With the structure and style out of the way, let’s look at the game logic implementation.

Core Type Definitions

The folder ./types contains several classes, enums, and interfaces that make up the fundamental types used within the game. Here is gameobjects.ts:

This file defines interfaces to be used by game objects and the board to draw the objects on the screen.

Next up is

The types folder with gameobjects.ts, position.ts, and enums.ts

In the above screenshot, three of the four module files within types are open. The file enums.ts contains four enum types that match these requirements:

  • GameKey: the key values for up, down, left, and right arrow keys
  • ScreenEdge: used to determine if the snake is colliding with a screen edge
  • Direction: the direction in which the player is currently moving (if any)
  • Speed: how fast the player is currently moving across the game board

Also we have an interface for Drawable along with two interfaces that extend it, one for the player and one for game objects which the player might collide with. Each of these will have a Position, which is defined as having an X and Y value along with a convenience method for copying the position.

The Timer class in src/timer.ts which runs the game clock

The last item under types is the Timer class, which serves as the main clock of the game. It can can be started, stopped, and reset as needed. There are two locally defined enums: ClockType, which denotes whether the timer has a duration or runs forever, and ClockTick, which provides a master clock “pulse” that is used by other parts of the game to, for example, move the player object at half (or twice) the normal speed as necessary.

That covers the main types used within the game. These will be referenced in many places and utilized to create other more complex objects as needed.

Game Object Classes

The next group of modules we’ll check out are the game objects:

The Coin class represents a coin that can be picked up for points

First in this group is the Coin class, which implements the IGameObject interface meaning it must have handle_collision( ) and draw() methods at the very least or the compiler would throw an error. This is useful during the process of building out something, as you wouldn’t want to accidentally leave off one of these methods on one of a hundred or so game objects, causing it to either not respond to the player, not draw on the screen, or end up an invisible item on the board that doesn’t show up yet still responds to a player collision.

The Coin class has some static properties, including default values and a static reference to all of the instances of Coin which currently exist in memory. This approach makes it incredibly easy to manipulate groups of items from within the concept definition of the item itself: for example, it would be trivial to make another item which when collected would cause all the coins on the screen to disappear, or cause more of them to rain down, or any other effect.

When handle_collision is fired, the snake which ran into it gets the total value of the coin added to its player score, and then the max_length of the snake is increased by 8 which allows the snake to grow by eight steps over the next eight clock cycles. Then the destroy() method is called, which causes the coin to be removed from the board, and erases any remaining traces of it.

The Coin class showing the draw() and destroy() methods

Each object having its own implementation of these methods allows for a great deal of flexibility while maintaining a simple and effective architecture that keeps the overall game design in check. One could, for example, wire in some special effects within the handle_collision() or destroy() methods, while having little effect on other parts of the system up the chain. This is one example of how to leverage some of the powerful OOP features of TypeScript.

Next up are the FastPlayer and SlowPlayer objects, which speed up and slow down the player respectively. Since these are effectively two variants of the exact same thing, I’ll show one of them, FastPlayer, as an example:

The FastPlayer class which causes the player to move fast when collected

The FastPlayer class looks pretty similar to the Coin example from earlier. The main differences here are that handle_collision() changes the speed at which the snake is moving, and that draw() produces a different graphic. Next up are the classes which make up the snake, SnakeSegment and Snake:

The SnakeSegment class which draws the snake pieces onto the screen

The SnakeSegment class defines a segment of a snake to be drawn on the screen. Each snake on the screen is made up of a group of these, each having a position for locating and drawing it onto the board, and also a color which is auto-rotated through eight values defined in the colors variable. One night I was showing this game to my daughter who decided it would be way cooler with a rainbow effect similar to a certain well-known meme, and so it was.

The Snake class with the various properties of a player-controller object

The last module within the objects folder is the Snake class in snake.ts, which controls both the lifespan of the player and the actual rendering of the snake object itself. In the above screenshot is a static default_length which is what determines how long the snake will get by default without the player having collected any coins yet. The jump_distance property determines how far the snake will jump when the jump key is pressed, which causes the snake to skip over obstacles or items in those spaces that were jumped over.

There are also boolean properties for skip_next_turn, hit_detected, and is_alive, which control gameplay functions and allow sanity checks like making sure the player is still alive before drawing it and so forth. There are also properties for speed, direction, and position, which are pretty self-explanatory here. The default speed and direction are Normal and None respectively, while position is initially undefined and set when the snake is placed on the gameboard.

In addition to the standard-fare properties of hi_score, points, and lives, there is an array of SnakeSegment objects called segment for holding the individual snake pieces which will follow behind it on the board. Also visible are the constructor and part of the jump() method, which figures out what direction the player is traveling and moves the head of the snake forward by the value of jump_distance, which causes the rest of the segments to follow suit.

More of the Snake class with various gameplay methods

Also in the Snake class are a few methods for handling the actual gameplay. In the screenshot above, on_hit_screen_edge() is left unused, as it was once set to kill the player when the snake collided with the wall, but instead I wanted the snake to jump around to the other side instead, so I removed that code and left a method stub with a TODO and empty switch statement in its place.

Also we find the die() method, which sets hit_detected to true, sets and resets the high scores, resets the game when out of lives, and also resets the position and direction of the snake to start over in the next turn. There’s also a method set_speed() which takes a Speed object and updates the snake speed from it.

More of the Snake class with the process_turn() method

The Snake class also has a process_turn() method that determines what is about to happen next based on the current speed, position, and direction of the snake along with whatever happens to be in the space where the snake is trying to occupy. First of all, if the snake is already dead, do nothing. If the snake is moving fast, skip processing every other clock tick (making it move two squares for every turn), and likewise if the player is moving slow, cause it to drag out by skipping every other turn altogether. Next, a few checks and updates are performed to update the snake position and determine whether it’s moving or not, and then handle events such as the screen wrap-around and collision with objects that occupy the space to be entered:

Even more of the Snake class with gameplay and positioning logic

If the player is still alive and moving around the board, the update_board() method is called, which re-draws each of the individual snake segments onto the board, causing each one to follow the one in front of it, wherever it is.

This wraps it up for the Snake class, which contains most of the game logic that deals with handling the snake on and off the screen. This is where the concepts of OOP really come in handy, because we can keep all of the snake logic inside a file called snake.ts instead of scattered around in random files.

Control and Gameplay

In the ux folder are classes which take input control and render the graphical output of the game. The first of these we’ll look at is src/ux/board.ts:

The Board class with various methods for handling in-game objects

The Board class contains various static methods and properties for handling the game board, which is a grid with height and width, along with block_size which defines the height and width of each game board square (in pixels). The actual board itself is represented by the grid property, which is an array of IDrawable, which allows this controller to easily keep track of what items are in play at any given time, updating them as the game progresses.

There are various methods such as place_object() and remove_object_at() which are mostly self-explanatory and only handle one or two things at a time. This is intentional, as the architecture is blatantly obvious and can be easily updated and modified without much chance of breaking something else, since as far as each individual part is concerned, everything else still works as expected. Game development in general lends itself well to object-oriented programming in this manner, since the program is effectively dealing with virtual objects which interact with each other, and with things like the game board, which is in turn just another game object.

The Board class with generate_random_position(), init(), and draw() methods

Also in the Board class we find the generate_random_position() method, which uses some standard Math.floor and Math.random JavaScript functions to come up with a random board position, and then returns that as a Position object. The init() method calculates the height and width of the board by dividing the Canvas height and width by the board’s block_size, and then intializes the grid itself with empty arrays. The draw() method makes full use of interchangeable object interfaces by iterating over the board and calling draw() on each instance of an object that implements IDrawable, regardless of whether the object is the snake, a coin, or some other on-screen item.

Next up is the Canvas class in src/ux/canvas.ts:

The lean and mean Canvas class for drawing the game contents

The Canvas is a pretty simple abstract class that has a width and height which have been initialized to a nostalgic resolution of 640x400 along with a context property for holding a reference to the HTML5 on-screen canvas context that is used for the actual drawing of graphics onto the screen. There is an init() method along with a few convenience methods for drawing basic shapes, and that’s pretty much it. Next up we’ll look at the Console in src/ux/console.ts:

The Console class with a few standard console buttons

The Console class is really simple and has a reference to the three buttons that control the game state (start, pause, and reset), with handlers for onclick events that call each respective game function on the Game object.

Next up is the Controls class in src/ux/controls.ts:

The Controls class which handles controller input for the game

The Controls class is used exclusively for handling user input. There is a handler for on_key_up (which is hooked up by the Game class as we will see later) that simply takes the current event key code and stores it locally. This way, the player can hit multiple control keys during one clock cycle and the controller will only use the last one pressed. This is really useful in this game for making split-second changes to the direction in which the snake will move.

Every time process_input() is called, which is once per game clock pulse to update the state of the game each turn, a series of checks are performed to determine what to do next. If no input was provided, leave everything as-is. This will result in, for example, the snake traveling all the way across the screen if we leave it alone. The snake can move in any direction other than the one from which it came, since it cannot pass through itself. If the last key pressed was the space bar, the jump() method is called on the snake. Once the last_key has been consumed by the Controls class, it is discarded and set back to null as this class has no concern with the rest of the game logic, and just sits patiently waiting to capture (and maybe process) controller input.

Next up is the GUI class in srx/ux/gui.ts:

The GUI class which renders game metrics and text

The GUI class is also super-simple and is only used for drawing things like the hi-score and how many lives are remaining. It holds a few DOM elements and updates their text based on various properties of game state.

With all of the supporting class and type definitions out of the way, let’s take a look at the last .ts file in this project, game.ts:

The main entry point for the game, the Game class

The Game class serves as the main entry point and controller for the game. There are static properties for clock and player_one which drive the core of the game action, along with the current hi_score which starts at 0, and is_running which is used to set (and check) whether the game is running or not.

The init() method is only called once when the game website is loaded, to initialize the canvas object and start forwarding onkeyup events to the game controller for further processing. The ready() method is called when these tasks are complete and whenever the game is reset, which in turn re-initializes various other game components including the Board, Clock, and Snake.

When the start() method is fired to start the game, a check is first made to make sure the game isn’t already running or in a paused state, at which point the game will either continue or pause/unpause depending on the current state of is_paused on the clock. Once these checks pass, the clock is started.

The Game class with pause(), reset(), and on_clock_tick() methods

In addition to starting the game, we also need to be able to pause and reset it. The pause() method checks the current state of is_paused on the game clock and will then either pause or resume the game as necessary. The reset() method will stop the game clock (if it exists), set is_running to false, and then call ready() to re-initialize the details of the game back to default.

The contents of on_clock_tick() are currently somewhat of a mess from still being in development, however the basic idea is here. Every clock pulse, input is received by the controller via process_input(). Next, the fate of the player is decided by calling process_turn() on that player’s game piece (the snake).

On even clock pulses, some makeshift item randomization logic is carried out to make the game more interesting by scattering a few coins and power-ups all across the board. The code for this is will likely change later to accommodate more complex gameplay logic for generating and distributing random items.

Not pictured in the screenshot are the final Board.draw() and GUI.draw() method calls at the end of on_clock_tick(). These simply invoke the draw methods on each of these objects, painting the screen with updated content.

Conclusion

I hope you enjoyed this article about building simple classic 2D games in TypeScript. This rudimentary game “engine” could be modified or upgraded in countless ways to be used for anything from online chess to web-based RPG games or anything else, limited only by the imagination of the developer.

Thanks for reading!

Kenneth Reilly (8_bit_hacker) is CTO of LevelUP

--

--