Wednesday, December 25, 2024

Glimmer Hangman (RubyConf 2024 Hack Day App)

During the RubyConf 2024 Hack Day Event for Glimmer DSL for LibUI, which was associated with the RubyConf 2024 workshop "How To Build Basic Desktop Applications in Ruby", I started the implementation of Hangman, the word game, in Ruby as a GUI desktop app. Well, I am happy to announce that I finally worked on it again yesterday and completed it, releasing version 1.0.0! And, it's implemented with some of the most amazing Ruby code! Just install the `glimmer_hangman` Ruby gem and run the command `hangman` and you'll have the game running on your machine instantly!

Project GitHub:

https://github.com/AndyObtiva/glimmer_hangman

Ruby Gem:

https://rubygems.org/gems/glimmer_hangman

I am including the project README and code below.

Merry Christmas!

Project README:

Glimmer Hangman Icon Glimmer Hangman 1.0.0

Gem Version

Hangman (word game) that runs on Mac, Windows, and Linux. Built with Ruby and Glimmer DSL for LibUI (Prerequisite-Free Ruby Desktop Development Cross-Platform Native GUI Library) using Application Scaffolding.

Implementation started at the RubyConf 2024 Hack Day Event for Glimmer DSL for LibUI, which was connected to the RubyConf 2024 workshop "How To Build Basic Desktop Applications in Ruby".

glimmer-dsl-libui-mac-hangman.gif

Setup

Assuming you have Ruby installed (standard MRI / CRuby).

Run:

gem install glimmer_hangman

Usage

Run:

glimmer_hangman

or just:

hangman

The goal of the game is to guess a word by typing letters on the keyboard one by one, with 10 guesses max.

As each letter is typed, the blanks are replaced with letters that gradually reveal the word being guessed if the typed letter is a correct part of the word, with multiple occurrences if any (e.g typing E shows E twice in a word that has it twice like RECOGNIZED).

correct guesses

Or otherwise, the drawing at the top shows more parts of the scene of execution by hanging if the typed letter is incorrect.

incorrect guesses

Also, typed letters show up at the bottom of the screen as green (correct) or (red) incorrect.

guessed letters

The game is won once the correct word is guessed completely.

guessed letters

The game is lost once the stick figure is fully hung (10 guessed letters are incorrect).

guessed letters

Restart game by:

  • Keyboard Shortcut: CMD+R on Mac or CTRL+R on Windows/Linux
  • Menu Item: Game -> Restart

Copyright

MIT

Copyright (c) 2024 Andy Maleh. See LICENSE.txt for further details.

--

Built with Glimmer DSL for LibUI (Prerequisite-Free Ruby Desktop Development Cross-Platform Native GUI Library)

Glimmer Hangman icon made by Freepik from www.flaticon.com


Project Code:

app/glimmer_hangman/model/game.rb
# Source: https://github.com/AndyObtiva/glimmer_hangman/blob/master/app/glimmer_hangman/model/game.rb
class GlimmerHangman
module Model
class Game
WORDS_FILE_PATH = File.join(APP_ROOT, './config/words.txt')
attr_accessor :word, :guess, :guessed_letters, :incorrect_guess_count
def initialize
load_words
restart
end
def restart
self.word = select_random_word
self.guess = ' ' * self.word.size
self.incorrect_guess_count = 0
self.guessed_letters = []
end
def guess_letter(letter)
letter = letter.to_s.downcase
return if guessed_letters.include?(letter) || won? || lost?
guessed_letters << letter
if correct_guess_letter?(letter)
update_guess_with_newly_guessed_letter(letter)
else
self.incorrect_guess_count += 1
end
end
def guessed_letter_at_index?(letter_index)
guess[letter_index] != ' '
end
def correct_guess_letter?(letter)
word.include?(letter)
end
def won?
guess.chars.count(' ') == 0
end
def lost?
incorrect_guess_count >= word.size
end
private
def load_words
@words = File.read(WORDS_FILE_PATH).lines.map(&:chomp).map(&:downcase)
end
def select_random_word
shuffle_word_indexes if @word_indexes.nil? || @word_indexes.empty?
word_index = @word_indexes.pop
@words[word_index]
end
def shuffle_word_indexes
@word_indexes = @words.size.times.to_a.shuffle
end
def update_guess_with_newly_guessed_letter(letter)
letter_indexes = word.chars.each_with_index.select {|c, i| c == letter }.map(&:last)
new_guess = guess.dup
letter_indexes.each { |letter_index| new_guess[letter_index] = letter }
self.guess = new_guess
end
end
end
end

app/glimmer_hangman/view/hangman.rb
# Source: https://github.com/AndyObtiva/glimmer_hangman/blob/master/app/glimmer_hangman/view/hangman.rb
require 'glimmer_hangman/model/game'
require 'glimmer_hangman/view/hangman_scene'
require 'glimmer_hangman/view/hangman_guess'
require 'glimmer_hangman/view/hangman_guessed_letters'
class GlimmerHangman
module View
class Hangman
include Glimmer::LibUI::Application
MODIFIER = OS.mac? ? :command : :control
option :size, default: 480
attr_reader :game
before_body do
@game = Model::Game.new
menu_bar
end
body {
window {
content_size size, size
title 'Hangman'
resizable false
area {
background = rectangle(0, 0, size, size) {
fill :white
}
hangman_scene(game:, size:)
hangman_guess(game:, size:)
hangman_guessed_letters(game:, size:)
on_key_down do |event|
case event
in {key: 'a'..'z', modifier: nil, modifiers: []}
game.guess_letter(event[:key])
handled = true
in {key: 'r', modifier: nil, modifiers: [MODIFIER]}
game.restart
handled = true
else
handled = false
end
handled
end
}
}
}
private
def menu_bar
menu('Game') {
menu_item('Restart') {
on_clicked do
game.restart
end
}
quit_menu_item if OS.mac?
}
menu('Help') {
if OS.mac?
about_menu_item {
on_clicked do
display_about_dialog
end
}
end
menu_item('About') {
on_clicked do
display_about_dialog
end
}
}
end
def display_about_dialog
message = "Hangman #{VERSION}\n\n#{LICENSE}"
msg_box('About', message)
end
end
end
end

app/glimmer_hangman/view/hangman_scene.rb
# Source: https://github.com/AndyObtiva/glimmer_hangman/blob/master/app/glimmer_hangman/view/hangman.rb
require 'glimmer_hangman/model/game'
class GlimmerHangman
module View
class HangmanScene
include Glimmer::LibUI::CustomShape
option :game
option :size, default: 480
option :thickness, default: 2
body {
composite_shape(0, 0) {
content(game, computed_by: [:guess, :incorrect_guess_count]) do
stroke stroke_color, thickness: thickness
with_one_incorrect_guess {
base = line(size*0.2, size*0.7, size*0.4, size*0.7)
}
with_one_more_incorrect_guess {
column = line(size*0.3, size*0.1, size*0.3, size*0.7)
}
with_one_more_incorrect_guess {
ceiling = line(size*0.3, size*0.1, size*0.7, size*0.1)
}
with_one_more_incorrect_guess {
rope = line(size*0.7, size*0.1, size*0.7, size*0.25)
}
with_one_more_incorrect_guess {
head = circle(size*0.7, size*0.25, size*0.05, size*0.1) {
fill :white
}
}
with_one_more_incorrect_guess {
torso = line(size*0.7, size*0.3, size*0.7, size*0.5)
}
with_one_more_incorrect_guess {
left_hand = line(size*0.7, size*0.3, size*0.6, size*0.4)
}
with_one_more_incorrect_guess {
right_hand = line(size*0.7, size*0.3, size*0.8, size*0.4)
}
with_one_more_incorrect_guess {
left_leg = line(size*0.7, size*0.5, size*0.6, size*0.6)
}
with_one_more_incorrect_guess {
right_leg = line(size*0.7, size*0.5, size*0.8, size*0.6)
}
end
}
}
private
def stroke_color
if game.lost?
:red
elsif game.won?
:green
else
:black
end
end
def with_one_incorrect_guess(&shape_content)
@required_incorrect_guess_count = 1
with_required_incorrect_guess_count(&shape_content)
end
def with_one_more_incorrect_guess(&shape_content)
@required_incorrect_guess_count += 1
with_required_incorrect_guess_count(&shape_content)
end
def with_required_incorrect_guess_count(&shape_content)
shape_content.call if game.incorrect_guess_count >= @required_incorrect_guess_count
end
end
end
end

app/glimmer_hangman/view/hangman_guess.rb
# Source: https://github.com/AndyObtiva/glimmer_hangman/blob/master/app/glimmer_hangman/view/hangman_guess.rb
require 'glimmer_hangman/model/game'
class GlimmerHangman
module View
class HangmanGuess
include Glimmer::LibUI::CustomShape
option :game
option :size, default: 480
option :thickness, default: 2
option :font_size, default: 33
body {
text(size*0.1, size*0.8, size*0.8) {
default_font family: 'Courier New', size: font_size
content(game, computed_by: [:guess, :incorrect_guess_count]) do
game.guess.size.times do |letter_index|
letter = rendered_letter(letter_index)
string(letter) {
color string_color(letter_index)
}
string(' ')
end
end
}
}
private
def rendered_letter(letter_index)
letter = game.lost? ? game.word[letter_index] : game.guess[letter_index]
letter = '_' if letter == ' '
letter.upcase
end
def string_color(letter_index)
if game.lost? && !game.guessed_letter_at_index?(letter_index)
:green
else
:black
end
end
end
end
end

app/glimmer_hangman/view/hangman_guessed_letters.rb
# Source: https://github.com/AndyObtiva/glimmer_hangman/blob/master/app/glimmer_hangman/view/hangman_guessed_letters.rb
require 'glimmer_hangman/model/game'
class GlimmerHangman
module View
class HangmanGuessedLetters
include Glimmer::LibUI::CustomShape
option :game
option :size, default: 480
option :thickness, default: 2
option :font_size, default: 17
body {
text(size*0.1, size*0.9, size*0.8) {
default_font family: 'Courier New', size: font_size
content(game, :guessed_letters) do
game.guessed_letters.each do |letter|
string(letter.upcase) {
color string_color(letter)
}
string(' ')
end
end
}
}
private
def string_color(letter)
game.correct_guess_letter?(letter) ? :green : :red
end
end
end
end


No comments: