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.
@@ -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