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
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../grid"
|
|
5
|
+
require_relative "../hangman"
|
|
6
|
+
|
|
7
|
+
module Mumble
|
|
8
|
+
module Screens
|
|
9
|
+
# Main gameplay screen - ties together grid, hangman, timer, and input
|
|
10
|
+
class Gameplay < Base
|
|
11
|
+
def initialize(level:, target_word:)
|
|
12
|
+
super()
|
|
13
|
+
@level = level
|
|
14
|
+
@target_word = target_word.upcase
|
|
15
|
+
@guesses = [] # Array of guess results
|
|
16
|
+
@current_input = "" # What user is typing
|
|
17
|
+
@wrong_count = 0 # Hangman stage (0-6)
|
|
18
|
+
@used_letters = {} # letter => :correct, :present, :absent
|
|
19
|
+
@start_time = nil # Timer starts when game begins
|
|
20
|
+
@game_over = false
|
|
21
|
+
@won = false
|
|
22
|
+
@timer_thread = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def show
|
|
26
|
+
Cursor.hide
|
|
27
|
+
draw_screen
|
|
28
|
+
|
|
29
|
+
# Main game loop
|
|
30
|
+
game_loop until @game_over
|
|
31
|
+
|
|
32
|
+
# Stop background timer
|
|
33
|
+
stop_timer_thread
|
|
34
|
+
|
|
35
|
+
# Show final state and wait for keypress
|
|
36
|
+
draw_screen
|
|
37
|
+
Input.wait_for_any_key
|
|
38
|
+
|
|
39
|
+
# Return result for game loop to handle
|
|
40
|
+
{ won: @won, level: @level, guesses: @guesses.length, time: elapsed_time }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# --- MAIN GAME LOOP ---
|
|
46
|
+
|
|
47
|
+
def game_loop
|
|
48
|
+
# Start timer immediately when game begins
|
|
49
|
+
@start_time ||= Time.now
|
|
50
|
+
|
|
51
|
+
# Start background timer thread if not already running
|
|
52
|
+
start_timer_thread unless @timer_thread&.alive?
|
|
53
|
+
|
|
54
|
+
# Read input (blocking)
|
|
55
|
+
char = Input.read_key
|
|
56
|
+
|
|
57
|
+
return unless char
|
|
58
|
+
|
|
59
|
+
handle_input(char)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Background thread to update timer display
|
|
63
|
+
def start_timer_thread
|
|
64
|
+
@timer_thread = Thread.new do
|
|
65
|
+
loop do
|
|
66
|
+
sleep(1)
|
|
67
|
+
break if @game_over
|
|
68
|
+
|
|
69
|
+
update_timer_display
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Stop the timer thread
|
|
75
|
+
def stop_timer_thread
|
|
76
|
+
@timer_thread&.kill
|
|
77
|
+
@timer_thread = nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def handle_input(char)
|
|
81
|
+
case char
|
|
82
|
+
when :enter
|
|
83
|
+
submit_guess if @current_input.length == 5
|
|
84
|
+
when :backspace
|
|
85
|
+
handle_backspace
|
|
86
|
+
when :escape
|
|
87
|
+
# Could add pause menu later
|
|
88
|
+
nil
|
|
89
|
+
else
|
|
90
|
+
handle_character(char)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def handle_character(char)
|
|
95
|
+
# Only accept letters, max 5 characters
|
|
96
|
+
return unless char.is_a?(String) && char.match?(/\A[a-zA-Z]\z/)
|
|
97
|
+
return if @current_input.length >= 5
|
|
98
|
+
|
|
99
|
+
@current_input += char.upcase
|
|
100
|
+
redraw_current_row
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def handle_backspace
|
|
104
|
+
return if @current_input.empty?
|
|
105
|
+
|
|
106
|
+
@current_input = @current_input[0...-1]
|
|
107
|
+
redraw_current_row
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def submit_guess
|
|
111
|
+
return if @current_input.length != 5
|
|
112
|
+
|
|
113
|
+
# Process the guess and get colored result
|
|
114
|
+
result = process_guess(@current_input)
|
|
115
|
+
|
|
116
|
+
# Animate the letter reveal
|
|
117
|
+
animate_letter_reveal(result)
|
|
118
|
+
|
|
119
|
+
# Clear input for next row
|
|
120
|
+
@current_input = ""
|
|
121
|
+
|
|
122
|
+
# Redraw to show results
|
|
123
|
+
draw_screen
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Animate each letter revealing with color
|
|
127
|
+
def animate_letter_reveal(result)
|
|
128
|
+
row_index = @guesses.length - 1 # Just added, so -1
|
|
129
|
+
base_row = grid_row + (row_index * (Grid.dimensions[:cell_height] + Grid.dimensions[:v_gap]))
|
|
130
|
+
|
|
131
|
+
result.each_with_index do |letter_result, col_index|
|
|
132
|
+
cell_col = grid_col + (col_index * (Grid.dimensions[:cell_width] + Grid.dimensions[:h_gap]))
|
|
133
|
+
|
|
134
|
+
# Brief flash white before showing color
|
|
135
|
+
Grid.draw_cell(row: base_row, col: cell_col, letter: letter_result[:letter], state: :typing)
|
|
136
|
+
sleep(0.1)
|
|
137
|
+
|
|
138
|
+
# Reveal with actual color
|
|
139
|
+
Grid.draw_cell(row: base_row, col: cell_col, letter: letter_result[:letter], state: letter_result[:state])
|
|
140
|
+
sleep(0.15)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# --- LAYOUT CALCULATIONS ---
|
|
145
|
+
|
|
146
|
+
def game_width
|
|
147
|
+
case Layout.size_category
|
|
148
|
+
when :large
|
|
149
|
+
Grid.total_width + spacing_between + Hangman.width + 20
|
|
150
|
+
else
|
|
151
|
+
Grid.total_width + spacing_between + Hangman.width + 10
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def game_height
|
|
156
|
+
case Layout.size_category
|
|
157
|
+
when :large
|
|
158
|
+
Grid.total_height + 10
|
|
159
|
+
else
|
|
160
|
+
Grid.total_height + 8
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def game_start_row
|
|
165
|
+
(Screen.height - game_height) / 2
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def game_start_col
|
|
169
|
+
(Screen.width - game_width) / 2
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def content_start_row
|
|
173
|
+
game_start_row + 4
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def grid_col
|
|
177
|
+
case Layout.size_category
|
|
178
|
+
when :large
|
|
179
|
+
game_start_col + 5
|
|
180
|
+
else
|
|
181
|
+
game_start_col + 2
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def grid_row
|
|
186
|
+
content_start_row
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def hangman_col
|
|
190
|
+
grid_col + Grid.total_width + spacing_between
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def hangman_row
|
|
194
|
+
content_start_row
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def spacing_between
|
|
198
|
+
case Layout.size_category
|
|
199
|
+
when :large
|
|
200
|
+
12
|
|
201
|
+
else
|
|
202
|
+
6
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def used_letters_row
|
|
207
|
+
hangman_row + Hangman.height + 2
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def used_letters_col
|
|
211
|
+
hangman_col
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def input_row
|
|
215
|
+
grid_row + Grid.total_height + 2
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def input_col
|
|
219
|
+
grid_col
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def timer_row
|
|
223
|
+
game_start_row + 2
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def timer_col
|
|
227
|
+
hangman_col
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# --- DRAWING METHODS ---
|
|
231
|
+
|
|
232
|
+
def draw_screen
|
|
233
|
+
Cursor.clear_screen
|
|
234
|
+
draw_game_border
|
|
235
|
+
draw_level_header
|
|
236
|
+
draw_timer
|
|
237
|
+
draw_grid
|
|
238
|
+
draw_hangman
|
|
239
|
+
draw_used_letters
|
|
240
|
+
draw_input_prompt
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Redraw only the current row being typed (for performance)
|
|
244
|
+
def redraw_current_row
|
|
245
|
+
Grid.draw_row(
|
|
246
|
+
row: grid_row + (@guesses.length * (Grid.dimensions[:cell_height] + Grid.dimensions[:v_gap])),
|
|
247
|
+
col: grid_col,
|
|
248
|
+
letters: [],
|
|
249
|
+
is_current: true,
|
|
250
|
+
current_input: @current_input
|
|
251
|
+
)
|
|
252
|
+
redraw_input_prompt
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Redraw just the input prompt area
|
|
256
|
+
def redraw_input_prompt
|
|
257
|
+
# Clear the line first
|
|
258
|
+
Cursor.move_to(input_row, input_col)
|
|
259
|
+
print " " * 50
|
|
260
|
+
Cursor.move_to(input_row + 1, input_col)
|
|
261
|
+
print " " * 50
|
|
262
|
+
draw_input_prompt
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Update timer without full redraw
|
|
266
|
+
def update_timer_display
|
|
267
|
+
Cursor.move_to(timer_row, timer_col)
|
|
268
|
+
print " " * 15 # Clear previous
|
|
269
|
+
Cursor.move_to(timer_row, timer_col)
|
|
270
|
+
time_text = "Time: #{format_time(elapsed_time)}"
|
|
271
|
+
print Colors.dim(time_text)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def draw_game_border
|
|
275
|
+
Box.draw(
|
|
276
|
+
row: game_start_row,
|
|
277
|
+
col: game_start_col,
|
|
278
|
+
width: game_width,
|
|
279
|
+
height: game_height,
|
|
280
|
+
color: :dim
|
|
281
|
+
)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def draw_level_header
|
|
285
|
+
text = "═══ LEVEL #{@level} ═══"
|
|
286
|
+
draw_centered(game_start_row + 1, text, color: :cyan)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def draw_timer
|
|
290
|
+
time_text = "Time: #{format_time(elapsed_time)}"
|
|
291
|
+
draw_at(timer_row, timer_col, time_text, color: :dim)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def format_time(seconds)
|
|
295
|
+
mins = seconds / 60
|
|
296
|
+
secs = seconds % 60
|
|
297
|
+
format("%d:%02d", mins, secs)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def elapsed_time
|
|
301
|
+
return 0 unless @start_time
|
|
302
|
+
|
|
303
|
+
(Time.now - @start_time).to_i
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def draw_grid
|
|
307
|
+
Grid.draw(
|
|
308
|
+
row: grid_row,
|
|
309
|
+
col: grid_col,
|
|
310
|
+
guesses: @guesses,
|
|
311
|
+
current_row: @guesses.length,
|
|
312
|
+
current_input: @current_input
|
|
313
|
+
)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def draw_hangman
|
|
317
|
+
Hangman.draw(
|
|
318
|
+
row: hangman_row,
|
|
319
|
+
col: hangman_col,
|
|
320
|
+
stage: @wrong_count
|
|
321
|
+
)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def draw_used_letters
|
|
325
|
+
return if @used_letters.empty?
|
|
326
|
+
|
|
327
|
+
draw_at(used_letters_row, used_letters_col, "Used Letters:", color: :dim)
|
|
328
|
+
|
|
329
|
+
row_offset = 1
|
|
330
|
+
letters_per_row = Layout.size_category == :large ? 13 : 9
|
|
331
|
+
|
|
332
|
+
@used_letters.keys.sort.each_slice(letters_per_row).with_index do |letter_group, index|
|
|
333
|
+
col = used_letters_col
|
|
334
|
+
Cursor.move_to(used_letters_row + row_offset + index, col)
|
|
335
|
+
|
|
336
|
+
letter_group.each do |letter|
|
|
337
|
+
state = @used_letters[letter]
|
|
338
|
+
color = case state
|
|
339
|
+
when :correct then :green
|
|
340
|
+
when :present then :orange
|
|
341
|
+
else :red
|
|
342
|
+
end
|
|
343
|
+
print Colors.send(color, "#{letter} ")
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def draw_input_prompt
|
|
349
|
+
if @game_over
|
|
350
|
+
if @won
|
|
351
|
+
draw_at(input_row, input_col, "🎉 Correct! Press any key...", color: :green)
|
|
352
|
+
else
|
|
353
|
+
draw_at(input_row, input_col, "The word was: #{@target_word}", color: :red)
|
|
354
|
+
draw_at(input_row + 1, input_col, "Press any key to continue...", color: :dim)
|
|
355
|
+
end
|
|
356
|
+
else
|
|
357
|
+
# Show current input with cursor indicator
|
|
358
|
+
display_input = @current_input.ljust(5, "_")
|
|
359
|
+
prompt = "Enter guess: #{display_input}"
|
|
360
|
+
draw_at(input_row, input_col, prompt, color: :white)
|
|
361
|
+
|
|
362
|
+
# Hint text
|
|
363
|
+
hint = if @current_input.length < 5
|
|
364
|
+
"(Type #{5 - @current_input.length} more letter#{"s" if 5 - @current_input.length > 1})"
|
|
365
|
+
else
|
|
366
|
+
"(Press ENTER to submit)"
|
|
367
|
+
end
|
|
368
|
+
draw_at(input_row + 1, input_col, hint, color: :dim)
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# --- GAME LOGIC ---
|
|
373
|
+
|
|
374
|
+
# NOTE: Two passes are intentional - Wordle scoring requires finding exact matches
|
|
375
|
+
# first, then checking remaining letters against what's left in the target word.
|
|
376
|
+
|
|
377
|
+
# rubocop:disable Style/CombinableLoops
|
|
378
|
+
def process_guess(guess)
|
|
379
|
+
guess = guess.upcase
|
|
380
|
+
result = []
|
|
381
|
+
target_letters = @target_word.chars
|
|
382
|
+
guess_letters = guess.chars
|
|
383
|
+
|
|
384
|
+
# First pass: mark correct positions
|
|
385
|
+
guess_letters.each_with_index do |letter, i|
|
|
386
|
+
if letter == target_letters[i]
|
|
387
|
+
result[i] = { letter: letter, state: :correct }
|
|
388
|
+
target_letters[i] = nil
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# Second pass: mark present/absent
|
|
393
|
+
guess_letters.each_with_index do |letter, i|
|
|
394
|
+
next if result[i]
|
|
395
|
+
|
|
396
|
+
target_index = target_letters.index(letter)
|
|
397
|
+
if target_index
|
|
398
|
+
result[i] = { letter: letter, state: :present }
|
|
399
|
+
target_letters[target_index] = nil
|
|
400
|
+
else
|
|
401
|
+
result[i] = { letter: letter, state: :absent }
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
# rubocop:enable Style/CombinableLoops
|
|
405
|
+
|
|
406
|
+
# Update used letters tracking
|
|
407
|
+
result.each do |r|
|
|
408
|
+
existing = @used_letters[r[:letter]]
|
|
409
|
+
next unless existing.nil? ||
|
|
410
|
+
(existing == :absent && r[:state] != :absent) ||
|
|
411
|
+
(existing == :present && r[:state] == :correct)
|
|
412
|
+
|
|
413
|
+
@used_letters[r[:letter]] = r[:state]
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Add to guesses
|
|
417
|
+
@guesses << result
|
|
418
|
+
|
|
419
|
+
# Check win condition
|
|
420
|
+
if result.all? { |r| r[:state] == :correct }
|
|
421
|
+
@won = true
|
|
422
|
+
@game_over = true
|
|
423
|
+
# Check lose condition - wrong guess increments hangman
|
|
424
|
+
elsif result.any? { |r| r[:state] != :correct }
|
|
425
|
+
@wrong_count += 1
|
|
426
|
+
@game_over = true if @wrong_count >= 6 || @guesses.length >= 6
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
result
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mumble
|
|
4
|
+
module Screens
|
|
5
|
+
# High scores leaderboard display
|
|
6
|
+
class HighScoresScreen < Base
|
|
7
|
+
def show
|
|
8
|
+
setup_screen
|
|
9
|
+
draw_leaderboard
|
|
10
|
+
wait_for_key
|
|
11
|
+
cleanup_screen
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def content_width
|
|
17
|
+
case size_category
|
|
18
|
+
when :large then 70
|
|
19
|
+
when :medium then 60
|
|
20
|
+
else 52
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def content_height
|
|
25
|
+
case size_category
|
|
26
|
+
when :large then 20
|
|
27
|
+
when :medium then 18
|
|
28
|
+
else 16
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def start_row
|
|
33
|
+
center_row_for(content_height)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def draw_leaderboard
|
|
37
|
+
box = draw_centered_box(
|
|
38
|
+
row: start_row,
|
|
39
|
+
width: content_width,
|
|
40
|
+
height: content_height,
|
|
41
|
+
title: "HIGH SCORES",
|
|
42
|
+
color: :yellow
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
col = box[:col]
|
|
46
|
+
row = box[:row]
|
|
47
|
+
|
|
48
|
+
scores = HighScores.all
|
|
49
|
+
|
|
50
|
+
if scores.empty?
|
|
51
|
+
draw_empty_state(row)
|
|
52
|
+
else
|
|
53
|
+
draw_header_row(row + 2, col)
|
|
54
|
+
draw_scores(row + 3, col, scores)
|
|
55
|
+
draw_player_best(row + content_height - 2, col)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Prompt
|
|
59
|
+
prompt_row = row + content_height + 1
|
|
60
|
+
draw_centered(prompt_row, "Press any key to continue...", color: :dim)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def draw_empty_state(row)
|
|
64
|
+
empty_row = row + (content_height / 2) - 1
|
|
65
|
+
draw_centered(empty_row, "No high scores yet!", color: :dim)
|
|
66
|
+
draw_centered(empty_row + 2, "Play a game to get on the leaderboard!", color: :yellow)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def draw_header_row(row, col)
|
|
70
|
+
draw_at(row, col + 3, "RANK", color: :dim)
|
|
71
|
+
draw_at(row, col + 10, "NAME", color: :dim)
|
|
72
|
+
draw_at(row, col + 28, "SCORE", color: :dim)
|
|
73
|
+
draw_at(row, col + 38, "LVL", color: :dim)
|
|
74
|
+
draw_at(row, col + 44, "DATE", color: :dim)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def draw_scores(start_row, col, scores)
|
|
78
|
+
scores.first(10).each_with_index do |score, index|
|
|
79
|
+
row = start_row + index
|
|
80
|
+
rank = index + 1
|
|
81
|
+
|
|
82
|
+
# Rank with medal for top 3
|
|
83
|
+
rank_text, rank_color = format_rank(rank)
|
|
84
|
+
draw_at(row, col + 3, rank_text, color: rank_color)
|
|
85
|
+
|
|
86
|
+
# Name (truncate if needed)
|
|
87
|
+
name = truncate(score[:name].to_s, 14)
|
|
88
|
+
name_color = score[:name] == player_name ? :cyan : :white
|
|
89
|
+
draw_at(row, col + 10, name, color: name_color)
|
|
90
|
+
|
|
91
|
+
# Score
|
|
92
|
+
score_text = format_score(score[:score])
|
|
93
|
+
draw_at(row, col + 28, score_text, color: :white)
|
|
94
|
+
|
|
95
|
+
# Level
|
|
96
|
+
draw_at(row, col + 38, score[:level].to_s, color: :dim)
|
|
97
|
+
|
|
98
|
+
# Date
|
|
99
|
+
date = score[:date].to_s[0, 10]
|
|
100
|
+
draw_at(row, col + 44, date, color: :dim)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def draw_player_best(row, col)
|
|
105
|
+
best_rank = HighScores.best_rank_for(player_name)
|
|
106
|
+
return unless best_rank
|
|
107
|
+
|
|
108
|
+
best = HighScores.best_score_for(player_name)
|
|
109
|
+
draw_at(row, col + 3, "Your best: ##{best_rank} with #{best[:score]} pts", color: :cyan)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def format_rank(rank)
|
|
113
|
+
case rank
|
|
114
|
+
when 1 then ["1st", :bold_yellow]
|
|
115
|
+
when 2 then ["2nd", :white]
|
|
116
|
+
when 3 then ["3rd", :red]
|
|
117
|
+
else ["#{rank}th", :dim]
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def format_score(score)
|
|
122
|
+
score.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Mumble
|
|
6
|
+
module Screens
|
|
7
|
+
# Game over screen with flashing text
|
|
8
|
+
class Lose < Base
|
|
9
|
+
GAME_OVER_SMALL = [
|
|
10
|
+
"╔═╗╔═╗╔╦╗╔═╗ ╔═╗╦ ╦╔═╗╦═╗",
|
|
11
|
+
"║ ╦╠═╣║║║║╣ ║ ║╚╗╔╝║╣ ╠╦╝",
|
|
12
|
+
"╚═╝╩ ╩╩ ╩╚═╝ ╚═╝ ╚╝ ╚═╝╩╚═"
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
GAME_OVER_LARGE = [
|
|
16
|
+
"██████╗ █████╗ ███╗ ███╗███████╗ ██████╗ ██╗ ██╗███████╗██████╗ ",
|
|
17
|
+
"██╔════╝ ██╔══██╗████╗ ████║██╔════╝ ██╔═══██╗██║ ██║██╔════╝██╔══██╗",
|
|
18
|
+
"██║ ███╗███████║██╔████╔██║█████╗ ██║ ██║██║ ██║█████╗ ██████╔╝",
|
|
19
|
+
"██║ ██║██╔══██║██║╚██╔╝██║██╔══╝ ██║ ██║╚██╗ ██╔╝██╔══╝ ██╔══██╗",
|
|
20
|
+
"╚██████╔╝██║ ██║██║ ╚═╝ ██║███████╗ ╚██████╔╝ ╚████╔╝ ███████╗██║ ██║",
|
|
21
|
+
"╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝"
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
def initialize(word:, level:, guesses:)
|
|
25
|
+
super()
|
|
26
|
+
@word = word
|
|
27
|
+
@level = level
|
|
28
|
+
@guesses = guesses
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def show
|
|
32
|
+
Cursor.hide
|
|
33
|
+
draw_screen
|
|
34
|
+
flash_game_over
|
|
35
|
+
Input.wait_for_any_key
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def game_over_text
|
|
41
|
+
Layout.size_category == :large ? GAME_OVER_LARGE : GAME_OVER_SMALL
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def text_start_row
|
|
45
|
+
(Screen.height - 25) / 2
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def text_start_col
|
|
49
|
+
Screen.center_col(game_over_text.first.length)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def draw_screen
|
|
53
|
+
Cursor.clear_screen
|
|
54
|
+
draw_game_over_text(:red)
|
|
55
|
+
draw_content
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def draw_game_over_text(color)
|
|
59
|
+
game_over_text.each_with_index do |line, index|
|
|
60
|
+
Cursor.move_to(text_start_row + index, text_start_col)
|
|
61
|
+
print Colors.send(color, line)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def flash_game_over
|
|
66
|
+
# Flash between red and dim red
|
|
67
|
+
6.times do
|
|
68
|
+
draw_game_over_text(:red)
|
|
69
|
+
pause(0.3)
|
|
70
|
+
draw_game_over_text(:dim)
|
|
71
|
+
pause(0.2)
|
|
72
|
+
end
|
|
73
|
+
# End on solid red
|
|
74
|
+
draw_game_over_text(:red)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def draw_content
|
|
78
|
+
content_row = text_start_row + game_over_text.length + 3
|
|
79
|
+
|
|
80
|
+
# Skull emoji line
|
|
81
|
+
draw_centered(content_row, "💀 💀 💀", color: :red)
|
|
82
|
+
|
|
83
|
+
# Word revealed
|
|
84
|
+
draw_centered(content_row + 3, "The word was:", color: :dim)
|
|
85
|
+
draw_centered(content_row + 5, @word, color: :cyan)
|
|
86
|
+
|
|
87
|
+
# Stats
|
|
88
|
+
draw_centered(content_row + 8, "Level: #{@level} | Guesses: #{@guesses}/6", color: :dim)
|
|
89
|
+
|
|
90
|
+
# Encouragement
|
|
91
|
+
messages = [
|
|
92
|
+
"Better luck next time!",
|
|
93
|
+
"Don't give up!",
|
|
94
|
+
"You'll get it next time!",
|
|
95
|
+
"Practice makes perfect!"
|
|
96
|
+
]
|
|
97
|
+
draw_centered(content_row + 11, messages.sample, color: :yellow)
|
|
98
|
+
|
|
99
|
+
# Continue prompt
|
|
100
|
+
draw_centered(content_row + 14, "Press any key to continue...", color: :dim)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def pause(seconds)
|
|
104
|
+
sleep(seconds)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|