git_game_show 0.1.2 → 0.1.4

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