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,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "storage"
|
|
4
|
+
require_relative "error_handler"
|
|
5
|
+
|
|
6
|
+
module Mumble
|
|
7
|
+
# Manages player profile, preferences, and statistics
|
|
8
|
+
module Config
|
|
9
|
+
FILENAME = "config.json"
|
|
10
|
+
|
|
11
|
+
# Default values for a new player
|
|
12
|
+
DEFAULTS = {
|
|
13
|
+
player_name: "Player",
|
|
14
|
+
created_at: nil,
|
|
15
|
+
updated_at: nil,
|
|
16
|
+
preferences: {
|
|
17
|
+
skip_rules: false,
|
|
18
|
+
show_timer: true
|
|
19
|
+
},
|
|
20
|
+
stats: {
|
|
21
|
+
games_played: 0,
|
|
22
|
+
games_won: 0,
|
|
23
|
+
current_streak: 0,
|
|
24
|
+
best_streak: 0,
|
|
25
|
+
highest_level: 0,
|
|
26
|
+
total_score: 0
|
|
27
|
+
}
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
# Load config with corruption recovery
|
|
32
|
+
def load
|
|
33
|
+
return create_new_config unless exists?
|
|
34
|
+
|
|
35
|
+
data = Storage.read_json(FILENAME)
|
|
36
|
+
|
|
37
|
+
# Validate the loaded data
|
|
38
|
+
unless valid_config?(data)
|
|
39
|
+
backup_corrupted_config
|
|
40
|
+
return create_new_config
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
merge_with_defaults(data)
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
ErrorHandler.log_error(e, "Config load failed")
|
|
46
|
+
backup_corrupted_config
|
|
47
|
+
create_new_config
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Check if config data is valid
|
|
51
|
+
def valid_config?(data)
|
|
52
|
+
return false unless data.is_a?(Hash)
|
|
53
|
+
return false unless data[:player_name].is_a?(String)
|
|
54
|
+
return false unless data[:stats].is_a?(Hash)
|
|
55
|
+
|
|
56
|
+
true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Backup corrupted config before replacing
|
|
60
|
+
def backup_corrupted_config
|
|
61
|
+
return unless exists?
|
|
62
|
+
|
|
63
|
+
backup_name = "config.backup.#{Time.now.to_i}.json"
|
|
64
|
+
Storage.write_json(backup_name, Storage.read_json(FILENAME))
|
|
65
|
+
rescue StandardError
|
|
66
|
+
# If backup fails, just continue
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Save config to file
|
|
71
|
+
def save(config)
|
|
72
|
+
config[:updated_at] = Time.now.iso8601
|
|
73
|
+
Storage.write_json(FILENAME, config)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Check if a config file exists (returning player vs new player)
|
|
77
|
+
def exists?
|
|
78
|
+
Storage.file_exists?(FILENAME)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Create a new config with defaults
|
|
82
|
+
def create_new_config
|
|
83
|
+
config = deep_copy(DEFAULTS)
|
|
84
|
+
config[:created_at] = Time.now.iso8601
|
|
85
|
+
config[:updated_at] = Time.now.iso8601
|
|
86
|
+
config
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Update player name
|
|
90
|
+
def update_name(config, new_name)
|
|
91
|
+
config[:player_name] = sanitize_name(new_name)
|
|
92
|
+
save(config)
|
|
93
|
+
config
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Update a preference
|
|
97
|
+
def update_preference(config, key, value)
|
|
98
|
+
config[:preferences][key] = value
|
|
99
|
+
save(config)
|
|
100
|
+
config
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Record a game result
|
|
104
|
+
def record_game(config, won:, score:, level:)
|
|
105
|
+
stats = config[:stats]
|
|
106
|
+
|
|
107
|
+
stats[:games_played] += 1
|
|
108
|
+
|
|
109
|
+
if won
|
|
110
|
+
stats[:games_won] += 1
|
|
111
|
+
stats[:current_streak] += 1
|
|
112
|
+
stats[:best_streak] = [stats[:best_streak], stats[:current_streak]].max
|
|
113
|
+
stats[:total_score] += score
|
|
114
|
+
else
|
|
115
|
+
stats[:current_streak] = 0
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
stats[:highest_level] = [stats[:highest_level], level].max
|
|
119
|
+
|
|
120
|
+
save(config)
|
|
121
|
+
config
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Reset all statistics
|
|
125
|
+
def reset_stats(config)
|
|
126
|
+
config[:stats] = deep_copy(DEFAULTS[:stats])
|
|
127
|
+
save(config)
|
|
128
|
+
config
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Delete all config data
|
|
132
|
+
def delete
|
|
133
|
+
Storage.delete_file(FILENAME)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Calculate win rate as a percentage
|
|
137
|
+
def win_rate(config)
|
|
138
|
+
played = config[:stats][:games_played]
|
|
139
|
+
return 0 if played.zero?
|
|
140
|
+
|
|
141
|
+
((config[:stats][:games_won].to_f / played) * 100).round
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
# Sanitize player name
|
|
147
|
+
def sanitize_name(name)
|
|
148
|
+
sanitized = name.to_s.strip[0, 20]
|
|
149
|
+
sanitized.empty? ? "Player" : sanitized
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Deep copy a hash (so we don't modify DEFAULTS)
|
|
153
|
+
def deep_copy(hash)
|
|
154
|
+
JSON.parse(JSON.generate(hash), symbolize_names: true)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Merge loaded data with defaults (handles missing keys)
|
|
158
|
+
def merge_with_defaults(data)
|
|
159
|
+
config = deep_copy(DEFAULTS)
|
|
160
|
+
|
|
161
|
+
config[:player_name] = data[:player_name] if data[:player_name]
|
|
162
|
+
config[:created_at] = data[:created_at] if data[:created_at]
|
|
163
|
+
config[:updated_at] = data[:updated_at] if data[:updated_at]
|
|
164
|
+
|
|
165
|
+
config[:preferences].merge!(data[:preferences]) if data[:preferences]
|
|
166
|
+
config[:stats].merge!(data[:stats]) if data[:stats]
|
|
167
|
+
|
|
168
|
+
config
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tty-cursor"
|
|
4
|
+
|
|
5
|
+
module Mumble
|
|
6
|
+
# Provides cursor control for terminal manipulation
|
|
7
|
+
# Uses TTY::Cursor for cross-platform support
|
|
8
|
+
module Cursor
|
|
9
|
+
class << self
|
|
10
|
+
# Memoized cursor instance
|
|
11
|
+
def cursor
|
|
12
|
+
@cursor ||= TTY::Cursor
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Move cursor to specific row and column (1-indexed)
|
|
16
|
+
def move_to(row, col)
|
|
17
|
+
print cursor.move_to(col - 1, row - 1)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Move cursor up by n rows
|
|
21
|
+
def up(n = 1)
|
|
22
|
+
print cursor.up(n)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Move cursor down by n rows
|
|
26
|
+
def down(n = 1)
|
|
27
|
+
print cursor.down(n)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Move cursor left by n columns
|
|
31
|
+
def left(n = 1)
|
|
32
|
+
print cursor.backward(n)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Move cursor right by n columns
|
|
36
|
+
def right(n = 1)
|
|
37
|
+
print cursor.forward(n)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Move cursor to beginning of current line
|
|
41
|
+
def line_start
|
|
42
|
+
print cursor.column(0)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Hide cursor (useful during animations)
|
|
46
|
+
def hide
|
|
47
|
+
print cursor.hide
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Show cursor (restore after hiding)
|
|
51
|
+
def show
|
|
52
|
+
print cursor.show
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Clear entire screen and move to top-left
|
|
56
|
+
def clear_screen
|
|
57
|
+
print cursor.clear_screen
|
|
58
|
+
print cursor.move_to(0, 0)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Clear from cursor to end of line
|
|
62
|
+
def clear_line
|
|
63
|
+
print cursor.clear_line
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Clear from cursor to end of screen
|
|
67
|
+
def clear_below
|
|
68
|
+
print cursor.clear_screen_down
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Save current cursor position
|
|
72
|
+
def save_position
|
|
73
|
+
print cursor.save
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Restore previously saved cursor position
|
|
77
|
+
def restore_position
|
|
78
|
+
print cursor.restore
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mumble
|
|
4
|
+
# Centralized error handling for graceful failures
|
|
5
|
+
module ErrorHandler
|
|
6
|
+
class << self
|
|
7
|
+
# Wrap any operation that might fail
|
|
8
|
+
def safely(fallback: nil, message: nil)
|
|
9
|
+
yield
|
|
10
|
+
rescue StandardError => e
|
|
11
|
+
log_error(e, message)
|
|
12
|
+
fallback
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Handle fatal errors with user-friendly message
|
|
16
|
+
def fatal(error, context: nil)
|
|
17
|
+
Cursor.clear_screen
|
|
18
|
+
Cursor.show
|
|
19
|
+
Cursor.move_to(1, 1)
|
|
20
|
+
|
|
21
|
+
puts Colors.red("╔═══════════════════════════════════════╗")
|
|
22
|
+
puts Colors.red("║ Oops! Something broke ║")
|
|
23
|
+
puts Colors.red("╚═══════════════════════════════════════╝")
|
|
24
|
+
puts ""
|
|
25
|
+
puts Colors.yellow("Context: #{context}") if context
|
|
26
|
+
puts Colors.dim("Error: #{error.message}")
|
|
27
|
+
puts ""
|
|
28
|
+
puts Colors.white("Please try restarting the game.")
|
|
29
|
+
puts Colors.dim("If this keeps happening, try deleting ~/.mumble/")
|
|
30
|
+
puts ""
|
|
31
|
+
|
|
32
|
+
log_error(error, context)
|
|
33
|
+
|
|
34
|
+
exit(1)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Log error to file for debugging
|
|
38
|
+
def log_error(error, context = nil)
|
|
39
|
+
log_file = File.join(Storage.data_dir, "error.log")
|
|
40
|
+
|
|
41
|
+
File.open(log_file, "a") do |f|
|
|
42
|
+
f.puts "=" * 50
|
|
43
|
+
f.puts "Time: #{Time.now}"
|
|
44
|
+
f.puts "Context: #{context}" if context
|
|
45
|
+
f.puts "Error: #{error.class}: #{error.message}"
|
|
46
|
+
f.puts "Backtrace:"
|
|
47
|
+
error.backtrace&.first(10)&.each { |line| f.puts " #{line}" }
|
|
48
|
+
f.puts ""
|
|
49
|
+
end
|
|
50
|
+
rescue StandardError
|
|
51
|
+
# If we can't even log, just continue
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Check if terminal is still valid size
|
|
56
|
+
def check_terminal_size!
|
|
57
|
+
return if Screen.valid_size?
|
|
58
|
+
|
|
59
|
+
Cursor.clear_screen
|
|
60
|
+
Cursor.show
|
|
61
|
+
Cursor.move_to(1, 1)
|
|
62
|
+
|
|
63
|
+
puts Colors.yellow("Terminal too small!")
|
|
64
|
+
puts ""
|
|
65
|
+
puts "Current size: #{Screen.width}x#{Screen.height}"
|
|
66
|
+
puts "Required: #{Screen::MIN_WIDTH}x#{Screen::MIN_HEIGHT}"
|
|
67
|
+
puts ""
|
|
68
|
+
puts Colors.dim("Please resize your terminal and restart.")
|
|
69
|
+
|
|
70
|
+
exit(1)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/mumble/grid.rb
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "layout"
|
|
4
|
+
require_relative "cursor"
|
|
5
|
+
require_relative "colors"
|
|
6
|
+
|
|
7
|
+
module Mumble
|
|
8
|
+
# The 5x6 letter grid display with size variants
|
|
9
|
+
module Grid
|
|
10
|
+
# Letter states for coloring
|
|
11
|
+
STATE_EMPTY = :empty
|
|
12
|
+
STATE_TYPING = :typing # Currently being typed (white)
|
|
13
|
+
STATE_CORRECT = :correct # Right letter, right position (green)
|
|
14
|
+
STATE_PRESENT = :present # Right letter, wrong position (orange)
|
|
15
|
+
STATE_ABSENT = :absent # Letter not in word (red)
|
|
16
|
+
|
|
17
|
+
# Small grid dimensions (for smaller terminals)
|
|
18
|
+
SMALL = {
|
|
19
|
+
cell_width: 5,
|
|
20
|
+
cell_height: 3,
|
|
21
|
+
h_gap: 1,
|
|
22
|
+
v_gap: 1
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
# Large grid dimensions (for larger terminals)
|
|
26
|
+
LARGE = {
|
|
27
|
+
cell_width: 7,
|
|
28
|
+
cell_height: 5,
|
|
29
|
+
h_gap: 2,
|
|
30
|
+
v_gap: 1
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
# Get dimensions based on terminal size
|
|
35
|
+
def dimensions
|
|
36
|
+
case Layout.size_category
|
|
37
|
+
when :large
|
|
38
|
+
LARGE
|
|
39
|
+
else
|
|
40
|
+
SMALL
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Total width of the entire grid
|
|
45
|
+
def total_width
|
|
46
|
+
dims = dimensions
|
|
47
|
+
(dims[:cell_width] * 5) + (dims[:h_gap] * 4)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Total height of the entire grid
|
|
51
|
+
def total_height
|
|
52
|
+
dims = dimensions
|
|
53
|
+
(dims[:cell_height] * 6) + (dims[:v_gap] * 5)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Draw the entire grid
|
|
57
|
+
# guesses: array of up to 6 guesses, each guess is array of {letter:, state:}
|
|
58
|
+
# current_row: which row is active (0-5)
|
|
59
|
+
# current_input: string being typed in current row
|
|
60
|
+
def draw(row:, col:, guesses: [], current_row: 0, current_input: "")
|
|
61
|
+
6.times do |row_index|
|
|
62
|
+
draw_row(
|
|
63
|
+
row: row + (row_index * (dimensions[:cell_height] + dimensions[:v_gap])),
|
|
64
|
+
col: col,
|
|
65
|
+
letters: guesses[row_index] || [],
|
|
66
|
+
is_current: row_index == current_row,
|
|
67
|
+
current_input: row_index == current_row ? current_input : ""
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Draw a single row of 5 cells
|
|
73
|
+
def draw_row(row:, col:, letters:, is_current: false, current_input: "")
|
|
74
|
+
5.times do |col_index|
|
|
75
|
+
cell_col = col + (col_index * (dimensions[:cell_width] + dimensions[:h_gap]))
|
|
76
|
+
|
|
77
|
+
# Determine what to show in this cell
|
|
78
|
+
if letters[col_index]
|
|
79
|
+
# Already guessed - show letter with state color
|
|
80
|
+
letter = letters[col_index][:letter]
|
|
81
|
+
state = letters[col_index][:state]
|
|
82
|
+
elsif is_current && current_input[col_index]
|
|
83
|
+
# Currently typing - show input letter
|
|
84
|
+
letter = current_input[col_index]
|
|
85
|
+
state = STATE_TYPING
|
|
86
|
+
else
|
|
87
|
+
# Empty cell
|
|
88
|
+
letter = nil
|
|
89
|
+
state = STATE_EMPTY
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
draw_cell(row: row, col: cell_col, letter: letter, state: state)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Draw a single cell with letter
|
|
97
|
+
def draw_cell(row:, col:, letter:, state:)
|
|
98
|
+
dims = dimensions
|
|
99
|
+
color = color_for_state(state)
|
|
100
|
+
|
|
101
|
+
if dims[:cell_height] == 3
|
|
102
|
+
draw_cell_small(row: row, col: col, letter: letter, color: color)
|
|
103
|
+
else
|
|
104
|
+
draw_cell_large(row: row, col: col, letter: letter, color: color)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
# Small cell (3 lines tall, 5 wide)
|
|
111
|
+
# ┌───┐
|
|
112
|
+
# │ A │
|
|
113
|
+
# └───┘
|
|
114
|
+
def draw_cell_small(row:, col:, letter:, color:)
|
|
115
|
+
display_letter = letter&.upcase || " "
|
|
116
|
+
|
|
117
|
+
# Top border
|
|
118
|
+
Cursor.move_to(row, col)
|
|
119
|
+
print colorize("┌───┐", color)
|
|
120
|
+
|
|
121
|
+
# Middle with letter
|
|
122
|
+
Cursor.move_to(row + 1, col)
|
|
123
|
+
print colorize("│ #{display_letter} │", color)
|
|
124
|
+
|
|
125
|
+
# Bottom border
|
|
126
|
+
Cursor.move_to(row + 2, col)
|
|
127
|
+
print colorize("└───┘", color)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Large cell (5 lines tall, 7 wide)
|
|
131
|
+
# ┌─────┐
|
|
132
|
+
# │ │
|
|
133
|
+
# │ A │
|
|
134
|
+
# │ │
|
|
135
|
+
# └─────┘
|
|
136
|
+
def draw_cell_large(row:, col:, letter:, color:)
|
|
137
|
+
display_letter = letter&.upcase || " "
|
|
138
|
+
|
|
139
|
+
# Top border
|
|
140
|
+
Cursor.move_to(row, col)
|
|
141
|
+
print colorize("┌─────┐", color)
|
|
142
|
+
|
|
143
|
+
# Empty line
|
|
144
|
+
Cursor.move_to(row + 1, col)
|
|
145
|
+
print colorize("│ │", color)
|
|
146
|
+
|
|
147
|
+
# Middle with letter
|
|
148
|
+
Cursor.move_to(row + 2, col)
|
|
149
|
+
print colorize("│ #{display_letter} │", color)
|
|
150
|
+
|
|
151
|
+
# Empty line
|
|
152
|
+
Cursor.move_to(row + 3, col)
|
|
153
|
+
print colorize("│ │", color)
|
|
154
|
+
|
|
155
|
+
# Bottom border
|
|
156
|
+
Cursor.move_to(row + 4, col)
|
|
157
|
+
print colorize("└─────┘", color)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Get color method name for state
|
|
161
|
+
def color_for_state(state)
|
|
162
|
+
case state
|
|
163
|
+
when STATE_CORRECT
|
|
164
|
+
:green
|
|
165
|
+
when STATE_PRESENT
|
|
166
|
+
:orange
|
|
167
|
+
when STATE_ABSENT
|
|
168
|
+
:red
|
|
169
|
+
when STATE_TYPING
|
|
170
|
+
:white
|
|
171
|
+
else
|
|
172
|
+
:dim
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Apply color to text
|
|
177
|
+
def colorize(text, color)
|
|
178
|
+
Colors.send(color, text)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|