Glimmer DSL for LibUI Tetris Example

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 resize window)
  • Support calling window.content_size = [x, y] as an alternative to window.set_content_size(x, y)
  • Fix issue with hooking on_content_size_changed listener to window
  • 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), and polybezier (open figure of beziers) shape keywords to use under path
  • 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, and polybezier
  • Refactor examples/histogram.rb to utilize new polygon and polyline keywords
  • Support area request_auto_redraw, pause_auto_redraw, and resume_auto_redraw, operations, and auto_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!

    35

    This website collects cookies to deliver better user experience

    Glimmer DSL for LibUI Tetris Example