Glimmer Wordle 1.1.5 Windows Support
Tags: ruby, gamedev, glimmer, gui
Published: 2023-10-18 23:12:03

[Glimmer Wordle 1.1.5](https://github.com/AndyObtiva/glimmer_wordle) (open-source desktop game) has just been released with official support for Windows. Although the game worked on Windows before, thanks to the platform-independent Glimmer DSL for SWT GUI library it was built with, styling was not tweaked for Windows till now in [version 1.1.5](https://rubygems.org/gems/glimmer_wordle).

![Windows Wordle](https://raw.githubusercontent.com/AndyObtiva/glimmer_wordle/master/screenshots/glimmer-wordle-windows.png)

To give you an idea of how the game flows, here is an animated Gif with it running on the Mac:

![Mac Wordle Video](https://github.com/AndyObtiva/glimmer_wordle/raw/master/screenshots/glimmer-wordle.gif)

Here is the Glimmer code of the GUI View:

```ruby
class Wordle
  module View
    class AppView
      include Glimmer::UI::CustomShell
      
      COLOR_TO_BACKGROUND_COLOR_MAP = {
        green:  rgb(106, 170, 100),
        yellow: rgb(201, 180, 88),
        gray:   rgb(120, 124, 126),
      }
      
      COLOR_TO_TEXT_COLOR_MAP = {
        green:  :white,
        yellow: :white,
        gray:   :white,
      }
      
      ALPHABET_LAYOUTS = {
        alphabetical: [
          %w[A B C D E F G H I J],
          %w[K L M N O P Q R S],
          %w[T U V W X Y Z],
        ],
        qwerty: [
          %w[Q W E R T Y U I O P],
          %w[A S D F G H J K L],
          %w[Z X C V B N M],
        ],
      }
      
      CONFIG_FILE = File.join(Dir.home, '.glimmer_wordle')
    
      before_body do
        @display = display {
          on_about {
            display_about_dialog
          }
          on_preferences {
            display_about_dialog
          }
          on_swt_keydown do |key_event|
            if key_event.keyCode == 8
              do_backspace
            elsif key_event.keyCode == swt(:arrow_left)
              do_left
            elsif key_event.keyCode == swt(:arrow_right)
              do_right
            elsif key_event.keyCode == swt(:cr)
              if @five_letter_word.status == :in_progress
                do_guess
              else
                do_restart
              end
            elsif valid_character?((key_event.keyCode.chr rescue ''))
              do_type(key_event.keyCode.chr)
            end
          end
        }
        @five_letter_word = Model::FiveLetterWord.new
        config = load_config
        @alphabet_layout = config[:alphabet_layout] || :alphabetical
      end
  
      ## Add widget content inside custom shell body
      ## Top-most widget must be a shell or another custom shell
      #
      body {
        shell(:no_resize) {
          grid_layout {
            margin_width 10
            margin_height 10
            vertical_spacing 0
          }
          
          # Replace example content below with custom shell content
          minimum_size 420, 540
          image File.join(APP_ROOT, 'icons', 'windows', "Glimmer Wordle.ico") if OS.windows?
          image File.join(APP_ROOT, 'icons', 'linux', "Glimmer Wordle.png") unless OS.windows?
          text "Glimmer Wordle"
          background :white
          
          app_menu_bar
          
          alphabet_container
          
          label {
            layout_data :center, :center, true, false
            text 'You have 6 tries to guess a 5-letter word'
            background :transparent if OS.windows?
          }
          
          word_guesser
          
          guess_button
        }
      }
      
      def app_menu_bar
        menu_bar {
          menu {
            text '&Game'
            
            menu_item {
              text '&Restart'
              
              on_widget_selected {
                do_restart
              }
            }
            
            menu_item {
              text 'E&xit'
              
              on_widget_selected {
                exit(0)
              }
            }
          }
          
          menu {
            text '&View'
            
            menu {
              text 'Alphabet &Layout'
              
              menu_item(:radio) {
                text '&Alphabetical'
                selection @alphabet_layout == :alphabetical
                
                on_widget_selected {
                  self.alphabet_layout = :alphabetical
                  rebuild_alphabet_container
                }
              }
              
              menu_item(:radio) {
                text '&Qwerty'
                selection @alphabet_layout == :qwerty
                
                on_widget_selected {
                  self.alphabet_layout = :qwerty
                  rebuild_alphabet_container
                }
              }
            }
          }
          
          menu {
            text '&Help'
            
            menu_item {
              text '&Instructions'
              
              on_widget_selected {
                display_instructions_dialog
              }
            }
            
            menu_item {
              text '&About'
              
              on_widget_selected {
                display_about_dialog
              }
            }
          }
        }
      end
  
      def display_instructions_dialog
        message_box(body_root) {
          text 'Instructions'
          message <<~MULTI_LINE_STRING
            Make 6 guesses for a 5-letter word.
            
            If you enter a letter that is part of the word, and at the right location, it becomes green,
            
            If you enter a letter that is part of the word, but at the wrong location, it becomes yellow.
            
            If you enter a letter that is not part of the word, it becomes red.
          MULTI_LINE_STRING
        }.open
      end
      
      def display_about_dialog
        message_box(body_root) {
          text 'About'
          message "Glimmer Wordle #{VERSION}\n\n#{LICENSE}"
        }.open
      end
      
      def alphabet_container
        @alphabet_container = composite {
          layout_data(:center, :center, true, false)
          
          grid_layout {
            margin_width 0
            margin_height 0
            vertical_spacing 0
          }
          
          background :white
        
          alphabets
        }
      end
      
      def alphabets
        alphabet_row(ALPHABET_LAYOUTS[@alphabet_layout][0]) {
          layout_data(:center, :center, true, false) {
            width_hint 318
            height_hint 50
          }
        }
        
        alphabet_row(ALPHABET_LAYOUTS[@alphabet_layout][1]) {
          layout_data(:center, :center, true, false) {
            width_hint 288
            height_hint 50
          }
        }
        
        alphabet_row(ALPHABET_LAYOUTS[@alphabet_layout][2]) {
          layout_data(:center, :center, true, false) {
            width_hint 222
            height_hint 50
          }
        }
      end
      
      def alphabet_row(alphabet_characters, &block)
        canvas {
          block.call
          background :white
          
          @alphabet_rectangles ||= []
          @alphabet_borders ||= []
          @alphabet_letters ||= []
          alphabet_characters.each_with_index do |alphabet_character, i|
            @alphabet_rectangles << rectangle(1 + i*32, @alphabet_row_offset_y, 28, 28) {
              background :transparent
              
              @alphabet_borders << rectangle {
                foreground :gray
                line_width 2
              }
              
              @alphabet_letters << text(alphabet_character, :default, [:default, OS.linux? ? 5 : (OS.windows? ? 1 : 0)]) {
                font alphabet_font
              }
            }
          end
        }
      end
      
      def alphabet_layout_alphabets
        ALPHABET_LAYOUTS[@alphabet_layout].reduce(:+)
      end
      
      def word_guesser
        @canvasses ||= []
        margin_x = 5
        margin_y = 5
        @canvasses << canvas {
          layout_data(:center, :center, true, false) {
            width_hint 230
            height_hint 50
          }
          background :white
          focus true
          
          @rectangles = []
          @borders = []
          @letters = []
          5.times do |i|
            @rectangles << rectangle(margin_x + i*45, margin_y, 40, 40) {
              background :transparent
              
              @borders << rectangle {
                foreground i == 0 ? :title_background : :gray
                line_width 2
              }
              
              @letters << text('', :default, [:default, OS.linux? ? 6 : (OS.windows? ? 1 : 0)]) {
                font letter_font
              }
            }
          end
        }
      end
      
      def guess_button
        @guess_button = button {
          layout_data :center, :center, true, false
          text 'Guess'
          
          on_widget_selected do
            do_guess
          end
        }
      end
      
      def do_backspace
        @letter = @letters[highlighted_letter_index]
        if @letter.string != ''
          index_to_delete = highlighted_letter_index
        else
          index_to_delete = [highlighted_letter_index - 1, 0].max
        end
        @letter = @letters[index_to_delete]
        @letter.string = ''
        @borders.each { |caret| caret.foreground = :gray} # refactor this reusable code into a method that highlights the caret
        @borders[index_to_delete].foreground = :title_background
      end
      
      def do_guess
        return if !word_filled_up?
        word = @letters.map(&:string).join
        if invalid_word?(word)
          message_box {
            text 'Invalid Word'
            message "The word you entered is not an allowed guess!\n\nPlease try another word!"
          }.open
          return
        end
        guess_result = @five_letter_word.guess(word)
        update_guess_word_background_colors(guess_result)
        update_alphabet_background_colors
        if @five_letter_word.status == :in_progress
          @guess_button.dispose
          body_root.content {
            word_guesser
            guess_button
          }
        else
          @guess_button.dispose
          body_root.content {
            @restart_button = button {
              layout_data :center, :center, true, false
              text 'Restart'
              focus true
              
              on_widget_selected do
                do_restart
              end
            }
          }
          display_share_text_dialog
        end
        body_root.layout(true, true)
        body_root.pack(true)
      end
      
      def highlighted_letter_index
        @borders.each_with_index.find {|border, i| border.foreground.first == color(:title_background).swt_color }.last
      end
      
      def do_type(character)
        index = highlighted_letter_index
        @letter = @letters[index]
        @letter.string = character.upcase
        if @letters.any? {|letter| letter.string == ''}
          @borders.each { |caret| caret.foreground = :gray} # refactor this reusable code into a method that highlights the caret
          @borders[index == 4 ? 4 : index + 1].foreground = :title_background
        end
      end
      
      def do_left
        index = [highlighted_letter_index - 1, 0].max
        @borders.each { |caret| caret.foreground = :gray} # refactor this reusable code into a method that highlights the caret
        @borders[index].foreground = :title_background
      end
      
      def do_right
        index = [highlighted_letter_index + 1, @letters.count - 1].min
        @borders.each { |caret| caret.foreground = :gray} # refactor this reusable code into a method that highlights the caret
        @borders[index].foreground = :title_background
      end
      
      def do_restart
        @share_text_dialog&.close
        alphabet_layout_alphabets.each_with_index do |alphabet_character, i|
          @alphabet_borders[i].foreground = :gray
          @alphabet_rectangles[i].background = :white
          @alphabet_letters[i].foreground = :black
        end
        @restart_button&.dispose
        @canvasses.dup.each(&:dispose)
        @canvasses.clear
        @guess_button&.dispose
        body_root.content {
          word_guesser
          guess_button
        }
        body_root.layout(true, true)
        body_root.pack(true)
        @five_letter_word.refresh
      end
      
      def update_guess_word_background_colors(guess_result)
        guess_result.each_with_index do |result_color, i|
          background_color = COLOR_TO_BACKGROUND_COLOR_MAP[result_color]
          @borders[i].foreground = background_color
          @rectangles[i].background = background_color
          @letters[i].foreground = COLOR_TO_TEXT_COLOR_MAP[result_color]
          async_exec { @canvasses.last.redraw }
        end
      end
      
      def update_alphabet_background_colors
        alphabet_layout_alphabets.each_with_index do |alphabet_character, i|
          result_color = @five_letter_word.colored_alphabets[alphabet_character.downcase]
          if result_color
            background_color = COLOR_TO_BACKGROUND_COLOR_MAP[result_color]
            @alphabet_borders[i].foreground = background_color
            @alphabet_rectangles[i].background = background_color
            @alphabet_letters[i].foreground = COLOR_TO_TEXT_COLOR_MAP[result_color]
          end
        end
      end
      
      def word_filled_up?
        @letters.find {|letter| letter.string == ''}.nil?
      end
      
      def invalid_word?(word)
        !Model::FiveLetterWord::WORLD_ALLOWED_GUESSES.include?(word.downcase)
      end
      
      def valid_character?(character)
        ((65..90).to_a + (97..122).to_a).map {|n| n.chr}.include?(character)
      end
      
      def display_share_text_dialog
        result = "#{@five_letter_word.answer.upcase}\n\n#{emoji_result}"
          
        Clipboard.copy(result)

        @share_text_dialog = dialog(body_root) {
          grid_layout
          text 'Share Result'
          
          label {
            layout_data :center, :center, true, false
            text 'Result is copied to clipboard!'
          }
          
          styled_text {
            layout_data :fill, :fill, true, true
            editable false
            caret nil
            alignment :center
            font name: 'Segoe UI Emoji' if OS.windows?
            text result
          }
        }
        @share_text_dialog.open
      end
      
      def alphabet_font
        the_font = {style: :bold, height: 28}
        the_font.merge!(name: 'Helvetica', height: 21) if OS.linux?
        the_font.merge!(name: 'Arial', height: 25) if OS.windows? 
        the_font
      end
      
      def letter_font
        the_font = {style: :bold, height: 40}
        the_font.merge!(name: 'Helvetica', height: 30) if OS.linux?
        the_font.merge!(name: 'Arial', height: 32) if OS.windows? 
        the_font
      end
      
      def dispose_alphabet_container_children
        @alphabet_rectangles.clear
        @alphabet_borders.clear
        @alphabet_letters.clear
        @alphabet_container.children.each(&:dispose)
      end
      
      def rebuild_alphabet_container
        dispose_alphabet_container_children
        @alphabet_container.content {
          alphabets
        }
        @alphabet_container.layout(true, true)
        @alphabet_container.pack(true)
        update_alphabet_background_colors
      end
      
      def alphabet_layout=(value)
         @alphabet_layout = value
         save_config
      end
      
      def new_config
        {
          alphabet_layout: @alphabet_layout
        }
      end
      
      def save_config
        File.write(CONFIG_FILE, YAML.dump(new_config))
      rescue => e
        puts e.full_message
      end
      
      def load_config
        File.exist?(CONFIG_FILE) ? YAML.load(File.read(CONFIG_FILE)) : {}
      rescue => e
        puts e.full_message
        {}
      end
      
      def emoji_result
        result = ''
        @five_letter_word.guess_results.each do |row|
          row.each do |result_color|
            case result_color
            when :green
              result << "🟩"
            when :yellow
              result << "🟨"
            when :gray
              result << "⬜"
            end
          end
          result << "\n"
        end
        result
      end
    end
  end
end
```

GitHub: https://github.com/AndyObtiva/glimmer_wordle

RubyGem: https://rubygems.org/gems/glimmer_wordle

Blog Post Announcement: https://andymaleh.blogspot.com/2023/10/glimmer-wordle-115.html