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 6 - glimmer_tetris Gem - Glimmer Tetris - The Score Lane - See Your Game Progress!

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 The Score Lane indicating the next Tetromino shape to drop (aka preview next Tetromino), the current score, the number of lines eliminated, and the current level.

Refactorings

What we are building in this article is what we call The Score Lane. But, before we add it, let us do some quick and fun refactorings!

First of all, although AppView’s body {} is pretty compact, would you believe me if I tell you we can do better?!!

Let us take advantage of Glimmer Custom Widgets to demonstrate.

Run this command in the terminal (ZShell friendly version):

glimmer "scaffold:customwidget[playfield]"

This should generate an app/glimmer_tetris/view/playfield.rb file.

Now, delete the content of body {} in Playfield (and delete all comments in the file).

Cut the following code from AppView:

          composite((:double_buffered unless OS.mac?)) {
            grid_layout {
              num_columns Model::Game::PLAYFIELD_WIDTH
              make_columns_equal_width true
              margin_width BLOCK_SIZE
              margin_height BLOCK_SIZE
              horizontal_spacing 0
              vertical_spacing 0
            }
  
            Model::Game::PLAYFIELD_HEIGHT.times do |row|
              Model::Game::PLAYFIELD_WIDTH.times do |column|
                canvas { |canvas_proxy|
                  layout_data {
                    width_hint BLOCK_SIZE
                    height_hint BLOCK_SIZE
                  }
                  
                  bevel(size: BLOCK_SIZE) {
                    base_color bind(@game.playfield[row][column], :color)
                  }
                }
              end
            end
          }

And, paste it inside the body {} block in Playfield.

Finally, go back to AppView and add the following require statement right above the class declaration:

require_relative 'playfield'

Afterwards, go down to where you cut the code at the bottom of the shell {} block and add this single keyword:

            playfield

This is how AppView’s body {} should be now:

      body {
        shell(:no_resize) {
          grid_layout
          text 'Glimmer Tetris'

          playfield
        }
      }

This is how the Playfield entire content should be now:

class GlimmerTetris
  module View
    class Playfield
      include Glimmer::UI::CustomWidget
  
      body {
        composite((:double_buffered unless OS.mac?)) {
          grid_layout {
            num_columns Model::Game::PLAYFIELD_WIDTH
            make_columns_equal_width true
            margin_width BLOCK_SIZE
            margin_height BLOCK_SIZE
            horizontal_spacing 0
            vertical_spacing 0
          }

          Model::Game::PLAYFIELD_HEIGHT.times do |row|
            Model::Game::PLAYFIELD_WIDTH.times do |column|
              canvas { |canvas_proxy|
                layout_data {
                  width_hint BLOCK_SIZE
                  height_hint BLOCK_SIZE
                }
                
                bevel(size: BLOCK_SIZE) {
                  base_color bind(@game.playfield[row][column], :color)
                }
              }
            end
          end
        }
      }
  
    end
  end
end

Finally, to confirm that the refactoring worked, let us run:

glimmer run

You should get an error that includes this statement:

...undefined method `playfield' for nil:NilClass (NoMethodError)...

Good! That means, we need to declare a Custom Widget option.

Just below the include Glimmer::UI::CustomWidget statement in Playfield, add the following:

      option :game_playfield

Now, go to this line inside the body {} block:

                base_color bind(@game.playfield[row][column], :color)

And, change to:

                base_color bind(game_playfield[row][column], :color)

Finally, go to AppView and update playfield with:

          playfield(game_playfield: @game.playfield)

Run:

glimmer run

This time, it should work fine!

(note: if you ever get stuck with bad code, remember you can checkout working Code inside a repo in the References below)

Can you believe we have been able to reduce the potentially already smallest Tetris implementation on Earth to this for the GUI part?

      body {
        shell(:no_resize) {
          grid_layout
          text 'Glimmer Tetris'

          playfield(game_playfield: @game.playfield)
        }
      }

Something is not quite right though. After all, we only moved the code representing The Playfield to Playfield, but we did not improve anything in its own code. Can we go more granular? Yes! The canvas! It is simply representing the concept of a block

Run this command in the terminal (shorter version of previous custom widget scaffolding command):

glimmer "scaffold:cw[block]"

This should generate app/glimmer_tetris/view/block.

Go back to Playfield and cut this code:

                canvas { |canvas_proxy|
                  layout_data {
                    width_hint BLOCK_SIZE
                    height_hint BLOCK_SIZE
                  }
                  
                  bevel(size: BLOCK_SIZE) {
                    base_color bind(@game.playfield[row][column], :color)
                  }
                }

Type this in its place:

                block

Also, add this above the declaration of the top class:

require_relative 'block'

Now, open Block, delete comments, and clear the body {} block.

Finally, paste the code you cut from Playfield inside the body {} block.

Run:

glimmer run

You should get an error including this message:

...Glimmer keyword game_playfield with args [] cannot be handled inside parent...

Again, that is good! It means we need to add Custom Widget options.

Just below the include Glimmer::UI::CustomWidget declaration, add the following code:

  
  option :game_block

Replace this line in Block:

            base_color bind(game_playfield[row][column], :color)

with this line:

            base_color bind(game_block, :color)

Finally, go back to Playfield and replace this line:

                block

with this line:

                block(game_block: game_playfield[row][column])

Run:

glimmer run

The game should be working again!

One last note is usually it is better to keep layout data out of an extracted Custom Widget to keep it flexible in accomodating different layouts with different layout data.

Let’s do one last refactoring regarding the layout_data inside Block.

Cut this line from Block:

          layout_data {
            width_hint BLOCK_SIZE
            height_hint BLOCK_SIZE
          }

Go to Playfield, and replace this line:

                block(game_block: game_playfield[row][column])

with the following:

                block(game_block: game_playfield[row][column]) {
                  layout_data {
                    width_hint BLOCK_SIZE
                    height_hint BLOCK_SIZE
                  }
                }

See, Glimmer DSL for SWT lets you treat block as a first-class-citizen widget, which can nest its own layout_data just like label or combo

Now, run:

glimmer run

The game should remain happily functional!

Score Lane

Now, we are ready to add The Score Lane. I am sure you can guess how. By using Custom Widgets and Scaffolding!

I will leave this section for you to do as an exercise with the following tips:

This is how The Score Lane must look like:

Glimmer Tetris

To know if you did well, know that at the end you should have code like this ScoreLane class under app/glimmer_tetris/view/score_lane.rb:

require_relative 'playfield'

class GlimmerTetris
  module View
    class ScoreLane
      include Glimmer::UI::CustomWidget
  
      options :game
      
      body {
        composite {
          row_layout {
            type :vertical
            center true
            fill true
            margin_width 0
            margin_height BLOCK_SIZE
          }
          label(:center) {
            text 'Next'
            font name: FONT_NAME, height: FONT_TITLE_HEIGHT, style: FONT_TITLE_STYLE
          }
          playfield(game_playfield: game.preview_playfield, playfield_width: Model::Game::PREVIEW_PLAYFIELD_WIDTH, playfield_height: Model::Game::PREVIEW_PLAYFIELD_HEIGHT)

          label(:center) {
            text 'Score'
            font name: FONT_NAME, height: FONT_TITLE_HEIGHT, style: FONT_TITLE_STYLE
          }
          label(:center) {
            text bind(game, :score)
            font height: FONT_TITLE_HEIGHT
          }
          
          label # spacer
          
          label(:center) {
            text 'Lines'
            font name: FONT_NAME, height: FONT_TITLE_HEIGHT, style: FONT_TITLE_STYLE
          }
          label(:center) {
            text bind(game, :lines)
            font height: FONT_TITLE_HEIGHT
          }
          
          label # spacer
          
          label(:center) {
            text 'Level'
            font name: FONT_NAME, height: FONT_TITLE_HEIGHT, style: FONT_TITLE_STYLE
          }
          label(:center) {
            text bind(game, :level)
            font height: FONT_TITLE_HEIGHT
          }
        }
      }
    end
  end
end

Playfield should have code like this:

require_relative 'block'

class GlimmerTetris
  module View
    class Playfield
      include Glimmer::UI::CustomWidget
  
      options :game_playfield, :playfield_width, :playfield_height
  
      body {
        composite((:double_buffered unless OS.mac?)) {
          grid_layout {
            num_columns playfield_width
            make_columns_equal_width true
            margin_width BLOCK_SIZE
            margin_height BLOCK_SIZE
            horizontal_spacing 0
            vertical_spacing 0
          }
          
          playfield_height.times do |row|
            playfield_width.times do |column|
              block(game_block: game_playfield[row][column]) {
                layout_data {
                  width_hint BLOCK_SIZE
                  height_hint BLOCK_SIZE
                }
              }
            end
          end
        }
      }
  
    end
  end
end

Finally, AppView should have code like this (note that it has a shell minimum_size to ensure proper sizing, and the score_lane has layout_data to ensure it fills the rest of the empty space):

require_relative '../model/game'

require_relative 'playfield'
require_relative 'score_lane'

class GlimmerTetris
  module View
    class AppView
      include Glimmer::UI::CustomShell

      attr_reader :game
      
      before_body {
        @mutex = Mutex.new
        @game = Model::Game.new
            
        @game.configure_beeper do
          display.beep
        end
        
        Display.app_name = 'Glimmer Tetris'
    
        display {
          on_swt_keydown { |key_event|
            case key_event.keyCode
            when swt(:arrow_down), 's'.bytes.first
              if OS.mac?
                game.down!
              else
                # rate limit downs in Windows/Linux as they go too fast when key is held
                @queued_downs ||= 0
                @queued_downs += 1
                async_exec do
                  game.down! if @queued_downs < 3
                  @queued_downs -= 1
                end
              end
            when swt(:arrow_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
            when swt(:arrow_left), 'a'.bytes.first
              game.left!
            when swt(:arrow_right), 'd'.bytes.first
              game.right!
            when swt(:shift), swt(:alt)
              if key_event.keyLocation == swt(:right) # right shift key
                game.rotate!(:right)
              elsif key_event.keyLocation == swt(:left) # left shift key
                game.rotate!(:left)
              end
            end
          }
    
          # if running in app mode, set the Mac app about dialog (ignored in platforms)
          on_about {
            show_about_dialog
          }
          
          on_quit {
            exit(0)
          }
        }
      }
      
      after_body {
        observe(@game, :game_over) do |game_over|
          if game_over
            show_game_over_message_box
          else
            start_moving_tetrominos_down
          end
        end
        @game.start!
      }
      
      body {
        shell(:no_resize) {
          grid_layout 2, false
          text 'Glimmer Tetris'
          minimum_size 500, 500

          playfield(game_playfield: @game.playfield, playfield_width: Model::Game::PLAYFIELD_WIDTH, playfield_height: Model::Game::PLAYFIELD_HEIGHT)
          
          score_lane(game: @game) {
            layout_data(:fill, :fill, true, true)
          }
        }
      }
      
      def start_moving_tetrominos_down
        Thread.new do
          @mutex.synchronize do
            loop do
              time = Time.now
              sleep @game.delay
              break if @game.game_over? || body_root.disposed?
              # ensure entire game tetromino down movement happens as one GUI update event with sync_exec (to avoid flicker/stutter)
              sync_exec {
                @game.down! unless @game.paused?
              }
            end
          end
        end
      end
      
      def show_game_over_message_box
        message_box {
          text 'Game Over'
          message 'Play Again?'
        }.open # this blocks until closed
        @game.start!
      end
      
      def show_about_dialog
        message_box {
          text 'Glimmer Tetris'
          message "Glimmer Tetris\n\nGlimmer DSL for SWT Sample\n\nCopyright (c) 2021 Andy Maleh"
        }.open
      end
    end
  end
end

Notice the ultra-compact body {}:

      body {
        shell(:no_resize) {
          grid_layout 2, false
          text 'Glimmer Tetris'
          minimum_size 475, 500

          playfield(game_playfield: @game.playfield, playfield_width: Model::Game::PLAYFIELD_WIDTH, playfield_height: Model::Game::PLAYFIELD_HEIGHT)
          
          score_lane(game: @game) {
            layout_data(:fill, :fill, true, true)
          }
        }
      }

Unheard of anywhere else, eh!?

Stay tuned for the next article where we cover High Scores!

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.