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.
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: []