Snake game in Ruby [with source code]

Table of contents:

  1. Introduction
  2. About ruby2d
  3. Building a snake game
    • Create Snake class and methods
    • Creating the snake
    • Snake movement and growth
    • Snake self collision and private methods
    • Create Game class and methods
    • Create food
    • Snake eat food and record eat food to update points
    • Make Game over and private method
    • Update game on each frame so it works correctly
    • Add directional movement and restart keys
    • Completed code structure
  4. Conclusion

Introduction

In this article at OpenGenus, we will be showing you how to build a snake game using ruby. Every thing is done in one file so it is easier for you to follow. To start the game you have to run 'ruby snake.rb' from the terminal, you do this once you are inside the folder that leads directly to the file. The keyboard direction arrows are used to control the movement of the snake, every time the snake eats a food it grows and you gain 1 point, the game is over when the snake hits itself so be careful and have fun.

About ruby2d

This is the 'gem' used when making the game. Ruby2d is used to create 2d applications, games and visualizations

Building a snake game

First we start by adding the ruby2d gem to our Gemfile and run bundle install

gem 'ruby2d'

then we open a snake.rb file and require ruby2d inside of the file.

require 'ruby2d'

we then set our background color of our 2d plane and call show. All of our code is going to fall between these 2 lines

set background: 'white'
---- code goes here----
show

Create Snake class and methods

Now we are going to make a snake class and add all the methods we will need for the snake, these methods are initialize, create_snake, move, direction_towards?, x, y, grow and self_hit?. we will also add a few private methods new_coordinates and head

class Snake
    def initialize
    end

    def create_snake
    end

    def move
    end

    def direction_towards?(new_direction)
    end

    def x
    end

    def y
    end

    def grow
    end

    def self_hit?
    end

    private

    def new_coords(x,y)
    end

    def head
    end
end

Creating the snake

Now to add code to these methods, so they do something. In the initialize we add the positions, directions and growing variables. The @positions gives us the position of each block that makes up the body of the snake, starting from the top of the grid and going down about four blocks the snakes stating positon will be in from the left a bit. The @direction tells up the direction in which the snake will move when we start our game and the @growing is set to false so our snake's size doesnt increase at the start of the game. The constant variable GRID_SIZE is added before our class to give us the size of the game plane/area, GRID_WIDTH and GRID_HEIGHT are used in the new_coords method to set the new coordinates when the snake moves.

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

class Snake
attr_writer :direction

    def initialize
        @positions = [[2,0], [2,1], [2,2], [2,3]]
        @direction = 'down'
        @growing = false
    end

We now add a loop to the draw method that will show each square on our game area using the built in Square class and give it a green color.

snake-draw

def create_snake
        @positions.each do |position|
            Square.new(x:position[0] * GRID_SIZE, y:position[1] * GRID_SIZE, size: GRID_SIZE - 1, color: 'green')
        end
    end

Snake movement and growth

Now we add code to the move method, our if statement says if not growing then we shift positions, then we write a case statement to add the directions it will move and push the new coordinates to each direction, at the end of the statement we then set @growing to false again

 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 direction_towards? method takes one argument, we call this new_direction. This method makes it so you can't move the opposite direction to where you are currently going

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

The methods x and y hold the values of head 0 and head 1, while the grow method sets @growing to true when used.

def x
    head[0]
end

def y
    head[1]
end

def grow
  @growing =true  
end

Snake self collision and private methods

The self_hit? method compares the @positions unique length to its current length, the uniq method shows the array without duplicates so if the current positions array has duplicates, it would mean a collision has happened, So if the current positions length is not the same as the unique positions length then we have a collision.

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

Lastly we have the private methods new_coords which we use in the move method to set the new position when the snake moves and the head method returns the last index of the postions array, this is used in the move method as well in place of x and y arguments of the new_coords method.

def new_coords(x,y)
    [x % GRID_WIDTH, y % GRID_HEIGHT]
end
def head
    @positions.last
end

Create Game class and methods

After we have create the snake class we will now create the game class, this holds the mechanics of the game. in this class we have methods to draw the food in the game have the snake eat the food, record when it eats the food and show when its game over, we also have a private method to display certain messages.

class Game
    def initialize
    end

    def draw_food
    end

    def snake_eat_food?
    end

    def record_eat_food
    end

    def game_over
    end

    def game_over?
    end

    private

    def message
    end
end

Create food

Our first method in this class is initialize, her we set our points, the position of the food when the game starts and the game over boolean

def initialize
        @points = 0
        @food_x = rand(GRID_WIDTH)
        @food_y = rand(GRID_HEIGHT)
        @game_over = false
    end

Then we have the draw_food method, we say unless the game is over we draw the food on the screen.The Text.new shows and positions the message for when the game is over or when we are still in play.

def draw_food
        unless game_over?
            Square.new(x: @food_x * GRID_SIZE, y: @food_y * GRID_SIZE, size: GRID_SIZE, color: 'red')
        end
        Text.new(message, color: 'blue', x:10, y:10, size: 20)
    end

Snake eat food and record eat food to update points

Next is the snake_eat_food and record_eat_food methods, snake_eat_food takes two arguments and makes a comparison, this food method will be used a bit later. The record_eat_food method updates the points by 1 each time the snake eats the food and also resets the position of food.

def snake_eat_food?(x, y)
   @food_x == x && @food_y == y 
end

def record_eat_food
    @points += 1
    @food_x = rand(GRID_WIDTH)
    @food_y = rand(GRID_HEIGHT)
end

Make Game over and private method

These next 2 method are pretty simple, the first game_over sets the @game_over variable to true and the next method game_over? calls the @game_over variable, we will see the usage of these two methods in a bit.

def game_over
    @game_over = true
end

def game_over?
    @game_over
end

And finally we have the only private method message, this show the text when the game is in progress or shows a different text when the game is over.

private

def message
    if game_over?
        "Game over, you final points are: #{@points}, Press 'R' to restart."
    else
        "Points: #{@points}"
    end
end

After you create these two classes you can then call them

snake = Snake.new
game = Game.new

Update game on each frame so it works correctly

It wont work just yet, now we have one to call the update method, this method updates our code each frame, here we go. After we call our new snake and new game we then have to use update, first we give the snake movement, we clear the blocks each frame and say unless the game is over the snake will keep moving, then we create the snake and draw the food.

update do
    clear
    unless game.game_over?
        snake.move
    end
    snake.create_snake
    game.draw_food

Our next condition is for what happens when the snake eats the food, it's going to record the food being eaten and also the snake will grow

---previous code---
if game.snake_eat_food?(snake.x, snake.y)
   game.record_eat_food
   snake.grow
end

Our next condition is for when the snake collides with itself, we simply call our game_over method which also triggers the message method

---previous code---
    if snake.self_hit?
       game.game_over 
    end
end

Add directional movement and restart keys

And finally the 'on :key_down' method to recognise when we press the direction arrows on the keyboard, our first condition give the directions and calls the methods if they are pressed and our else condition has 'r' as our key to be pressed in order to reset the game.

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

Completed code structure

And there you have it our snake game built with ruby, at this point you would realise the snake is moving suber fast and that is beacuse it is moving at 60fps so all you have to do is use another ruby2d method to set the fps and you can do this below where you set the background set fps_cap: 10, you can set the speed to what ever you want. Full code is below.

require 'ruby2d'

set background: 'white'
set fps_cap: 10

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

class Snake
    attr_writer :direction

    def initialize
        @positions = [[2,0], [2,1], [2,2], [2,3]]
        @direction = 'down'
        @growing = false
    end

    def create_snake
        @positions.each do |position|
            Square.new(x:position[0] * GRID_SIZE, y:position[1] * GRID_SIZE, size: GRID_SIZE - 1, color: 'green')
        end
    end

    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

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

    def x
        head[0]
    end

    def y
        head[1]
    end

    def grow
      @growing =true  
    end

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

    private

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

    def head
        @positions.last
    end
end

class Game
    def initialize
        @points = 0
        @food_x = rand(GRID_WIDTH)
        @food_y = rand(GRID_HEIGHT)
        @game_over = false
    end

    def draw_food
        unless finish?
            Square.new(x: @food_x * GRID_SIZE, y: @food_y * GRID_SIZE, size: GRID_SIZE, color: 'red')
        end
        Text.new(message, color: 'blue', x:10, y:10, size: 20)
    end

    def snake_eat_food?(x, y)
       @food_x == x && @food_y == y 
    end

    def record_eat_food
        @points += 1
        @food_x = rand(GRID_WIDTH)
        @food_y = rand(GRID_HEIGHT)
    end

    def game_over
        @game_over = true
    end

    def game_over?
        @game_over
    end

    private

    def message
        if game_over?
            "Game over, you final points are: #{@points}, Press 'R' to restart."
        else
            "Points: #{@points}"
        end
    end
end

snake = Snake.new
game = Game.new

update do
    clear
    unless game.game_over?
        snake.move
    end
    snake.create_snake
    game.draw_food

    if game.snake_eat_food?(snake.x, snake.y)
       game.record_eat_food
       snake.grow
    end
    
    if snake.self_hit?
       game.game_over 
    end
end
 on :key_down do |event|
    if['left', 'right', 'up', 'down'].include?(event.key)
        if snake.direction_towards?(event.key)
            snake.direction = event.key
        end
    elsif event.key == 'r'
        snake = Snake.new
        game = Game.new
    end
 end
show

Conclusion

Time to enjoy your game, congrats, this little project showcases the power of ruby and what it can do, time to build a tetris game maybe?