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.
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".
Assuming you have Ruby installed (standard MRI / CRuby).
Run:
gem install glimmer_hangman
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
).
Or otherwise, the drawing at the top shows more parts of the scene of execution by hanging if the typed letter is incorrect.
Also, typed letters show up at the bottom of the screen as green (correct) or (red) incorrect.
The game is won once the correct word is guessed completely.
The game is lost once the stick figure is fully hung (10 guessed letters are incorrect).
Restart game by:
- Keyboard Shortcut: CMD+R on Mac or CTRL+R on Windows/Linux
- Menu Item: Game -> Restart
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
# 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 |
# 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 |
# 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 |
# 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 |
# 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:
Post a Comment