Sunday, October 22, 2023

Glimmer DSL for LibUI Scaffolding + Snake Game

Glimmer DSL for LibUI (Prerequisite-Free Ruby Desktop Development Cross-Platform Native GUI Library) versions 0.9.x include support for a new Glimmer CommandApplication Scaffolding, Custom Component Scaffolding, Custom Component Gem Scaffolding, and more. These features greatly improve Software Engineering Productivity when building desktop applications with Glimmer DSL for LibUI. Glimmer Scaffolding could be thought of as the "Desktop Application" equivalent of Rails Scaffolding and the Rails Application Generator.

Application Scaffolding enables automatically generating the directories/files of a new desktop GUI application that follows a cleanly decoupled MVC architecture and can be packaged as a Ruby gem that includes a script for running the app conveniently.

In fact, I ate my own dog food and used the new Application Scaffolding feature to effortlessly scaffold a Glimmer Snake game (code is included near the bottom of this post):

https://github.com/AndyObtiva/glimmer_snake

This is the gemified app edition of the Snake game example that is included in Glimmer DSL for LibUI.

Here is the Glimmer DSL for LibUI Glimmer Command guide (straight out of the project README), which covers all Scaffolding features:

Glimmer Command

The glimmer command allows you to conveniently run applications (glimmer app_path), run examples (glimmer examples), and scaffold applications (glimmer "scaffold[app_name]").

You can bring up usage instructions by running the glimmer command without arguments:

glimmer
Glimmer DSL for LibUI (Prerequisite-Free Ruby Desktop Development Cross-Platform Native GUI Library) - Ruby Gem: glimmer-dsl-libui v0.8.0

Usage: glimmer [--bundler] [--pd] [--quiet] [--debug] [--log-level=VALUE] [[ENV_VAR=VALUE]...] [[-ruby-option]...] (application.rb or task[task_args])

Runs Glimmer applications and tasks.

When applications are specified, they are run using Ruby,
automatically preloading the glimmer-dsl-libui Ruby gem.

Optionally, extra Glimmer options, Ruby options, and/or environment variables may be passed in.

Glimmer options:
- "--bundler=GROUP"   : Activates gems in Bundler default group in Gemfile
- "--pd=BOOLEAN"      : Requires puts_debuggerer to enable pd method
- "--quiet=BOOLEAN"   : Does not announce file path of Glimmer application being launched
- "--debug"           : Displays extra debugging information and enables debug logging
- "--log-level=VALUE" : Sets Glimmer's Ruby logger level ("ERROR" / "WARN" / "INFO" / "DEBUG"; default is none)

Tasks are run via rake. Some tasks take arguments in square brackets (surround with double-quotes if using Zsh).

Available tasks are below (if you do not see any, please add `require 'glimmer/rake_task'` to Rakefile and rerun or run rake -T):

Select a Glimmer task to run: (Press ↑/↓ arrow to move, Enter to select and letters to filter)
‣ glimmer examples                                            # Brings up the Glimmer Meta-Sample app to allow browsing, running, and viewing code of Glimmer samples
  glimmer list:gems:customcontrol[query]                      # List Glimmer custom control gems available at rubygems.org (query is optional) [alt: list:gems:cc]
  glimmer list:gems:customshape[query]                        # List Glimmer custom shape gems available at rubygems.org (query is optional) [alt: list:gems:cs]
  glimmer list:gems:customwindow[query]                       # List Glimmer custom window gems available at rubygems.org (query is optional) [alt: list:gems:cw]
  glimmer list:gems:dsl[query]                                # List Glimmer DSL gems available at rubygems.org (query is optional)
  glimmer run[app_path]                                       # Runs Glimmer app or custom window gem in the current directory, unless app_path is specified, then runs it instead (app_path is optional)
  glimmer scaffold[app_name]                                  # Scaffold Glimmer application directory structure to build a new app
  glimmer scaffold:customcontrol[name,namespace]              # Scaffold Glimmer::UI::CustomControl subclass (part of a view) under app/views (namespace is optional) [alt: scaffold:cc]
  glimmer scaffold:customshape[name,namespace]                # Scaffold Glimmer::UI::CustomShape subclass (part of a view) under app/views (namespace is optional) [alt: scaffold:cs]
  glimmer scaffold:customwindow[name,namespace]               # Scaffold Glimmer::UI::CustomWindow subclass (full window view) under app/views (namespace is optional) [alt: scaffold:cw]
  glimmer scaffold:gem:customcontrol[name,namespace]          # Scaffold Glimmer::UI::CustomControl subclass (part of a view) under its own Ruby gem project (namespace is required) [alt: scaffold:gem:cc]
  glimmer scaffold:gem:customshape[name,namespace]            # Scaffold Glimmer::UI::CustomShape subclass (part of a view) under its own Ruby gem project (namespace is required) [alt: scaffold:gem:cs]
  glimmer scaffold:gem:customwindow[name,namespace]           # Scaffold Glimmer::UI::CustomWindow subclass (full window view) under its own Ruby gem + app project (namespace is required) [alt: scaffold:gem:cw]

On Mac and Linux, it brings up a TUI (Text-based User Interface) for interactive navigation and execution of Glimmer tasks (courtesy of rake-tui).

On Windows and ARM64 machines, it simply lists the available Glimmer tasks at the end (courtsey of rake).

Note: If you encounter an issue running the glimmer command, run bundle exec glimmer instead.

Run Application

Run Glimmer DSL for LibUI applications via this command:

glimmer app_path

For example, from a cloned glimmer-dsl-libui repository:

glimmer examples/basic_window.rb
Mac Windows Linux
glimmer-dsl-libui-mac-basic-window.png glimmer-dsl-libui-windows-basic-window.png glimmer-dsl-libui-linux-basic-window.png

Run Examples

Run Glimmer DSL for LibUI included examples via this command:

glimmer examples

That brings up the Glimmer Meta-Example)

Mac Windows Linux
glimmer-dsl-libui-mac-meta-example.png glimmer-dsl-libui-windows-meta-example.png glimmer-dsl-libui-linux-meta-example.png

Scaffold Application

Application scaffolding enables automatically generating the directories/files of a new desktop GUI application that follows the MVC architecture and can be packaged as a Ruby gem that includes an executable script for running the app conveniently.

Scaffold Glimmer DSL for LibUI application with this command:

glimmer "scaffold[app_name]"

That will automatically generate the general MVC structure of a new Glimmer DSL for LibUI application and launch the application when done.

For example, if we run:

glimmer "scaffold[hello_world]"

The following files are generated and reported by the glimmer command:

Created hello_world/.gitignore
Created hello_world/.ruby-version
Created hello_world/.ruby-gemset
Created hello_world/VERSION
Created hello_world/LICENSE.txt
Created hello_world/Gemfile
Created hello_world/Rakefile
Created hello_world/app/hello_world.rb
Created hello_world/app/hello_world/view/hello_world.rb
Created hello_world/app/hello_world/model/greeting.rb
Created hello_world/icons/windows/Hello World.ico
Created hello_world/icons/macosx/Hello World.icns
Created hello_world/icons/linux/Hello World.png
Created hello_world/app/hello_world/launch.rb
Created hello_world/bin/hello_world

They include a basic Hello, World! application with menus and about/preferences dialogs.

Views live under app/app_name/view (e.g. app/hello_world/view)

Models live under app/app_name/model (e.g. app/hello_world/model)

The application runs automatically once scaffolding is done.

glimmer-dsl-libui-mac-scaffold-app-initial-screen.png

glimmer-dsl-libui-mac-scaffold-app-preferences.png

glimmer-dsl-libui-mac-scaffold-app-changed-greeting.png

glimmer-dsl-libui-mac-scaffold-app-about.png

Once you step into the application directory, you can run it in one of multiple ways:

bin/app_name

For example:

bin/hello_world

Or using the Glimmer generic command for running applications, which will automatically detect the application running script:

glimmer run

glimmer-dsl-libui-mac-scaffold-app-initial-screen.png

The application comes with the juwelier gem for auto-generating an application gem from the app Rakefile and Gemfile configuration (no need to manually declare gems in a gemspec... just use Gemfile normally and juwelier takes care of the rest by generating an app gemspec automatically from Gemfile).

You can package the newly scaffolded app as a Ruby gem by running this command:

glimmer package:gem

Or by using the raw rake command:

rake build

You can generate the application gemspec explicitly if needed with this command (though it is not needed to build the gem):

glimmer package:gemspec

Or by using the raw rake command:

rake gemspec:generate

Once you install the gem (e.g. gem install hello_world), you can simply run the app with its executable script:

app_name

For example:

hello_world

glimmer-dsl-libui-mac-scaffold-app-initial-screen.png

Scaffold Custom Window

When you are in a scaffolded application, you can scaffold a new custom window (a window that you can put anything in to represent a view concept in your application) by running this command:

glimmer scaffold:customwindow[name,namespace]

The name represents the custom window view class name (it can be underscored, and Glimmer will automatically classify it).

The namespace is optional and represents the module that the custom window view class will live under. If left off, the main application class namespace is used (e.g. the top-level HelloWorld class namespace for a hello_world application).

You can also use the shorter cw alias for customwindow:

glimmer scaffold:cw[name,namespace]

For example by running this command under a hello_world application:

glimmer scaffold:cw[greeting_window]

That will generate this class under app/hello_world/view/greeting_window:

class HelloWorld
  module View
    class GreetingWindow
      include Glimmer::LibUI::CustomWindow
    
          
      ## Add options like the following to configure CustomWindow by outside consumers
      #
      # options :title, :background_color
      # option :width, default: 320
      # option :height, default: 240
  
      ## Use before_body block to pre-initialize variables to use in body and
      #  to setup application menu
      #
      # before_body do
      #
      # end
  
      ## Use after_body block to setup observers for controls in body
      #
      # after_body do
      #
      # end
  
      ## Add control content inside custom window body
      ## Top-most control must be a window or another custom window
      #
      body {
        window {
          # Replace example content below with custom window content
          content_size 240, 240
          title 'Hello World'
          
          margined true
          
          label {
            text 'Hello World'
          }
        }
      }
    end
  end
end

When the generated file is required in another view (e.g. require 'hello_world/view/greeting_window'), the custom window keyword greeting_window become available and reusable, like by calling:

greeting_window.show

Here is an example that generates a custom window with a namespace:

glimmer scaffold:cw[train,station]

That will generate this class under app/station/view/train:

module Station
  module View
    class Train
      include Glimmer::LibUI::CustomWindow
    
          
      ## Add options like the following to configure CustomWindow by outside consumers
      #
      # options :title, :background_color
      # option :width, default: 320
      # option :height, default: 240
  
      ## Use before_body block to pre-initialize variables to use in body and
      #  to setup application menu
      #
      # before_body do
      #
      # end
  
      ## Use after_body block to setup observers for controls in body
      #
      # after_body do
      #
      # end
  
      ## Add control content inside custom window body
      ## Top-most control must be a window or another custom window
      #
      body {
        window {
          # Replace example content below with custom window content
          content_size 240, 240
          title 'Station'
          
          margined true
          
          label {
            text 'Station'
          }
        }
      }
    end
  end
end

When that file is required in another view (e.g. require 'station/view/train'), the train keyword becomes available:

train.show

If for whatever reason, you end up with 2 custom window views having the same name with different namespaces, then you can invoke the specific custom window you want by including the Ruby namespace in underscored format separated by double-underscores:

station__view__train.show

Or another train custom window view:

hello_world__view__train.show

Scaffold Custom Control

When you are in a scaffolded application, you can scaffold a new custom control (a control that you can put anything in to represent a view concept in your application) by running this command:

glimmer scaffold:customcontrol[name,namespace]

The name represents the custom control view class name (it can be underscored, and Glimmer will automatically classify it).

The namespace is optional and represents the module that the custom control view class will live under. If left off, the main application class namespace is used (e.g. the top-level HelloWorld class namespace for a hello_world application).

You can also use the shorter cc alias for customcontrol:

glimmer scaffold:cc[name,namespace]

For example by running this command under a hello_world application:

glimmer scaffold:cc[model_form]

That will generate this class under app/hello_world/view/model_form:

class HelloWorld
  module View
    class ModelForm
      include Glimmer::LibUI::CustomControl
  
      ## Add options like the following to configure CustomControl by outside consumers
      #
      # options :custom_text, :background_color
      # option :foreground_color, default: :red
      
      # Replace example options with your own options
      option :model
      option :attributes
  
      ## Use before_body block to pre-initialize variables to use in body
      #
      #
      before_body do
        # Replace example code with your own before_body code
        default_model_attributes = [:first_name, :last_name, :email]
        default_model_class = Struct.new(*default_model_attributes)
        self.model ||= default_model_class.new
        self.attributes ||= default_model_attributes
      end
  
      ## Use after_body block to setup observers for controls in body
      #
      # after_body do
      #
      # end
  
      ## Add control content under custom control body
      ##
      ## If you want to add a window as the top-most control,
      ## consider creating a custom window instead
      ## (Glimmer::LibUI::CustomWindow offers window convenience methods, like show and hide)
      #
      body {
        # Replace example content (model_form custom control) with your own custom control content.
        form {
          attributes.each do |attribute|
            entry { |e|
              label attribute.to_s.underscore.split('_').map(&:capitalize).join(' ')
              text <=> [model, attribute]
            }
          end
        }
      }
  
    end
  end
end

When the generated file is required in another view (e.g. require 'hello_world/view/model_form'), the custom control keyword model_form become available and reusable, like by calling:

window {
  vertical_box {
    label('Form:')
    model_form(model: some_model, attributes: array_of_attributes)
  }
}

Here is an example that generates a custom control with a namespace:

glimmer scaffold:cc[model_form,common]

That will generate this class under app/common/view/model_form:

module Common
  module View
    class ModelForm
      include Glimmer::LibUI::CustomControl
  
      ## Add options like the following to configure CustomControl by outside consumers
      #
      # options :custom_text, :background_color
      # option :foreground_color, default: :red
      
      # Replace example options with your own options
      option :model
      option :attributes
  
      ## Use before_body block to pre-initialize variables to use in body
      #
      #
      before_body do
        # Replace example code with your own before_body code
        default_model_attributes = [:first_name, :last_name, :email]
        default_model_class = Struct.new(*default_model_attributes)
        self.model ||= default_model_class.new
        self.attributes ||= default_model_attributes
      end
  
      ## Use after_body block to setup observers for controls in body
      #
      # after_body do
      #
      # end
  
      ## Add control content under custom control body
      ##
      ## If you want to add a window as the top-most control,
      ## consider creating a custom window instead
      ## (Glimmer::LibUI::CustomWindow offers window convenience methods, like show and hide)
      #
      body {
        # Replace example content (model_form custom control) with your own custom control content.
        form {
          attributes.each do |attribute|
            entry { |e|
              label attribute.to_s.underscore.split('_').map(&:capitalize).join(' ')
              text <=> [model, attribute]
            }
          end
        }
      }
  
    end
  end
end

When that file is required in another view (e.g. require 'common/view/model_form'), the model_form keyword becomes available:

window {
  vertical_box {
    label('Form:')
    model_form(model: some_model, attributes: array_of_attributes)
  }
}

If for whatever reason, you end up with 2 custom control views having the same name with different namespaces, then you can invoke the specific custom control you want by including the Ruby namespace in underscored format separated by double-underscores:

window {
  vertical_box {
    label('Form:')
    common__view__model_form(model: some_model, attributes: array_of_attributes)
  }
}

Or another model_form custom control view:

window {
  vertical_box {
    label('Form:')
    hello_world__view__model_form(model: some_model, attributes: array_of_attributes)
  }
}

Scaffold Custom Window Gem

You can scaffold a Ruby gem around a reusable custom window by running this command:

glimmer scaffold:gem:customwindow[name,namespace]

That will generate a custom window gem project under the naming convention: glimmer-libui-cw-name-namespace

The naming convention helps with discoverability of Ruby gems using the command glimmer list:gems:customwindow[query] (or alias: glimmer list:gems:cw[query]) where filtering query is optional.

The name is the custom window class name, which must not contain dashes by convention (multiple words can be concatenated or can use underscores between them).

The namespace is needed to avoid clashing with other custom window gems that other software engineers might have thought of. It is recommended not to include dashes between words in it by convention yet concatenated words or underscores between them.

Here is a shorter alias for the custom window gem scaffolding command:

glimmer scaffold:gem:cw[name,namespace]

You can package the newly scaffolded project as a Ruby gem by running this command:

glimmer package:gem

Or by using the raw rake command:

rake build

You can generate the application gemspec explicitly if needed with this command (though it is not needed to build the gem):

glimmer package:gemspec

Or by using the raw rake command:

rake gemspec:generate

The project optionally allows you to run the custom window as its own separate app with a executable script (bin/gem_name) to see it, which helps with prototyping it.

But, typically consumers of the gem would include it in their own project, which makes the gem keyword available in the Glimmer GUI DSL anywhere Glimmer. Glimmer::LibUI::Application, Glimmer::LibUI::CustomWindow, or Glimmer::LibUI::CustomControl is mixed.

For example:

require 'glimmer-libui-cw-greeter-acme'

...
greeter.show
...

Scaffold Custom Control Gem

You can scaffold a Ruby gem around a reusable custom control by running this command:

glimmer scaffold:gem:customcontrol[name,namespace]

That will generate a custom control gem project under the naming convention: glimmer-libui-cc-name-namespace

The naming convention helps with discoverability of Ruby gems using the command glimmer list:gems:customcontrol[query] (or alias: glimmer list:gems:cc[query]) where filtering query is optional.

The name is the custom control class name, which must not contain dashes by convention (multiple words can be concatenated or can use underscores between them).

The namespace is needed to avoid clashing with other custom control gems that other software engineers might have thought of. It is recommended not to include dashes between words in it by convention yet concatenated words or underscores between them.

Here is a shorter alias for the custom control gem scaffolding command:

glimmer scaffold:gem:cc[name,namespace]

You can package the newly scaffolded project as a Ruby gem by running this command:

glimmer package:gem

Or by using the raw rake command:

rake build

You can generate the application gemspec explicitly if needed with this command (though it is not needed to build the gem):

glimmer package:gemspec

Or by using the raw rake command:

rake gemspec:generate

Typically, consumers of the gem would include it in their own project, which makes the gem keyword available in the Glimmer GUI DSL anywhere Glimmer. Glimmer::LibUI::Application, Glimmer::LibUI::CustomWindow, or Glimmer::LibUI::CustomControl is mixed.

For example:

require 'glimmer-libui-cc-model_form-acme'

...
window {
  vertical_box {
    label('Form:')
    
    model_form(model: some_model, attributes: some_attributes)
  }
}
...

List Custom Window Gems

Custom window gems are scaffolded to follow the naming convention: glimmer-libui-cw-name-namespace

The naming convention helps with discoverability of Ruby gems using the command:

glimmer list:gems:customwindow[query]

Or by using the shorter alias:

glimmer list:gems:cw[query]

The filtering query is optional.

List Custom Control Gems

Custom control gems are scaffolded to follow the naming convention: glimmer-libui-cw-name-namespace

The naming convention helps with discoverability of Ruby gems using the command:

glimmer list:gems:customcontrol[query]

Or by using the shorter alias:

glimmer list:gems:cc[query]

The filtering query is optional.

List Glimmer DSLs

Glimmer DSLs can be listed with this command:

glimmer list:gems:dsl[query]

The filtering query is optional.

Here are the Glimmer Snake implementation classes that were added on top of the Glimmer DSL for LibUI Application Scaffolding following a cleanly decoupled MVC architecture:

app/glimmer_snake/view/glimmer_snake.rb

# Source: https://github.com/AndyObtiva/glimmer_snake/blob/master/app/glimmer_snake/view/glimmer_snake.rb
require 'glimmer_snake/model/grid'
class GlimmerSnake
module View
class GlimmerSnake
include Glimmer::LibUI::Application
CELL_SIZE = 15
SNAKE_MOVE_DELAY = 0.1
before_body do
@game = Model::Game.new
@grid = Presenter::Grid.new(@game)
@game.start
@keypress_queue = []
end
after_body do
register_observers
end
body {
window {
# data-bind window title to game score, converting it to a title string on read from the model
title <= [@game, :score, on_read: -> (score) {"Snake (Score: #{@game.score})"}]
content_size @game.width * CELL_SIZE, @game.height * CELL_SIZE
resizable false
vertical_box {
padded false
@game.height.times do |row|
horizontal_box {
padded false
@game.width.times do |column|
area {
square(0, 0, CELL_SIZE) {
fill <= [@grid.cells[row][column], :color] # data-bind square fill to grid cell color
}
on_key_down do |area_key_event|
handled = true # assume we will handle the event
if area_key_event[:key] == ' '
@game.toggle_pause
elsif %i[up right down left].include?(area_key_event[:ext_key])
@keypress_queue << area_key_event[:ext_key]
else
handled = false # we won't handle the event after all
end
handled
end
}
end
}
end
}
}
}
def register_observers
observe(@game, :over) 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
Glimmer::LibUI.timer(SNAKE_MOVE_DELAY) do
unless @game.paused? || @game.over?
process_queued_keypress
@game.snake.move
end
end
end
def process_queued_keypress
# key press queue ensures one turn per snake move to avoid a double-turn resulting in instant death (due to snake illogically going back against itself)
key = @keypress_queue.shift
case [@game.snake.head.orientation, 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

app/glimmer_snake/presenter/grid.rb

# Source: https://github.com/AndyObtiva/glimmer_snake/blob/master/app/glimmer_snake/presenter/grid.rb
require 'glimmer'
require_relative '../model/game'
require_relative 'cell'
class GlimmerSnake
module Presenter
class Grid
include Glimmer # used only for observer support (`observe` method only, not GUI)
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
observe(@game.snake, :vertebrae) 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
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

app/glimmer_snake/presenter/cell.rb

# Source: https://github.com/AndyObtiva/glimmer_snake/blob/master/app/glimmer_snake/presenter/cell.rb
class GlimmerSnake
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

app/glimmer_snake/model/game.rb

# Source: https://github.com/AndyObtiva/glimmer_snake/blob/master/app/glimmer_snake/model/game.rb
require 'fileutils'
require_relative 'snake'
require_relative 'apple'
class GlimmerSnake
module Model
class Game
WIDTH_DEFAULT = 20
HEIGHT_DEFAULT = 20
FILE_HIGH_SCORE = File.expand_path(File.join(Dir.home, '.glimmer-snake'))
attr_reader :width, :height
attr_accessor :snake, :apple, :over, :score, :high_score, :paused
alias over? over
alias paused? paused
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
def pause
self.paused = true
end
def resume
self.paused = false
end
def toggle_pause
unless paused?
pause
else
resume
end
end
# inspect is overridden to prevent printing very long stack traces
def inspect
"#{super[0, 75]}... >"
end
end
end
end

app/glimmer_snake/model/snake.rb

# Source: https://github.com/AndyObtiva/glimmer_snake/blob/master/app/glimmer_snake/model/snake.rb
require_relative 'vertebra'
class GlimmerSnake
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 turn_right
head.orientation = RIGHT_TURN_MAP[head.orientation]
end
def turn_left
head.orientation = LEFT_TURN_MAP[head.orientation]
end
def move
create_new_head
remove_old_tail
if detect_collision?
collide_and_die
else
append_new_head
eat_apple if detect_apple?
end
end
def remove_old_tail
@old_tail = tail.dup # save in case of growing and keeping old tail
@vertebrae.delete(tail)
end
def create_new_head
@new_head = head.dup
case 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
end
def append_new_head
@vertebrae.append(@new_head)
end
def detect_collision?
@vertebrae.map {|v| [v.row, v.column]}.include?([@new_head.row, @new_head.column])
end
def collide_and_die
self.collided = true
@game.over = true
end
def detect_apple?
head.row == @game.apple.row && head.column == @game.apple.column
end
def eat_apple
grow
@game.apple.generate
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

app/glimmer_snake/model/vertebra.rb

# Source: https://github.com/AndyObtiva/glimmer_snake/blob/master/app/glimmer_snake/model/vertebra.rb
class GlimmerSnake
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

app/glimmer_snake/model/apple.rb

# Source: https://github.com/AndyObtiva/glimmer_snake/blob/master/app/glimmer_snake/model/apple.rb
class GlimmerSnake
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 is the Change Log of the 0.9.x version releases of Glimmer DSL for LibUI ordered from newest to oldest:

0.9.7

- Scaffold Custom Control Gem via `glimmer scaffold:gem:customcontrol[name,namespace]` (or alias: `glimmer scaffold:gem:cc[name,namespace]`)

- List Custom Control Gems (expected name format: `glimmer-libui-cc-gemname-namespace`) via `glimmer list:gems:customcontrol[query]` (or alias: `glimmer list:gems:cc[query]`)

0.9.6

- Scaffold Custom Window Gem via `glimmer scaffold:gem:customwindow[name,namespace]` (or alias: `glimmer scaffold:gem:cw[name,namespace]`)

- List Custom Window Gems (expected name format: `glimmer-libui-cw-gemname-namespace`) via `glimmer list:gems:customwindow[query]` (or alias: `glimmer list:gems:cw[query]`)

- List Glimmer DSLs via `glimmer list:gems:dsl[query]`

0.9.5

- Scaffold Custom Control via `glimmer scaffold:customcontrol[name,namespace]` (or alias: `glimmer scaffold:cc[name,namespace]`)

0.9.4

- Scaffold Custom Window via `glimmer scaffold:customwindow[name,namespace]` (or alias: `glimmer scaffold:cw[name,namespace]`)

0.9.3

- Application Scaffolding via `glimmer scaffold[app_name]` includes a Model layer

0.9.2

- Add `glimmer` commands `glimmer package:gem`, `glimmer package:gemspec`, and `glimmer package:clean`

0.9.1

- Scaffold an application via Glimmer Command: `glimmer scaffold[app_name]`

- Hide unsupported Scaffolding tasks in Glimmer Command

- Add missing Glimmer Command gem dependencies: `rake`, `rake-tui`, `text-table`, `puts_debuggerer`

0.9.0

- Support `glimmer` command to more conveniently run applications (`glimmer app_path`) and examples (`glimmer examples`)

Happy Glimmering!

No comments: