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,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mumble
|
|
4
|
+
module Screens
|
|
5
|
+
# Main menu with game options
|
|
6
|
+
class MainMenu < Base
|
|
7
|
+
MENU_OPTIONS = [
|
|
8
|
+
{ key: :new_game, label: "New Game" },
|
|
9
|
+
{ key: :high_scores, label: "High Scores" },
|
|
10
|
+
{ key: :profile, label: "My Profile" },
|
|
11
|
+
{ key: :rules, label: "How to Play" },
|
|
12
|
+
{ key: :quit, label: "Quit" }
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
LOGO_SMALL = [
|
|
16
|
+
"╔╦╗╦ ╦╔╦╗╔╗ ╦ ╔═╗",
|
|
17
|
+
"║║║║ ║║║║╠╩╗║ ║╣ ",
|
|
18
|
+
"╩ ╩╚═╝╩ ╩╚═╝╩═╝╚═╝"
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
LOGO_LARGE = [
|
|
22
|
+
"███╗ ███╗██╗ ██╗███╗ ███╗██████╗ ██╗ ███████╗",
|
|
23
|
+
"████╗ ████║██║ ██║████╗ ████║██╔══██╗██║ ██╔════╝",
|
|
24
|
+
"██╔████╔██║██║ ██║██╔████╔██║██████╔╝██║ █████╗ ",
|
|
25
|
+
"██║╚██╔╝██║██║ ██║██║╚██╔╝██║██╔══██╗██║ ██╔══╝ ",
|
|
26
|
+
"██║ ╚═╝ ██║╚██████╔╝██║ ╚═╝ ██║██████╔╝███████╗███████╗",
|
|
27
|
+
"╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝╚══════╝"
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
def show
|
|
31
|
+
setup_screen
|
|
32
|
+
draw_header
|
|
33
|
+
draw_menu
|
|
34
|
+
selection = handle_input
|
|
35
|
+
cleanup_screen
|
|
36
|
+
selection
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def logo
|
|
42
|
+
Layout.use_large_logo? ? LOGO_LARGE : LOGO_SMALL
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def box_width
|
|
46
|
+
Layout.menu_box_width
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def item_spacing
|
|
50
|
+
Layout.menu_item_spacing
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def box_padding
|
|
54
|
+
Layout.menu_box_padding
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def box_height
|
|
58
|
+
# Options count + spacing between items + top/bottom padding
|
|
59
|
+
option_lines = MENU_OPTIONS.length + ((MENU_OPTIONS.length - 1) * item_spacing)
|
|
60
|
+
option_lines + (box_padding * 2) + 2
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def menu_start_row
|
|
64
|
+
Screen.center_row - (box_height / 2)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def draw_header
|
|
68
|
+
# Draw logo
|
|
69
|
+
logo_row = menu_start_row - logo.length - 5
|
|
70
|
+
logo.each_with_index do |line, index|
|
|
71
|
+
draw_centered(logo_row + index, line, color: :cyan)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Welcome message
|
|
75
|
+
welcome_row = logo_row + logo.length + 2
|
|
76
|
+
if Config.exists?
|
|
77
|
+
draw_centered(welcome_row, "Welcome back, #{player_name}!", color: :white)
|
|
78
|
+
|
|
79
|
+
# Show streak if any
|
|
80
|
+
streak = config[:stats][:current_streak]
|
|
81
|
+
draw_centered(welcome_row + 1, "Current streak: #{streak} 🔥", color: :yellow) if streak.positive?
|
|
82
|
+
else
|
|
83
|
+
draw_centered(welcome_row, "Welcome to Mumble!", color: :white)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def draw_menu(selected_index = 0)
|
|
88
|
+
box_row = menu_start_row
|
|
89
|
+
box_col = Screen.center_col(box_width)
|
|
90
|
+
|
|
91
|
+
Box.draw(row: box_row, col: box_col, width: box_width, height: box_height, color: :dim)
|
|
92
|
+
|
|
93
|
+
MENU_OPTIONS.each_with_index do |option, index|
|
|
94
|
+
# Calculate row with spacing
|
|
95
|
+
row = box_row + box_padding + 1 + (index * (1 + item_spacing))
|
|
96
|
+
|
|
97
|
+
if index == selected_index
|
|
98
|
+
text = "▸ #{option[:label]}"
|
|
99
|
+
color = :green
|
|
100
|
+
else
|
|
101
|
+
text = " #{option[:label]}"
|
|
102
|
+
color = :white
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
col = box_col + ((box_width - text.length) / 2)
|
|
106
|
+
draw_at(row, col, text, color: color)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Navigation hint
|
|
110
|
+
hint_row = box_row + box_height + 2
|
|
111
|
+
draw_centered(hint_row, "Use ↑↓ or 1-5, then ENTER", color: :dim)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def handle_input
|
|
115
|
+
selected = 0
|
|
116
|
+
|
|
117
|
+
loop do
|
|
118
|
+
draw_menu(selected)
|
|
119
|
+
|
|
120
|
+
result = Input.read_menu_selection(options_count: MENU_OPTIONS.length, initial: selected) do |new_index|
|
|
121
|
+
selected = new_index
|
|
122
|
+
draw_menu(selected)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
return MENU_OPTIONS[result][:key] if result
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mumble
|
|
4
|
+
module Screens
|
|
5
|
+
# Name input screen for new players or name changes
|
|
6
|
+
class NameInput < Base
|
|
7
|
+
MAX_NAME_LENGTH = 20
|
|
8
|
+
|
|
9
|
+
def initialize(config = nil, mode: :new)
|
|
10
|
+
super(config)
|
|
11
|
+
@mode = mode # :new for first time, :change for updating
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def show
|
|
15
|
+
setup_screen
|
|
16
|
+
draw_screen
|
|
17
|
+
name = handle_input
|
|
18
|
+
cleanup_screen
|
|
19
|
+
name
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def content_width
|
|
25
|
+
case size_category
|
|
26
|
+
when :large then 50
|
|
27
|
+
when :medium then 44
|
|
28
|
+
else 38
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def content_height
|
|
33
|
+
case size_category
|
|
34
|
+
when :large then 12
|
|
35
|
+
when :medium then 10
|
|
36
|
+
else 9
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def start_row
|
|
41
|
+
center_row_for(content_height)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def title
|
|
45
|
+
@mode == :new ? "WELCOME" : "CHANGE NAME"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def prompt_text
|
|
49
|
+
@mode == :new ? "What should we call you?" : "Enter your new name:"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def draw_screen(current_input = "")
|
|
53
|
+
Cursor.clear_screen
|
|
54
|
+
|
|
55
|
+
box = draw_centered_box(
|
|
56
|
+
row: start_row,
|
|
57
|
+
width: content_width,
|
|
58
|
+
height: content_height,
|
|
59
|
+
title: title,
|
|
60
|
+
color: :cyan
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
box[:col]
|
|
64
|
+
row = box[:row]
|
|
65
|
+
|
|
66
|
+
# Welcome message or current name
|
|
67
|
+
if @mode == :new
|
|
68
|
+
draw_centered(row + 2, "Welcome to Mumble!", color: :white)
|
|
69
|
+
else
|
|
70
|
+
draw_centered(row + 2, "Current name: #{player_name}", color: :dim)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Prompt
|
|
74
|
+
draw_centered(row + 4, prompt_text, color: :white)
|
|
75
|
+
|
|
76
|
+
# Input field
|
|
77
|
+
input_row = row + 6
|
|
78
|
+
input_width = MAX_NAME_LENGTH + 2
|
|
79
|
+
input_col = Screen.center_col(input_width)
|
|
80
|
+
|
|
81
|
+
# Draw input box
|
|
82
|
+
draw_at(input_row, input_col - 1, "[", color: :dim)
|
|
83
|
+
draw_at(input_row, input_col + MAX_NAME_LENGTH, "]", color: :dim)
|
|
84
|
+
|
|
85
|
+
# Draw current input with cursor
|
|
86
|
+
padded_input = current_input.ljust(MAX_NAME_LENGTH, "_")
|
|
87
|
+
draw_at(input_row, input_col, padded_input, color: :green)
|
|
88
|
+
|
|
89
|
+
# Hint
|
|
90
|
+
hint_row = row + content_height + 1
|
|
91
|
+
draw_centered(hint_row, "Press ENTER when done (ESC to cancel)", color: :dim)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def handle_input
|
|
95
|
+
buffer = ""
|
|
96
|
+
|
|
97
|
+
loop do
|
|
98
|
+
draw_screen(buffer)
|
|
99
|
+
|
|
100
|
+
key = Input.read_key
|
|
101
|
+
|
|
102
|
+
case key
|
|
103
|
+
when :enter
|
|
104
|
+
return finalize_name(buffer)
|
|
105
|
+
when :escape
|
|
106
|
+
return nil
|
|
107
|
+
when :backspace
|
|
108
|
+
buffer = buffer[0...-1] unless buffer.empty?
|
|
109
|
+
else
|
|
110
|
+
buffer += key if key.is_a?(String) && key.match?(/[a-zA-Z0-9 ]/) && buffer.length < MAX_NAME_LENGTH
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def finalize_name(input)
|
|
116
|
+
name = input.strip
|
|
117
|
+
name.empty? ? "Player" : name
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Mumble
|
|
6
|
+
module Screens
|
|
7
|
+
# Play again prompt after win or lose
|
|
8
|
+
class PlayAgain < Base
|
|
9
|
+
OPTIONS_WIN = [
|
|
10
|
+
{ label: "Next Level", key: :next_level },
|
|
11
|
+
{ label: "Main Menu", key: :menu },
|
|
12
|
+
{ label: "Quit", key: :quit }
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
OPTIONS_LOSE = [
|
|
16
|
+
{ label: "Try Again", key: :retry },
|
|
17
|
+
{ label: "Main Menu", key: :menu },
|
|
18
|
+
{ label: "Quit", key: :quit }
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
def initialize(won:, level:, score: 0)
|
|
22
|
+
super()
|
|
23
|
+
@won = won
|
|
24
|
+
@level = level
|
|
25
|
+
@score = score
|
|
26
|
+
@options = won ? OPTIONS_WIN : OPTIONS_LOSE
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def show
|
|
30
|
+
Cursor.hide
|
|
31
|
+
selected = 0
|
|
32
|
+
|
|
33
|
+
loop do
|
|
34
|
+
draw_screen(selected)
|
|
35
|
+
|
|
36
|
+
result = Input.read_menu_selection(options_count: @options.length, initial: selected) do |new_index|
|
|
37
|
+
selected = new_index
|
|
38
|
+
draw_screen(selected)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
return @options[result][:key] if result
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def draw_screen(selected_index)
|
|
48
|
+
Cursor.clear_screen
|
|
49
|
+
|
|
50
|
+
# Header
|
|
51
|
+
header_row = game_start_row
|
|
52
|
+
if @won
|
|
53
|
+
draw_centered(header_row, "🎉 Level #{@level} Complete! 🎉", color: :green)
|
|
54
|
+
draw_centered(header_row + 2, "Score: #{@score}", color: :yellow)
|
|
55
|
+
else
|
|
56
|
+
draw_centered(header_row, "Level #{@level}", color: :red)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Question
|
|
60
|
+
question_row = header_row + 5
|
|
61
|
+
draw_centered(question_row, "What would you like to do?", color: :white)
|
|
62
|
+
|
|
63
|
+
# Options box
|
|
64
|
+
box_width = 25
|
|
65
|
+
box_height = @options.length + 4
|
|
66
|
+
box_row = question_row + 3
|
|
67
|
+
box_col = Screen.center_col(box_width)
|
|
68
|
+
|
|
69
|
+
Box.draw(row: box_row, col: box_col, width: box_width, height: box_height, color: :dim)
|
|
70
|
+
|
|
71
|
+
# Draw options
|
|
72
|
+
@options.each_with_index do |option, index|
|
|
73
|
+
row = box_row + 2 + index
|
|
74
|
+
|
|
75
|
+
if index == selected_index
|
|
76
|
+
text = "▸ #{option[:label]}"
|
|
77
|
+
color = :green
|
|
78
|
+
else
|
|
79
|
+
text = " #{option[:label]}"
|
|
80
|
+
color = :white
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
col = box_col + ((box_width - text.length) / 2)
|
|
84
|
+
draw_at(row, col, text, color: color)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Navigation hint
|
|
88
|
+
hint_row = box_row + box_height + 2
|
|
89
|
+
draw_centered(hint_row, "Use ↑↓ arrows, then ENTER", color: :dim)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def game_start_row
|
|
93
|
+
(Screen.height - 20) / 2
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mumble
|
|
4
|
+
module Screens
|
|
5
|
+
# Player profile with stats and settings
|
|
6
|
+
class Profile < Base
|
|
7
|
+
MENU_OPTIONS = [
|
|
8
|
+
{ key: :change_name, label: "Change Name" },
|
|
9
|
+
{ key: :reset_stats, label: "Reset Statistics" },
|
|
10
|
+
{ key: :delete_all, label: "Delete All Data" },
|
|
11
|
+
{ key: :back, label: "Back to Menu" }
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
def show
|
|
15
|
+
setup_screen
|
|
16
|
+
draw_profile
|
|
17
|
+
selection = handle_input
|
|
18
|
+
cleanup_screen
|
|
19
|
+
selection
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def content_width
|
|
25
|
+
case size_category
|
|
26
|
+
when :large then 60
|
|
27
|
+
when :medium then 50
|
|
28
|
+
else 44
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def content_height
|
|
33
|
+
case size_category
|
|
34
|
+
when :large then 22
|
|
35
|
+
when :medium then 20
|
|
36
|
+
else 18
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def start_row
|
|
41
|
+
center_row_for(content_height)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def draw_profile(selected_index = 0)
|
|
45
|
+
Cursor.clear_screen
|
|
46
|
+
|
|
47
|
+
box = draw_centered_box(
|
|
48
|
+
row: start_row,
|
|
49
|
+
width: content_width,
|
|
50
|
+
height: content_height,
|
|
51
|
+
title: "MY PROFILE",
|
|
52
|
+
color: :cyan
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
col = box[:col]
|
|
56
|
+
row = box[:row]
|
|
57
|
+
|
|
58
|
+
current_row = row + 2
|
|
59
|
+
|
|
60
|
+
# Player info
|
|
61
|
+
draw_at(current_row, col + 3, "Name:", color: :dim)
|
|
62
|
+
draw_at(current_row, col + 15, player_name, color: :white)
|
|
63
|
+
current_row += 1
|
|
64
|
+
|
|
65
|
+
draw_at(current_row, col + 3, "Playing since:", color: :dim)
|
|
66
|
+
created = format_date(config[:created_at])
|
|
67
|
+
draw_at(current_row, col + 18, created, color: :white)
|
|
68
|
+
current_row += 2
|
|
69
|
+
|
|
70
|
+
# Stats header
|
|
71
|
+
draw_at(current_row, col + 3, "STATISTICS", color: :yellow)
|
|
72
|
+
current_row += 2
|
|
73
|
+
|
|
74
|
+
# Stats
|
|
75
|
+
stats = config[:stats]
|
|
76
|
+
|
|
77
|
+
draw_stat(current_row, col, "Games Played", stats[:games_played])
|
|
78
|
+
current_row += 1
|
|
79
|
+
|
|
80
|
+
win_rate = Config.win_rate(config)
|
|
81
|
+
draw_at(current_row, col + 5, "Games Won:", color: :dim)
|
|
82
|
+
draw_at(current_row, col + 20, "#{stats[:games_won]} (#{win_rate}%)", color: :green)
|
|
83
|
+
current_row += 1
|
|
84
|
+
|
|
85
|
+
draw_stat(current_row, col, "Current Streak", stats[:current_streak],
|
|
86
|
+
suffix: streak_fire(stats[:current_streak]))
|
|
87
|
+
current_row += 1
|
|
88
|
+
|
|
89
|
+
draw_stat(current_row, col, "Best Streak", stats[:best_streak])
|
|
90
|
+
current_row += 1
|
|
91
|
+
|
|
92
|
+
draw_stat(current_row, col, "Highest Level", stats[:highest_level])
|
|
93
|
+
current_row += 1
|
|
94
|
+
|
|
95
|
+
draw_stat(current_row, col, "Total Score", format_number(stats[:total_score]))
|
|
96
|
+
current_row += 2
|
|
97
|
+
|
|
98
|
+
# Menu options
|
|
99
|
+
draw_at(current_row, col + 3, "OPTIONS", color: :yellow)
|
|
100
|
+
current_row += 1
|
|
101
|
+
|
|
102
|
+
MENU_OPTIONS.each_with_index do |option, index|
|
|
103
|
+
prefix = index == selected_index ? "> " : " "
|
|
104
|
+
color = index == selected_index ? :green : :dim
|
|
105
|
+
draw_at(current_row, col + 5, "#{prefix}#{option[:label]}", color: color)
|
|
106
|
+
current_row += 1
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Navigation hint
|
|
110
|
+
hint_row = row + content_height + 1
|
|
111
|
+
draw_centered(hint_row, "Use arrow keys, then ENTER", color: :dim)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def handle_input
|
|
115
|
+
selected = 0
|
|
116
|
+
|
|
117
|
+
loop do
|
|
118
|
+
draw_profile(selected)
|
|
119
|
+
|
|
120
|
+
result = Input.read_menu_selection(options_count: MENU_OPTIONS.length, initial: selected) do |new_index|
|
|
121
|
+
selected = new_index
|
|
122
|
+
draw_profile(selected)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
return MENU_OPTIONS[result][:key] if result
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def draw_stat(row, col, label, value, suffix: "")
|
|
130
|
+
draw_at(row, col + 5, "#{label}:", color: :dim)
|
|
131
|
+
draw_at(row, col + 22, "#{value}#{suffix}", color: :white)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def format_date(iso_date)
|
|
135
|
+
return "Unknown" unless iso_date
|
|
136
|
+
|
|
137
|
+
date = Time.parse(iso_date)
|
|
138
|
+
date.strftime("%b %d, %Y")
|
|
139
|
+
rescue StandardError
|
|
140
|
+
"Unknown"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def format_number(num)
|
|
144
|
+
num.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def streak_fire(streak)
|
|
148
|
+
return "" if streak < 3
|
|
149
|
+
|
|
150
|
+
" #{"🔥" * [streak / 3, 3].min}"
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mumble
|
|
4
|
+
module Screens
|
|
5
|
+
# Quit confirmation screen
|
|
6
|
+
class QuitConfirm < Base
|
|
7
|
+
MENU_OPTIONS = [
|
|
8
|
+
{ key: :quit, label: "Yes, quit" },
|
|
9
|
+
{ key: :cancel, label: "No, keep playing" }
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(config = nil, session_stats: nil)
|
|
13
|
+
super(config)
|
|
14
|
+
@session_stats = session_stats || default_session_stats
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def show
|
|
18
|
+
setup_screen
|
|
19
|
+
draw_screen
|
|
20
|
+
selection = handle_input
|
|
21
|
+
cleanup_screen
|
|
22
|
+
selection
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def default_session_stats
|
|
28
|
+
{ games: 0, won: 0, score: 0 }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def content_width
|
|
32
|
+
case size_category
|
|
33
|
+
when :large then 50
|
|
34
|
+
when :medium then 44
|
|
35
|
+
else 38
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def content_height
|
|
40
|
+
case size_category
|
|
41
|
+
when :large then 14
|
|
42
|
+
when :medium then 12
|
|
43
|
+
else 11
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def start_row
|
|
48
|
+
center_row_for(content_height)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def draw_screen(selected_index = 0)
|
|
52
|
+
Cursor.clear_screen
|
|
53
|
+
|
|
54
|
+
box = draw_centered_box(
|
|
55
|
+
row: start_row,
|
|
56
|
+
width: content_width,
|
|
57
|
+
height: content_height,
|
|
58
|
+
title: "QUIT GAME",
|
|
59
|
+
color: :yellow
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
box[:col]
|
|
63
|
+
row = box[:row]
|
|
64
|
+
|
|
65
|
+
# Farewell message
|
|
66
|
+
draw_centered(row + 2, "Thanks for playing, #{player_name}!", color: :white)
|
|
67
|
+
|
|
68
|
+
# Session stats
|
|
69
|
+
if @session_stats[:games].positive?
|
|
70
|
+
draw_centered(row + 4, "Session Stats:", color: :dim)
|
|
71
|
+
stats_text = "Games: #{@session_stats[:games]} | Won: #{@session_stats[:won]} | Score: #{@session_stats[:score]}"
|
|
72
|
+
draw_centered(row + 5, stats_text, color: :dim)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Confirmation question
|
|
76
|
+
draw_centered(row + 7, "Are you sure you want to quit?", color: :white)
|
|
77
|
+
|
|
78
|
+
# Menu options
|
|
79
|
+
option_row = row + 9
|
|
80
|
+
MENU_OPTIONS.each_with_index do |option, index|
|
|
81
|
+
prefix = index == selected_index ? "> " : " "
|
|
82
|
+
color = index == selected_index ? :green : :dim
|
|
83
|
+
draw_centered(option_row + index, "#{prefix}#{option[:label]}", color: color)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def handle_input
|
|
88
|
+
selected = 0
|
|
89
|
+
|
|
90
|
+
loop do
|
|
91
|
+
draw_screen(selected)
|
|
92
|
+
|
|
93
|
+
result = Input.read_menu_selection(options_count: MENU_OPTIONS.length, initial: selected) do |new_index|
|
|
94
|
+
selected = new_index
|
|
95
|
+
draw_screen(selected)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
return MENU_OPTIONS[result][:key] if result
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mumble
|
|
4
|
+
module Screens
|
|
5
|
+
# How to play screen with game rules
|
|
6
|
+
class Rules < Base
|
|
7
|
+
def show
|
|
8
|
+
setup_screen
|
|
9
|
+
draw_rules
|
|
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 54
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def content_height
|
|
25
|
+
case size_category
|
|
26
|
+
when :large then 26
|
|
27
|
+
when :medium then 24
|
|
28
|
+
else 22
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def start_row
|
|
33
|
+
center_row_for(content_height)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def draw_rules
|
|
37
|
+
box = draw_centered_box(
|
|
38
|
+
row: start_row,
|
|
39
|
+
width: content_width,
|
|
40
|
+
height: content_height,
|
|
41
|
+
title: "HOW TO PLAY",
|
|
42
|
+
color: :cyan
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
col = box[:col]
|
|
46
|
+
row = box[:row]
|
|
47
|
+
|
|
48
|
+
current_row = row + 2
|
|
49
|
+
|
|
50
|
+
# Objective
|
|
51
|
+
draw_at(current_row, col + 3, "OBJECTIVE", color: :yellow)
|
|
52
|
+
current_row += 1
|
|
53
|
+
draw_at(current_row, col + 5, "Guess the 5-letter word before the", color: :dim)
|
|
54
|
+
current_row += 1
|
|
55
|
+
draw_at(current_row, col + 5, "hangman is complete!", color: :dim)
|
|
56
|
+
current_row += 2
|
|
57
|
+
|
|
58
|
+
# Gameplay
|
|
59
|
+
draw_at(current_row, col + 3, "GAMEPLAY", color: :yellow)
|
|
60
|
+
current_row += 1
|
|
61
|
+
draw_at(current_row, col + 5, "Type a 5-letter word and press ENTER", color: :dim)
|
|
62
|
+
current_row += 1
|
|
63
|
+
draw_at(current_row, col + 5, "You have 6 attempts to guess the word", color: :dim)
|
|
64
|
+
current_row += 2
|
|
65
|
+
|
|
66
|
+
# Color guide
|
|
67
|
+
draw_at(current_row, col + 3, "COLOR GUIDE", color: :yellow)
|
|
68
|
+
current_row += 1
|
|
69
|
+
draw_color_example(current_row, col + 5, "GREEN", :green, "Correct position")
|
|
70
|
+
current_row += 1
|
|
71
|
+
draw_color_example(current_row, col + 5, "ORANGE", :orange, "Wrong position")
|
|
72
|
+
current_row += 1
|
|
73
|
+
draw_color_example(current_row, col + 5, "RED", :red, "Not in word")
|
|
74
|
+
current_row += 2
|
|
75
|
+
|
|
76
|
+
# Duplicate letters
|
|
77
|
+
draw_at(current_row, col + 3, "DUPLICATE LETTERS", color: :yellow)
|
|
78
|
+
current_row += 1
|
|
79
|
+
draw_at(current_row, col + 5, "If you guess a letter twice but the word", color: :dim)
|
|
80
|
+
current_row += 1
|
|
81
|
+
draw_at(current_row, col + 5, "has it once, best match shows green/orange,", color: :dim)
|
|
82
|
+
current_row += 1
|
|
83
|
+
draw_at(current_row, col + 5, "the extra shows red.", color: :dim)
|
|
84
|
+
current_row += 2
|
|
85
|
+
|
|
86
|
+
# Scoring
|
|
87
|
+
draw_at(current_row, col + 3, "SCORING", color: :yellow)
|
|
88
|
+
current_row += 1
|
|
89
|
+
draw_at(current_row, col + 5, "Fewer guesses + faster time = more points!", color: :dim)
|
|
90
|
+
|
|
91
|
+
# Prompt
|
|
92
|
+
prompt_row = row + content_height + 1
|
|
93
|
+
draw_centered(prompt_row, "Press any key to continue...", color: :dim)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def draw_color_example(row, col, label, color, description)
|
|
97
|
+
draw_at(row, col, label.to_s, color: color)
|
|
98
|
+
draw_at(row, col + 9, "- #{description}", color: :dim)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|