Building Snake Game in Golang

Simant Thapa Magar
readytowork, Inc.
Published in
18 min readSep 16, 2022

Golang isn’t particularly famous as a famous game development platform however as a programming challenge this article will focus on building a simple snake game. The challenge exposes different aspects of programming in go such as channel, goroutine, rendering in the terminal, accepting inputs, and so on. Hence building terminal games in Golang can be a great way to get familiar with the programming language.

For building this game we will require an additional package named Tcell. This package allows us to take advantage of its rendering and input capabilities in the terminal. Besides this, we will be using the packages that come along with go to accomplish our goal.

Variables and Data Structures

First, let’s discuss the variables and data structures that will be used in building this snake game.

type Coordinate struct {
x, y int
}

First of all, the terminal’s display is simply a big box consisting of small cells. We will need to render particular points to give the impression of the game which will consist of game borders, snake’s positions, and apple’s positions. Therefore Coordinate is the first data structure in our snake game which will hold multiple integers name x and y representing the position on the terminal screen.

type Snake struct {
points []*Coordinate
columnVelocity, rowVelocity int
symbol rune
}
type Apple struct {
point *Coordinate
symbol rune
}

Other data structures that we will be using are Snake and Apple which are both types of the struct. The snake type consists of a few things. Firstly the snake won't be a single point, it will grow as it eats an apple and can move freely. Hence we require multiple points to represent a snake and therefore it consists of a field name points which will be a slice of pointers of type Coordinate discussed earlier. Next, we need to fields to determine in which direction the snake is moving. It can move up, down, left, or right depending on user control and we need to keep it that way unless the user wishes to change the direction. To achieve this we will be using fields named columnVelocity which determine the horizontal direction and rowVelocity which will determine the vertical direction. Both of these fields are of type int. The remaining field in Snake type is symbol which simply holds a character to represent the snake’s body. We will declare a constant later which will represent a rectangle character to be used for the field.

The Apple type is fairly straightforward. Since it’s a single point its field point will hold a pointer of Coordinate type and symbol will be a character representing apple’s display in the terminal.

Next, there will be a number of variables used in the application mentioned and discussed here below

// variable to hold Snake type
var snake *Snake
// variable to hold Apple type
var apple *Apple
// variable to hold Screen information that comes from tcell package
var Screen tcell.Screen
// variable to hold Screen's full width and height respectively
var screenWidth, screenHeight int
// variable to hold game's state if it has been paused or over
var isGamePaused, isGameOver bool
// variable to hold game's score
var score int
// variable to keep track of coordinates to clear after each movement
var coordinatesToClear []*Coordinate
// snake and apple's representation character in screen
// these will be stored in symbol field of snake and apple type respectively
const SNAKE_SYMBOL = 0x2588
const APPLE_SYMBOL = 0x25CF
// game frame width and height
// feel free to change it according to your need
const FRAME_WIDTH = 80
const FRAME_HEIGHT = 15
// game frame's border thickness
const FRAME_BORDER_THICKNESS = 1
// game frame border representation symbols to make it look abit fancy
const FRAME_BORDER_VERTICAL = '║'
const FRAME_BORDER_HORIZONTAL = '═'
const FRAME_BORDER_TOP_LEFT = '╔'
const FRAME_BORDER_TOP_RIGHT = '╗'
const FRAME_BORDER_BOTTOM_RIGHT = '╝'
const FRAME_BORDER_BOTTOM_LEFT = '╚'

Game Algorithm

Let’s understand the algorithm for this game which will help us to understand the code implementation even better.

  1. Initialize Screen
  2. Initialize Game Objects
    - Set initial values for snake variable
    - Set initial values for apple variable
  3. Display Game Frame
    - Based on frame’s width and height call function to draw game’s border
  4. Display Initial Game Score
  5. Run goroutine to continuously listen for user input
  6. If isGameOver is set to true then to step 13
  7. If isGamePaused set to true then
    - Display Game Pause Info
  8. Read user input and perform accordingly
    - If no user input go to step 9
    - If user input is q exit the game
    - If user input is p toggle value of isGamePaused
    - If user input if up arrow key and snake is moving horizontally then set snake’s rowVelocity to -1 and columnVelocity to 0
    - If user input if down arrow key and snake is moving horizontally then set snake’s rowVelocity to 1 and columnVelocity to 0
    - If user input if left arrow key and snake is moving vertically then set snake’s rowVelocity to 0 and columnVelocity to -1
    - If user input if right arrow key and snake is moving vertically then set snake’s rowVelocity to 0 and columnVelocity to 1
  9. Update game state
    - If isGamePaused set to true go to step 10
    - Clear the screen; Based on coordinatesToClear data call print function to display empty space on desired coordinates
    - Update snake variable’s values
    — Get snake’s current head coordinates
    — Create new coordinate by adding snake head’s x-coordinate by snake’s columnVelocity and adding snake head’s y-coordinate by snake’s rowVelocity

    — Add new coordinate to snake’s point field

    — Set snake’s coordinates within game frame
    — — Get game frame’s top left x and y coordinate and bottom right x and y coordinate
    — — Determine game frame’s boundaries as follows
    — — — left boundary is same as frame’s top left x coordinate
    — — — top boundary axis is same as frame’s top left y coordinate
    — — — right boundary is bottom right y axis
    — — — bottom boundary is bottom right x axis
    — — — For each snake’s coordinate update snake’s coordinate such that it is inside game’s frame
    — — — If snake’s y coordinate is less than or equal to top boundary then set new y coordinate as bottom boundary — 1
    — — — If snake’s y coordinate is greater than or equal to bottom boundary then set new y coordinate as top boundary + 1
    — — — If snake’s x coordinate is less than or equal to left boundary then set new x coordinate as right boundary — 1
    — — — If snake’s x coordinate is greater than or equal to right boundary then set new x coordinate as left boundary + 1

    — Check if snake ate apple
    — — For each coordinate of snake check if it is same as apple’s coordinate
    — — — If any match is found then
    — — — — increase score by 1
    — — — — Call function that updates rendering for game score
    — — — Else
    — — — — Append snake’s first coordinate to coordinatesToClear variable
    — — — — Slice snake’s points to range from index 1 to end

    — Check if snake is eating itself
    — — Get snake’s head coordinates
    — — For all other coordinates of snake check if any is equal to snake’s head coordinates
    — — If any match is found then set isGameOver to true

    -Update apple variable’s value
    — As long as for all coordinates of snake if any matches with apple’s coordinate generate a new coordinate for apple
  10. Display game objects
    - Display all coordinates of snake in screen
    - Display coordinate of apple in screen
  11. Wait for 75ms so that game isn’t too fast for human eye
  12. Go to step 6
  13. Display game over info
  14. Exit game

Program Skeleton and Functions

Now let’s quickly look into the program skeleton and functions that will eventually create the snake game.

Packages to be used

import (
"fmt"
"math/rand"
"os"
"time"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/encoding""
)

Program Skeleton

Main function

// the starting point of our application
// we will initialize few things here such as screen, game's state and then run necessary game functions
func main() {
}

Initializing functions

// function to initialize screen
func initScreen() {
}
// function to initialize snake and apple types by assigning initial values
func initializeGameObjects() {
}
// function to set some initial coordinates for snake
func getInitialSnakeCoordinates() []*Coordinate {
}
// function to set initial coordinate for apple
func getInitialAppleCoordinates() *Coordinate {
}

Functions to handle input

// this function will run a goroutine in a loop and returns a channel string based on user input
// combination of goroutine and channel because we want our game to be flowing freely even though when user hasn't provided any input
// goroutine runs in background hence our application won't stop when there's no user input
func readUserInput() chan string {
}
// this function will be passed the channel string and will simply return key user entered or empty string
func getUserInput(userInput chan string) string {
}
// based on what user has entered, this function will update our game's state such as snake movement, game pause, game quit
// func handleUserInput(key string) {
}

Functions to validate & update game state

// a simple utility function that returns head coordinate (two integers)
// NOTE since snake's coordinates is a slice, we will consider the last element of slice to be head since its slightly convenient in our application to append new element and clear first element as snake moves
func getSnakeHeadCoordinates() (int, int) {
}
// this function will be responsible for updating snake's coordinates
// it will also call some functions to check if game should be over (snake eating itself) or grow the snake (snake eating apple)
func updateSnake() {
}
// another utility function that will make sure the snake is moving inside the game frame we have defined
// initially we will be defining coordinates irrespective of game frame and hence this is where this function comes in handy
func setSnakeWithinFrame(snakeCoordinate *Coordinate){
}
// function to check if it is eating itself and return a boolean result
func isSnakeEatingItself() bool {
}
// function to check if snake ate an apple and return a boolean result
func isAppleInsideSnake() bool {
}
// function to generate and return new coordinate for apple
func getNewAppleCoordinate() (int, int) {
}
// function that is responsible for calling above function based on snake and apple's position
func updateApple() {
}
// function that is responsible for calling different functions declared above for updating game's state
func updateGameState() {
}
// utility function that returns top left x-coordinate, top left y-coordinate, bottom right x-coordinate and bottom right y-coordinate
func getBoundaries() (int, int, int, int) {
}

Functions for rendering

// utility function that accepts a coordinate and sets it inside frame
// This function is helpful as we can randomly generate coordinate and then set it inside frame as we won't be using full screen
func transformCoordinateInsideFrame(coordinate *Coordinate) {
}
// function to print a character with some style (color) at specific positions as passed
func print(x, y, w, h int, style tcell.Style, char rune) {
}
// utility function to get top left coordinates of game frame
func getFrameOrigin() (int, int) {
}
// function to draw game frame
func displayFrame() {
}
// function to display snake based on its coordinates
func displaySnake() {
}
// function to display apple based on its coordinate
func displayApple() {
}
// function to call above mentioned functions to draw snake and apple in the screen
func displayGameObjects() {
}
// function to display info when game is paused
func displayGamePausedInfo() {
}
// function to display info when game is over
func displayGameOverInfo() {
}
// function to display game's score
func displayGameScore() {
}
// function that prints passed content at center horizontally, while vertical coordinate is passed to function
func printAtCenter(startY int, content string, trackClear bool) {
}
// function to clear necessary coordinates represented by variable coordinatesToClear
func clearScreen() {
}
// function to game frame
func printUnfilledRectangle(xOrigin, yOrigin, width, height, borderThickness int, horizontalOutline, verticalOutline, topLeftOutline, topRightOutline, bottomRightOutline, bottomLeftOutline rune) {
}

Code Implementation

Now here’s for the main part. Let’s look into the implementation of each function which will complete our snake game application.

Initializing different game components

As the first step is to initialize the game components, we will implement the initialization of the screen, game states here. During initialization of screen we will also assign values to screenWidth and screenHeight variables. In addition to that a validation is done to see if the game’s frame can be fit within the screen and if not then the application will exit.

func initScreen() {
encoding.Register()
var err error
Screen, err = tcell.NewScreen()
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
if err = Screen.Init(); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
defStyle := tcell.StyleDefault.
Background(tcell.ColorBlack).
Foreground(tcell.ColorWhite)
Screen.SetStyle(defStyle)
screenWidth, screenHeight = Screen.Size()
if screenWidth < FRAME_WIDTH || screenHeight < FRAME_HEIGHT {
fmt.Printf("The game frame is defined with %d width and %d height. Increase terminal size and try again ", FRAME_WIDTH, FRAME_HEIGHT)
os.Exit(1)
}
}

Next initialization of game objects i.e. snake and apple. For the snake’s points field we will set 4 initial coordinates. The coordinates are hard coded to appear somewhere at left section of game frame. While the coordinates are hardcoded, they haven’t been calculated to be placed within game frame. This is where the function to place provided coordinates within game’s frame comes to play. In this way we can generate any coordinate and let this function place it inside the frame. As mentioned earlier the last coordinate of snake will represent its head and hence based on the coordinates we have set initially its only feasible to move snake either left, right or downward. I have chosen to move the snake downward initially therefore set columnVelocity to 0 and rowVelocity to 1. The symbol for snake has been defined as constant SNAKE_SYMBOL so we will use the same.

Similarly, for apple we will place it at the center of the screen and its symbol will be value held by constant APPLE_SYMBOL

func initializeGameObjects() {
snake = &Snake{
points: getInitialSnakeCoordinates(),
columnVelocity: 0,
rowVelocity: 1,
symbol: SNAKE_SYMBOL,
}
apple = &Apple{
point: getInitialAppleCoordinates(),
symbol: APPLE_SYMBOL,
}
}
func getInitialSnakeCoordinates() []*Coordinate {
snakeInitialCoordinate1 := &Coordinate{8, 4}
transformCoordinateInsideFrame(snakeInitialCoordinate1)
snakeInitialCoordinate2 := &Coordinate{8, 5}
transformCoordinateInsideFrame(snakeInitialCoordinate2)
snakeInitialCoordinate3 := &Coordinate{8, 6}
transformCoordinateInsideFrame(snakeInitialCoordinate3)
snakeInitialCoordinate4 := &Coordinate{8, 7}
transformCoordinateInsideFrame(snakeInitialCoordinate4)
return []*Coordinate{
{snakeInitialCoordinate1.x, snakeInitialCoordinate1.y},
{snakeInitialCoordinate2.x, snakeInitialCoordinate2.y},
{snakeInitialCoordinate3.x, snakeInitialCoordinate3.y},
{snakeInitialCoordinate4.x, snakeInitialCoordinate4.y},
}
}
func getInitialAppleCoordinates() *Coordinate {
appleInitialCoordinate := &Coordinate{FRAME_WIDTH / 2, FRAME_HEIGHT / 2}
transformCoordinateInsideFrame(appleInitialCoordinate)
return appleInitialCoordinate
}
func transformCoordinateInsideFrame(coordinate *Coordinate) {
leftX, topY, rightX, bottomY := getBoundaries()
coordinate.x += leftX + FRAME_BORDER_THICKNESS
coordinate.y += topY + FRAME_BORDER_THICKNESS
for coordinate.x >= rightX {
coordinate.x--
}
for coordinate.y >= bottomY {
coordinate.y--
}
}
func getBoundaries() (int, int, int, int) {
originX, originY := getFrameOrigin()
topY := originY
bottomY := originY + FRAME_HEIGHT - FRAME_BORDER_THICKNESS
leftX := originX
rightX := originX + FRAME_WIDTH - FRAME_BORDER_THICKNESS
return leftX, topY, rightX, bottomY
}

At this point, our main function will look something like the below.

func main() {
initScreen()
initializeGameObjects()
}

Now we will get into the displaying functions as we need to display the game’s frame and initial score. What’s done here is that based on screen size and frame’s size we will determine what should be the topmost coordinate for frame’s x and y coordinate such that the frame appears at the center of the screen. Once it’s determined we will simply print border symbols around the border of the frame which will be 1 cell apart from the actual playable screen.

func getFrameOrigin() (int, int) {
return (screenWidth-FRAME_WIDTH)/2 - FRAME_BORDER_THICKNESS, (screenHeight-FRAME_HEIGHT)/2 - FRAME_BORDER_THICKNESS
}
func print(x, y, w, h int, style tcell.Style, char rune) {
for i := 0; i < w; i++ {
for j := 0; j < h; j++ {
Screen.SetContent(x+i, y+j, char, nil, style)
}
}
}
func displayFrame() {
frameOriginX, frameOriginY := getFrameOrigin()
printUnfilledRectangle(frameOriginX, frameOriginY, FRAME_WIDTH, FRAME_HEIGHT, FRAME_BORDER_THICKNESS, FRAME_BORDER_HORIZONTAL, FRAME_BORDER_VERTICAL, FRAME_BORDER_TOP_LEFT, FRAME_BORDER_TOP_RIGHT, FRAME_BORDER_BOTTOM_RIGHT, FRAME_BORDER_BOTTOM_LEFT)
Screen.Show()
}
func printUnfilledRectangle(xOrigin, yOrigin, width, height, borderThickness int, horizontalOutline, verticalOutline, topLeftOutline, topRightOutline, bottomRightOutline, bottomLeftOutline rune) {
var upperBorder, lowerBorder rune
verticalBorder := verticalOutline
for i := 0; i < width; i++ {
// upper boundary
if i == 0 {
upperBorder = topLeftOutline
lowerBorder = bottomLeftOutline
} else if i == width-1 {
upperBorder = topRightOutline
lowerBorder = bottomRightOutline
} else {
upperBorder = horizontalOutline
lowerBorder = horizontalOutline
}
print(xOrigin+i, yOrigin, borderThickness, borderThickness, tcell.StyleDefault, upperBorder)
print(xOrigin+i, yOrigin+height-1, borderThickness, borderThickness, tcell.StyleDefault, lowerBorder)
// lower boundary
}
// side boundary
for i := 1; i < height - 1; i++ {
print(xOrigin, yOrigin+i, borderThickness, borderThickness, tcell.StyleDefault, verticalBorder)
print(xOrigin+width-1, yOrigin+i, borderThickness, borderThickness, tcell.StyleDefault, verticalBorder)
}
}

We will use the screen outside the game frame to display the score implemented as below. One thing to note is that we are using printAtCenter a function that will horizontally center the content to be printed while the vertical axis must be specified. Besides the y-axis and content to be displayed; the function also accepts a Boolean variable. This variable declared as trackClear if set to true will append the coordinates where contents will display to coordinatesToClear a variable. This is used to clear screen. Since the score is displayed outside the frame it doesn’t overlap anything else and hence we have passed false for displaying the game score. However, on other functions such as displaying game’s paused info we will set trackClear to true so that its cleared when game resumes.

func displayGameScore() {
_, frameY := getFrameOrigin()
printAtCenter(frameY+FRAME_HEIGHT+2, fmt.Sprintf("Current Score : %d", score), false)
}
func printAtCenter(startY int, content string, trackClear bool) {
startX := (screenWidth - len(content)) / 2
for i := 0; i < len(content); i++ {
print(startX+i, startY, 1, 1, tcell.StyleDefault, rune(content[i]))
if trackClear {
coordinatesToClear = append(coordinatesToClear, &Coordinate{startX + i, startY})
}
}
Screen.Show()
}

The main function should now look like the below after initialization and displaying the border and score.

func main() {
initScreen()
initializeGameObjects()
displayFrame()
displayGameScore()
}

Next is where things can go a bit confusing. At this stage, we need to run our application in a loop so that game is set in motion. But before that we need to declare and defined variables and functions that will accept the user input and take action based on it. We will accept the user input however our application shouldn’t be paused until user enters some key. The game should be in motion even though user is not active as long as the game is not over. To achieve this we will continuously listen to user input but in the background, the function executed by goroutine. If user enters some key the function will pass the input to the channel and then change in game if permissible takes place. In terms of permissible I mean if it’s a key that has some action in our game like we will use ‘q’ to quit game, ‘p’ to pause/resume the game and arrow keys to change the snake’s direction. One thing to note that in order to change snake’s direction, it should be the inapplicable direction. For example if the snake is moving up and user enters down arrow then we can’t simply reverse the snake’s direction and same applies for horizontal movement. Also if the game is in a pause state then user’s input to change direction shouldn’t be dealt so we will add the condition as well.

func readUserInput() chan string {
userInput := make(chan string)
go func() {
for {
switch ev := Screen.PollEvent().(type) {
case *tcell.EventKey:
userInput <- ev.Name()
}
}
}()
return userInput
}
func getUserInput(userInput chan string) string {
var key string
select {
case key = <-userInput:
default:
key = ""
}
return key
}
func handleUserInput(key string) {
if key == "Rune[q]" {
Screen.Fini()
os.Exit(0)
} else if key == "Rune[p]" {
isGamePaused = !isGamePaused
} else if !isGamePaused {
if key == "Up" && snake.rowVelocity == 0 {
snake.rowVelocity = -1
snake.columnVelocity = 0
} else if key == "Down" && snake.rowVelocity == 0 {
snake.rowVelocity = 1
snake.columnVelocity = 0
} else if key == "Left" && snake.columnVelocity == 0 {
snake.rowVelocity = 0
snake.columnVelocity = -1
} else if key == "Right" && snake.columnVelocity == 0 {
snake.rowVelocity = 0
snake.columnVelocity = 1
}
}
}

Now the next thing to do is set our game in a continuous loop until the game is over. During this loop the user’s input will be read, the snake’s movement will be updated, apple’s coordinate will be updated, determination of game progress will be done and continuous rendering and clearing of the screen will be conducted.

As we have already dealt with the user’s input, we will now update the game’s state. If the game is paused then we won’t do anything and return. Else we need to update the snake’s coordinates, apple’s coordinates and clear coordinates to be cleared.

First, let’s clear the screen where we will just loop through coordinates maintained by variable coordinatesToClear and display space. While we could have simply called Screen.Clear() and cleared everything on screen and then render everything again which could have reduced lines of code, it may not be the optimum solution for all devices. In order to get more smooth experience rather than flickering output, this process is a lot more convenient.

func clearScreen() {
for _, coordinate := range coordinatesToClear {
print(coordinate.x, coordinate.y, 1, 1, tcell.StyleDefault, ' ')
}
}

Next, it's the snake’s turn to be updated. We will update the snake’s coordinates on every iteration unless the game is paused or over. User input determines in which direction the snake coordinates should be updated as discussed earlier. Based on those change snake is continuously updated. Another important thing to consider here is that I have opted for the snake to transition through the game frame instead of ending the game. So if the snake is moving towards the left and hits the frame border then it will appear from the opposite border. The function setSnakeWithinFrame maintains the coordinates of the snake within the border. After determining if the snake’s new head coordinate we just need to append to existing coordinates. Next, we need to determine if the snake has eaten an apple. If it hasn’t then we need to remove the first coordinate from the snake’s points to maintain its length otherwise we can just increase the score and call the function to display the updated score. One last thing to check while updating the snake is to determine if the snake’s movement is such that it has bitten itself. For this check, we can simply loop over all coordinates except the head of the snake to see if any of them matches with the head. If it does it's game over.

func getSnakeHeadCoordinates() (int, int) {
snakeHead := snake.points[len(snake.points)-1]
return snakeHead.x, snakeHead.y
}
func setSnakeWithinFrame(snakeCoordinate *Coordinate) {
leftX, topY, rightX, bottomY := getBoundaries()
if snakeCoordinate.y <= topY {
// if above
snakeCoordinate.y = bottomY - 1
} else if snakeCoordinate.y >= bottomY {
// if below
snakeCoordinate.y = topY + 1
} else if snakeCoordinate.x >= rightX {
// if right
snakeCoordinate.x = leftX + 1
} else if snakeCoordinate.x <= leftX {
// if left
snakeCoordinate.x = rightX - 1
}
}
func isAppleInsideSnake() bool {
for _, snakeCoordinate := range snake.points {
if snakeCoordinate.x == apple.point.x && snakeCoordinate.y == apple.point.y {
return true
}
}
return false
}
func isSnakeEatingItself() bool {
headX, headY := getSnakeHeadCoordinates()
for _, snakeCoordinate := range snake.points[:len(snake.points)-1] {
if headX == snakeCoordinate.x && headY == snakeCoordinate.y {
return true
}
}
return false
}
func updateSnake() {
snakeHeadX, snakeHeadY := getSnakeHeadCoordinates()
newSnakeHead := &Coordinate{
snakeHeadX + snake.columnVelocity,
snakeHeadY + snake.rowVelocity,
}
setSnakeWithinFrame(newSnakeHead)
snake.points = append(snake.points, newSnakeHead)
if !isAppleInsideSnake() {
coordinatesToClear = append(coordinatesToClear, snake.points[0])
snake.points = snake.points[1:]
} else {
score++
displayGameScore()
}
if isSnakeEatingItself() {
isGameOver = true
}
}

For apple, we just need to check if snake ate apple which we have already implemented on the function isAppleInsideSnake . If it has then we need to determine new coordinates for the apple which is outside the snake’s body. We will generate random coordinates of limited frame width and height and then transition it to the game frame.

func getNewAppleCoordinate() (int, int) {
rand.Seed(time.Now().UnixMicro())
randomX := rand.Intn(FRAME_WIDTH - 2*FRAME_BORDER_THICKNESS)
randomY := rand.Intn(FRAME_HEIGHT - 2*FRAME_BORDER_THICKNESS)
newCoordinate := &Coordinate{
randomX, randomY,
}
transformCoordinateInsideFrame(newCoordinate)return newCoordinate.x, newCoordinate.y
}
func updateApple() {
for isAppleInsideSnake() {
coordinatesToClear = append(coordinatesToClear, apple.point)
apple.point.x, apple.point.y = getNewAppleCoordinate()
}
}

All the functions on the game update are called from a function updateGameState

func updateGameState() {
if isGamePaused {
return
}
clearScreen()
updateSnake()
updateApple()
}

We have completed updating the game state and it's time to display those changes. It's pretty straightforward, we just need to print the snake’s symbol for all of its points and the apple’s symbol for its point. We will use different colors to make our game a bit more appealing.

func displaySnake() {
style := tcell.StyleDefault.Foreground(tcell.ColorDarkGreen.TrueColor())
for _, snakeCoordinate := range snake.points {
print(snakeCoordinate.x, snakeCoordinate.y, 1, 1, style, snake.symbol)
}
}
func displayApple() {
style := tcell.StyleDefault.Foreground(tcell.ColorDarkRed.TrueColor())
print(apple.point.x, apple.point.y, 1, 1, style, apple.symbol)
}
func displayGameObjects() {
displaySnake()
displayApple()
Screen.Show()
}

We have also implemented a pausing game. So it's better to show some info that the game has been paused.

func displayGamePausedInfo() {
_, frameY := getFrameOrigin()
printAtCenter(frameY-2, "Game Paused !!", true)
printAtCenter(frameY-1, "Press p to resume", true)
}

The main function will now look as below and our game is ready to be played. We will use a delay of 75ms for each iteration to make the movement visible to the human eye.

func main() {
initScreen()
initializeGameObjects()
displayFrame()
displayGameScore()
userInput := readUserInput()
var key string
for !isGameOver {
if isGamePaused {
displayGamePausedInfo()
}
key = getUserInput(userInput)
handleUserInput(key)
updateGameState()
displayGameObjects()
time.Sleep(75 * time.Millisecond)
}
}

Our snake game is now playable. However, the final touch for this game would be to show some info after the game is over and wait for a few seconds before the application is terminated.

func displayGameOverInfo() {
centerY := (screenHeight - FRAME_HEIGHT) / 2
printAtCenter(centerY-1, "Game Over !!", false)
printAtCenter(centerY, fmt.Sprintf("Your Score : %d", score), false)
}

The final main function will be as follows

func main() {
initScreen()
initializeGameObjects()
displayFrame()
displayGameScore()
userInput := readUserInput()
var key string
for !isGameOver {
if isGamePaused {
displayGamePausedInfo()
}
key = getUserInput(userInput)
handleUserInput(key)
updateGameState()
displayGameObjects()
time.Sleep(75 * time.Millisecond)
}
displayGameOverInfo()
time.Sleep(3 * time.Second)
}

Our game is now complete. You can either run go run main.go or use go build command to build the executable file. Here below is an image of the executed application.

The complete code for this can be found on Snake Game in Golang.

Happy Coding 🎉

--

--