Glimmer DSL for LibUI (Prerequisite-Free Ruby Desktop Development Cross-Platform Native GUI Library) versions 0.9.x include support for a new Glimmer Command, Application 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:
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 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 |
---|---|---|
![]() |
![]() |
![]() |
Run Glimmer DSL for LibUI included examples via this command:
glimmer examples
That brings up the Glimmer Meta-Example)
Mac | Windows | Linux |
---|---|---|
![]() |
![]() |
![]() |
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.
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
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
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
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)
}
}
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
...
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)
}
}
...
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.
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.
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:
Post a Comment