mumble_game 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +43 -0
- data/LICENSE +21 -0
- data/README.md +130 -0
- data/bin/mumble +6 -0
- data/lib/mumble/animations.rb +112 -0
- data/lib/mumble/box.rb +139 -0
- data/lib/mumble/colors.rb +83 -0
- data/lib/mumble/config.rb +172 -0
- data/lib/mumble/cursor.rb +82 -0
- data/lib/mumble/error_handler.rb +74 -0
- data/lib/mumble/grid.rb +182 -0
- data/lib/mumble/hangman.rb +440 -0
- data/lib/mumble/high_scores.rb +109 -0
- data/lib/mumble/input.rb +143 -0
- data/lib/mumble/layout.rb +208 -0
- data/lib/mumble/scorer.rb +78 -0
- data/lib/mumble/screen.rb +61 -0
- data/lib/mumble/screens/base.rb +142 -0
- data/lib/mumble/screens/gameplay.rb +433 -0
- data/lib/mumble/screens/high_scores.rb +126 -0
- data/lib/mumble/screens/lose.rb +108 -0
- data/lib/mumble/screens/main_menu.rb +130 -0
- data/lib/mumble/screens/name_input.rb +121 -0
- data/lib/mumble/screens/play_again.rb +97 -0
- data/lib/mumble/screens/profile.rb +154 -0
- data/lib/mumble/screens/quit_confirm.rb +103 -0
- data/lib/mumble/screens/rules.rb +102 -0
- data/lib/mumble/screens/splash.rb +130 -0
- data/lib/mumble/screens/win.rb +139 -0
- data/lib/mumble/storage.rb +85 -0
- data/lib/mumble/version.rb +5 -0
- data/lib/mumble/word_cache.rb +131 -0
- data/lib/mumble/word_service.rb +192 -0
- data/lib/mumble.rb +340 -0
- metadata +137 -0
data/lib/mumble.rb
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "mumble/version"
|
|
4
|
+
require_relative "mumble/colors"
|
|
5
|
+
require_relative "mumble/cursor"
|
|
6
|
+
require_relative "mumble/screen"
|
|
7
|
+
require_relative "mumble/box"
|
|
8
|
+
require_relative "mumble/input"
|
|
9
|
+
require_relative "mumble/layout"
|
|
10
|
+
require_relative "mumble/storage"
|
|
11
|
+
require_relative "mumble/config"
|
|
12
|
+
require_relative "mumble/high_scores"
|
|
13
|
+
require_relative "mumble/word_cache"
|
|
14
|
+
require_relative "mumble/word_service"
|
|
15
|
+
require_relative "mumble/scorer"
|
|
16
|
+
require_relative "mumble/grid"
|
|
17
|
+
require_relative "mumble/hangman"
|
|
18
|
+
require_relative "mumble/animations"
|
|
19
|
+
require_relative "mumble/error_handler"
|
|
20
|
+
|
|
21
|
+
# Screens
|
|
22
|
+
require_relative "mumble/screens/base"
|
|
23
|
+
require_relative "mumble/screens/splash"
|
|
24
|
+
require_relative "mumble/screens/main_menu"
|
|
25
|
+
require_relative "mumble/screens/rules"
|
|
26
|
+
require_relative "mumble/screens/high_scores"
|
|
27
|
+
require_relative "mumble/screens/profile"
|
|
28
|
+
require_relative "mumble/screens/name_input"
|
|
29
|
+
require_relative "mumble/screens/quit_confirm"
|
|
30
|
+
require_relative "mumble/screens/gameplay"
|
|
31
|
+
require_relative "mumble/screens/win"
|
|
32
|
+
require_relative "mumble/screens/lose"
|
|
33
|
+
require_relative "mumble/screens/play_again"
|
|
34
|
+
|
|
35
|
+
module Mumble
|
|
36
|
+
class Error < StandardError; end
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
# Boots the game - called from bin/mumble executable
|
|
40
|
+
def start
|
|
41
|
+
Game.new.run
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Main game controller
|
|
46
|
+
class Game
|
|
47
|
+
def initialize
|
|
48
|
+
@config = Config.load
|
|
49
|
+
@session_stats = { games: 0, won: 0, score: 0 }
|
|
50
|
+
@current_level = 1
|
|
51
|
+
setup_signal_handlers
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def run
|
|
55
|
+
ErrorHandler.check_terminal_size!
|
|
56
|
+
show_splash
|
|
57
|
+
check_first_time
|
|
58
|
+
main_loop
|
|
59
|
+
rescue Interrupt
|
|
60
|
+
# Ctrl+C pressed - exit gracefully
|
|
61
|
+
graceful_exit
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
ErrorHandler.fatal(e, context: "Main game loop")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# Handle Ctrl+C and other signals
|
|
69
|
+
def setup_signal_handlers
|
|
70
|
+
# Ctrl+C - graceful exit
|
|
71
|
+
Signal.trap("INT") { graceful_exit }
|
|
72
|
+
|
|
73
|
+
# Terminal resize - check size is still valid
|
|
74
|
+
Signal.trap("WINCH") { handle_resize } if Signal.list.key?("WINCH")
|
|
75
|
+
rescue StandardError
|
|
76
|
+
# Some signals might not be available on all platforms
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def graceful_exit
|
|
81
|
+
Cursor.clear_screen
|
|
82
|
+
Cursor.show
|
|
83
|
+
Cursor.move_to(Screen.center_row, Screen.center_col(25))
|
|
84
|
+
print Colors.cyan("Thanks for playing!")
|
|
85
|
+
Cursor.move_to(Screen.center_row + 2, 1)
|
|
86
|
+
exit(0)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def handle_resize
|
|
90
|
+
# rubocop:disable Style/GuardClause
|
|
91
|
+
unless Screen.valid_size?
|
|
92
|
+
Cursor.clear_screen
|
|
93
|
+
Cursor.move_to(1, 1)
|
|
94
|
+
print Colors.yellow("Terminal too small! Please resize.")
|
|
95
|
+
end
|
|
96
|
+
# rubocop:enable Style/GuardClause
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def show_splash
|
|
100
|
+
Screens::Splash.new(@config).show
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def check_first_time
|
|
104
|
+
return if Config.exists?
|
|
105
|
+
|
|
106
|
+
name = Screens::NameInput.new(@config, mode: :new).show
|
|
107
|
+
@config = Config.update_name(@config, name) if name
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def main_loop
|
|
111
|
+
loop do
|
|
112
|
+
case show_main_menu
|
|
113
|
+
when :new_game
|
|
114
|
+
@current_level = 1
|
|
115
|
+
play_game_loop
|
|
116
|
+
when :high_scores
|
|
117
|
+
show_high_scores
|
|
118
|
+
when :profile
|
|
119
|
+
handle_profile
|
|
120
|
+
when :rules
|
|
121
|
+
show_rules
|
|
122
|
+
when :quit
|
|
123
|
+
break if confirm_quit
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
goodbye
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def show_main_menu
|
|
131
|
+
Screens::MainMenu.new(@config).show
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Main game loop - handles level progression
|
|
135
|
+
def play_game_loop
|
|
136
|
+
loop do
|
|
137
|
+
result = play_single_game
|
|
138
|
+
|
|
139
|
+
if result[:won]
|
|
140
|
+
# Show win screen and get score
|
|
141
|
+
score = Screens::Win.new(
|
|
142
|
+
guesses: result[:guesses],
|
|
143
|
+
time: result[:time],
|
|
144
|
+
level: @current_level,
|
|
145
|
+
word: result[:word]
|
|
146
|
+
).show
|
|
147
|
+
|
|
148
|
+
# Update stats
|
|
149
|
+
update_stats(won: true, score: score)
|
|
150
|
+
|
|
151
|
+
# Show play again prompt
|
|
152
|
+
choice = Screens::PlayAgain.new(won: true, level: @current_level, score: score).show
|
|
153
|
+
|
|
154
|
+
case choice
|
|
155
|
+
when :next_level
|
|
156
|
+
@current_level += 1
|
|
157
|
+
when :menu
|
|
158
|
+
break
|
|
159
|
+
when :quit
|
|
160
|
+
goodbye
|
|
161
|
+
exit
|
|
162
|
+
end
|
|
163
|
+
else
|
|
164
|
+
# Show lose screen
|
|
165
|
+
Screens::Lose.new(
|
|
166
|
+
word: result[:word],
|
|
167
|
+
level: @current_level,
|
|
168
|
+
guesses: result[:guesses]
|
|
169
|
+
).show
|
|
170
|
+
|
|
171
|
+
# Update stats
|
|
172
|
+
update_stats(won: false, score: 0)
|
|
173
|
+
|
|
174
|
+
# Show play again prompt
|
|
175
|
+
choice = Screens::PlayAgain.new(won: false, level: @current_level).show
|
|
176
|
+
|
|
177
|
+
case choice
|
|
178
|
+
when :retry
|
|
179
|
+
# Same level, try again
|
|
180
|
+
next
|
|
181
|
+
when :menu
|
|
182
|
+
break
|
|
183
|
+
when :quit
|
|
184
|
+
goodbye
|
|
185
|
+
exit
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Play a single round
|
|
192
|
+
def play_single_game
|
|
193
|
+
# Fetch a word for this game
|
|
194
|
+
word = fetch_word
|
|
195
|
+
|
|
196
|
+
# Run gameplay screen
|
|
197
|
+
gameplay = Screens::Gameplay.new(level: @current_level, target_word: word)
|
|
198
|
+
result = gameplay.show
|
|
199
|
+
|
|
200
|
+
# Add the word to the result
|
|
201
|
+
result[:word] = word
|
|
202
|
+
result
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Fetch a word from the word service
|
|
206
|
+
def fetch_word
|
|
207
|
+
# Show loading spinner while fetching
|
|
208
|
+
word = nil
|
|
209
|
+
|
|
210
|
+
if WordCache.needs_refresh?
|
|
211
|
+
Cursor.clear_screen
|
|
212
|
+
word = Animations.spinner(message: "Fetching words") do
|
|
213
|
+
WordService.next_word
|
|
214
|
+
end
|
|
215
|
+
else
|
|
216
|
+
word = WordService.next_word
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Fallback if service fails
|
|
220
|
+
word || fallback_words.sample
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Emergency fallback words if API and cache both fail
|
|
224
|
+
def fallback_words
|
|
225
|
+
%w[CRANE HOUSE PLANT BREAD CHAIR MUSIC WATER LIGHT STONE DREAM]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Update session and persistent stats
|
|
229
|
+
def update_stats(won:, score:)
|
|
230
|
+
@session_stats[:games] += 1
|
|
231
|
+
@session_stats[:won] += 1 if won
|
|
232
|
+
@session_stats[:score] += score
|
|
233
|
+
|
|
234
|
+
# Update persistent config
|
|
235
|
+
@config = Config.record_game(@config, won: won, score: score, level: @current_level)
|
|
236
|
+
|
|
237
|
+
# Check for high score
|
|
238
|
+
return unless won && score.positive?
|
|
239
|
+
|
|
240
|
+
player_name = @config[:player_name]
|
|
241
|
+
HighScores.add(name: player_name, score: score, level: @current_level)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def show_high_scores
|
|
245
|
+
Screens::HighScoresScreen.new(@config).show
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def handle_profile
|
|
249
|
+
loop do
|
|
250
|
+
case Screens::Profile.new(@config).show
|
|
251
|
+
when :change_name
|
|
252
|
+
change_name
|
|
253
|
+
when :reset_stats
|
|
254
|
+
reset_stats
|
|
255
|
+
when :delete_all
|
|
256
|
+
delete_all_data
|
|
257
|
+
break
|
|
258
|
+
when :back
|
|
259
|
+
break
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def change_name
|
|
265
|
+
name = Screens::NameInput.new(@config, mode: :change).show
|
|
266
|
+
@config = Config.update_name(@config, name) if name
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def reset_stats
|
|
270
|
+
Cursor.clear_screen
|
|
271
|
+
Cursor.move_to(Screen.center_row, Screen.center_col(30))
|
|
272
|
+
print Colors.yellow("Reset all statistics? (y/n)")
|
|
273
|
+
|
|
274
|
+
loop do
|
|
275
|
+
key = Input.read_key.to_s.downcase
|
|
276
|
+
if key == "y"
|
|
277
|
+
@config = Config.reset_stats(@config)
|
|
278
|
+
break
|
|
279
|
+
elsif %w[n escape].include?(key)
|
|
280
|
+
break
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def delete_all_data
|
|
286
|
+
Cursor.clear_screen
|
|
287
|
+
Cursor.move_to(Screen.center_row - 2, Screen.center_col(45))
|
|
288
|
+
print Colors.red("WARNING: This will delete ALL your data!")
|
|
289
|
+
Cursor.move_to(Screen.center_row, Screen.center_col(35))
|
|
290
|
+
print Colors.yellow("Type DELETE to confirm: ")
|
|
291
|
+
Cursor.show
|
|
292
|
+
|
|
293
|
+
input = ""
|
|
294
|
+
loop do
|
|
295
|
+
key = Input.read_key
|
|
296
|
+
case key
|
|
297
|
+
when :enter
|
|
298
|
+
break
|
|
299
|
+
when :escape
|
|
300
|
+
return
|
|
301
|
+
when :backspace
|
|
302
|
+
input = input[0...-1]
|
|
303
|
+
else
|
|
304
|
+
input += key.to_s if key.is_a?(String)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
Cursor.move_to(Screen.center_row, Screen.center_col(35) + 24)
|
|
308
|
+
print "#{input} "
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
Cursor.hide
|
|
312
|
+
|
|
313
|
+
return unless input == "DELETE"
|
|
314
|
+
|
|
315
|
+
Config.delete
|
|
316
|
+
HighScores.clear
|
|
317
|
+
WordCache.clear
|
|
318
|
+
@config = Config.load
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def show_rules
|
|
322
|
+
Screens::Rules.new(@config).show
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def confirm_quit
|
|
326
|
+
result = Screens::QuitConfirm.new(@config, session_stats: @session_stats).show
|
|
327
|
+
result == :quit
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def goodbye
|
|
331
|
+
Cursor.clear_screen
|
|
332
|
+
Cursor.move_to(Screen.center_row, Screen.center_col(30))
|
|
333
|
+
print Colors.cyan("Thanks for playing Mumble!")
|
|
334
|
+
Cursor.move_to(Screen.center_row + 2, Screen.center_col(15))
|
|
335
|
+
print Colors.dim("See you soon! 👋")
|
|
336
|
+
Cursor.move_to(Screen.center_row + 5, 1)
|
|
337
|
+
Cursor.show
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mumble_game
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Hady Mohamed
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: pastel
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.8'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.8'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: tty-cursor
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.7'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.7'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: tty-reader
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.9'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.9'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: tty-screen
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.8'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.8'
|
|
68
|
+
description: Mumble combines Wordle-style word guessing with classic hangman. Guess
|
|
69
|
+
the 5-letter word before the hangman is complete! Features colorful terminal UI,
|
|
70
|
+
high scores, and statistics tracking.
|
|
71
|
+
email:
|
|
72
|
+
- devhady87@gmail.com
|
|
73
|
+
executables:
|
|
74
|
+
- mumble
|
|
75
|
+
extensions: []
|
|
76
|
+
extra_rdoc_files: []
|
|
77
|
+
files:
|
|
78
|
+
- CHANGELOG.md
|
|
79
|
+
- LICENSE
|
|
80
|
+
- README.md
|
|
81
|
+
- bin/mumble
|
|
82
|
+
- lib/mumble.rb
|
|
83
|
+
- lib/mumble/animations.rb
|
|
84
|
+
- lib/mumble/box.rb
|
|
85
|
+
- lib/mumble/colors.rb
|
|
86
|
+
- lib/mumble/config.rb
|
|
87
|
+
- lib/mumble/cursor.rb
|
|
88
|
+
- lib/mumble/error_handler.rb
|
|
89
|
+
- lib/mumble/grid.rb
|
|
90
|
+
- lib/mumble/hangman.rb
|
|
91
|
+
- lib/mumble/high_scores.rb
|
|
92
|
+
- lib/mumble/input.rb
|
|
93
|
+
- lib/mumble/layout.rb
|
|
94
|
+
- lib/mumble/scorer.rb
|
|
95
|
+
- lib/mumble/screen.rb
|
|
96
|
+
- lib/mumble/screens/base.rb
|
|
97
|
+
- lib/mumble/screens/gameplay.rb
|
|
98
|
+
- lib/mumble/screens/high_scores.rb
|
|
99
|
+
- lib/mumble/screens/lose.rb
|
|
100
|
+
- lib/mumble/screens/main_menu.rb
|
|
101
|
+
- lib/mumble/screens/name_input.rb
|
|
102
|
+
- lib/mumble/screens/play_again.rb
|
|
103
|
+
- lib/mumble/screens/profile.rb
|
|
104
|
+
- lib/mumble/screens/quit_confirm.rb
|
|
105
|
+
- lib/mumble/screens/rules.rb
|
|
106
|
+
- lib/mumble/screens/splash.rb
|
|
107
|
+
- lib/mumble/screens/win.rb
|
|
108
|
+
- lib/mumble/storage.rb
|
|
109
|
+
- lib/mumble/version.rb
|
|
110
|
+
- lib/mumble/word_cache.rb
|
|
111
|
+
- lib/mumble/word_service.rb
|
|
112
|
+
homepage: https://github.com/hadym/mumble_game
|
|
113
|
+
licenses:
|
|
114
|
+
- MIT
|
|
115
|
+
metadata:
|
|
116
|
+
homepage_uri: https://github.com/hadym/mumble_game
|
|
117
|
+
source_code_uri: https://github.com/hadym/mumble_game/tree/main
|
|
118
|
+
changelog_uri: https://github.com/hadym/mumble_game/blob/main/CHANGELOG.md
|
|
119
|
+
rubygems_mfa_required: 'true'
|
|
120
|
+
rdoc_options: []
|
|
121
|
+
require_paths:
|
|
122
|
+
- lib
|
|
123
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
124
|
+
requirements:
|
|
125
|
+
- - ">="
|
|
126
|
+
- !ruby/object:Gem::Version
|
|
127
|
+
version: 3.0.0
|
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
129
|
+
requirements:
|
|
130
|
+
- - ">="
|
|
131
|
+
- !ruby/object:Gem::Version
|
|
132
|
+
version: '0'
|
|
133
|
+
requirements: []
|
|
134
|
+
rubygems_version: 3.7.2
|
|
135
|
+
specification_version: 4
|
|
136
|
+
summary: A terminal-based word guessing game with hangman
|
|
137
|
+
test_files: []
|