git_game_show 0.1.4 → 0.1.5

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.
@@ -78,10 +78,16 @@ module GitGameShow
78
78
 
79
79
  # Draw horizontal divider line between main area and command area
80
80
  print @cursor.move_to(0, @command_line - 1)
81
- print "=" * @terminal_width
81
+ print "" * (@terminal_width - @sidebar_width - 3) + "╧" + "═" * (@sidebar_width + 2)
82
82
 
83
83
  # Draw vertical divider line between main area and sidebar
84
- (0...@command_line-1).each do |line|
84
+ print @cursor.move_to(@main_width, 0)
85
+ print "│"
86
+ print @cursor.move_to(@main_width, 1)
87
+ print "│"
88
+ print @cursor.move_to(@main_width, 2)
89
+ print "╞═"
90
+ (3...@command_line-1).each do |line|
85
91
  print @cursor.move_to(@main_width, line)
86
92
  print "│"
87
93
  end
@@ -114,16 +120,16 @@ module GitGameShow
114
120
  start_y = 8
115
121
 
116
122
  print @cursor.move_to(start_x, start_y)
117
- print "" + "─" * (link_box_width - 2) + ""
123
+ print "" + "─" * (link_box_width - 2) + ""
118
124
 
119
125
  print @cursor.move_to(start_x, start_y + 1)
120
- print "│" + " JOIN LINK (Copied to Clipboard) ".center(link_box_width - 2).colorize(:green) + "│"
126
+ print "│" + " Join Link (Copied to Clipboard) ".center(link_box_width - 2).colorize(:green) + "│"
121
127
 
122
128
  print @cursor.move_to(start_x, start_y + 2)
123
129
  print "│" + @join_link.center(link_box_width - 2).colorize(:yellow) + "│"
124
130
 
125
131
  print @cursor.move_to(start_x, start_y + 3)
126
- print "" + "─" * (link_box_width - 2) + ""
132
+ print "" + "─" * (link_box_width - 2) + ""
127
133
 
128
134
  # Also log that the link was copied
129
135
  log_message("Join link copied to clipboard", :green)
@@ -132,7 +138,7 @@ module GitGameShow
132
138
  def draw_sidebar
133
139
  # Draw sidebar header
134
140
  print @cursor.move_to(@main_width + 2, 1)
135
- print "PLAYERS".colorize(:cyan)
141
+ print "Players".colorize(:cyan)
136
142
 
137
143
  print @cursor.move_to(@main_width + 2, 2)
138
144
  print "═" * (@sidebar_width - 2)
@@ -250,7 +256,7 @@ module GitGameShow
250
256
  end
251
257
 
252
258
  def display_welcome_banner
253
- puts <<-BANNER.colorize(:green)
259
+ banner = <<-BANNER.colorize(:green)
254
260
  ██████╗ ██╗████████╗ ██████╗ █████╗ ███╗ ███╗███████╗
255
261
  ██╔════╝ ██║╚══██╔══╝ ██╔════╝ ██╔══██╗████╗ ████║██╔════╝
256
262
  ██║ ███╗██║ ██║ ██║ ███╗███████║██╔████╔██║█████╗
@@ -258,7 +264,7 @@ module GitGameShow
258
264
  ╚██████╔╝██║ ██║ ╚██████╔╝██║ ██║██║ ╚═╝ ██║███████╗
259
265
  ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝
260
266
  BANNER
261
- puts "\n SERVER STARTED - PORT: #{port}\n".colorize(:light_blue)
267
+ banner.each{|line| puts line.center(80)}
262
268
  end
263
269
 
264
270
  private
@@ -410,22 +416,30 @@ module GitGameShow
410
416
  log_message("#{truncated_name} timed out after #{time_taken.round(2)}s ⏰", :yellow)
411
417
  else
412
418
  # Regular answer processing
413
- # Calculate points for this answer
414
- correct = answer == current_question[:correct_answer]
415
- points = 0
416
-
417
- if correct
418
- points = 10 # Base points for correct answer
419
-
420
- # Bonus points for fast answers
421
- if time_taken < 5
422
- points += 5
423
- elsif time_taken < 10
424
- points += 3
419
+ # For ordering quizzes, we'll calculate points in evaluate_answers
420
+ # using the custom scoring systems in each mini-game
421
+ if current_question[:question_type] == 'ordering'
422
+ # Just store the answer and time, points will be calculated in evaluate_answers
423
+ correct = false # Will be properly set during evaluation
424
+ points = 0 # Will be properly set during evaluation
425
+ else
426
+ # For regular quizzes, calculate points immediately
427
+ correct = answer == current_question[:correct_answer]
428
+ points = 0
429
+
430
+ if correct
431
+ points = 10 # Base points for correct answer
432
+
433
+ # Bonus points for fast answers
434
+ if time_taken < 5
435
+ points += 5
436
+ elsif time_taken < 10
437
+ points += 3
438
+ end
425
439
  end
426
440
  end
427
441
 
428
- # Store the answer with points pre-calculated
442
+ # Store the answer
429
443
  @player_answers[player_name] = {
430
444
  answer: answer,
431
445
  time_taken: time_taken,
@@ -439,7 +453,11 @@ module GitGameShow
439
453
 
440
454
  # Log this answer - ensure the name is not too long
441
455
  truncated_name = player_name.length > 15 ? "#{player_name[0...12]}..." : player_name
442
- log_message("#{truncated_name} answered in #{time_taken.round(2)}s: #{correct ? "Correct ✓" : "Wrong ✗"}", correct ? :green : :red)
456
+ if current_question[:question_type] == 'ordering'
457
+ log_message("#{truncated_name} submitted ordering in #{time_taken.round(2)}s ⏱️", :cyan)
458
+ else
459
+ log_message("#{truncated_name} answered in #{time_taken.round(2)}s: #{correct ? "Correct ✓" : "Wrong ✗"}", correct ? :green : :red)
460
+ end
443
461
  end
444
462
 
445
463
  # Check if all players have answered, regardless of timeout or manual answer
@@ -458,6 +476,14 @@ module GitGameShow
458
476
  correct_answer: question[:correct_answer],
459
477
  points: points # Include points in the feedback
460
478
  }
479
+
480
+ # For ordering quizzes, we can't determine correctness immediately
481
+ # Instead we'll indicate that scoring will be calculated after timeout
482
+ if question[:question_type] == 'ordering'
483
+ feedback[:correct] = nil # nil means "scoring in progress"
484
+ feedback[:points] = nil
485
+ feedback[:message] = "Ordering submitted. Points will be calculated at the end of the round."
486
+ end
461
487
 
462
488
  ws.send(feedback.to_json)
463
489
  end
@@ -473,46 +499,156 @@ module GitGameShow
473
499
  end
474
500
 
475
501
  def evaluate_answers
476
- return unless @current_mini_game && @current_question_index < @round_questions.size
477
-
478
- # We can't actually cancel timers in the current EM implementation
479
- # Just set a flag indicating that we've already evaluated this question
502
+ # Safety checks
503
+ return unless @current_mini_game
504
+ return unless @round_questions && @current_question_index < @round_questions.size
480
505
  return if @question_already_evaluated
481
- @question_already_evaluated = true
482
506
 
483
- current_question = @round_questions[@current_question_index]
507
+ @question_already_evaluated = true
508
+
509
+ # Safety check - make sure we have a current question
510
+ begin
511
+ current_question = @round_questions[@current_question_index]
512
+ return unless current_question # Skip if no current question
513
+ rescue => e
514
+ log_message("Error accessing current question: #{e.message}", :red)
515
+ return
516
+ end
484
517
 
485
- # Use our pre-calculated answers instead of running evaluation again
486
- # This ensures consistency between immediate feedback and final results
487
518
  results = {}
488
- @player_answers.each do |player_name, answer_data|
489
- results[player_name] = {
490
- answer: answer_data[:answer],
491
- correct: answer_data[:correct],
492
- points: answer_data[:points]
493
- }
494
- end
519
+
520
+ begin
521
+ # For ordering quizzes or other special types, use the mini-game's evaluation method
522
+ if current_question[:question_type] == 'ordering'
523
+ # Convert the player_answers to the format expected by the mini-game's evaluate_answers
524
+ mini_game_answers = {}
525
+ @player_answers.each do |player_name, answer_data|
526
+ next unless player_name && answer_data # Skip nil entries
527
+
528
+ mini_game_answers[player_name] = {
529
+ answer: answer_data[:answer],
530
+ time_taken: answer_data[:time_taken] || 20
531
+ }
532
+ end
533
+
534
+ # Call the mini-game's evaluate_answers method with error handling
535
+ begin
536
+ results = @current_mini_game.evaluate_answers(current_question, mini_game_answers) || {}
537
+ rescue => e
538
+ log_message("Error in mini-game evaluate_answers: #{e.message}", :red)
539
+ # Create fallback results
540
+ results = {}
541
+ @player_answers.each do |player_name, answer_data|
542
+ next unless player_name
543
+
544
+ results[player_name] = {
545
+ answer: answer_data[:answer] || [],
546
+ correct: false,
547
+ points: 0,
548
+ partial_score: "Error calculating score"
549
+ }
550
+ end
551
+ end
552
+ else
553
+ # For regular quizzes, use our pre-calculated points
554
+ results = {}
555
+ @player_answers.each do |player_name, answer_data|
556
+ next unless player_name && answer_data # Skip nil entries
557
+
558
+ results[player_name] = {
559
+ answer: answer_data[:answer] || "No answer",
560
+ correct: answer_data[:correct] || false,
561
+ points: answer_data[:points] || 0
562
+ }
563
+ end
564
+ end
495
565
 
496
- # Update scores
497
- results.each do |player, result|
498
- @scores[player] += result[:points]
566
+ # Verify that results have required fields
567
+ results.each do |player_name, result|
568
+ # Ensure each result has the required fields with fallback values
569
+ results[player_name][:answer] = result[:answer] || "No answer"
570
+ results[player_name][:correct] = !!result[:correct] # Convert to boolean
571
+ results[player_name][:points] = result[:points] || 0
572
+ end
573
+
574
+ # Update scores
575
+ results.each do |player, result|
576
+ @scores[player] = (@scores[player] || 0) + (result[:points] || 0)
577
+ end
578
+ rescue => e
579
+ log_message("Error evaluating answers: #{e.message}", :red)
499
580
  end
500
581
 
501
- # Send results to all players
502
- broadcast_message({
503
- type: MessageType::ROUND_RESULT,
504
- question: current_question,
505
- results: results,
506
- correct_answer: current_question[:correct_answer],
507
- scores: @scores.sort_by { |_, score| -score }.to_h # Include current scores
508
- })
582
+ # Send results to all players - with error handling
583
+ begin
584
+ # Ensure we have valid data to broadcast
585
+ safe_results = {}
586
+ results.each do |player, result|
587
+ safe_results[player] = {
588
+ answer: result[:answer] || "No answer",
589
+ correct: !!result[:correct], # Convert to boolean
590
+ points: result[:points] || 0,
591
+ partial_score: result[:partial_score] || ""
592
+ }
593
+ end
594
+
595
+ # Sort scores safely
596
+ safe_scores = {}
597
+ begin
598
+ safe_scores = @scores.sort_by { |_, score| -(score || 0) }.to_h
599
+ rescue => e
600
+ log_message("Error sorting scores: #{e.message}", :red)
601
+ safe_scores = @scores.dup # Use unsorted if sorting fails
602
+ end
603
+
604
+ # For ordering questions, format the correct_answer as a list with numbers
605
+ formatted_correct_answer = current_question[:correct_answer] || []
606
+ if current_question[:question_type] == 'ordering'
607
+ formatted_correct_answer = current_question[:correct_answer].map.with_index do |item, idx|
608
+ "#{idx + 1}. #{item}" # Add numbers for easier reading
609
+ end
610
+ end
611
+
612
+ broadcast_message({
613
+ type: MessageType::ROUND_RESULT,
614
+ question: current_question,
615
+ results: safe_results,
616
+ correct_answer: formatted_correct_answer,
617
+ scores: safe_scores
618
+ })
619
+ rescue => e
620
+ log_message("Error broadcasting results: #{e.message}", :red)
621
+ end
509
622
 
510
- # Log current scores for the host
511
- log_message("Current scores:", :cyan)
512
- @scores.sort_by { |_, score| -score }.each do |player, score|
513
- # Truncate player names if too long
514
- truncated_name = player.length > 15 ? "#{player[0...12]}..." : player
515
- log_message("#{truncated_name}: #{score} points", :light_blue)
623
+ # Log current scores for the host - with error handling
624
+ begin
625
+ log_message("Current scores:", :cyan)
626
+
627
+ # Safety check for scores
628
+ if @scores.nil? || @scores.empty?
629
+ log_message("No scores available", :yellow)
630
+ else
631
+ # Sort scores safely
632
+ begin
633
+ sorted_scores = @scores.sort_by { |_, score| -(score || 0) }
634
+ rescue => e
635
+ log_message("Error sorting scores for display: #{e.message}", :red)
636
+ sorted_scores = @scores.to_a
637
+ end
638
+
639
+ # Display each score with error handling
640
+ sorted_scores.each do |player_entry|
641
+ # Extract player and score safely
642
+ player = player_entry[0].to_s
643
+ score = player_entry[1] || 0
644
+
645
+ # Truncate player names if too long
646
+ truncated_name = player.length > 15 ? "#{player[0...12]}..." : player
647
+ log_message("#{truncated_name}: #{score} points", :light_blue)
648
+ end
649
+ end
650
+ rescue => e
651
+ log_message("Error displaying scores: #{e.message}", :red)
516
652
  end
517
653
 
518
654
  # Move to next question or round
@@ -628,29 +764,71 @@ module GitGameShow
628
764
 
629
765
  # Send question to all players
630
766
  # Use mini-game specific timeout if available, otherwise use default
631
- timeout = @current_mini_game.class.respond_to?(:question_timeout) ?
632
- @current_mini_game.class.question_timeout :
633
- GitGameShow::DEFAULT_CONFIG[:question_timeout]
634
-
635
- question_data = {
636
- type: MessageType::QUESTION,
637
- question_id: @current_question_id,
638
- question: current_question[:question],
639
- options: current_question[:options],
640
- timeout: timeout,
641
- round: @current_round,
642
- question_number: @current_question_index + 1,
643
- total_questions: @round_questions.size
644
- }
767
+ # Ensure timeout is a number
768
+ timeout = 0
769
+ begin
770
+ if @current_mini_game.class.respond_to?(:question_timeout)
771
+ timeout = @current_mini_game.class.question_timeout.to_i
772
+ else
773
+ timeout = (GitGameShow::DEFAULT_CONFIG[:question_timeout] || 20).to_i
774
+ end
775
+ # Make sure we have a positive timeout value
776
+ timeout = 20 if timeout <= 0
777
+ rescue => e
778
+ log_message("Error getting timeout value: #{e.message}", :red)
779
+ timeout = 20 # Default fallback
780
+ end
645
781
 
646
- # Add question_type if it's a special question type (like ordering)
647
- if current_question[:question_type]
648
- question_data[:question_type] = current_question[:question_type]
782
+ # Prepare question data with type safety
783
+ begin
784
+ question_data = {
785
+ type: MessageType::QUESTION,
786
+ question_id: @current_question_id.to_s,
787
+ question: current_question[:question].to_s,
788
+ options: current_question[:options] || [],
789
+ timeout: timeout, # Now guaranteed to be a number
790
+ round: @current_round.to_i,
791
+ question_number: (@current_question_index + 1).to_i,
792
+ total_questions: @round_questions.size.to_i
793
+ }
794
+ rescue => e
795
+ log_message("Error preparing question data: #{e.message}", :red)
796
+ # Create a minimal fallback question if something went wrong
797
+ question_data = {
798
+ type: MessageType::QUESTION,
799
+ question_id: "#{@current_round}-#{@current_question_index}",
800
+ question: "Question #{@current_question_index + 1}",
801
+ options: ["Option 1", "Option 2", "Option 3", "Option 4"],
802
+ timeout: 20,
803
+ round: @current_round.to_i,
804
+ question_number: (@current_question_index + 1).to_i,
805
+ total_questions: @round_questions.size.to_i
806
+ }
649
807
  end
650
808
 
651
- # Add commit info if available (for AuthorQuiz)
652
- if current_question[:commit_info]
653
- question_data[:commit_info] = current_question[:commit_info]
809
+ # Add additional question data safely
810
+ begin
811
+ # Add question_type if it's a special question type (like ordering)
812
+ if current_question && current_question[:question_type]
813
+ question_data[:question_type] = current_question[:question_type].to_s
814
+ end
815
+
816
+ # Add commit info if available (for AuthorQuiz)
817
+ if current_question && current_question[:commit_info]
818
+ # Make a safe copy to avoid potential issues with the original object
819
+ if current_question[:commit_info].is_a?(Hash)
820
+ safe_commit_info = {}
821
+ current_question[:commit_info].each do |key, value|
822
+ safe_commit_info[key.to_s] = value.to_s
823
+ end
824
+ question_data[:commit_info] = safe_commit_info
825
+ else
826
+ question_data[:commit_info] = current_question[:commit_info].to_s
827
+ end
828
+ end
829
+ rescue => e
830
+ log_message("Error adding additional question data: #{e.message}", :red)
831
+ # Continue without the additional data
654
832
  end
655
833
 
656
834
  # Don't log detailed question info to prevent author lists from showing
@@ -660,7 +838,7 @@ module GitGameShow
660
838
  broadcast_message(question_data)
661
839
 
662
840
  # Set a timer for question timeout - ALWAYS evaluate after timeout
663
- # Use same timeout value we sent to clients
841
+ # Use same timeout value we sent to clients (already guaranteed to be a number)
664
842
  EM.add_timer(timeout) do
665
843
  log_message("Question timeout (#{timeout}s) - evaluating", :yellow)
666
844
  evaluate_answers unless @current_question_index >= @round_questions.size
@@ -668,63 +846,132 @@ module GitGameShow
668
846
  end
669
847
 
670
848
  def broadcast_scoreboard
671
- broadcast_message({
672
- type: MessageType::SCOREBOARD,
673
- scores: @scores.sort_by { |_, score| -score }.to_h
674
- })
849
+ begin
850
+ # Create a safe copy of scores
851
+ safe_scores = {}
852
+ if @scores && !@scores.empty?
853
+ @scores.each do |player, score|
854
+ next unless player && player.to_s != ""
855
+ safe_scores[player.to_s] = score.to_i
856
+ end
857
+ end
858
+
859
+ # Sort scores safely
860
+ sorted_scores = {}
861
+ begin
862
+ sorted_scores = safe_scores.sort_by { |_, score| -(score || 0) }.to_h
863
+ rescue => e
864
+ log_message("Error sorting scores for scoreboard: #{e.message}", :red)
865
+ sorted_scores = safe_scores # Use unsorted if sorting fails
866
+ end
867
+
868
+ broadcast_message({
869
+ type: MessageType::SCOREBOARD,
870
+ scores: sorted_scores
871
+ })
872
+ rescue => e
873
+ log_message("Error broadcasting scoreboard: #{e.message}", :red)
874
+ end
675
875
  end
676
876
 
677
877
  def end_game
678
878
  @game_state = :ended
679
879
 
680
- # Safety check - make sure we have scores
681
- if @scores.empty?
682
- log_message("Game ended, but no scores were recorded.", :yellow)
683
-
684
- # Reset game state for potential restart
685
- @current_round = 0
686
- @game_state = :lobby
687
- @current_mini_game = nil
688
- @round_questions = []
689
- @current_question_index = 0
690
- @question_already_evaluated = false
691
- @player_answers = {}
880
+ # Initialize winner variable outside the begin block so it's visible throughout the method
881
+ winner = nil
882
+
883
+ # Wrap the main logic in a begin/rescue block
884
+ begin
885
+ # Safety check - make sure we have scores and they're not nil
886
+ if @scores.nil? || @scores.empty?
887
+ log_message("Game ended, but no scores were recorded.", :yellow)
888
+
889
+ # Reset game state for potential restart
890
+ @current_round = 0
891
+ @game_state = :lobby
892
+ @current_mini_game = nil
893
+ @round_questions = []
894
+ @current_question_index = 0
895
+ @question_already_evaluated = false
896
+ @player_answers = {}
897
+ @scores = {}
898
+
899
+ # Update UI
900
+ update_player_list
901
+ log_message("Ready for a new game! Type 'start' when players have joined.", :green)
902
+ return
903
+ end
692
904
 
693
- # Update UI
694
- update_player_list
695
- log_message("Ready for a new game! Type 'start' when players have joined.", :green)
696
- return
697
- end
905
+ # Create a safe copy of scores to work with
906
+ safe_scores = {}
907
+ @scores.each do |player, score|
908
+ next unless player && player != ""
909
+ safe_scores[player] = score || 0
910
+ end
911
+
912
+ # Determine the winner with safety checks
913
+ begin
914
+ winner = safe_scores.max_by { |_, score| score || 0 }
915
+ rescue => e
916
+ log_message("Error determining winner: #{e.message}", :red)
917
+ end
698
918
 
699
- # Determine the winner
700
- winner = @scores.max_by { |_, score| score }
919
+ # Safety check - ensure winner isn't nil and has valid data
920
+ if winner.nil? || winner[0].nil? || winner[1].nil?
921
+ log_message("Error: Could not determine winner. No valid scores found.", :red)
922
+
923
+ # Create a synthetic winner as a fallback
924
+ if !safe_scores.empty?
925
+ # Take the first player as a last resort
926
+ player_name = safe_scores.keys.first.to_s
927
+ player_score = safe_scores.values.first || 0
928
+ winner = [player_name, player_score]
929
+ log_message("Using fallback winner: #{player_name}", :yellow)
930
+ else
931
+ # Reset and return early if we truly have no scores
932
+ @scores = {}
933
+ @current_round = 0
934
+ @game_state = :lobby
935
+ update_player_list
936
+ return
937
+ end
938
+ end
701
939
 
702
- # Safety check - ensure winner isn't nil
703
- if winner.nil?
704
- log_message("Error: Could not determine winner. No valid scores found.", :red)
940
+ # Sort scores safely
941
+ sorted_scores = {}
942
+ begin
943
+ sorted_scores = safe_scores.sort_by { |_, score| -(score || 0) }.to_h
944
+ rescue => e
945
+ log_message("Error sorting scores: #{e.message}", :red)
946
+ sorted_scores = safe_scores # Use unsorted if sorting fails
947
+ end
705
948
 
706
- # Reset and return early
949
+ # Notify all players
950
+ begin
951
+ broadcast_message({
952
+ type: MessageType::GAME_END,
953
+ winner: winner[0].to_s,
954
+ scores: sorted_scores
955
+ })
956
+ rescue => e
957
+ log_message("Error broadcasting final results: #{e.message}", :red)
958
+ end
959
+ rescue => e
960
+ # Catch-all for any unhandled exceptions
961
+ log_message("Critical error in end_game: #{e.message}", :red)
962
+ # Still try to reset game state
963
+ @game_state = :lobby
707
964
  @scores = {}
708
965
  @current_round = 0
709
- @game_state = :lobby
710
- update_player_list
711
- return
712
966
  end
713
967
 
714
- # Notify all players
715
- begin
716
- broadcast_message({
717
- type: MessageType::GAME_END,
718
- winner: winner[0],
719
- scores: @scores.sort_by { |_, score| -score }.to_h
720
- })
721
- rescue => e
722
- log_message("Error broadcasting final results: #{e.message}", :red)
968
+ # Display the final results on screen - with safety check
969
+ if winner && winner[0] && winner[1]
970
+ display_final_results(winner)
971
+ else
972
+ log_message("No valid winner data to display final results", :red)
723
973
  end
724
974
 
725
- # Display the final results on screen
726
- display_final_results(winner)
727
-
728
975
  # Reset game state for potential restart
729
976
  @scores = {}
730
977
  @current_round = 0
@@ -751,25 +998,47 @@ module GitGameShow
751
998
 
752
999
  def display_final_results(winner)
753
1000
  begin
1001
+ # Safety check - make sure we have a main_width value
1002
+ main_width = @main_width || 80
1003
+
754
1004
  # Use log messages instead of clearing screen
755
- divider = "=" * (@main_width - 5)
1005
+ divider = "=" * (main_width - 5)
756
1006
  log_message(divider, :yellow)
757
1007
  log_message("🏆 GAME OVER - FINAL SCORES 🏆", :yellow)
758
1008
 
759
- # Safety check for winner
1009
+ # Safety check for winner - we already checked in end_game but double-check here
760
1010
  if !winner || !winner[0] || !winner[1]
761
1011
  log_message("Error: Invalid winner data", :red)
762
1012
  log_message("Ready for a new game! Type 'start' when players have joined.", :green)
763
1013
  return
764
1014
  end
765
1015
 
766
- # Announce winner
767
- winner_name = winner[0].to_s
768
- winner_name = winner_name.length > 20 ? "#{winner_name[0...17]}..." : winner_name
769
- log_message("Winner: #{winner_name} with #{winner[1]} points!", :green)
1016
+ # Announce winner with defensive processing
1017
+ begin
1018
+ winner_name = winner[0].to_s
1019
+ winner_name = winner_name.length > 20 ? "#{winner_name[0...17]}..." : winner_name
1020
+ winner_score = winner[1].to_i
1021
+ log_message("Winner: #{winner_name} with #{winner_score} points!", :green)
1022
+ rescue => e
1023
+ log_message("Error displaying winner: #{e.message}", :red)
1024
+ log_message("A winner was determined but couldn't be displayed", :yellow)
1025
+ end
770
1026
 
1027
+ # Create a safe copy of scores to work with
1028
+ safe_scores = {}
1029
+ begin
1030
+ if @scores && !@scores.empty?
1031
+ @scores.each do |player, score|
1032
+ next unless player && player.to_s != ""
1033
+ safe_scores[player.to_s] = score.to_i
1034
+ end
1035
+ end
1036
+ rescue => e
1037
+ log_message("Error copying scores: #{e.message}", :red)
1038
+ end
1039
+
771
1040
  # Safety check for scores
772
- if @scores.nil? || @scores.empty?
1041
+ if safe_scores.empty?
773
1042
  log_message("No scores available to display", :yellow)
774
1043
  log_message(divider, :yellow)
775
1044
  log_message("Ready for a new game! Type 'start' when players have joined.", :green)
@@ -782,65 +1051,91 @@ module GitGameShow
782
1051
  leaderboard_entries = []
783
1052
 
784
1053
  # Sort scores safely
1054
+ sorted_scores = []
785
1055
  begin
786
- sorted_scores = @scores.sort_by { |_, score| -(score || 0) }
1056
+ sorted_scores = safe_scores.sort_by { |_, score| -(score || 0) }.to_a
787
1057
  rescue => e
788
- log_message("Error sorting scores: #{e.message}", :red)
789
- sorted_scores = @scores.to_a
1058
+ log_message("Error sorting scores for display: #{e.message}", :red)
1059
+ sorted_scores = safe_scores.to_a
790
1060
  end
791
1061
 
792
- # Show limited entries in console
793
- sorted_scores.take(10).each_with_index do |(name, score), index|
794
- # Safely handle name and score
795
- player_name = name.to_s
796
- player_score = score || 0
797
-
798
- # Truncate name if needed
799
- display_name = player_name.length > 15 ? "#{player_name[0...12]}..." : player_name
800
-
801
- # Format based on position
802
- case index
803
- when 0
804
- log_message("🥇 #{display_name}: #{player_score} points", :yellow)
805
- when 1
806
- log_message("🥈 #{display_name}: #{player_score} points", :light_blue)
807
- when 2
808
- log_message("🥉 #{display_name}: #{player_score} points", :light_magenta)
809
- else
810
- log_message("#{(index + 1).to_s}. #{display_name}: #{player_score} points", :white)
1062
+ max_to_show = 10
1063
+
1064
+ # Show limited entries in console with extra safety checks
1065
+ begin
1066
+ # Ensure we don't try to take more entries than exist
1067
+ entries_to_show = [sorted_scores.size, max_to_show].min
1068
+
1069
+ sorted_scores.take(entries_to_show).each_with_index do |score_entry, index|
1070
+ # Extra safety check for each entry
1071
+ next unless score_entry && score_entry.is_a?(Array) && score_entry.size >= 2
1072
+
1073
+ name = score_entry[0]
1074
+ score = score_entry[1]
1075
+
1076
+ # Safely handle name and score
1077
+ player_name = name.to_s
1078
+ player_score = score.to_i
1079
+
1080
+ # Truncate name if needed
1081
+ display_name = player_name.length > 15 ? "#{player_name[0...12]}..." : player_name
1082
+
1083
+ # Format based on position
1084
+ case index
1085
+ when 0
1086
+ log_message("🥇 #{display_name}: #{player_score} points", :yellow)
1087
+ when 1
1088
+ log_message("🥈 #{display_name}: #{player_score} points", :light_blue)
1089
+ when 2
1090
+ log_message("🥉 #{display_name}: #{player_score} points", :light_magenta)
1091
+ else
1092
+ log_message("#{(index + 1).to_s}. #{display_name}: #{player_score} points", :white)
1093
+ end
811
1094
  end
1095
+ rescue => e
1096
+ log_message("Error displaying leaderboard entries: #{e.message}", :red)
812
1097
  end
813
1098
 
814
1099
  # If there are more players than shown, add a note
815
- if sorted_scores.size > 10
816
- log_message("... and #{sorted_scores.size - 10} more (see full results in file)", :light_black)
1100
+ if sorted_scores.size > max_to_show
1101
+ log_message("... and #{sorted_scores.size - max_to_show} more (see full results in file)", :light_black)
817
1102
  end
818
1103
 
819
- # Build complete entries array for file
820
- sorted_scores.each_with_index do |(name, score), index|
821
- # Use safe values
822
- player_name = name.to_s
823
- player_score = score || 0
824
-
825
- # Add medals for file format
826
- medal = case index
827
- when 0 then "🥇"
828
- when 1 then "🥈"
829
- when 2 then "🥉"
830
- else "#{index + 1}."
831
- end
832
-
833
- leaderboard_entries << "#{medal} #{player_name}: #{player_score} points"
1104
+ # Build complete entries array for file with safety checks
1105
+ begin
1106
+ sorted_scores.each_with_index do |score_entry, index|
1107
+ # Skip invalid entries
1108
+ next unless score_entry && score_entry.is_a?(Array) && score_entry.size >= 2
1109
+
1110
+ # Use safe values
1111
+ player_name = score_entry[0].to_s
1112
+ player_score = score_entry[1].to_i
1113
+
1114
+ # Add medals for file format
1115
+ medal = case index
1116
+ when 0 then "🥇"
1117
+ when 1 then "🥈"
1118
+ when 2 then "🥉"
1119
+ else "#{index + 1}."
1120
+ end
1121
+
1122
+ leaderboard_entries << "#{medal} #{player_name}: #{player_score} points"
1123
+ end
1124
+ rescue => e
1125
+ log_message("Error preparing leaderboard entries for file: #{e.message}", :red)
834
1126
  end
835
1127
 
836
- # Save leaderboard to file
837
- filename = save_leaderboard_to_file(winner, leaderboard_entries)
1128
+ # Only try to save file if we have entries
1129
+ filename = nil
1130
+ if !leaderboard_entries.empty? && winner
1131
+ filename = save_leaderboard_to_file(winner, leaderboard_entries)
1132
+ end
838
1133
 
839
1134
  log_message(divider, :yellow)
840
1135
  if filename
841
1136
  log_message("Leaderboard saved to: #{filename}", :cyan)
842
1137
  else
843
- log_message("Failed to save leaderboard to file", :red)
1138
+ log_message("No leaderboard file generated", :yellow)
844
1139
  end
845
1140
  log_message("Ready for a new game! Type 'start' when players have joined.", :green)
846
1141
  rescue => e
@@ -852,17 +1147,23 @@ module GitGameShow
852
1147
 
853
1148
  def save_leaderboard_to_file(winner, leaderboard_entries)
854
1149
  begin
855
- # Validate parameters
856
- if !winner || !leaderboard_entries
857
- log_message("Error: Invalid data for leaderboard file", :red)
1150
+ # Validate parameters with thorough checks
1151
+ if !winner || !winner.is_a?(Array) || winner.size < 2 || winner[0].nil? || winner[1].nil?
1152
+ log_message("Error: Invalid winner data for leaderboard file", :red)
1153
+ return nil
1154
+ end
1155
+
1156
+ if !leaderboard_entries || !leaderboard_entries.is_a?(Array) || leaderboard_entries.empty?
1157
+ log_message("Error: Invalid entries data for leaderboard file", :red)
858
1158
  return nil
859
1159
  end
860
1160
 
861
1161
  # Create a unique filename with timestamp
862
- timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
1162
+ timestamp = Time.now.strftime("%Y%m%d_%H%M%S") rescue "unknown_time"
863
1163
  filename = "git_game_show_results_#{timestamp}.txt"
864
1164
 
865
1165
  # Use a base path that should be writable
1166
+ file_path = nil
866
1167
  begin
867
1168
  # First try current directory
868
1169
  file_path = File.join(Dir.pwd, filename)
@@ -873,56 +1174,88 @@ module GitGameShow
873
1174
  file_path = File.join(Dir.home, filename)
874
1175
  filename = File.join(Dir.home, filename) # Update filename to show full path
875
1176
  end
876
- rescue
1177
+ rescue => e
1178
+ log_message("Error with file path: #{e.message}", :red)
877
1179
  # If all else fails, use /tmp (Unix) or %TEMP% (Windows)
878
- temp_dir = ENV['TEMP'] || ENV['TMP'] || '/tmp'
879
- file_path = File.join(temp_dir, filename)
880
- filename = file_path # Update filename to show full path
1180
+ begin
1181
+ temp_dir = ENV['TEMP'] || ENV['TMP'] || '/tmp'
1182
+ file_path = File.join(temp_dir, filename)
1183
+ filename = file_path # Update filename to show full path
1184
+ rescue => e2
1185
+ log_message("Error setting up temp file path: #{e2.message}", :red)
1186
+ return nil
1187
+ end
1188
+ end
1189
+
1190
+ # Make sure we have a valid file path
1191
+ unless file_path && !file_path.empty?
1192
+ log_message("Could not determine a valid file path for leaderboard", :red)
1193
+ return nil
881
1194
  end
882
1195
 
883
1196
  # Get repo name from git directory path safely
1197
+ repo_name = "Unknown"
1198
+ begin
1199
+ if @repo && @repo.respond_to?(:dir) && @repo.dir && @repo.dir.respond_to?(:path)
1200
+ path = @repo.dir.path
1201
+ repo_name = path ? File.basename(path) : "Unknown"
1202
+ end
1203
+ rescue => e
1204
+ log_message("Error getting repo name: #{e.message}", :yellow)
1205
+ end
1206
+
1207
+ # Get player count safely
1208
+ player_count = 0
884
1209
  begin
885
- repo_name = @repo && @repo.dir ? File.basename(@repo.dir.path) : "Unknown"
886
- rescue
887
- repo_name = "Unknown"
1210
+ player_count = @players && @players.respond_to?(:keys) ? @players.keys.size : 0
1211
+ rescue => e
1212
+ log_message("Error getting player count: #{e.message}", :yellow)
888
1213
  end
889
1214
 
890
- File.open(file_path, "w") do |file|
891
- # Write header
892
- file.puts "GIT GAME SHOW - FINAL RESULTS"
893
- file.puts "==========================="
894
- file.puts "Date: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
895
- file.puts "Repository: #{repo_name}"
896
- file.puts "Players: #{@players.keys.size}"
897
- file.puts ""
1215
+ # Extract winner data safely
1216
+ winner_name = "Unknown"
1217
+ winner_score = 0
1218
+ begin
1219
+ winner_name = winner[0].to_s
1220
+ winner_score = winner[1].to_i
1221
+ rescue => e
1222
+ log_message("Error extracting winner data: #{e.message}", :yellow)
1223
+ end
898
1224
 
899
- # Write winner safely
900
- begin
901
- winner_name = winner[0].to_s
902
- winner_score = winner[1].to_i
1225
+ # Write the file with error handling
1226
+ begin
1227
+ File.open(file_path, "w") do |file|
1228
+ # Write header
1229
+ file.puts "Git Game Show - Final Results"
1230
+ file.puts "═════════════════════════════"
1231
+ file.puts "Date: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
1232
+ file.puts "Repository: #{repo_name}"
1233
+ file.puts "Players: #{player_count}"
1234
+ file.puts ""
1235
+
1236
+ # Write winner
903
1237
  file.puts "WINNER: #{winner_name} with #{winner_score} points!"
904
- rescue
905
- file.puts "WINNER: Unknown (error retrieving winner data)"
906
- end
907
- file.puts ""
1238
+ file.puts ""
908
1239
 
909
- # Write full leaderboard
910
- file.puts "FULL LEADERBOARD:"
911
- file.puts "---------------"
912
- if leaderboard_entries.empty?
913
- file.puts "No entries recorded"
914
- else
1240
+ # Write full leaderboard
1241
+ file.puts "Full Leaderboard:"
1242
+ file.puts "─────────────────"
915
1243
  leaderboard_entries.each do |entry|
916
1244
  file.puts entry.to_s
1245
+ rescue => e
1246
+ file.puts "Error with entry: #{e.message}"
917
1247
  end
1248
+
1249
+ # Write footer
1250
+ file.puts ""
1251
+ file.puts "Thanks for playing Git Game Show!"
918
1252
  end
919
1253
 
920
- # Write footer
921
- file.puts ""
922
- file.puts "Thanks for playing Git Game Show!"
1254
+ return filename
1255
+ rescue => e
1256
+ log_message("Error writing leaderboard file: #{e.message}", :red)
1257
+ return nil
923
1258
  end
924
-
925
- return filename
926
1259
  rescue => e
927
1260
  log_message("Error saving leaderboard: #{e.message}", :red)
928
1261
  return nil
@@ -932,10 +1265,44 @@ module GitGameShow
932
1265
  # Removed old full-screen methods as we now use log_message based approach
933
1266
 
934
1267
  def broadcast_message(message, exclude: nil)
935
- @players.each do |player_name, ws|
936
- # Skip excluded player if specified
937
- next if exclude && player_name == exclude
938
- ws.send(message.to_json)
1268
+ return if message.nil?
1269
+
1270
+ begin
1271
+ # Convert message to JSON safely
1272
+ json_message = nil
1273
+ begin
1274
+ json_message = message.to_json
1275
+ rescue => e
1276
+ log_message("Error converting message to JSON: #{e.message}", :red)
1277
+
1278
+ # Try to simplify the message to make it JSON-compatible
1279
+ simplified_message = {
1280
+ type: message[:type] || "unknown",
1281
+ message: "Error processing full message"
1282
+ }
1283
+ json_message = simplified_message.to_json
1284
+ end
1285
+
1286
+ return unless json_message
1287
+
1288
+ # Send to each player with error handling
1289
+ @players.each do |player_name, ws|
1290
+ # Skip excluded player if specified
1291
+ next if exclude && player_name == exclude
1292
+
1293
+ # Skip nil websockets
1294
+ next unless ws
1295
+
1296
+ # Send with error handling for each individual player
1297
+ begin
1298
+ ws.send(json_message)
1299
+ rescue => e
1300
+ log_message("Error sending to #{player_name}: #{e.message}", :yellow)
1301
+ # We don't remove the player here, as they might just have temporary connection issues
1302
+ end
1303
+ end
1304
+ rescue => e
1305
+ log_message("Fatal error in broadcast_message: #{e.message}", :red)
939
1306
  end
940
1307
  end
941
1308
 
@@ -1070,34 +1437,30 @@ module GitGameShow
1070
1437
  if !@ui_drawn
1071
1438
  system("clear") || system("cls")
1072
1439
 
1073
- # Header - only show on first draw
1074
- puts <<-HEADER.colorize(:green)
1075
- ██████╗ ██╗████████╗ ██████╗ █████╗ ███╗ ███╗███████╗
1076
- ██╔════╝ ██║╚══██╔══╝ ██╔════╝ ██╔══██╗████╗ ████║██╔════╝
1077
- ██║ ███╗██║ ██║ ██║ ███╗███████║██╔████╔██║█████╗
1078
- ██║ ██║██║ ██║ ██║ ██║██╔══██║██║╚██╔╝██║██╔══╝
1079
- ╚██████╔╝██║ ██║ ╚██████╔╝██║ ██║██║ ╚═╝ ██║███████╗
1080
- ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝
1081
- HEADER
1440
+ display_welcome_banner
1441
+
1442
+ puts "\n Server Started - Port: #{port}\n".colorize(:light_blue).center(80)
1443
+
1444
+ @ui_drawn = true
1082
1445
  else
1083
1446
  # Just print a separator for subsequent updates
1084
- puts "\n\n" + ("=" * 60)
1085
- puts "GIT GAME SHOW - STATUS UPDATE".center(60).colorize(:green)
1086
- puts ("=" * 60)
1447
+ puts "\n\n" + ("" * 60)
1448
+ puts "Git Game Show - Status Update".center(60).colorize(:green)
1449
+ puts ("" * 60)
1087
1450
  end
1088
1451
 
1089
1452
  # Server info
1090
- puts "\n==================== SERVER INFO ====================".colorize(:cyan)
1453
+ puts "\n════════════════════ Server Info ════════════════════".colorize(:cyan)
1091
1454
  puts "Status: #{game_state_text}".colorize(game_state_color)
1092
1455
  puts "Rounds: #{@current_round}/#{rounds}".colorize(:light_blue)
1093
1456
  puts "Repository: #{repo.dir.path}".colorize(:light_blue)
1094
1457
 
1095
1458
  # Display join link prominently
1096
- puts "\n==================== JOIN LINK =====================".colorize(:green)
1459
+ puts "\n════════════════════ Join Link ═════════════════════".colorize(:green)
1097
1460
  puts @join_link.to_s.colorize(:yellow)
1098
1461
 
1099
1462
  # Player list
1100
- puts "\n==================== PLAYERS =======================".colorize(:cyan)
1463
+ puts "\n════════════════════ Players ═══════════════════════".colorize(:cyan)
1101
1464
  if @players.empty?
1102
1465
  puts "No players have joined yet".colorize(:yellow)
1103
1466
  else
@@ -1113,13 +1476,13 @@ module GitGameShow
1113
1476
  puts "Players can join using the link above.".colorize(:yellow)
1114
1477
  puts "Type 'players' to see the current list of players.".colorize(:yellow)
1115
1478
  when :playing
1116
- puts "\n==================== GAME INFO =======================".colorize(:cyan)
1479
+ puts "\n════════════════════ Game Info ═══════════════════════".colorize(:cyan)
1117
1480
  puts "Current round: #{@current_round}/#{rounds}".colorize(:light_blue)
1118
1481
  puts "Current mini-game: #{@current_mini_game&.class&.name || 'N/A'}".colorize(:light_blue)
1119
1482
  puts "Question: #{@current_question_index + 1}/#{@round_questions.size}".colorize(:light_blue) if @round_questions&.any?
1120
1483
 
1121
1484
  # Show scoreboard
1122
- puts "\n=================== SCOREBOARD ======================".colorize(:cyan)
1485
+ puts "\n═══════════════════ Scoreboard ══════════════════════".colorize(:cyan)
1123
1486
  if @scores.empty?
1124
1487
  puts "No scores yet".colorize(:yellow)
1125
1488
  else
@@ -1149,7 +1512,8 @@ module GitGameShow
1149
1512
  end
1150
1513
 
1151
1514
  def print_help_message
1152
- puts "\n==================== HELP =========================="
1515
+ puts ""
1516
+ puts "═══════════════════ Help ═════════════════════════"
1153
1517
  puts "Available commands:"
1154
1518
  puts " help - Show this help message"
1155
1519
  puts " start - Start the game with current players"
@@ -1157,7 +1521,7 @@ module GitGameShow
1157
1521
  puts " status - Refresh the status display"
1158
1522
  puts " end - End the current game"
1159
1523
  puts " exit - Shut down the server and exit"
1160
- puts "=================================================="
1524
+ puts "══════════════════════════════════════════════════"
1161
1525
  end
1162
1526
 
1163
1527
  def print_status_message(message, status)
@@ -1188,10 +1552,22 @@ module GitGameShow
1188
1552
 
1189
1553
  # Select the next mini-game to ensure variety and avoid repetition
1190
1554
  def select_next_mini_game
1555
+ # Special case for when only one mini-game type is enabled
1556
+ if @mini_games.size == 1
1557
+ selected_game = @mini_games.first
1558
+ log_message("Only one mini-game type available: #{selected_game.name}", :light_black)
1559
+ return selected_game
1560
+ end
1561
+
1191
1562
  # If we have no more available mini-games, reset the cycle
1192
1563
  if @available_mini_games.empty?
1193
- # Repopulate with all mini-games except the last one used
1194
- @available_mini_games = @mini_games.reject { |game| game == @used_mini_games.last }
1564
+ # Handle the case where we might have only one game left after excluding the last used
1565
+ if @mini_games.size <= 2
1566
+ @available_mini_games = @mini_games.dup
1567
+ else
1568
+ # Repopulate with all mini-games except the last one used (if possible)
1569
+ @available_mini_games = @mini_games.reject { |game| game == @used_mini_games.last }
1570
+ end
1195
1571
 
1196
1572
  # Log that we're starting a new cycle
1197
1573
  log_message("Starting a new cycle of mini-games", :light_black)
@@ -1199,6 +1575,7 @@ module GitGameShow
1199
1575
 
1200
1576
  # Select a random game from the available ones
1201
1577
  selected_game = @available_mini_games.sample
1578
+ return @mini_games.first if selected_game.nil? # Fallback for safety
1202
1579
 
1203
1580
  # Remove the selected game from available and add to used
1204
1581
  @available_mini_games.delete(selected_game)
@@ -1214,9 +1591,9 @@ module GitGameShow
1214
1591
  def load_mini_games
1215
1592
  # Enable all mini-games
1216
1593
  [
1217
- GitGameShow::AuthorQuiz,
1218
- GitGameShow::CommitMessageQuiz,
1219
- GitGameShow::CommitMessageCompletion,
1594
+ # GitGameShow::AuthorQuiz,
1595
+ # GitGameShow::CommitMessageQuiz,
1596
+ # GitGameShow::CommitMessageCompletion,
1220
1597
  GitGameShow::DateOrderingQuiz
1221
1598
  ]
1222
1599
  end