The Classic Snake Game Recreated With Ruby

Alexandra Pestruyeva
7 min readJan 23, 2020

--

Being a 90s kid and experiencing the recent launch of Disney Plus has left me feeling nothing short of nostalgic. Thinking back on the times, I quickly remembered the best phone game of my childhood: the Classic Snake Game.

I found a YouTube tutorial that recreates the Classic Snake Game using Ruby. The original game is played by maneuvering a snake around the screen. Food in the form of a ball appears randomly on the screen. As the snake collects the food, the snake grows in length. This increase in length makes it more difficult to control the snake because the snake is not allowed to touch its body. The game is over when the snake crashes into itself. The goal is to make the snake as long as possible before the game is over. For this Ruby version, the snake can go through the walls. The arrow keys on the keyboard control the snake.

Ruby 2D is the gem required for this tutorial. Ruby 2D is a gem that makes 2D applications in Ruby. You can create applications, games, and visualizations easily with just a few lines of code. When you require ‘ruby2d’, it adds the Ruby 2D domain-specific language and classes. Then a new window object is created for you by calling Ruby2D::Window.new. The show method called at the end of the program tells Ruby 2D to show the window.

require 'ruby2d'show

You can reference the window class directly, to see its attributes:

Window.title   # returns "Ruby 2D”Window.width   # returns 640Window.height  # returns 480

For our game, the background color is set to ‘navy’, and the speed is set by the fps_cap at 8. The fps is the current frame rate expressed in frames per second.

set background: 'navy'set fps_cap: 8

We set the grid size to be 20. Now every time we draw something, we will have 20 pixels. Our grid is 32 by 24:

GRID_SIZE = 20GRID_WIDTH = Window.width / GRID_SIZEGRID_HEIGHT = Window.height / GRID_SIZE

So this results in:

Width: 640 / 20 = 32

Height: 480 / 20 = 24

The snake is going to be created as a few boxes on the screen. First, we must create a Snake Class. The snake will be initialized with 4 starting positions corresponding to where we want the boxes to appear when we start the program. For this, we create an array for @positions. The positions will hold each of the squares that the snake currently occupies. Inside the @positions array, there are 4 sets of arrays with x and y coordinates for each of the 4 starting positions. The starting direction is also specified as “down”. Our snake is a straight line consisting of 4 boxes, going from the top to the bottom.

def initialize   @positions = [[2, 0], [2, 1], [2, 2], [2, 3]]   @direction = “down"   @growing = falseend

Next, we will draw a square for each of these positions by making a method called “draw”. We will iterate through each position, and for each position our program will draw a new square. The x will be the value of the element at index 0 multiplied by the grid size, which is 20. The y will be the value of the element at index 1 multiplied by the grid size. Then, we set the size of each square equal to the grid size minus 1. The reason we minus 1 pixel is so that a line appears before each new box. Otherwise, the snake would be solid all throughout. We then set the color to white.

def draw
@positions.each do |position|
Square.new(x: position[0] * GRID_SIZE, y: position[1] *
GRID_SIZE, size: GRID_SIZE - 1, color: “white")
end
end

The “move” method will update the initial positions. To make the snake move, we must add a new square at the head of the snake, and remove the square at the tail of the snake.

For the tail:

@positions.shift will remove the first element in the positions array, which is the first set of coordinates. We must also use the update loop here.

The update loop: The update loop is an infinite loop that runs 60 times per second. The Window manages the update loop. We can enter this loop by using the update method. We must put “clear” in the update method so that the tail of the snake clears. Otherwise it will just draw on top of the boxes that are there already.

snake = Snake.newupdate do
clear
snake.move
snake.draw
end

For the head:

First, make a method to return the head of the snake:

def head
@positions.last
end

Then, @positions.push will add the new coordinates of the head to the @positions array.

def move
@positions.shift
case @direction
when "down"
@positions.push(new_coords(head[0], head[1] + 1))
when "up"
@positions.push(new_coords(head[0], head[1] - 1))
when "left"
@positions.push(new_coords(head[0] - 1, head[1]))
when "right"
@positions.push(new_coords(head[0] + 1, head[1]))
end
end

The “new_coords” method solves another problem. The snake disappears when it leaves the window. We can fix this by using the modulo operator. This operator gives you the remainder of a division. Our grid is 32 by 24. If we go through the top right side of the screen out of the visible window, our x value will be 33. If we use the modulo operator, the result is 1. We can take the modulus of the x and y values, and this means that when the snake exits one side of the window, it will reappear on the opposite side.

def new_coords(x, y)
[x % GRID_WIDTH, y % GRID_HEIGHT]
end

The next problem is that we want to prevent the snake going backwards in any direction. For example, when going down, the snake should not be able to go up. The way to fix this is:

def can_change_direction_to?(new_direction)
case @direction
when "down" then new_direction != "up"
when "up" then new_direction != "down"
when "left" then new_direction != "right"
when "right" then new_direction != "left"
end
end

The Event Listener that we will use is “:key_down”. When a key is pressed down, the keyboard event is captured by the Window:

on :key_down do |event|
# A key was pressed
puts event.key
end

In our case, it would look like this:

on :key_down do |event|
if ["down", "up", "left", "right"].include?(event.key)
if snake.can_change_direction_to?(event.key)
snake.direction = event.key
end
end
end

The next step is to generate the food ball. We will make a Game Class for this. The game class will be initialized with a score of 0, randomly generated x and y coordinates for the ball, and a variable to check if the game is finished.

class Game
def initialize
@score = 0
@ball_x = rand(GRID_WIDTH)
@ball_y = rand(GRID_HEIGHT)
@finished = false
end
end

Then, we create a “draw” method that generates a square for the ball, and a message to display texts to the user.

def draw
unless finished?
Square.new(x: @ball_x * GRID_SIZE, y: @ball_y * GRID_SIZE,
size: GRID_SIZE, color: "yellow")
end
Text.new(message, color: "green", x: 10, y: 10, size: 25)
end
def message
if finished?
"Game Over. Your Score Was: #{@score}. Press 'R' To Restart."
else
"Score: #{@score}"
end
end

Now the score is in the top left, and when the game is over, the “Game Over” message will be displayed.

The next step is to detect when the snake reaches the ball. Then we must increment the score by 1 point and make our snake grow.

In the Snake Class, add these:

def x
head[0]
end
def y
head[1]
end

We want to compare the position of the head of snake with the position of the randomly generated ball. In the game class, add this:

def snake_hit_ball?(x, y)
@ball_x == x && @ball_y == y
end

Then, in the update loop, add:

if game.snake_hit_ball?(snake.x, snake.y)
game.record_hit
snake.grow
end

The “record_hit” and “grow” methods go into their respective classes.

def record_hit
@score += 1
@ball_x = rand(GRID_WIDTH)
@ball_y = rand(GRID_HEIGHT)
end
def grow
@growing = true
end

To make the snake grow, change the “move” method of the snake class to this:

def move
if !@growing
@positions.shift
end
case @direction
when "down"
@positions.push(new_coords(head[0], head[1] + 1))
when "up"
@positions.push(new_coords(head[0], head[1] - 1))
when "left"
@positions.push(new_coords(head[0] - 1, head[1]))
when "right"
@positions.push(new_coords(head[0] + 1, head[1]))
end
@growing = false
end

The snake was initialized with @growing = false. @positions.shift will remove the first element in the positions array, which is the first set of coordinates. That is the tail. Here, we are telling the program to not remove the tail if we are currently growing. Then, at the end of the “move” method, we make @growing equal to false each time the snake moves.

Now we need to detect when the snake crashes into itself. To do this, we can use the ruby .uniq method to compare the length of the @positions array of the snake body with the length of calling the .uniq method on it. If they are equal, then the snake did not crash into itself. If they are not equal, then the snake is occupying a set of coordinates twice. This means the snake crashed into itself, and the game is over.

In the snake class, add:

def hit_itself?
@positions.uniq.length != @positions.length
end

Change the update loop to look like this:

update do
clear
unless game.finished?
snake.move
end
snake.draw
game.draw
if game.snake_hit_ball?(snake.x, snake.y)
game.record_hit
snake.grow
end
if snake.hit_itself?
game.finish
end
end

In the game class, add:

def finish
@finished = true
end
def finished?
@finished
end

We are almost done! The game is now over. All that is left is to make the “Restart” option available to the user. For this, we will use the “:key_down” Event Listener again. Recall that when a key is pressed down, the keyboard event is captured by the Window. So update it like this:

on :key_down do |event|
if ["down", "up", "left", "right"].include?(event.key)
if snake.can_change_direction_to?(event.key)
snake.direction = event.key
end
elsif event.key == 'r'
snake = Snake.new
game = Game.new
end
end

The Snake Game is now complete! A possible additional feature would be to increase the speed of the snake as the snake collects the food. Currently, the speed is the same speed throughout the game.

If you’re looking for a more detailed guide to recreating the Snake Game and reliving all the glory of the late 90s/early 2000s era, check out this youtube tutorial that helped me get started:

https://youtu.be/2UVhYHBT_1o

--

--

Alexandra Pestruyeva

Software Engineer | Full-Stack Web Developer experienced in Ruby & JavaScript based programming