Class: GitGameShow::PlayerClient
- Inherits:
-
Object
- Object
- GitGameShow::PlayerClient
- Defined in:
- lib/git_game_show/player_client.rb
Instance Attribute Summary collapse
-
#host ⇒ Object
readonly
Returns the value of attribute host.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#password ⇒ Object
readonly
Returns the value of attribute password.
-
#port ⇒ Object
readonly
Returns the value of attribute port.
-
#secure ⇒ Object
readonly
Returns the value of attribute secure.
Instance Method Summary collapse
- #clear_screen ⇒ Object
- #connect ⇒ Object
- #display_waiting_room ⇒ Object
-
#draw_ordering_list(items, cursor_index, selected_index, start_line, num_options) ⇒ Object
Helper method to draw just the list portion of the ordering UI.
-
#handle_answer_feedback(data) ⇒ Object
Handle immediate feedback after submitting an answer.
- #handle_chat(data) ⇒ Object
- #handle_game_end(data) ⇒ Object
-
#handle_game_reset(data) ⇒ Object
Add a special method to handle game reset notifications.
- #handle_game_start(data) ⇒ Object
- #handle_join_response(data) ⇒ Object
-
#handle_message(msg) ⇒ Object
Make public for WebSocket callback.
-
#handle_ordering_question(options, question_text = nil) ⇒ Object
Super simple ordering implementation with minimal screen updates.
- #handle_player_update(data) ⇒ Object
- #handle_question(data) ⇒ Object
-
#handle_round_result(data) ⇒ Object
Handle round results showing all players’ answers.
- #handle_round_start(data) ⇒ Object
- #handle_scoreboard(data) ⇒ Object
-
#initialize(host:, port:, password:, name:, secure: false) ⇒ PlayerClient
constructor
A new instance of PlayerClient.
-
#move_cursor_to(row, col) ⇒ Object
Helper to position cursor at a specific row/column.
-
#read_char ⇒ Object
Simplified key input reader that uses numbers for arrow keys.
-
#read_char_with_timeout ⇒ Object
Non-blocking key input reader that supports timeouts.
-
#send_join_request ⇒ Object
Make these methods public so they can be called from the WebSocket callbacks.
- #send_message(message) ⇒ Object
-
#update_countdown_display(seconds, original_seconds) ⇒ Object
Helper method to display countdown using a status bar at the bottom of the screen.
-
#update_title_timer(seconds) ⇒ Object
Helper method to print a countdown timer status in the window title This doesn’t interfere with the terminal content.
Constructor Details
#initialize(host:, port:, password:, name:, secure: false) ⇒ PlayerClient
Returns a new instance of PlayerClient.
9 10 11 12 13 14 15 16 17 18 19 20 21 |
# File 'lib/git_game_show/player_client.rb', line 9 def initialize(host:, port:, password:, name:, secure: false) @host = host @port = port @password = password @name = name @secure = secure @ws = nil @prompt = TTY::Prompt.new @players = [] @game_state = :lobby # :lobby, :playing, :ended @current_timer_id = nil @game_width = 80 end |
Instance Attribute Details
#host ⇒ Object (readonly)
Returns the value of attribute host.
7 8 9 |
# File 'lib/git_game_show/player_client.rb', line 7 def host @host end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
7 8 9 |
# File 'lib/git_game_show/player_client.rb', line 7 def name @name end |
#password ⇒ Object (readonly)
Returns the value of attribute password.
7 8 9 |
# File 'lib/git_game_show/player_client.rb', line 7 def password @password end |
#port ⇒ Object (readonly)
Returns the value of attribute port.
7 8 9 |
# File 'lib/git_game_show/player_client.rb', line 7 def port @port end |
#secure ⇒ Object (readonly)
Returns the value of attribute secure.
7 8 9 |
# File 'lib/git_game_show/player_client.rb', line 7 def secure @secure end |
Instance Method Details
#clear_screen ⇒ Object
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/git_game_show/player_client.rb', line 214 def clear_screen # Reset cursor and clear entire screen print "\033[H\033[2J" # Move to home position and clear screen print "\033[3J" # Clear scrollback buffer # Reserve bottom line for timer status term_height = `tput lines`.to_i rescue 24 # Move to bottom of screen and clear status line print "\e[#{term_height};1H" print "\e[K" print "\e[H" # Move cursor back to home position STDOUT.flush end |
#connect ⇒ Object
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/git_game_show/player_client.rb', line 23 def connect begin client = self # Store reference to the client instance # Check if the connection should use a secure protocol # For ngrok TCP tunnels, we should use regular ws:// since ngrok tcp doesn't provide SSL termination # Only use wss:// if the secure flag is explicitly set (for configured HTTPS endpoints) protocol = if @secure puts "Using secure WebSocket connection (wss://)".colorize(:light_blue) 'wss' else 'ws' end @ws = WebSocket::Client::Simple.connect("#{protocol}://#{host}:#{port}") @ws.on :open do puts "Connected to server".colorize(:green) # Use the stored client reference client.send_join_request end @ws.on :message do |msg| client.(msg) end @ws.on :error do |e| puts "Error: #{e.}".colorize(:red) end @ws.on :close do |e| puts "Connection closed (#{e.code}: #{e.reason})".colorize(:yellow) exit(1) end # Keep the client running loop do sleep(1) # Check if connection is still alive if @ws.nil? || @ws.closed? puts "Connection lost. Exiting...".colorize(:red) exit(1) end end rescue => e puts "Failed to connect: #{e.}".colorize(:red) end end |
#display_waiting_room ⇒ Object
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/git_game_show/player_client.rb', line 132 def display_waiting_room clear_screen # Draw header with fancy box terminal_width = `tput cols`.to_i rescue @game_width terminal_height = `tput lines`.to_i rescue 24 # Create title box puts "╭#{"─" * (terminal_width - 2)}╮".colorize(:green) puts "│#{" Git Game Show - Waiting Room ".center(terminal_width - 2)}│".colorize(:green) puts "╰#{"─" * (terminal_width - 2)}╯".colorize(:green) # Left column width (2/3 of terminal) for main content left_width = (terminal_width * 0.65).to_i # Display instructions and welcome information puts "\n" puts " Welcome to Git Game Show!".colorize(:yellow) puts " Test your knowledge about Git and your team's commits through fun mini-games.".colorize(:light_black) puts "\n" puts " 🔹 Instructions:".colorize(:light_blue) puts " • The game consists of multiple rounds with different question types".colorize(:light_black) puts " • Each round has a theme based on Git commit history".colorize(:light_black) puts " • Answer questions as quickly as possible for maximum points".colorize(:light_black) puts " • The player with the most points at the end wins!".colorize(:light_black) puts "\n" puts " 🔹 Status: Waiting for the host to start the game...".colorize(:yellow) puts "\n" # Draw player section in a box player_box_width = terminal_width - 4 puts ("╭#{"─" * player_box_width}╮").center(terminal_width).colorize(:light_blue) puts ("│#{" Players ".center(player_box_width)}│").center(terminal_width).colorize(:light_blue) puts ("╰#{"─" * player_box_width}╯").center(terminal_width).colorize(:light_blue) # Display list of players in a nicer format if @players.empty? puts " (No other players yet)".colorize(:light_black) else # Calculate number of columns based on terminal width and name lengths max_name_length = @players.map(&:length).max + 10 # Extra space for number and "(You)" text # Add more spacing between players - increase padding from 4 to 10 column_width = max_name_length + 12 # More generous spacing num_cols = [terminal_width / column_width, 3].min # Cap at 3 columns max num_cols = 1 if num_cols < 1 # Use fewer columns for better spacing if num_cols > 1 && @players.size > 6 # If we have many players, prefer fewer columns with more space num_cols = [num_cols, 2].min end # Split players into rows for multi-column display player_rows = @players.each_slice(((@players.size + num_cols - 1) / num_cols).ceil).to_a puts "\n" player_rows.each do |row_players| row_str = " " row_players.each_with_index do |player, idx| col_idx = player_rows.index { |rp| rp.include?(player) } player_num = col_idx * player_rows[0].length + idx + 1 # Apply different color for current player if player == @name row_str += "#{player_num}. #{player} (You)".colorize(:green).ljust(column_width) else row_str += "#{player_num}. #{player}".colorize(:light_blue).ljust(column_width) end end # Add a blank line between rows for vertical spacing too puts row_str puts "" end end puts "\n" puts " When the game starts, you'll see questions appear automatically.".colorize(:light_black) puts " Get ready to test your Git knowledge!".colorize(:yellow) puts "\n" end |
#draw_ordering_list(items, cursor_index, selected_index, start_line, num_options) ⇒ Object
Helper method to draw just the list portion of the ordering UI
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 |
# File 'lib/git_game_show/player_client.rb', line 346 def draw_ordering_list(items, cursor_index, selected_index, start_line, ) # Clear the line above the list (was used for debugging) debug_line = start_line - 1 move_cursor_to(debug_line, 0) print "\r\033[K" # Clear debug line # Move cursor to the start position for the list move_cursor_to(start_line, 0) # Clear all lines that will contain list items and the submit button ( + 2).times do |i| move_cursor_to(start_line + i, 0) print "\r\033[K" # Clear current line without moving cursor end # Draw each item with appropriate highlighting items.each_with_index do |item, idx| # Calculate the line for this item item_line = start_line + idx move_cursor_to(item_line, 0) if selected_index == idx # Selected item (being moved) print " → #{idx + 1}. #{item}".colorize(:light_green) elsif cursor_index == idx # Cursor is on this item print " → #{idx + 1}. #{item}".colorize(:light_blue) else # Normal item print " #{idx + 1}. #{item}".colorize(:white) end end # Add the Submit option at the bottom move_cursor_to(start_line + , 0) if cursor_index == print " → Submit Answer".colorize(:yellow) else print " Submit Answer".colorize(:white) end # Move cursor after the list move_cursor_to(start_line + + 1, 0) # Ensure output is visible STDOUT.flush end |
#handle_answer_feedback(data) ⇒ Object
Handle immediate feedback after submitting an answer
829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 |
# File 'lib/git_game_show/player_client.rb', line 829 def handle_answer_feedback(data) # Invalidate any running timer and reset window title @current_timer_id = SecureRandom.uuid print "\033]0;Git Game Show\007" # Reset window title # Clear the timer status line at bottom term_height = `tput lines`.to_i rescue 24 print "\e7" # Save cursor position print "\e[#{term_height};1H" # Move to bottom line print "\e[K" # Clear line print "\e8" # Restore cursor position # Don't clear screen, just display the feedback under the question # This keeps the context of the question while showing the result # Add a visual separator puts "\n #{"─" * 40}".colorize(:light_black) puts "\n" points_text = "(#{data['points']} points)" # Show immediate feedback if data['answer'] == "TIMEOUT" # Special handling for timeouts puts " ⏰ TIME'S UP! You didn't answer in time.".colorize(:red) puts " The correct answer was: #{data['correct_answer']}".colorize(:yellow) puts " (0 points)".colorize(:light_black) elsif data['correct'] # Correct answer puts " ✅ CORRECT! Your answer was correct: #{data['answer']}#{points_text}".colorize(:green) # Show bonus points details if applicable if data['points'] > 10 # More than base points bonus = data['points'] - 10 puts " 🎉 SPEED BONUS: +#{bonus} points for fast answer!".colorize(:yellow) end else if data['correct_answer'].is_a?(Array) puts "Correct Order".rjust(39).colorize(:green) + " Your Order" 0.upto(data['correct_answer'].size - 1) do |i| answer_color = data['correct_answer'][i] == data['answer'][i] ? :green : :red truncated_1 = data['correct_answer'][i].length > 38 ? data['correct_answer'][i][0..35] + "..." : data['correct_answer'][i] truncated_2 = data['answer'][i].length > 38 ? data['answer'][i][0..35] + "..." : data['answer'][i] puts "#{truncated_1}".rjust(38).colorize(:green) + " #{truncated_2}".colorize(answer_color) end puts "\n\n" puts points_text.center(80).colorize(:yellow) else # Incorrect answer puts " ❌ INCORRECT! The correct answer was: #{data['correct_answer']}".colorize(:red) puts " You answered: #{data['answer']} (0 points)".colorize(:yellow) end end puts "\n Waiting for the round to complete. Please wait for the next question...".colorize(:light_blue) end |
#handle_chat(data) ⇒ Object
1183 1184 1185 |
# File 'lib/git_game_show/player_client.rb', line 1183 def handle_chat(data) puts "[#{data['sender']}]: #{data['message']}".colorize(:light_blue) end |
#handle_game_end(data) ⇒ Object
1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 |
# File 'lib/git_game_show/player_client.rb', line 1056 def handle_game_end(data) # Invalidate any running timer and reset window title @current_timer_id = SecureRandom.uuid print "\033]0;Git Game Show - Game Over\007" # Reset window title with context # Clear any timer status line at the bottom term_height = `tput lines`.to_i rescue 24 print "\e7" # Save cursor position print "\e[#{term_height};1H" # Move to bottom line print "\e[K" # Clear line print "\e8" # Restore cursor position # Completely clear the screen clear_screen @game_state = :ended winner = data['winner'] # ASCII trophy art trophy = [ "___________", "'._==_==_=_.'", ".-\\: /-.", "| (|:. |) |", "'-|:. |-'", "\\::. /", "'::. .'", ") (", "_.' '._" ] box_width = 40 puts "\n\n" trophy.each{|line| puts line.center(@game_width).colorize(:yellow)} puts "\n" puts ("╭" + "─" * box_width + "╮").center(@game_width).colorize(:green) puts "│#{'Game Over'.center(box_width)}│".center(@game_width).colorize(:green) puts ("╰" + "─" * box_width + "╯").center(@game_width).colorize(:green) puts "\n" winner_is_you = winner == name if winner_is_you puts "🎉 Congratulations! You won! 🎉".center(@game_width).colorize(:yellow) else puts "Winner: #{winner}! 🏆".center(@game_width).colorize(:yellow) end puts "" puts "Final Scores".center(@game_width).colorize(:light_blue) puts "" # Get player positions position = 1 last_score = nil data['scores'].each do |player, score| # Determine position (handle ties) position = data['scores'].values.index(score) + 1 if last_score != score last_score = score # Highlight current player player_str = player == name ? "#{player} (You)" : player # Format with position position_str = "#{position}." score_str = "#{score} points" # Add emoji for top 3 scores_width = @game_width - 30 case position when 1 position_str = "🥇 #{position_str}" left_string = (position_str.rjust(5) + ' ' + player_str).ljust(scores_width - score_str.length) puts "#{left_string}#{score_str}".center(@game_width).colorize(:yellow) when 2 position_str = "🥈 #{position_str}" left_string = (position_str.rjust(5) + ' ' + player_str).ljust(scores_width - score_str.length) puts "#{left_string}#{score_str}".center(@game_width).colorize(:light_blue) when 3 position_str = "🥉 #{position_str}" left_string = (position_str.rjust(5) + ' ' + player_str).ljust(scores_width - score_str.length) puts "#{left_string}#{score_str}".center(@game_width).colorize(:light_magenta) else left_string = " " + (position_str.rjust(5) + ' ' + player_str).ljust(scores_width - score_str.length) puts "#{left_string}#{score_str}".center(@game_width) end end puts "\n" puts " Thanks for playing Git Game Show!".colorize(:green) puts " Waiting for the host to start a new game...".colorize(:light_blue) puts " Press Ctrl+C to exit, or wait for the next game".colorize(:light_black) # Keep client ready to receive a new game start or reset message @game_over_timer = Thread.new do begin loop do # Just keep waiting for host to start a new game # The client will receive GAME_START or GAME_RESET when the host takes action sleep 1 end rescue => e # Silence any errors in the waiting thread end end end |
#handle_game_reset(data) ⇒ Object
Add a special method to handle game reset notifications
1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 |
# File 'lib/git_game_show/player_client.rb', line 1164 def handle_game_reset(data) # Stop the game over timer if it's running @game_over_timer&.kill if @game_over_timer&.alive? # Reset game state @game_state = :lobby # Clear any lingering state @players = @players || [] # Keep existing players list if we have one # Show the waiting room again clear_screen display_waiting_room # Show a prominent message that we're back in waiting room mode puts "\n 🔄 The game has been reset by the host. Waiting for a new game to start...".colorize(:light_blue) puts " You can play again or press Ctrl+C to exit.".colorize(:light_blue) end |
#handle_game_start(data) ⇒ Object
537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 |
# File 'lib/git_game_show/player_client.rb', line 537 def handle_game_start(data) @game_state = :playing @players = data['players'] @total_rounds = data['rounds'] clear_screen # Display a fun "Game Starting" animation box_width = 40 puts "\n" puts ("╭" + "─" * box_width + "╮").center(@game_width).colorize(:green) puts ("│" + "Game starting...".center(box_width) + "│").center(@game_width).colorize(:green) puts ("╰" + "─" * box_width + "╯").center(@game_width).colorize(:green) puts "\n\n" puts " Total rounds: #{@total_rounds}".colorize(:light_blue) puts " Players: #{@players.join(', ')}".colorize(:light_blue) puts "\n\n" puts " Get ready for the first round!".colorize(:yellow) puts "\n\n" end |
#handle_join_response(data) ⇒ Object
122 123 124 125 126 127 128 129 130 |
# File 'lib/git_game_show/player_client.rb', line 122 def handle_join_response(data) if data['success'] @players = data['players'] # Get the full player list from server display_waiting_room else puts "Failed to join: #{data['message']}".colorize(:red) exit(1) end end |
#handle_message(msg) ⇒ Object
Make public for WebSocket callback
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/git_game_show/player_client.rb', line 83 def (msg) begin data = JSON.parse(msg.data) # Remove debug print to reduce console noise case data['type'] when MessageType::JOIN_RESPONSE handle_join_response(data) when MessageType::GAME_START handle_game_start(data) when MessageType::GAME_RESET # New handler for game reset handle_game_reset(data) when 'player_joined', 'player_left' handle_player_update(data) when 'round_start' handle_round_start(data) when MessageType::QUESTION handle_question(data) when MessageType::ROUND_RESULT handle_round_result(data) when MessageType::SCOREBOARD handle_scoreboard(data) when MessageType::GAME_END handle_game_end(data) when MessageType::ANSWER_FEEDBACK handle_answer_feedback(data) when MessageType::CHAT handle_chat(data) else puts "Unknown message type: #{data['type']}".colorize(:yellow) end rescue JSON::ParserError => e puts "Invalid message format: #{e.}".colorize(:red) rescue => e puts "Error processing message: #{e.}\n#{e.backtrace.join("\n")}".colorize(:red) end end |
#handle_ordering_question(options, question_text = nil) ⇒ Object
Super simple ordering implementation with minimal screen updates
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 |
# File 'lib/git_game_show/player_client.rb', line 241 def handle_ordering_question(, question_text = nil) # Create a copy of the options that we can modify current_order = .dup cursor_index = 0 selected_index = nil = current_order.size question_text ||= "Put these commits in chronological order (oldest to newest)" # Extract question data if available data = Thread.current[:question_data] || {} question_number = data['question_number'] total_questions = data['total_questions'] # Draw the initial screen once # system('clear') || system('cls') # Draw question header once if question_number && total_questions box_width = 42 puts "" puts ("╭" + "─" * box_width + "╮").center(@game_width).colorize(:light_blue) puts ("│#{'Question #{question_number} of #{total_questions}'.center(box_width-2)}│").center(@game_width).colorize(:light_blue) puts ("╰" + "─" * box_width + "╯").center(@game_width).colorize(:light_blue) end # Draw the main question text once puts "\n #{question_text}".colorize(:light_blue) puts " Put in order from oldest (1) to newest (#{})".colorize(:light_blue) # Draw instructions once puts "\n INSTRUCTIONS:".colorize(:yellow) puts " • Use ↑/↓ arrows to move cursor".colorize(:white) puts " • Press ENTER to select/deselect an item to move".colorize(:white) puts " • Selected items move with cursor when you press ↑/↓".colorize(:white) puts " • Navigate to Submit and press ENTER when finished".colorize(:white) # Calculate where the list content starts on screen content_start_line = question_number ? 15 : 12 # Draw the list content (this will be redrawn repeatedly) draw_ordering_list(current_order, cursor_index, selected_index, content_start_line, ) # Main interaction loop loop do # Read a single keypress char = read_char # Clear any message on this line move_cursor_to(content_start_line + + 2, 0) print "\r\033[K" # Check if the timer has expired if @timer_expired # If timer expired, just return the current ordering return current_order end # Now char is an integer (ASCII code) case char when 13, 10 # Enter key (CR or LF) if cursor_index == # Submit the answer # Move to end of list and print a message move_cursor_to(content_start_line + + 3, 0) print "\r\033[K" print " Submitting your answer...".colorize(:green) return current_order elsif selected_index == cursor_index # Deselect the currently selected item selected_index = nil else # Select the item at cursor position selected_index = cursor_index end when 65, 107, 119 # Up arrow (65='A'), k (107), w (119) # Move cursor up if selected_index == cursor_index && cursor_index > 0 # Move the selected item up in the order current_order[cursor_index], current_order[cursor_index - 1] = current_order[cursor_index - 1], current_order[cursor_index] cursor_index -= 1 selected_index = cursor_index elsif cursor_index > 0 # Just move the cursor up cursor_index -= 1 end when 66, 106, 115 # Down arrow (66='B'), j (106), s (115) if selected_index == cursor_index && cursor_index < - 1 # Move the selected item down in the order current_order[cursor_index], current_order[cursor_index + 1] = current_order[cursor_index + 1], current_order[cursor_index] cursor_index += 1 selected_index = cursor_index elsif cursor_index < # Just move the cursor down cursor_index += 1 end end # Redraw just the list portion of the screen draw_ordering_list(current_order, cursor_index, selected_index, content_start_line, ) end end |
#handle_player_update(data) ⇒ Object
559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 |
# File 'lib/git_game_show/player_client.rb', line 559 def handle_player_update(data) # Update the players list @players = data['players'] if @game_state == :lobby # If we're in the lobby, refresh the waiting room UI with updated player list display_waiting_room # Show notification at the bottom if data['type'] == 'player_joined' puts "\n 🟢 #{data['name']} has joined the game".colorize(:green) else puts "\n 🔴 #{data['name']} has left the game".colorize(:yellow) end else # During gameplay, just show a notification without disrupting the game UI terminal_width = `tput cols`.to_i rescue @game_width # Create a notification box that won't interfere with ongoing gameplay puts "" puts "╭#{"─" * (terminal_width - 2)}╮".colorize(:light_blue) if data['type'] == 'player_joined' puts "│#{" 🟢 #{data['name']} has joined the game ".center(terminal_width - 2)}│".colorize(:green) else puts "│#{" 🔴 #{data['name']} has left the game ".center(terminal_width - 2)}│".colorize(:yellow) end # Don't show all players during gameplay - can be too disruptive # Just show the total count puts "│#{" Total players: #{data['players'].size} ".center(terminal_width - 2)}│".colorize(:light_blue) puts "╰#{"─" * (terminal_width - 2)}╯".colorize(:light_blue) end end |
#handle_question(data) ⇒ Object
630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 |
# File 'lib/git_game_show/player_client.rb', line 630 def handle_question(data) # Invalidate any previous timer @current_timer_id = SecureRandom.uuid # Clear the screen completely clear_screen question_num = data['question_number'] total_questions = data['total_questions'] question = data['question'] timeout = data['timeout'] # Store question data in thread-local storage for access in other methods Thread.current[:question_data] = data # No need to reserve space for timer - it will be at the bottom of the screen # Draw a simple box for the question header box_width = 42 box_top = ("╭" + "─" * (box_width - 2) + "╮").center(@game_width) box_bottom = ("╰" + "─" * (box_width - 2) + "╯").center(@game_width) box_middle = "│#{"Question #{question_num} of #{total_questions}".center(box_width - 2)}│".center(@game_width) # Output the question box puts "\n" puts box_top.colorize(:light_blue) puts box_middle.colorize(:light_blue) puts box_bottom.colorize(:light_blue) puts "\n" # Display question puts " #{question}".colorize(:light_blue) # Display commit info if available if data['commit_info'] puts "\n Commit: #{data['commit_info']}".colorize(:yellow) end # Display code context if available (for BlameGame) if data['context'] puts "\n Code Context:".colorize(:light_blue) data['context'].split("\n").each do |line| # Check if this is the target line (with the > prefix) if line.start_with?('>') puts " #{line}".colorize(:yellow) # Highlight the target line else puts " #{line}".colorize(:white) end end end puts "\n" # Create a unique timer ID for this question timer_id = SecureRandom.uuid @current_timer_id = timer_id # Initialize remaining time for scoring @time_remaining = timeout # Update the timer display immediately update_countdown_display(timeout, timeout) # Variable to track if the timer has expired @timer_expired = false # Start countdown in a background thread with new approach countdown_thread = Thread.new do begin remaining = timeout while remaining > 0 && @current_timer_id == timer_id # Update both window title and fixed position display update_title_timer(remaining) update_countdown_display(remaining, timeout) # Sound alert when time is almost up (< 5 seconds) if remaining < 5 && remaining > 0 print "\a" if remaining % 2 == 0 # Beep on even seconds end # Store time for scoring @time_remaining = remaining # Wait one second sleep 1 remaining -= 1 end # Final update when timer reaches zero if @current_timer_id == timer_id update_countdown_display(0, timeout) # IMPORTANT: Send a timeout answer when time expires # without waiting for user input @timer_expired = true # Clear the screen to break out of any prompt/UI state clear_screen puts "\n ⏰ TIME'S UP! Timeout answer submitted.".colorize(:red) puts " Waiting for the next question...".colorize(:yellow) # Force terminal back to normal mode in case something is waiting for input system("stty sane") rescue nil system("tput cnorm") rescue nil # Re-enable cursor # Send a timeout answer to the server ({ type: MessageType::ANSWER, name: name, answer: nil, # nil indicates timeout question_id: data['question_id'] }) # Force kill other input methods by returning directly from handle_question # This breaks out of the entire method, bypassing any pending input operations return end rescue => e # Silent failure for robustness end end # Handle different question types - but wrap in a separate thread # so that timeouts can interrupt the UI input_thread = Thread.new do if data['question_type'] == 'ordering' # Special UI for ordering questions answer = handle_ordering_question(data['options'], data['question']) elsif data['options'] && !data['options'].empty? # Regular multiple choice question - with interrupt check begin # Configure prompt to be interruptible answer = @prompt.select(" Choose your answer:", data['options'], per_page: 10) do || # Check for timeout periodically during menu interactions .help "" .default 1 end rescue TTY::Reader::InputInterrupt # If interrupted, just return nil nil end else # Free text answer - with interrupt check begin answer = @prompt.ask(" Your answer:") do |q| # Check for timeout periodically q.help "" end rescue TTY::Reader::InputInterrupt # If interrupted, just return nil nil end end end # Wait for input but with timeout answer = nil begin # Try to join the thread but allow for interruption Timeout.timeout(timeout + 0.5) do answer = input_thread.value end rescue Timeout::Error # If timeout occurs during join, kill the thread input_thread.kill if input_thread.alive? end # Only send user answer if timer hasn't expired unless @timer_expired # Send answer back to server ({ type: MessageType::ANSWER, name: name, answer: answer, question_id: data['question_id'] }) puts "\n Answer submitted! Waiting for feedback...".colorize(:green) end # Stop the timer by invalidating its ID and terminating the thread @current_timer_id = SecureRandom.uuid # Change timer ID to signal thread to stop countdown_thread.kill if countdown_thread.alive? # Force kill the thread # Reset window title print "\033]0;Git Game Show\007" # Clear the timer status line at bottom term_height = `tput lines`.to_i rescue 24 print "\e7" # Save cursor position print "\e[#{term_height};1H" # Move to bottom line print "\e[K" # Clear line print "\e8" # Restore cursor position # The server will send ANSWER_FEEDBACK message right away, then we'll see feedback end |
#handle_round_result(data) ⇒ Object
Handle round results showing all players’ answers
887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 |
# File 'lib/git_game_show/player_client.rb', line 887 def handle_round_result(data) # Invalidate any running timer and reset window title @current_timer_id = SecureRandom.uuid print "\033]0;Git Game Show - Question Results\007" # Reset window title with context # Start with a clean screen clear_screen # Box is drawn with exactly 45 "━" characters for the top and bottom borders # The top and bottom including borders are 48 characters wide box_width = 40 box_top = ("╭" + "─" * box_width + "╮").center(@game_width) box_bottom = ("╰" + "─" * box_width + "╯").center(@game_width) box_middle = "│#{'Question Results'.center(box_width)}│".center(@game_width) # Output the box puts "\n" puts box_top.colorize(:light_blue) puts box_middle.colorize(:light_blue) puts box_bottom.colorize(:light_blue) puts "\n" # Show question again puts " Question: #{data['question'][:question]}".colorize(:light_blue) # Handle different display formats for correct answers if data['correct_answer'].is_a?(Array) puts " Correct order".colorize(:green) data['correct_answer'].each do |item| puts " #{item}".colorize(:green) end else puts " Correct answer: #{data['correct_answer']}".colorize(:green) end puts "\n All player results:".colorize(:light_blue) # Debug data temporarily removed # Handle results based on structure if data['results'].is_a?(Hash) data['results'].each do |player, result| # Ensure result is a hash with the expected keys if result.is_a?(Hash) # Check if 'correct' is a boolean or check string equality if it's a string correct = result['correct'] || false answer = result['answer'].is_a?(Array) ? "" : (result['answer'] || "No answer") points = result['points'] || 0 status = correct ? "✓" : "✗" points_str = "(+#{points} points)" player_str = player == name ? "#{player} (You)" : player # For ordering questions with array answers, show them with numbers if data['question'][:question_type] == 'ordering' && answer.is_a?(Array) # First display player name and points header = " #{player_str.ljust(20)} #{points_str.ljust(15)} #{status}" # Color according to correctness if correct puts header.colorize(:green) puts " Submitted order:".colorize(:green) answer.each_with_index do |item, idx| puts " #{idx + 1}. #{item}".colorize(:green) end else puts header.colorize(:red) puts " Submitted order:".colorize(:red) answer.each_with_index do |item, idx| puts " #{idx + 1}. #{item}".colorize(:red) end end else # Standard display for non-ordering questions player_output = " #{player_str.ljust(20)} #{points_str.ljust(15)} #{answer} #{status}" if correct puts player_output.colorize(:green) else puts player_output.colorize(:red) end end else # Fallback for unexpected result format puts " #{player}: #{result.inspect}".colorize(:yellow) end end else # Fallback message if results isn't a hash puts " No detailed results available".colorize(:yellow) end # Display current scoreboard if data['scores'] puts "\n Current Standings:".colorize(:yellow) data['scores'].each_with_index do |(player, score), index| player_str = player == name ? "#{player} (You)" : player rank = index + 1 # Add medal emoji for top 3 rank_display = case rank when 1 then "🥇" when 2 then "🥈" when 3 then "🥉" else "#{rank}." end output = " #{rank_display} #{player_str.ljust(20)} #{score} points" if player == name puts output.colorize(:yellow) else puts output.colorize(:light_blue) end end end puts "\n Next question coming up automatically...".colorize(:yellow) end |
#handle_round_start(data) ⇒ Object
594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 |
# File 'lib/git_game_show/player_client.rb', line 594 def handle_round_start(data) clear_screen # Draw a fancy round header round_num = data['round'] total_rounds = data['total_rounds'] mini_game = data['mini_game'] description = data['description'] example = data['example'] # Box is drawn with exactly 45 "━" characters for the top and bottom borders # The top and bottom including borders are 48 characters wide box_width = 42 box_top = ("╭" + "─" * (box_width - 2) + "╮").center(@game_width) box_bottom = ("╰" + "─" * (box_width - 2) + "╯").center(@game_width) box_middle = "│#{"Round #{round_num} of #{total_rounds}".center(box_width - 2)}│".center(@game_width) # Output the box puts "\n" puts box_top.colorize(:green) puts box_middle.colorize(:green) puts box_bottom.colorize(:green) puts "\n" puts " Mini-game: #{mini_game}".colorize(:light_blue) puts " #{description}".colorize(:light_blue) puts "\n" puts " Example: #{example}" puts "\n" # Count down to the start - don't sleep here as we're waiting for the server # to send us the questions after a fixed delay puts " Get ready for the first question...".colorize(:yellow) puts " Questions will appear automatically when the game begins.".colorize(:yellow) puts " The host is controlling the timing of all questions.".colorize(:light_blue) puts "\n\n" end |
#handle_scoreboard(data) ⇒ Object
1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 |
# File 'lib/git_game_show/player_client.rb', line 1006 def handle_scoreboard(data) # Invalidate any running timer and reset window title @current_timer_id = SecureRandom.uuid print "\033]0;Git Game Show - Scoreboard\007" # Reset window title with context # Always start with a clean screen for the scoreboard clear_screen box_width = 40 puts "\n" puts ("╭" + "─" * box_width + "╮").center(@game_width).colorize(:yellow) puts "│#{'Scoreboard'.center(box_width)}│".center(@game_width).colorize(:yellow) puts ("╰" + "─" * box_width + "╯").center(@game_width).colorize(:yellow) puts "\n" # Get player positions position = 1 last_score = nil data['scores'].each do |player, score| # Determine position (handle ties) position = data['scores'].values.index(score) + 1 if last_score != score last_score = score # Highlight current player player_str = player == name ? "#{player} (You)" : player # Format with position position_str = "#{position}." score_str = "#{score} points" # Add emoji for top 3 case position when 1 position_str = "🥇 #{position_str}" puts " #{position_str.ljust(5)} #{player_str.ljust(25)} #{score_str}".colorize(:yellow) when 2 position_str = "🥈 #{position_str}" puts " #{position_str.ljust(5)} #{player_str.ljust(25)} #{score_str}".colorize(:light_blue) when 3 position_str = "🥉 #{position_str}" puts " #{position_str.ljust(5)} #{player_str.ljust(25)} #{score_str}".colorize(:light_magenta) else puts " #{position_str.ljust(5)} #{player_str.ljust(25)} #{score_str}" end end puts "\n Next round coming up soon...".colorize(:light_blue) end |
#move_cursor_to(row, col) ⇒ Object
Helper to position cursor at a specific row/column
395 396 397 |
# File 'lib/git_game_show/player_client.rb', line 395 def move_cursor_to(row, col) print "\033[#{row};#{col}H" end |
#read_char ⇒ Object
Simplified key input reader that uses numbers for arrow keys
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 |
# File 'lib/git_game_show/player_client.rb', line 400 def read_char begin system("stty raw -echo") # Read a character c = STDIN.getc # Special handling for escape sequences if c == "\e" # Could be an arrow key - read more begin # Check if there's more data to read if IO.select([STDIN], [], [], 0.1) c2 = STDIN.getc if c2 == "[" # This is an arrow key or similar control sequence if IO.select([STDIN], [], [], 0.1) c3 = STDIN.getc case c3 when 'A' then return 65 # Up arrow (ASCII 'A') when 'B' then return 66 # Down arrow (ASCII 'B') when 'C' then return 67 # Right arrow when 'D' then return 68 # Left arrow else return c3.ord # Other control character end end else return c2.ord # ESC followed by another key end end rescue => e # Just return ESC if there's an error return 27 # ESC key end end # Just return the ASCII value for the key return c.ord ensure system("stty -raw echo") end end |
#read_char_with_timeout ⇒ Object
Non-blocking key input reader that supports timeouts
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 |
# File 'lib/git_game_show/player_client.rb', line 445 def read_char_with_timeout begin # Check if there's input data available if IO.select([STDIN], [], [], 0.1) # Read a character c = STDIN.getc # Handle nil case (EOF) return nil if c.nil? # Special handling for escape sequences if c == "\e" # Could be an arrow key - read more begin # Check if there's more data to read if IO.select([STDIN], [], [], 0.1) c2 = STDIN.getc if c2 == "[" # This is an arrow key or similar control sequence if IO.select([STDIN], [], [], 0.1) c3 = STDIN.getc case c3 when 'A' then return 65 # Up arrow (ASCII 'A') when 'B' then return 66 # Down arrow (ASCII 'B') when 'C' then return 67 # Right arrow when 'D' then return 68 # Left arrow else return c3.ord # Other control character end end else return c2.ord # ESC followed by another key end end rescue => e # Just return ESC if there's an error return 27 # ESC key end end # Just return the ASCII value for the key return c.ord end # No input available - return nil for timeout return nil rescue => e # In case of error, return nil return nil end end |
#send_join_request ⇒ Object
Make these methods public so they can be called from the WebSocket callbacks
74 75 76 77 78 79 80 |
# File 'lib/git_game_show/player_client.rb', line 74 def send_join_request ({ type: MessageType::JOIN_REQUEST, name: name, password: password }) end |
#send_message(message) ⇒ Object
1187 1188 1189 1190 1191 1192 1193 |
# File 'lib/git_game_show/player_client.rb', line 1187 def () begin @ws.send(.to_json) rescue => e puts "Error sending message: #{e.}".colorize(:red) end end |
#update_countdown_display(seconds, original_seconds) ⇒ Object
Helper method to display countdown using a status bar at the bottom of the screen
498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 |
# File 'lib/git_game_show/player_client.rb', line 498 def update_countdown_display(seconds, original_seconds) # Get terminal dimensions term_height = `tput lines`.to_i rescue 24 # Calculate a simple progress bar total_width = 30 progress_width = ((seconds.to_f / original_seconds) * total_width).to_i remaining_width = total_width - progress_width # Choose color based on time remaining color = if seconds <= 5 :red elsif seconds <= 10 :yellow else :green end # Create status bar with progress indicator = "[#{"█" * progress_width}#{" " * remaining_width}]" status_text = " ⏱️ Time remaining: #{seconds.to_s.rjust(2)} seconds ".colorize(color) + # Save cursor position print "\e7" # Move to bottom of screen (status line) print "\e[#{term_height};1H" # Clear the line print "\e[K" # Print status bar at bottom of screen print status_text # Restore cursor position print "\e8" STDOUT.flush end |
#update_title_timer(seconds) ⇒ Object
Helper method to print a countdown timer status in the window title This doesn’t interfere with the terminal content
233 234 235 236 237 238 |
# File 'lib/git_game_show/player_client.rb', line 233 def update_title_timer(seconds) # Use terminal escape sequence to update window title # This is widely supported and doesn't interfere with content print "\033]0;Git Game Show - #{seconds} seconds remaining\007" STDOUT.flush end |