Andy Maleh
Posted on November 4, 2021
I recently got an issue request to build games in Glimmer DSL for LibUI, so I went ahead and built Glimmer Tetris.
Of course, I followed the Glimmer Process in building it, so I released the following version changes of Glimmer DSL for LibUI along the way:
0.2.20:
- Improve examples/tetris.rb with menus, high score dialog, and options
- Prevent examples/tetris.rb
window
from being resized - Support
window
resizable
property (resizable false
means one cannot resizewindow
) - Support calling
window.content_size = [x, y]
as an alternative towindow.set_content_size(x, y)
- Fix issue with hooking
on_content_size_changed
listener towindow
- Fix issue with using
window
content_size
property getter
0.2.19:
- Improve examples/tetris.rb with a score board (indicating next Tetromino, score, level, and lines)
- Add instant down action to examples/tetris.rb upon hitting the space button
0.2.18:
- Support
polygon
(closed figure of lines),polyline
(open figure of lines), andpolybezier
(open figure of beziers) shape keywords to use underpath
- Improve examples/tetris.rb with bevel block 3D look and restarting upon game over
- Update examples/area_gallery.rb to add uses of
polygon
,polyline
, andpolybezier
- Refactor examples/histogram.rb to utilize new
polygon
andpolyline
keywords - Support
area
request_auto_redraw
,pause_auto_redraw
, andresume_auto_redraw
, operations, andauto_redraw_enabled
property.
0.2.17:
- Tetris example - basic version with simple color squares
Screenshots:
Code:
# From: https://github.com/AndyObtiva/glimmer-dsl-libui#tetris
require 'glimmer-dsl-libui'
require_relative 'tetris/model/game'
class Tetris
include Glimmer
BLOCK_SIZE = 25
BEVEL_CONSTANT = 20
COLOR_GRAY = {r: 192, g: 192, b: 192}
def initialize
@game = Model::Game.new
end
def launch
create_gui
register_observers
@game.start!
@main_window.show
end
def create_gui
menu_bar
@main_window = window('Glimmer Tetris') {
content_size Model::Game::PLAYFIELD_WIDTH * BLOCK_SIZE, Model::Game::PLAYFIELD_HEIGHT * BLOCK_SIZE + 98
resizable false
vertical_box {
label { # filler
stretchy false
}
score_board(block_size: BLOCK_SIZE) {
stretchy false
}
@playfield_blocks = playfield(playfield_width: Model::Game::PLAYFIELD_WIDTH, playfield_height: Model::Game::PLAYFIELD_HEIGHT, block_size: BLOCK_SIZE)
}
}
end
def register_observers
Glimmer::DataBinding::Observer.proc do |game_over|
if game_over
@pause_menu_item.enabled = false
show_game_over_dialog
else
@pause_menu_item.enabled = true
start_moving_tetrominos_down
end
end.observe(@game, :game_over)
Model::Game::PLAYFIELD_HEIGHT.times do |row|
Model::Game::PLAYFIELD_WIDTH.times do |column|
Glimmer::DataBinding::Observer.proc do |new_color|
Glimmer::LibUI.queue_main do
color = Glimmer::LibUI.interpret_color(new_color)
block = @playfield_blocks[row][column]
block[:background_square].fill = color
block[:top_bevel_edge].fill = {r: color[:r] + 4*BEVEL_CONSTANT, g: color[:g] + 4*BEVEL_CONSTANT, b: color[:b] + 4*BEVEL_CONSTANT}
block[:right_bevel_edge].fill = {r: color[:r] - BEVEL_CONSTANT, g: color[:g] - BEVEL_CONSTANT, b: color[:b] - BEVEL_CONSTANT}
block[:bottom_bevel_edge].fill = {r: color[:r] - BEVEL_CONSTANT, g: color[:g] - BEVEL_CONSTANT, b: color[:b] - BEVEL_CONSTANT}
block[:left_bevel_edge].fill = {r: color[:r] - BEVEL_CONSTANT, g: color[:g] - BEVEL_CONSTANT, b: color[:b] - BEVEL_CONSTANT}
block[:border_square].stroke = new_color == Model::Block::COLOR_CLEAR ? COLOR_GRAY : color
end
end.observe(@game.playfield[row][column], :color)
end
end
Model::Game::PREVIEW_PLAYFIELD_HEIGHT.times do |row|
Model::Game::PREVIEW_PLAYFIELD_WIDTH.times do |column|
Glimmer::DataBinding::Observer.proc do |new_color|
Glimmer::LibUI.queue_main do
color = Glimmer::LibUI.interpret_color(new_color)
block = @preview_playfield_blocks[row][column]
block[:background_square].fill = color
block[:top_bevel_edge].fill = {r: color[:r] + 4*BEVEL_CONSTANT, g: color[:g] + 4*BEVEL_CONSTANT, b: color[:b] + 4*BEVEL_CONSTANT}
block[:right_bevel_edge].fill = {r: color[:r] - BEVEL_CONSTANT, g: color[:g] - BEVEL_CONSTANT, b: color[:b] - BEVEL_CONSTANT}
block[:bottom_bevel_edge].fill = {r: color[:r] - BEVEL_CONSTANT, g: color[:g] - BEVEL_CONSTANT, b: color[:b] - BEVEL_CONSTANT}
block[:left_bevel_edge].fill = {r: color[:r] - BEVEL_CONSTANT, g: color[:g] - BEVEL_CONSTANT, b: color[:b] - BEVEL_CONSTANT}
block[:border_square].stroke = new_color == Model::Block::COLOR_CLEAR ? COLOR_GRAY : color
end
end.observe(@game.preview_playfield[row][column], :color)
end
end
Glimmer::DataBinding::Observer.proc do |new_score|
Glimmer::LibUI.queue_main do
@score_label.text = new_score.to_s
end
end.observe(@game, :score)
Glimmer::DataBinding::Observer.proc do |new_lines|
Glimmer::LibUI.queue_main do
@lines_label.text = new_lines.to_s
end
end.observe(@game, :lines)
Glimmer::DataBinding::Observer.proc do |new_level|
Glimmer::LibUI.queue_main do
@level_label.text = new_level.to_s
end
end.observe(@game, :level)
end
def menu_bar
menu('Game') {
@pause_menu_item = check_menu_item('Pause') {
enabled false
on_clicked do
@game.paused = @pause_menu_item.checked?
end
}
menu_item('Restart') {
on_clicked do
@game.restart!
end
}
separator_menu_item
menu_item('Exit') {
on_clicked do
exit(0)
end
}
quit_menu_item if OS.mac?
}
menu('View') {
menu_item('Show High Scores') {
on_clicked do
show_high_scores
end
}
menu_item('Clear High Scores') {
on_clicked {
@game.clear_high_scores!
}
}
}
menu('Options') {
radio_menu_item('Instant Down on Up Arrow') {
on_clicked do
@game.instant_down_on_up = true
end
}
radio_menu_item('Rotate Right on Up Arrow') {
on_clicked do
@game.rotate_right_on_up = true
end
}
radio_menu_item('Rotate Left on Up Arrow') {
on_clicked do
@game.rotate_left_on_up = true
end
}
}
menu('Help') {
if OS.mac?
about_menu_item {
on_clicked do
show_about_dialog
end
}
end
menu_item('About') {
on_clicked do
show_about_dialog
end
}
}
end
def playfield(playfield_width: , playfield_height: , block_size: , &extra_content)
blocks = []
vertical_box {
padded false
playfield_height.times.map do |row|
blocks << []
horizontal_box {
padded false
playfield_width.times.map do |column|
blocks.last << block(row: row, column: column, block_size: block_size)
end
}
end
extra_content&.call
}
blocks
end
def block(row: , column: , block_size: , &extra_content)
block = {}
bevel_pixel_size = 0.16 * block_size.to_f
color = Glimmer::LibUI.interpret_color(Model::Block::COLOR_CLEAR)
area {
block[:background_square] = path {
square(0, 0, block_size)
fill color
}
block[:top_bevel_edge] = path {
polygon(0, 0, block_size, 0, block_size - bevel_pixel_size, bevel_pixel_size, bevel_pixel_size, bevel_pixel_size)
fill r: color[:r] + 4*BEVEL_CONSTANT, g: color[:g] + 4*BEVEL_CONSTANT, b: color[:b] + 4*BEVEL_CONSTANT
}
block[:right_bevel_edge] = path {
polygon(block_size, 0, block_size - bevel_pixel_size, bevel_pixel_size, block_size - bevel_pixel_size, block_size - bevel_pixel_size, block_size, block_size)
fill r: color[:r] - BEVEL_CONSTANT, g: color[:g] - BEVEL_CONSTANT, b: color[:b] - BEVEL_CONSTANT
}
block[:bottom_bevel_edge] = path {
polygon(block_size, block_size, 0, block_size, bevel_pixel_size, block_size - bevel_pixel_size, block_size - bevel_pixel_size, block_size - bevel_pixel_size)
fill r: color[:r] - BEVEL_CONSTANT, g: color[:g] - BEVEL_CONSTANT, b: color[:b] - BEVEL_CONSTANT
}
block[:left_bevel_edge] = path {
polygon(0, 0, 0, block_size, bevel_pixel_size, block_size - bevel_pixel_size, bevel_pixel_size, bevel_pixel_size)
fill r: color[:r] - BEVEL_CONSTANT, g: color[:g] - BEVEL_CONSTANT, b: color[:b] - BEVEL_CONSTANT
}
block[:border_square] = path {
square(0, 0, block_size)
stroke COLOR_GRAY
}
on_key_down do |key_event|
case key_event
in ext_key: :down
@game.down!
in key: ' '
@game.down!(instant: true)
in ext_key: :up
case @game.up_arrow_action
when :instant_down
@game.down!(instant: true)
when :rotate_right
@game.rotate!(:right)
when :rotate_left
@game.rotate!(:left)
end
in ext_key: :left
@game.left!
in ext_key: :right
@game.right!
in modifier: :shift
@game.rotate!(:right)
in modifier: :control
@game.rotate!(:left)
else
# Do Nothing
end
end
extra_content&.call
}
block
end
def score_board(block_size: , &extra_content)
vertical_box {
horizontal_box {
label # filler
@preview_playfield_blocks = playfield(playfield_width: Model::Game::PREVIEW_PLAYFIELD_WIDTH, playfield_height: Model::Game::PREVIEW_PLAYFIELD_HEIGHT, block_size: block_size)
label # filler
}
horizontal_box {
label # filler
grid {
stretchy false
label('Score') {
left 0
top 0
halign :fill
}
@score_label = label {
left 0
top 1
halign :center
}
label('Lines') {
left 1
top 0
halign :fill
}
@lines_label = label {
left 1
top 1
halign :center
}
label('Level') {
left 2
top 0
halign :fill
}
@level_label = label {
left 2
top 1
halign :center
}
}
label # filler
}
extra_content&.call
}
end
def start_moving_tetrominos_down
Glimmer::LibUI.timer(@game.delay) do
@game.down! if !@game.game_over? && !@game.paused?
end
end
def show_game_over_dialog
Glimmer::LibUI.queue_main do
msg_box('Game Over!', "Score: #{@game.high_scores.first.score}\nLines: #{@game.high_scores.first.lines}\nLevel: #{@game.high_scores.first.level}")
@game.restart!
end
end
def show_high_scores
Glimmer::LibUI.queue_main do
if @game.high_scores.empty?
high_scores_string = "No games have been scored yet."
else
high_scores_string = @game.high_scores.map do |high_score|
"#{high_score.name} | Score: #{high_score.score} | Lines: #{high_score.lines} | Level: #{high_score.level}"
end.join("\n")
end
msg_box('High Scores', high_scores_string)
end
end
def show_about_dialog
Glimmer::LibUI.queue_main do
msg_box('About', 'Glimmer Tetris - Glimmer DSL for LibUI Example - Copyright (c) 2021 Andy Maleh')
end
end
end
Tetris.new.launch
Happy Glimmering!
💖 💪 🙅 🚩
Andy Maleh
Posted on November 4, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.