Snake Game by Glimmer DSL for LibUI
Tags: ruby, desktop, gui, libui
Published: 2021-11-09 00:31:11

I mentioned in a [previous blog post](https://dev.to/andyobtiva/glimmer-dsl-for-libui-tetris-example-3apn) that I received an [issue request](https://github.com/AndyObtiva/glimmer-dsl-libui/issues/2) to build games in [Glimmer DSL for LibUI](https://github.com/AndyObtiva/glimmer-dsl-libui) as examples.

Three games were built to fully address that issue request: [Tetris](https://github.com/AndyObtiva/glimmer-dsl-libui#tetris), [Tic-Tac-Toe](https://github.com/AndyObtiva/glimmer-dsl-libui#tic-tac-toe), and most recently [Snake](https://github.com/AndyObtiva/glimmer-dsl-libui#snake).

![Snake Screenshot](https://github.com/AndyObtiva/glimmer-dsl-libui/raw/master/images/glimmer-dsl-libui-mac-snake.png)

In fact, [Snake](https://github.com/AndyObtiva/glimmer-dsl-libui#tetris) has been built [test-first](https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/spec/examples/snake/model/game_spec.rb) following the MVP ([Model](https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/examples/snake/model/game.rb) / [View](https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/examples/snake.rb) / [Presenter](https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/examples/snake/presenter/grid.rb)) architectural pattern.

The View code written in [Glimmer DSL for LibUI](https://github.com/AndyObtiva/glimmer-dsl-libui) is very simple and short:

```ruby
# 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](https://github.com/AndyObtiva/glimmer-dsl-libui) Snake example app, which wires everything together.

Here are the game specs ([spec/examples/snake/model/game_spec.rb](https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/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):

```ruby
# 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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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
```

![Game Over Screenshot](https://github.com/AndyObtiva/glimmer-dsl-libui/raw/master/images/glimmer-dsl-libui-mac-snake-game-over.png)

Happy [Glimmering](https://github.com/AndyObtiva/glimmer-dsl-libui)!