DEV Community

GermƔn Alberto Gimenez Silva
GermƔn Alberto Gimenez Silva

Posted on ā€¢ Originally published at rubystacknews.com on

Recreating the Classic Snake Game in Ruby: A Nostalgic Coding Adventure šŸšŸŽ®

February 27, 2025

Thereā€™s something timeless about the Snake gameā€”a simple yet addictive concept that has captivated players for decades. From old-school Nokia phones to modern programming projects, this classic game continues to inspire developers to flex their coding muscles. Recently, I decided to take a trip down memory lane and recreate the Snake game using Ruby and the curses library. Along the way, I learned some valuable lessons about modular design, debugging, and how not to let your code ā€œsnakeā€ its way into chaos!


Need Expert Ruby on Rails Developers to Elevate Your Project?

Fill out our form! >>


Need Expert Ruby on Rails Developers to Elevate Your Project?


Why Build a Snake Game?

Before we dive into the code, letā€™s address the elephant (or should I say snake?) in the room: Why build a Snake game in 2025?

  1. Nostalgia Meets Learning : The Snake game is a perfect sandbox for practicing fundamental programming concepts like object-oriented design, user input handling, and collision detection.
  2. A Fun Challenge : Itā€™s a great way to test your problem-solving skills while having fun. After all, who doesnā€™t love watching a digital snake grow longer with every bite of food?
  3. Terminal-Based Graphics : Using Rubyā€™s curses library, you can create interactive terminal applicationsā€”a skill thatā€™s both practical and impressive

And letā€™s be honest, itā€™s always satisfying to see your code come to life as a playable game.


How Does the Code Work?

The implementation is divided into four main components:

1. The Food Class

The Food class represents the food that the snake eats. It randomly generates positions within the game window and ensures the food doesnā€™t spawn on the snake itself.

class Food
  DEFAULT_SYMBOL = "*"

  def initialize(window, x = nil, y = nil)
    @window = window
    @x = x || generate_random_x
    @y = y || generate_random_y
    @symbol = DEFAULT_SYMBOL
    @points = 1
  end

  def relocate_without_conflict!(snake)
    loop do
      @x = generate_random_x
      @y = generate_random_y
      break unless snake.include?([@x, @y])
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

2. The Snake Class

The Snake class inherits from Rubyā€™s Array to represent the snakeā€™s body as a list of coordinates. It includes a crashed? method that uses a Set to detect collisions efficiently.

class Snake < Array
  SYMBOL = 'O'

  def crashed?
    visited = Set.new
    each do |position|
      return true if visited.include?(position)
      visited << position
    end
    false
  end
end
Enter fullscreen mode Exit fullscreen mode

3. The TermWindow Class

The TermWindow class manages the game display using the curses library. It draws the snake, food, and game boundaries, providing a clean and interactive interface.

class TermWindow < Curses::Window
  DEFAULT_WIDTH = 60
  DEFAULT_HEIGHT = 20

  def initialize(height = DEFAULT_HEIGHT, width = DEFAULT_WIDTH)
    super(height, width, 0, 0)
    self.box('|', '-')
    self.keypad = true
  end

  def paint_food(food)
    setpos(food.y, food.x)
    addstr(food.symbol)
    refresh
  end
end
Enter fullscreen mode Exit fullscreen mode

4. The Game Class

The Game class ties everything together, handling user input, snake movement, collision detection, and scoring. It also allows the snake to wrap around the screen edges, giving it an endless playground to exploreā€”or crash in.

class Game
  def run
    key = Curses::KEY_RIGHT
    score = 0

    loop do
      # Handle user input, move the snake, check for collisions, etc.
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

How to Run the Game

Running the game is straightforward:

  1. Install the curses Gem :
  2. Save the Code : Copy the complete code into a file, e.g., snake_game.rb.
  3. Run the Game :

Controls are simple: Use the arrow keys (UP, DOWN, LEFT, RIGHT) to control the snake, and press ESC to exit.


Key Features

  • Dynamic Food Placement : Food relocates to a new position after being eaten.
  • Collision Detection : The game ends if the snake collides with itself.
  • Wall Wrapping : The snake can cross screen edges and reappear on the opposite side

A Joke to Keep Things Light

Why did the snake refuse to play hide-and-seek? Because it always ended up hissing under pressure!

But seriously, building this game was a fantastic learning experience. It reinforced my understanding of object-oriented programming, modular design, and the importance of debugging (because trust me, there were plenty of bugs along the way).


Final Thoughts

Recreating the Snake game in Ruby was a delightful blend of nostalgia and technical challenge. Whether youā€™re a seasoned developer or just starting out, I encourage you to try building your own version of this classic game. Who knows? You might discover new ways to improve itā€”like adding obstacles, levels, or even multiplayer mode!

Feel free to share your thoughts or ask questions in the comments. Happy coding, and remember: donā€™t let your projects get tangled up like a snake in a knot! šŸ šŸ’»

Complete source code:

require 'curses'
require 'set'

# Food class representing the food in the game
class Food
  attr_accessor :window, :points, :symbol, :x, :y

  DEFAULT_SYMBOL = "*"

  def initialize(window, x = nil, y = nil)
    @window = window
    @x = x || generate_random_x
    @y = y || generate_random_y
    @symbol = DEFAULT_SYMBOL
    @points = 1
  end

  # Check if the snake has eaten the food
  def has_been_eaten_by?(snake)
    snake[0][0] == @x && snake[0][1] == @y
  end

  # Relocate the food to a new position without conflict
  def relocate_without_conflict!(snake)
    loop do
      @x = generate_random_x
      @y = generate_random_y
      break unless snake.include?([@x, @y])
    end
  end

  private

  def generate_random_x
    rand(1..@window.width - 2)
  end

  def generate_random_y
    rand(1..@window.height - 2)
  end
end

# Snake class representing the snake in the game
class Snake < Array
  SYMBOL = 'O'

  # Check if the snake has crashed into itself
  def crashed?
    visited = Set.new
    each do |position|
      return true if visited.include?(position)
      visited << position
    end
    false
  end
end

# TermWindow class managing the game display
class TermWindow < Curses::Window
  DEFAULT_WIDTH = 60
  DEFAULT_HEIGHT = 20

  attr_accessor :width, :height

  def initialize(height = DEFAULT_HEIGHT, width = DEFAULT_WIDTH)
    super(height, width, 0, 0)
    @width, @height = width, height
    self.box('|', '-')
    self.keypad = true
  end

  # Paint the food on the screen
  def paint_food(food)
    setpos(food.y, food.x)
    addstr(food.symbol)
    refresh
  end

  # Paint the snake on the screen
  def paint_snake(snake)
    setpos(snake[0][1], snake[0][0])
    addstr(Snake::SYMBOL)
    refresh
  end
end

# Game class managing the main game logic
class Game
  INVISIBLE_CURSOR = 0

  attr_reader :window, :snake

  def initialize
    @window = TermWindow.new
    @snake = Snake.new([[4, 10], [4, 9], [4, 8]])
  end

  def create
    Curses.init_screen
    Curses.cbreak
    Curses.noecho
    Curses.curs_set(INVISIBLE_CURSOR)

    run
  ensure
    Curses.close_screen
  end

  private

  def run
    key = Curses::KEY_RIGHT
    score = 0

    food = Food.new(@window)
    food.relocate_without_conflict!(@snake)
    @window.paint_food(food)

    loop do
      # Display score
      @window.setpos(0, (@window.width / 2) - 10)
      @window.addstr("Score: #{score}")

      # Handle user input
      event = @window.getch
      key = event == -1 ? key : event
      key = prev_key(key) unless valid_key?(key)

      # Move the snake
      move_snake(key)

      # Check for crashes
      break if @snake.crashed?

      # Check if food is eaten
      if food.has_been_eaten_by?(@snake)
        score += food.points
        food = Food.new(@window)
        food.relocate_without_conflict!(@snake)
        @window.paint_food(food)
      else
        last_part = @snake.pop
        @window.setpos(last_part[1], last_part[0])
        @window.addstr(' ')
      end

      @window.paint_snake(@snake)
    end

    # Game over screen
    puts "----- GAME OVER -----"
    puts "----- Score: #{score} -----"
  end

  # Prevent reversing direction (e.g., moving left while heading right)
  def prev_key(current_key)
    case current_key
    when Curses::KEY_DOWN then Curses::KEY_UP
    when Curses::KEY_UP then Curses::KEY_DOWN
    when Curses::KEY_LEFT then Curses::KEY_RIGHT
    when Curses::KEY_RIGHT then Curses::KEY_LEFT
    else current_key
    end
  end

  # Validate user input
  def valid_key?(key)
    [Curses::KEY_DOWN, Curses::KEY_UP, Curses::KEY_LEFT, Curses::KEY_RIGHT, 27].include?(key)
  end

  # Move the snake based on the current key
  def move_snake(key)
    case key
    when Curses::KEY_DOWN
      @snake.unshift([@snake[0][0], @snake[0][1] + 1])
    when Curses::KEY_UP
      @snake.unshift([@snake[0][0], @snake[0][1] - 1])
    when Curses::KEY_LEFT
      @snake.unshift([@snake[0][0] - 1, @snake[0][1]])
    when Curses::KEY_RIGHT
      @snake.unshift([@snake[0][0] + 1, @snake[0][1]])
    end

    # Wrap around walls
    @snake[0][0] = 1 if @snake[0][0] == 0
    @snake[0][1] = 1 if @snake[0][1] == 0
    @snake[0][0] = @window.width - 2 if @snake[0][0] == @window.width - 1
    @snake[0][1] = @window.height - 2 if @snake[0][1] == @window.height - 1
  end
end

# Run the game
Game.new.create
Enter fullscreen mode Exit fullscreen mode

Complete source code:

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)