Snake Game by Glimmer DSL for LibUI

I mentioned in a previous blog post that I received an issue request to build games in Glimmer DSL for LibUI as examples.

Three games were built to fully address that issue request: Tetris, Tic-Tac-Toe, and most recently Snake.

In fact, Snake has been built test-first following the MVP (Model / View / Presenter) architectural pattern.

The View code written in Glimmer DSL for LibUI is very simple and short:

# From: https://github.com/AndyObtiva/glimmer-dsl-libui#snake

require 'glimmer-dsl-libui'
require 'glimmer/data_binding/observer'

require_relative 'snake/presenter/grid'

class Snake
  CELL_SIZE = 15
  SNAKE_MOVE_DELAY = 0.1
  include Glimmer

  def initialize
    @game = Model::Game.new
    @grid = Presenter::Grid.new(@game)
    @game.start
    create_gui
    register_observers
  end

  def launch
    @main_window.show
  end

  def register_observers
    @game.height.times do |row|
      @game.width.times do |column|
        Glimmer::DataBinding::Observer.proc do |new_color|
          @cell_grid[row][column].fill = new_color
        end.observe(@grid.cells[row][column], :color)
      end
    end

    Glimmer::DataBinding::Observer.proc do |game_over|
      Glimmer::LibUI.queue_main do
        if game_over
          msg_box('Game Over!', "Score: #{@game.score} | High Score: #{@game.high_score}")
          @game.start
        end
      end
    end.observe(@game, :over)

    Glimmer::LibUI.timer(SNAKE_MOVE_DELAY) do
      unless @game.over?
        @game.snake.move
        @main_window.title = "Glimmer Snake (Score: #{@game.score} | High Score: #{@game.high_score})"
      end
    end
  end

  def create_gui
    @cell_grid = []
    @main_window = window('Glimmer Snake', @game.width * CELL_SIZE, @game.height * CELL_SIZE) {
      resizable false

      vertical_box {
        padded false

        @game.height.times do |row|
          @cell_grid << []
          horizontal_box {
            padded false

            @game.width.times do |column|
              area {
                @cell_grid.last << path {
                  square(0, 0, CELL_SIZE)

                  fill Presenter::Cell::COLOR_CLEAR
                }

                on_key_up do |area_key_event|
                  orientation_and_key = [@game.snake.head.orientation, area_key_event[:ext_key]]
                  case orientation_and_key
                  in [:north, :right] | [:east, :down] | [:south, :left] | [:west, :up]
                    @game.snake.turn_right
                  in [:north, :left] | [:west, :down] | [:south, :right] | [:east, :up]
                    @game.snake.turn_left
                  else
                    # No Op
                  end
                end
              }
            end
          }
        end
      }
    }
  end
end

Snake.new.launch

Basically, the game consists of the following models in the Model layer:

  • Game: general manager of the game including scoring and game over state
  • Snake: handles snake movement including vertebra locations and collided state
  • Vertebra: represents a small part of a snake's body that gets added every time the snake eats an apple
  • Apple: represents the apple that is generated at random locations while the snake is moving

Additionally, the game has the following presenters in the Presenter layer:

  • Grid: contains all colored 40x40 cells that are shown in the View. The Grid basically monitors the Game Snake and Apple locations and updates its cell colors accordingly following the Observer pattern.
  • Cell: represents a single cell with its color that will be shown in the View

Finally, the View layer is simply the Glimmer DSL for LibUI Snake example app, which wires everything together.

Here are the game specs (spec/examples/snake/model/game_spec.rb), which start by gradually testing the movement of a bodyless snake head and then test adding vertabrae bit by bit by eating apples (you may skip if you want to check out the Model and Presenter code included after the specs):

# From: https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/spec/examples/snake/model/game_spec.rb

require 'spec_helper'

require 'examples/snake/model/game'

RSpec.describe Snake::Model::Game do
  it 'has a grid of vertebrae of width of 40 and height of 40' do
    expect(subject).to be_a(Snake::Model::Game)
    expect(subject.width).to eq(40)
    expect(subject.height).to eq(40)
  end

  it 'starts game by generating snake and apple in random locations' do
    subject.start

    expect(subject).to_not be_over
    expect(subject.score).to eq(0)
    expect(subject.snake).to be_a(Snake::Model::Snake)
    expect(subject.snake.length).to eq(1)
    expect(subject.snake.head).to be_a(Snake::Model::Vertebra)
    expect(subject.snake.head).to eq(subject.snake.vertebrae.last)
    expect(subject.snake.head.row).to be_between(0, subject.height)
    expect(subject.snake.head.column).to be_between(0, subject.width)
    expect(Snake::Model::Vertebra::ORIENTATIONS).to include(subject.snake.head.orientation)
    expect(subject.snake.length).to eq(1)

    expect(subject.apple).to be_a(Snake::Model::Apple)
    expect(subject.snake.vertebrae.map {|v| [v.row, v.column]}).to_not include([subject.apple.row, subject.apple.column])
    expect(subject.apple.row).to be_between(0, subject.height)
    expect(subject.apple.column).to be_between(0, subject.width)
  end

  it 'moves snake of length 1 east without going through a wall' do
    direction = :east
    subject.start

    subject.snake.generate(initial_row: 0, initial_column: 0, initial_orientation: direction)
    expect(subject.snake.head.row).to eq(0)
    expect(subject.snake.head.column).to eq(0)
    expect(subject.snake.head.orientation).to eq(direction)
    subject.apple.generate(initial_row: 20, initial_column: 20)
    expect(subject.apple.row).to eq(20)
    expect(subject.apple.column).to eq(20)

    subject.snake.move

    expect(subject.snake.length).to eq(1)
    expect(subject.snake.head.row).to eq(0)
    expect(subject.snake.head.column).to eq(1)
  end

  it 'moves snake of length 1 east going through a wall' do
    direction = :east
    subject.start

    subject.snake.generate(initial_row: 0, initial_column: 39, initial_orientation: direction)
    expect(subject.snake.head.row).to eq(0)
    expect(subject.snake.head.column).to eq(39)
    expect(subject.snake.head.orientation).to eq(direction)
    subject.apple.generate(initial_row: 20, initial_column: 20)
    expect(subject.apple.row).to eq(20)
    expect(subject.apple.column).to eq(20)

    subject.snake.move

    expect(subject.snake.length).to eq(1)
    expect(subject.snake.head.row).to eq(0)
    expect(subject.snake.head.column).to eq(0)
  end

  it 'moves snake of length 1 west without going through a wall' do
    direction = :west
    subject.start

    subject.snake.generate(initial_row: 0, initial_column: 39, initial_orientation: direction)
    expect(subject.snake.head.row).to eq(0)
    expect(subject.snake.head.column).to eq(39)
    expect(subject.snake.head.orientation).to eq(direction)
    subject.apple.generate(initial_row: 20, initial_column: 20)
    expect(subject.apple.row).to eq(20)
    expect(subject.apple.column).to eq(20)

    subject.snake.move

    expect(subject.snake.length).to eq(1)
    expect(subject.snake.head.row).to eq(0)
    expect(subject.snake.head.column).to eq(38)
  end

  it 'moves snake of length 1 west going through a wall' do
    direction = :west
    subject.start

    subject.snake.generate(initial_row: 0, initial_column: 0, initial_orientation: direction)
    expect(subject.snake.head.row).to eq(0)
    expect(subject.snake.head.column).to eq(0)
    expect(subject.snake.head.orientation).to eq(direction)
    subject.apple.generate(initial_row: 20, initial_column: 20)
    expect(subject.apple.row).to eq(20)
    expect(subject.apple.column).to eq(20)

    subject.snake.move

    expect(subject.snake.length).to eq(1)
    expect(subject.snake.head.row).to eq(0)
    expect(subject.snake.head.column).to eq(39)
  end

  it 'moves snake of length 1 south without going through a wall' do
    direction = :south
    subject.start

    subject.snake.generate(initial_row: 0, initial_column: 0, initial_orientation: direction)
    expect(subject.snake.head.row).to eq(0)
    expect(subject.snake.head.column).to eq(0)
    expect(subject.snake.head.orientation).to eq(direction)
    subject.apple.generate(initial_row: 20, initial_column: 20)
    expect(subject.apple.row).to eq(20)
    expect(subject.apple.column).to eq(20)

    subject.snake.move

    expect(subject.snake.length).to eq(1)
    expect(subject.snake.head.row).to eq(1)
    expect(subject.snake.head.column).to eq(0)
  end

  it 'moves snake of length 1 south going through a wall' do
    direction = :south
    subject.start

    subject.snake.generate(initial_row: 39, initial_column: 0, initial_orientation: direction)
    expect(subject.snake.head.row).to eq(39)
    expect(subject.snake.head.column).to eq(0)
    expect(subject.snake.head.orientation).to eq(direction)
    subject.apple.generate(initial_row: 20, initial_column: 20)
    expect(subject.apple.row).to eq(20)
    expect(subject.apple.column).to eq(20)

    subject.snake.move

    expect(subject.snake.length).to eq(1)
    expect(subject.snake.head.row).to eq(0)
    expect(subject.snake.head.column).to eq(0)
  end

  it 'moves snake of length 1 north without going through a wall' do
    direction = :north
    subject.start

    subject.snake.generate(initial_row: 39, initial_column: 0, initial_orientation: direction)
    expect(subject.snake.head.row).to eq(39)
    expect(subject.snake.head.column).to eq(0)
    expect(subject.snake.head.orientation).to eq(direction)
    subject.apple.generate(initial_row: 20, initial_column: 20)
    expect(subject.apple.row).to eq(20)
    expect(subject.apple.column).to eq(20)

    subject.snake.move

    expect(subject.snake.length).to eq(1)
    expect(subject.snake.head.row).to eq(38)
    expect(subject.snake.head.column).to eq(0)
  end

  it 'moves snake of length 1 north going through a wall' do
    direction = :north
    subject.start

    subject.snake.generate(initial_row: 0, initial_column: 0, initial_orientation: direction)
    expect(subject.snake.head.row).to eq(0)
    expect(subject.snake.head.column).to eq(0)
    expect(subject.snake.head.orientation).to eq(direction)
    subject.apple.generate(initial_row: 20, initial_column: 20)
    expect(subject.apple.row).to eq(20)
    expect(subject.apple.column).to eq(20)

    subject.snake.move

    expect(subject.snake.length).to eq(1)
    expect(subject.snake.head.row).to eq(39)
    expect(subject.snake.head.column).to eq(0)
  end

  it 'starts snake going east, moves, turns right south, and moves south' do
    direction = :east
    subject.start

    subject.snake.generate(initial_row: 0, initial_column: 0, initial_orientation: direction)
    subject.apple.generate(initial_row: 20, initial_column: 20)

    new_direction = :south
    subject.snake.move
    subject.snake.turn_right
    expect(subject.snake.head.orientation).to eq(new_direction)
    subject.snake.move

    expect(subject.snake.length).to eq(1)
    expect(subject.snake.head.row).to eq(1)
    expect(subject.snake.head.column).to eq(1)
    expect(subject.snake.head.orientation).to eq(new_direction)
  end

  it 'starts snake going west, moves, turns right north, and moves south' do
    direction = :west
    subject.start

    subject.snake.generate(initial_row: 39, initial_column: 39, initial_orientation: direction)
    subject.apple.generate(initial_row: 20, initial_column: 20)

    new_direction = :north
    subject.snake.move
    subject.snake.turn_right
    expect(subject.snake.head.orientation).to eq(new_direction)
    subject.snake.move

    expect(subject.snake.length).to eq(1)
    expect(subject.snake.head.row).to eq(38)
    expect(subject.snake.head.column).to eq(38)
    expect(subject.snake.head.orientation).to eq(new_direction)
  end

  it 'starts snake going south, moves, turns right west, and moves south' do
    direction = :south
    subject.start

    subject.snake.generate(initial_row: 0, initial_column: 39, initial_orientation: direction)
    subject.apple.generate(initial_row: 20, initial_column: 20)

    new_direction = :west
    subject.snake.move
    subject.snake.turn_right
    expect(subject.snake.head.orientation).to eq(new_direction)
    subject.snake.move

    expect(subject.snake.length).to eq(1)
    expect(subject.snake.head.row).to eq(1)
    expect(subject.snake.head.column).to eq(38)
    expect(subject.snake.head.orientation).to eq(new_direction)
  end

  it 'starts snake going north, moves, turns right east, and moves south' do
    direction = :north
    subject.start

    subject.snake.generate(initial_row: 39, initial_column: 0, initial_orientation: direction)
    subject.apple.generate(initial_row: 20, initial_column: 20)

    new_direction = :east
    subject.snake.move
    subject.snake.turn_right
    expect(subject.snake.head.orientation).to eq(new_direction)
    subject.snake.move

    expect(subject.snake.length).to eq(1)
    expect(subject.snake.head.row).to eq(38)
    expect(subject.snake.head.column).to eq(1)
    expect(subject.snake.head.orientation).to eq(new_direction)
  end

  it 'starts snake going east, moves, turns left north, and moves south' do
    direction = :east
    subject.start

    subject.snake.generate(initial_row: 39, initial_column: 0, initial_orientation: direction)
    subject.apple.generate(initial_row: 20, initial_column: 20)

    new_direction = :north
    subject.snake.move
    subject.snake.turn_left
    expect(subject.snake.head.orientation).to eq(new_direction)
    subject.snake.move

    expect(subject.snake.length).to eq(1)
    expect(subject.snake.head.row).to eq(38)
    expect(subject.snake.head.column).to eq(1)
    expect(subject.snake.head.orientation).to eq(new_direction)
  end

  it 'starts snake going west, moves, turns left south, and moves south' do
    direction = :west
    subject.start

    subject.snake.generate(initial_row: 0, initial_column: 39, initial_orientation: direction)
    subject.apple.generate(initial_row: 20, initial_column: 20)

    new_direction = :south
    subject.snake.move
    subject.snake.turn_left
    expect(subject.snake.head.orientation).to eq(new_direction)
    subject.snake.move

    expect(subject.snake.length).to eq(1)
    expect(subject.snake.head.row).to eq(1)
    expect(subject.snake.head.column).to eq(38)
    expect(subject.snake.head.orientation).to eq(new_direction)
  end

  it 'starts snake going south, moves, turns left east, and moves south' do
    direction = :south
    subject.start

    subject.snake.generate(initial_row: 0, initial_column: 0, initial_orientation: direction)
    subject.apple.generate(initial_row: 20, initial_column: 20)

    new_direction = :east
    subject.snake.move
    subject.snake.turn_left
    expect(subject.snake.head.orientation).to eq(new_direction)
    subject.snake.move

    expect(subject.snake.length).to eq(1)
    expect(subject.snake.head.row).to eq(1)
    expect(subject.snake.head.column).to eq(1)
    expect(subject.snake.head.orientation).to eq(new_direction)
  end

  it 'starts snake going north, moves, turns left west, and moves south' do
    direction = :north
    subject.start

    subject.snake.generate(initial_row: 39, initial_column: 39, initial_orientation: direction)
    subject.apple.generate(initial_row: 20, initial_column: 20)

    new_direction = :west
    subject.snake.move
    subject.snake.turn_left
    expect(subject.snake.head.orientation).to eq(new_direction)
    subject.snake.move

    expect(subject.snake.length).to eq(1)
    expect(subject.snake.head.row).to eq(38)
    expect(subject.snake.head.column).to eq(38)
    expect(subject.snake.head.orientation).to eq(new_direction)
  end

  it 'starts snake going east, moves, turns right south, and eats apple while moving south' do
    direction = :east
    subject.start

    subject.snake.generate(initial_row: 0, initial_column: 0, initial_orientation: direction)
    subject.apple.generate(initial_row: 1, initial_column: 1)

    new_direction = :south
    subject.snake.move
    subject.snake.turn_right
    subject.snake.move

    expect(subject.snake.length).to eq(2)
    expect(subject.snake.vertebrae[0].row).to eq(0)
    expect(subject.snake.vertebrae[0].column).to eq(1)
    expect(subject.snake.vertebrae[0].orientation).to eq(new_direction)
    expect(subject.snake.vertebrae[1].row).to eq(1)
    expect(subject.snake.vertebrae[1].column).to eq(1)
    expect(subject.snake.vertebrae[1].orientation).to eq(new_direction)
  end

  it 'starts snake going east, moves, turns right south, eats apple while moving south, turns left, eats apple while moving east' do
    direction = :east
    subject.start

    subject.snake.generate(initial_row: 0, initial_column: 0, initial_orientation: direction)
    subject.apple.generate(initial_row: 1, initial_column: 1)

    subject.snake.move
    subject.snake.turn_right
    subject.snake.move # eats apple
    subject.apple.generate(initial_row: 1, initial_column: 2)
    subject.snake.turn_left
    subject.snake.move # eats apple

    expect(subject.snake.length).to eq(3)
    expect(subject.snake.vertebrae[0].row).to eq(0)
    expect(subject.snake.vertebrae[0].column).to eq(1)
    expect(subject.snake.vertebrae[0].orientation).to eq(:south)
    expect(subject.snake.vertebrae[1].row).to eq(1)
    expect(subject.snake.vertebrae[1].column).to eq(1)
    expect(subject.snake.vertebrae[1].orientation).to eq(:east)
    expect(subject.snake.vertebrae[2].row).to eq(1)
    expect(subject.snake.vertebrae[2].column).to eq(2)
    expect(subject.snake.vertebrae[2].orientation).to eq(:east)
  end

  it 'starts snake going east, moves, turns right south, eats apple while moving south, turns left, eats apple while moving east, turns right, moves south' do
    direction = :east
    subject.start

    subject.snake.generate(initial_row: 0, initial_column: 0, initial_orientation: direction)
    subject.apple.generate(initial_row: 1, initial_column: 1)

    subject.snake.move
    subject.snake.turn_right
    subject.snake.move # eats apple
    subject.apple.generate(initial_row: 1, initial_column: 2)
    subject.snake.turn_left
    subject.snake.move # eats apple
    subject.apple.generate(initial_row: 20, initial_column: 20)
    subject.snake.turn_right
    subject.snake.move

    expect(subject.snake.length).to eq(3)
    expect(subject.snake.vertebrae[0].row).to eq(1)
    expect(subject.snake.vertebrae[0].column).to eq(1)
    expect(subject.snake.vertebrae[0].orientation).to eq(:east)
    expect(subject.snake.vertebrae[1].row).to eq(1)
    expect(subject.snake.vertebrae[1].column).to eq(2)
    expect(subject.snake.vertebrae[1].orientation).to eq(:south)
    expect(subject.snake.vertebrae[2].row).to eq(2)
    expect(subject.snake.vertebrae[2].column).to eq(2)
    expect(subject.snake.vertebrae[2].orientation).to eq(:south)
  end

  it 'starts snake going east, moves, turns right south, eats apple while moving south, turns left, eats apple while moving east, turns left, eats apple while moving north, turns left, collides while moving west and game is over' do
    direction = :east
    subject.start

    subject.snake.generate(initial_row: 0, initial_column: 0, initial_orientation: direction)
    subject.apple.generate(initial_row: 1, initial_column: 1)

    subject.snake.move # 0, 1
    subject.snake.turn_right
    subject.snake.move # 1, 1 eats apple
    subject.apple.generate(initial_row: 1, initial_column: 2)
    subject.snake.turn_left
    subject.snake.move # 1, 2 eats apple
    subject.apple.generate(initial_row: 1, initial_column: 3)
    subject.snake.move # 1, 3 eats apple
    subject.apple.generate(initial_row: 1, initial_column: 4)
    subject.snake.move # 1, 4 eats apple
    subject.snake.turn_left
    subject.snake.move # 0, 4
    subject.snake.turn_left
    subject.snake.move # 0, 3
    subject.snake.turn_left
    subject.snake.move # 1, 3 (collision)

    expect(subject).to be_over
    expect(subject.score).to eq(50 * 4)
    expect(subject.snake).to be_collided
    expect(subject.snake.length).to eq(5)
    expect(subject.snake.vertebrae[0].row).to eq(1)
    expect(subject.snake.vertebrae[0].column).to eq(2)
    expect(subject.snake.vertebrae[0].orientation).to eq(:east)
    expect(subject.snake.vertebrae[1].row).to eq(1)
    expect(subject.snake.vertebrae[1].column).to eq(3)
    expect(subject.snake.vertebrae[1].orientation).to eq(:east)
    expect(subject.snake.vertebrae[2].row).to eq(1)
    expect(subject.snake.vertebrae[2].column).to eq(4)
    expect(subject.snake.vertebrae[2].orientation).to eq(:north)
    expect(subject.snake.vertebrae[3].row).to eq(0)
    expect(subject.snake.vertebrae[3].column).to eq(4)
    expect(subject.snake.vertebrae[3].orientation).to eq(:west)
    expect(subject.snake.vertebrae[4].row).to eq(0)
    expect(subject.snake.vertebrae[4].column).to eq(3)
    expect(subject.snake.vertebrae[4].orientation).to eq(:south)
  end
end

Here are the Models:

Game:

require 'fileutils'

require_relative 'snake'
require_relative 'apple'

class Snake
  module Model
    class Game
      WIDTH_DEFAULT = 40
      HEIGHT_DEFAULT = 40
      FILE_HIGH_SCORE = File.expand_path(File.join(Dir.home, '.glimmer-snake'))

      attr_reader :width, :height
      attr_accessor :snake, :apple, :over, :score, :high_score
      alias over? over

      def initialize(width = WIDTH_DEFAULT, height = HEIGHT_DEFAULT)
        @width = width
        @height = height
        @snake = Snake.new(self)
        @apple = Apple.new(self)
        FileUtils.touch(FILE_HIGH_SCORE)
        @high_score = File.read(FILE_HIGH_SCORE).to_i rescue 0
      end

      def score=(new_score)
        @score = new_score
        self.high_score = @score if @score > @high_score
      end

      def high_score=(new_high_score)
        @high_score = new_high_score
        File.write(FILE_HIGH_SCORE, @high_score.to_s)
      rescue => e
        puts e.full_message
      end

      def start
        self.over = false
        self.score = 0
        self.snake.generate
        self.apple.generate
      end

      # inspect is overridden to prevent printing very long stack traces
      def inspect
        "#{super[0, 75]}... >"
      end
    end
  end
end

Snake:

require_relative 'vertebra'

class Snake
  module Model
    class Snake
      SCORE_EAT_APPLE = 50
      RIGHT_TURN_MAP = {
        north: :east,
        east: :south,
        south: :west,
        west: :north
      }
      LEFT_TURN_MAP = RIGHT_TURN_MAP.invert

      attr_accessor :collided
      alias collided? collided

      attr_reader :game
      # vertebrae and joins are ordered from tail to head
      attr_accessor :vertebrae

      def initialize(game)
        @game = game
      end

      # generates a new snake location and orientation from scratch or via dependency injection of what head_cell and orientation are (for testing purposes)
      def generate(initial_row: nil, initial_column: nil, initial_orientation: nil)
        self.collided = false
        initial_vertebra = Vertebra.new(snake: self, row: initial_row, column: initial_column, orientation: initial_orientation)
        self.vertebrae = [initial_vertebra]
      end

      def length
        @vertebrae.length
      end

      def head
        @vertebrae.last
      end

      def tail
        @vertebrae.first
      end

      def remove
        self.vertebrae.clear
        self.joins.clear
      end

      def move
        @old_tail = tail.dup
        @new_head = head.dup
        case @new_head.orientation
        when :east
          @new_head.column = (@new_head.column + 1) % @game.width
        when :west
          @new_head.column = (@new_head.column - 1) % @game.width
        when :south
          @new_head.row = (@new_head.row + 1) % @game.height
        when :north
          @new_head.row = (@new_head.row - 1) % @game.height
        end
        if @vertebrae.map {|v| [v.row, v.column]}.include?([@new_head.row, @new_head.column])
          self.collided = true
          @game.over = true
        else
          @vertebrae.append(@new_head)
          @vertebrae.delete(tail)
          if head.row == @game.apple.row && head.column == @game.apple.column
            grow
            @game.apple.generate
          end
        end
      end

      def turn_right
        head.orientation = RIGHT_TURN_MAP[head.orientation]
      end

      def turn_left
        head.orientation = LEFT_TURN_MAP[head.orientation]
      end

      def grow
        @game.score += SCORE_EAT_APPLE
        @vertebrae.prepend(@old_tail)
      end

      # inspect is overridden to prevent printing very long stack traces
      def inspect
        "#{super[0, 150]}... >"
      end
    end
  end
end

Vertebra:

class Snake
  module Model
    class Vertebra
      ORIENTATIONS = %i[north east south west]
      # orientation is needed for snake occuppied cells (but not apple cells)
      attr_reader :snake
      attr_accessor :row, :column, :orientation

      def initialize(snake: , row: , column: , orientation: )
        @row = row || rand(snake.game.height)
        @column = column || rand(snake.game.width)
        @orientation = orientation || ORIENTATIONS.sample
        @snake = snake
      end

      # inspect is overridden to prevent printing very long stack traces
      def inspect
        "#{super[0, 150]}... >"
      end
    end
  end
end

Apple:

class Snake
  module Model
    class Apple
      attr_reader :game
      attr_accessor :row, :column

      def initialize(game)
        @game = game
      end

      # generates a new location from scratch or via dependency injection of what cell is (for testing purposes)
      def generate(initial_row: nil, initial_column: nil)
        if initial_row && initial_column
          self.row, self.column = initial_row, initial_column
        else
          self.row, self.column = @game.height.times.zip(@game.width.times).reject do |row, column|
            @game.snake.vertebrae.map {|v| [v.row, v.column]}.include?([row, column])
          end.sample
        end
      end

      def remove
        self.row = nil
        self.column = nil
      end

      # inspect is overridden to prevent printing very long stack traces
      def inspect
        "#{super[0, 120]}... >"
      end
    end
  end
end

Here are the Presenters:

Grid:

require 'glimmer/data_binding/observer'
require_relative '../model/game'
require_relative 'cell'

class Snake
  module Presenter
    class Grid
      attr_reader :game, :cells

      def initialize(game = Model::Game.new)
        @game = game
        @cells = @game.height.times.map do |row|
          @game.width.times.map do |column|
            Cell.new(grid: self, row: row, column: column)
          end
        end
        Glimmer::DataBinding::Observer.proc do |new_vertebrae|
          occupied_snake_positions = @game.snake.vertebrae.map {|v| [v.row, v.column]}
          @cells.each_with_index do |row_cells, row|
            row_cells.each_with_index do |cell, column|
              if [@game.apple.row, @game.apple.column] == [row, column]
                cell.color = Cell::COLOR_APPLE
              elsif occupied_snake_positions.include?([row, column])
                cell.color = Cell::COLOR_SNAKE
              else
                cell.clear
              end
            end
          end
        end.observe(@game.snake, :vertebrae)
      end

      def clear
        @cells.each do |row_cells|
          row_cells.each do |cell|
            cell.clear
          end
        end
      end

      # inspect is overridden to prevent printing very long stack traces
      def inspect
        "#{super[0, 75]}... >"
      end
    end
  end
end

Cell:

class Snake
  module Presenter
    class Cell
      COLOR_CLEAR = :white
      COLOR_SNAKE = :green
      COLOR_APPLE = :red

      attr_reader :row, :column, :grid
      attr_accessor :color

      def initialize(grid: ,row: ,column: )
        @row = row
        @column = column
        @grid = grid
      end

      def clear
        self.color = COLOR_CLEAR unless color == COLOR_CLEAR
      end

      # inspect is overridden to prevent printing very long stack traces
      def inspect
        "#{super[0, 150]}... >"
      end
    end
  end
end

Happy Glimmering!

31