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!

    43

    This website collects cookies to deliver better user experience

    Snake Game by Glimmer DSL for LibUI