You [Gerald Bauer¹] have been permanently banned [for life] from participating in r/ruby (because of your writing off / outside of r/ruby). I do not see your participation adding anything to this [ruby] community.

-- Richard Schneeman (r/ruby mod and fanatic illiberal ultra leftie on a cancel culture mission)

¹: I know. Who cares? Who is this Gerald Bauer anyway. A random nobody for sure. It just happens that I am the admin among other things of Planet Ruby.

Case Studies of Code of Conduct "Cancel Culture" Out-Of-Control Power Abuse - Ruby - A Call for Tolerance On Ruby-Talk Results In Ban On Reddit Ruby

Update (August, 2022) - A Call for More Tolerance And Call For No-Ban Policy Results In Ban On Ruby-Talk (With No Reason Given)

>  I just banned gerald.bauer@gmail.com.
>
>  -- SHIBATA Hiroshi
>
>> THANK YOU
>> 
>>  -- Ryan Davis
>>
>>
>> My full support to moderators.
>>
>> -- Xavier Noria
>> 
>> My full support to moderators.
>>
>>  -- Carlo E. Prelz
>>
>>  That's fun.
>>
>>  -- Alice

Read the full story »


« Ruby Glimmer Days 2021, January 26th to January 29th - 4 Days of Ruby (Desktop) Gems

Day 7 - glimmer_tetris Gem - Glimmer Tetris - High Scores, Menu, & Icon - Show Others How Good You Are!

Written by AndyObtiva Andy Maleh

Software Engineering Expert from Montreal, Quebec. Creator of Glimmer and Abstract Feature Branch. Speaker at RailsConf, RubyConf, AgileConf, EclipseCon, EclipseWorld. Master in Software Engineering, DePaul University, Chicago. Blogs at Code Mastery Takes Commitment To Bold Coding Adventures. Snowboarder and Drummer.

Goal

Show a High Score Dialog at the end of a game with the ability to enter the player name.

Provide the ability to show the High Score Dialog at any point during the game too, pausing the game until the dialog is closed.

Add a top menu bar with menus for various functions such as: Game, View, Options, and Help

Finally, generate an app icon with Glimmer GUI DSL syntax, save it to a file, and set it as the game app icon.

High Score Dialog

Glimmer Tetris High Score Dialog

Keep in mind that dialog is a form of a shell. Although it has its own keyword dialog in Glimmer DSL for SWT for convenience, behind the scenes, it is a shell with the :dialog_trim SWT style.

The point in this is you need to run this command in the terminal to generate a Custom Shell (not Custom Widget):

glimmer "scaffold:customshell[high_score_dialog]"

Open HighScoreDialog under app/glimmer_tetris/view/high_score_dialog.rb, and delete all comments, options, and the body {} block.

Replace them with the following:

      options :parent_shell, :game
      
      after_body {
        @game_over_observer = observe(game, :game_over) do |game_over|
          close if !game_over
        end
      }
      
      body {
        dialog(parent_shell) {
          row_layout {
            type :vertical
            center true
          }
          text 'Tetris'
          
          label(:center) {
            text bind(game, :game_over) {|game_over| game_over ? 'Game Over!' : 'High Scores'}
            font name: FONT_NAME, height: FONT_TITLE_HEIGHT, style: FONT_TITLE_STYLE
          }
          @high_score_table = table {
            layout_data {
              height 100
            }
            
            table_column {
              text 'Name'
            }
            table_column {
              text 'Score'
            }
            table_column {
              text 'Lines'
            }
            table_column {
              text 'Level'
            }
            
            items bind(game, :high_scores, read_only_sort: true), column_properties(:name, :score, :lines, :level)
          }
          composite {
            row_layout :horizontal
                        
            @play_close_button = button {
              text bind(game, :game_over) {|game_over| game_over ? 'Play Again?' : 'Close'}
              focus true # initial focus
              
              on_widget_selected {
                async_exec { close }
                game.paused = @game_paused
                game.restart! if game.game_over?
              }
            }
          }
          
          on_swt_show {
            @game_paused = game.paused?
            game.paused = true
            if game.game_over? && game.added_high_score?
              game.added_high_score = false
              game.save_high_scores!
              @high_score_table.edit_table_item(
                @high_score_table.items.first, # row item
                0, # column
                write_on_cancel: true,
                after_write: -> {
                  game.save_high_scores!
                  @play_close_button.set_focus
                },
              )
            end
          }
          
          on_shell_closed {
            # guard is needed because there is an observer in Tetris closing on
            # game.show_high_scores change, which gets set below
            unless @closing
              @closing = true
              @high_score_table.cancel_edit!
              game.paused = @game_paused
              game.show_high_scores = false
            else
              @closing = false
            end
          }
          
          on_widget_disposed {
            @game_over_observer.deregister
          }
        }
      }

Now, go back to AppView and replace the after_body {} block code with the following:

      observe(@game, :game_over) do |game_over|
        if game_over
          show_high_score_dialog
        else
          start_moving_tetrominos_down
        end
      end
      observe(@game, :show_high_scores) do |show_high_scores|
        if show_high_scores
          show_high_score_dialog
        else
          @high_score_dialog.close unless @high_score_dialog.nil? || @high_score_dialog.disposed? || !@high_score_dialog.visible?
        end
      end
      @game.start!

Next, delete the show_game_over_message_box method in AppView and replace it with the following:

      def show_high_score_dialog
        return if @high_score_dialog&.visible?
        @high_score_dialog = high_score_dialog(parent_shell: body_root, game: @game) if @high_score_dialog.nil? || @high_score_dialog.disposed?
        @high_score_dialog.show
      end

Do not forget to add this require statement above the top class definition:

require_relative 'high_score_dialog'

Run:

glimmer run

Finish a game and you should get the High Score Dialog!

Glimmer Tetris High Score Dialog

If you hit Play Again? you will start a new game.

If you do not want to start another game, you can hit the ESC keyboard button, and you will exit back to a finished stand-still game. To start a new one, you need the menu_item to do it. That’s what the next section is about!

This is how the menu bar nested menus look like.

Tetris Game Menu

Tetris View Menu

Tetris Options Menu

Tetris Help Menu

Keep in mind that each shell (including dialogs) have their own menu_bar. To reuse the same menu items in both the game and the High Score Dialog, you ought to generate a menu bar Custom Widget tetris_menu_bar and then reuse in both.

Run this command:

glimmer "scaffold:cw[tetris_menu_bar]"

Open TetrisMenuBar under app/glimmer_tetris/view/tetris_menu_bar.rb, delete all comments and the body {} block, and add the following code instead:

      COMMAND_KEY = OS.mac? ? :command : :ctrl
  
      options :game
      
      body {
        menu_bar {
          menu {
            text '&Game'
            
            menu_item {
              text '&Start'
              enabled bind(game, :game_over)
              accelerator COMMAND_KEY, :s
              
              on_widget_selected {
                game.start!
              }
            }
            menu_item(:check) {
              text '&Pause'
              accelerator COMMAND_KEY, :p
              enabled bind(game, :game_over, on_read: :!) {|value| value && !game.show_high_scores}
              enabled bind(game, :show_high_scores, on_read: :!) {|value| value && !game.game_over}
              selection bind(game, :paused)
            }
            menu_item {
              text '&Restart'
              accelerator COMMAND_KEY, :r
              
              on_widget_selected {
                game.restart!
              }
            }
            menu_item(:separator)
            menu_item {
              text '&Exit'
              accelerator COMMAND_KEY, :x
              
              on_widget_selected {
                parent_proxy.close
              }
            }
          } # end of menu
          
          menu {
            text '&View'
            
            menu {
              text '&High Scores'
              menu_item(:check) {
                text '&Show'
                accelerator COMMAND_KEY, :shift, :h
                selection bind(game, :show_high_scores)
              }
              menu_item {
                text '&Clear'
                accelerator COMMAND_KEY, :shift, :c
                
                on_widget_selected {
                  game.clear_high_scores!
                }
              }
            }
          } # end of menu
          
          menu {
            text '&Options'
            menu_item(:check) {
              text '&Beeping'
              accelerator COMMAND_KEY, :b
              selection bind(game, :beeping)
            }
            menu {
              text 'Up Arrow'
              menu_item(:radio) {
                text '&Instant Down'
                accelerator COMMAND_KEY, :shift, :i
                selection bind(game, :instant_down_on_up, computed_by: :up_arrow_action)
              }
              menu_item(:radio) {
                text 'Rotate &Right'
                accelerator COMMAND_KEY, :shift, :r
                selection bind(game, :rotate_right_on_up, computed_by: :up_arrow_action)
              }
              menu_item(:radio) {
                text 'Rotate &Left'
                accelerator COMMAND_KEY, :shift, :l
                selection bind(game, :rotate_left_on_up, computed_by: :up_arrow_action)
              }
            }
          } # end of menu
          
          menu {
            text '&Help'
            
            menu_item {
              text '&About'
              accelerator COMMAND_KEY, :shift, :a
              
              on_widget_selected {
                parent_custom_shell&.show_about_dialog
              }
            }
          } # end of menu
        }
      }
      
      def parent_custom_shell
        # grab custom shell widget wrapping parent widget proxy (i.e. Tetris) and invoke method on it
        the_parent_custom_shell = parent_proxy&.get_data('custom_shell')
        the_parent_custom_shell if the_parent_custom_shell&.visible?
      end

Next, go to AppView and add the following require statement above the top class declaration:

require_relative 'tetris_menu_bar'

And, add the following lines to AppView above the playfield declaration:


      tetris_menu_bar(game: @game)

Afterwards, go to HighScoreDialog and add the following require statement above the top class declaration:

require_relative 'tetris_menu_bar'

And, add the following line to HighScoreDialog above the first label declaration:


      tetris_menu_bar(game: game)

Run:

glimmer run

Now, you get access to all the menus:

Tetris Game Menu

Tetris View Menu

Tetris Options Menu

Tetris Help Menu

Play around with them to get a full understanding of what each menu item does.

Next, we are going to conclude this article by generating an app icon.

App Icon

This section should be a fun diversion. After all, we will drop down to Ruby scripting and write a script that generates a Tetris icon.

Create a file under bin/ called generate_app_icon.rb

Open the new file and paste this code in it:

require 'glimmer-dsl-swt'
require 'glimmer-cp-bevel'

require_relative '../app/glimmer_tetris/model/tetromino'

include Glimmer

puts 'Building app icon...'
icon_block_size = 64
icon_bevel_size = icon_block_size.to_f / 25.to_f
icon_bevel_pixel_size = 0.16*icon_block_size.to_f
icon_size = 8
icon_pixel_size = icon_block_size * icon_size
tetric_icon_image = image(icon_pixel_size, icon_pixel_size) {
  icon_size.times { |row|
    icon_size.times { |column|
      colored = row >= 1 && column.between?(1, 6)
      color = colored ? color(([:white] + GlimmerTetris::Model::Tetromino::LETTER_COLORS.values).sample) : color(:white)
      x = column * icon_block_size
      y = row * icon_block_size
      bevel(x: x, y: y, base_color: color, size: icon_block_size)
    }
  }
}

puts 'Preparing app icon for saving to file...'
i = org.eclipse.swt.graphics.Image.new(display.swt_display, 512, 512)
gc = org.eclipse.swt.graphics.GC.new(i)
gc.drawImage(tetric_icon_image.swt_image, 0, 0)
il = ImageLoader.new
il.data = [i.image_data]

puts "Saving #{File.expand_path(File.join('..', 'package','linux', 'Glimmer Tetris.png'), __dir__)}"
il.save(File.expand_path(File.join('..', 'package','linux', 'Glimmer Tetris.png'), __dir__), swt(:image_png))
puts 'Done generating app icon.'

Now, run:

glimmer bin/generate_app_icon.rb

This should generate the icon as a .png file placing it under package/linux since that is the format Linux expects when packaging with the glimmer package command.

Glimmer Tetris App Icon

For the Mac and Windows, you need the .icns and .ico formats respectively. To obtain, simply perform conversion yourself using a cloud service like cloudconvert.com

The icon is generated randomly. If you would like a different arrangement, simply regenerate until it fits your taste.

Packaging

Finally, you might want to package the game as a native executable on Mac or Windows (e.g. DMG on the Mac or MSI on Windows).

To do so, simply run:

glimmer package

This should generate all the native executable installable package formats available on your platform, placed under packages or packages/bundles depending on the format. Note that Windows requires installing extra tools to produce MSI packages.

On the Mac, you would see something like this:

Glimmer Tetris Package

If you see an error like the following, it is just a false alarm (ignore it as you should find the packages anyways):

Error: Bundler "DMG Installer" (dmg) failed to produce a bundle.

Alternatively, for a faster run, you can just request a specific format by passing an argument.

On the Mac:

glimmer "package[DMG]"

On Windows:

glimmer "package[MSI]"

The packaging includes Java and JRuby on Windows and Mac, so consumers can simply run the app without extra effort beyond installation.

Note that on Linux, you simply package as a Ruby gem. To do so, you must ensure the project can optionally work without bundler by replacing the following two lines in app/glimmer_tetris.rb:

require 'bundler/setup'
Bundler.require(:default)

with the following lines:

begin
  require 'bundler/setup'
  Bundler.require(:default)
rescue
  require 'glimmer-dsl-swt'
  require 'glimmer-cp-bevel'
end

Now, you can run this task to build the gem:

glimmer package:gem

This uses juwelier to produce a gem under pkg. You may distribute expecting Glimmer’s pre-requisites of Java and JRuby to be pre-installed on Linux.

Once the gem is installed, users may run the game with the included glimmer_tetris Ruby script:

glimmer_tetris

Advanced packaging is beyond the scope of this article series, but you may learn more about it at the Glimmer DSL for SWT Packaging & Distribution documentation.

Find Out More

References

Built with Ruby (running Jekyll) on 2023-01-25 18:05:39 +0000 in 0.371 seconds.
Hosted on GitHub Pages. </> Source on GitHub. (0) Dedicated to the public domain.