quiz-app / player_interface.py
techiejohn's picture
Update player_interface.py
0b60c42 verified
# player_interface.py
import gradio as gr
import pandas as pd
from quiz_manager import quiz_manager # Assuming quiz_manager exists and is accessible
class PlayerInterface:
def __init__(self):
self.current_game_pin = None
self.current_question_index = -1
self.current_quiz_data = None
self.player_name = None
self.total_questions = 0
# Modified join_game to return updates for the Group visibility
def join_game(self, game_pin, player_name):
"""Validate the game pin and allow player to join."""
print(f"Attempting to join game: Pin={game_pin}, Player={player_name}")
if not game_pin or not game_pin.strip() or not player_name or not player_name.strip():
print("JOIN_GAME: Empty pin or name")
# Return outputs for staying on the join screen
return (
"Please enter both Game Pin and your Name.", # Join Status
gr.update(visible=True), # Show Join Group
gr.update(visible=False), # Hide Game Area
gr.update(visible=False), # Hide Leaderboard Area
gr.update(choices=[], visible=False, value=None), # Hide Question Dropdown
gr.update(visible=False), # Hide Next Button Row
gr.update(value="") # Clear Question Display
)
try:
# Check if quiz exists
quiz = quiz_manager.get_quiz(game_pin.strip())
if not quiz:
print(f"JOIN_GAME: Invalid pin: {game_pin}")
# Return outputs for staying on the join screen with error
return (
f"Invalid game pin: {game_pin.strip()}. Please try again.",
gr.update(visible=True), # Show Join Group
gr.update(visible=False), # Hide Game Area
gr.update(visible=False), # Hide Leaderboard Area
gr.update(choices=[], visible=False, value=None), # Hide Question Dropdown
gr.update(visible=False), # Hide Next Button Row
gr.update(value="") # Clear Question Display
)
self.current_game_pin = game_pin.strip()
self.player_name = player_name.strip()
self.current_quiz_data = quiz_manager.quizzes.get(self.current_game_pin)
# Initialize player score if they are new to this quiz
quiz_manager.initialize_player_score(self.current_game_pin, self.player_name)
self.total_questions = len(self.current_quiz_data.get('questions', []))
question_numbers = list(range(1, self.total_questions + 1))
print(f"JOIN_GAME: Success for {self.player_name} in {self.current_game_pin}. Total questions: {self.total_questions}")
# Return updates to hide Join Group, show Game Area, show Leaderboard Area, and update dropdown
return (
f"Joined game {self.current_game_pin} successfully, {self.player_name}!", # Join Status
gr.update(visible=False), # Hide Join Group
gr.update(visible=True), # Show Game Area
gr.update(visible=True), # Show Leaderboard Area
# Update choices, make visible if questions exist, reset value
gr.update(choices=question_numbers, visible=self.total_questions > 0, value=None),
gr.update(visible=self.total_questions > 0), # Show Next Button Row only if questions exist
gr.update(value="Click 'Start Quiz' to begin.") # Set initial text for Question Display
)
except Exception as e:
print(f"JOIN_GAME: Error - {str(e)}")
# Return updates for all relevant components on error, stay on join screen
return (
f"Error joining game: {str(e)}",
gr.update(visible=True), # Show Join Group
gr.update(visible=False), # Hide Game Area
gr.update(visible=False), # Hide Leaderboard Area
gr.update(choices=[], visible=False, value=None), # Hide Question Dropdown
gr.update(visible=False), # Hide Next Button Row
gr.update(value="") # Clear Question Display
)
def show_question(self):
"""Prepare updates for displaying the current question."""
print("SHOW_QUESTION: Called")
# Prepare default updates for components
default_options_update = gr.update(choices=[], value=None, visible=False)
default_prompt_label_update = gr.update(value="", visible=False)
default_question_area_update = gr.update(visible=True) # Assume area itself is visible when showing a question
default_submit_btn_update = gr.update(visible=True)
default_answer_output_update = gr.update(value="")
if not self.current_quiz_data or self.current_question_index == -1:
print("SHOW_QUESTION: No quiz data or invalid index")
return (
"Error: Could not load question.",
default_options_update,
default_prompt_label_update,
gr.update(visible=False), # Hide question area if no data
gr.update(visible=False), # Hide submit button if no data
gr.update(value="")
)
questions = self.current_quiz_data.get('questions', [])
if not questions or self.current_question_index < 0 or self.current_question_index >= len(questions):
print("SHOW_QUESTION: Invalid question data or index out of bounds")
return (
"Error loading question data.",
default_options_update,
default_prompt_label_update,
gr.update(visible=False),
gr.update(visible=False),
gr.update(value="")
)
question_data = questions[self.current_question_index]
options = question_data.get('options', [])
question_text = question_data.get('text', 'Error loading question text')
print(f"SHOW_QUESTION: Displaying question - index: {self.current_question_index}, text: {question_text}")
# Return specific updates for the current question
return (
f"**Question {self.current_question_index + 1}/{self.total_questions}:** {question_text}", # Update question_display value with bolding
gr.update(choices=options, value=None, label="Answer Choices", visible=True), # Update options_radio, make visible
gr.update(value="Select your answer:", visible=True), # Update prompt label, make visible
gr.update(visible=True), # Make question_area visible
gr.update(visible=True), # Make submit_btn visible
gr.update(value="") # Clear answer_output value
)
# Modified methods to return updates for dropdown and next button row visibility
# next_step returns 8 outputs
def next_step(self):
"""Starts the quiz, showing the first question."""
print("NEXT_STEP: Called (Start Quiz)")
if self.current_quiz_data and self.total_questions > 0:
self.current_question_index = 0
question_updates = self.show_question() # 6 outputs
print(f"NEXT_STEP: Showing question - index: {self.current_question_index}")
return (
*question_updates, # Unpack the 6 updates
gr.update(visible=False), # Hide Next Button Row
gr.update(value=self.current_question_index + 1) # Update dropdown value
)
else:
print("NEXT_STEP: No questions available")
# Return default updates if no questions, hide game elements
return (
"No questions available for this quiz.", # Question Display (1)
gr.update(choices=[], value=None, label="Answer Choices", visible=False), # Options Radio (2)
gr.update(value="", visible=False), # Prompt Label (3)
gr.update(visible=False), # Question Area (4)
gr.update(visible=False), # Submit Button (5)
gr.update(value=""), # Answer Output (6)
gr.update(visible=False), # Hide Next Button Row (7)
gr.update(value=None) # Clear dropdown value (8)
)
# next_question returns 8 outputs
def next_question(self):
"""Move to the next question."""
print("NEXT_QUESTION: Called")
if not self.current_quiz_data or not self.current_game_pin or not self.player_name:
print("NEXT_QUESTION: No quiz data or player context")
return (
"Please join a game first.", # Question Display (1)
gr.update(choices=[], value=None, label="Answer Choices", visible=False), # (2)
gr.update(value="", visible=False), # (3)
gr.update(visible=False), # (4)
gr.update(visible=False), # (5)
gr.update(value=""), # (6)
gr.update(visible=True), # Keep Next Button Row visible if no quiz data (7)
gr.update(value=None) # Clear dropdown value (8)
)
questions = self.current_quiz_data.get('questions', [])
if self.current_question_index < len(questions) - 1:
self.current_question_index += 1
question_updates = self.show_question() # 6 outputs
print(f"NEXT_QUESTION: Showing question - index: {self.current_question_index}")
return (
*question_updates,
gr.update(visible=False), # Keep Next Button Row hidden (7)
gr.update(value=self.current_question_index + 1) # Update dropdown value (8)
)
else:
print("NEXT_QUESTION: Quiz finished")
# Reset instance variables
self.current_question_index = -1
self.current_quiz_data = None
self.current_game_pin = None
self.player_name = None
self.total_questions = 0
# Return updates for quiz finished state, hide game elements, show join box needs separate trigger
return (
"Quiz finished! You can join another game.", # Question Display (1)
gr.update(choices=[], value=None, label="Answer Choices", visible=False), # (2)
gr.update(value="", visible=False), # (3)
gr.update(visible=False), # (4)
gr.update(visible=False), # (5)
gr.update(value=""), # (6)
gr.update(visible=False), # Hide Next Button Row (7)
gr.update(value=None) # Clear dropdown value (8)
# Note: Join Box, Game Area, Leaderboard Area visibility needs to be handled by a separate action or logic
# A 'Play Again' or 'Join New Game' button in the finished state might be better UI.
)
# previous_question returns 8 outputs
def previous_question(self):
"""Move to the previous question."""
print("PREVIOUS_QUESTION: Called")
if not self.current_quiz_data or not self.current_game_pin or not self.player_name:
print("PREVIOUS_QUESTION: No quiz data or player context")
return (
"Please join a game first.", # Question Display (1)
gr.update(choices=[], value=None, label="Answer Choices", visible=False), # (2)
gr.update(value="", visible=False), # (3)
gr.update(visible=False), # (4)
gr.update(visible=False), # (5)
gr.update(value=""), # (6)
gr.update(visible=True), # Keep Next Button Row visible if no quiz data (7)
gr.update(value=None) # Clear dropdown value (8)
)
if self.current_question_index > 0:
self.current_question_index -= 1
question_updates = self.show_question() # 6 outputs
print(f"PREVIOUS_QUESTION: Showing question - index: {self.current_question_index}")
return (
*question_updates,
gr.update(visible=False), # Keep Next Button Row hidden (7)
gr.update(value=self.current_question_index + 1) # Update dropdown value (8)
)
else:
print("PREVIOUS_QUESTION: First question")
# show_question already sets visibility for elements in question_area
question_updates = self.show_question() # 6 outputs for current question (index 0)
return (
*question_updates, # Includes showing question area elements
gr.update(visible=False), # Keep Next Button Row hidden (7)
gr.update(value=self.current_question_index + 1) # Keep dropdown value at 1 (8)
)
# select_question_by_number returns 8 outputs
def select_question_by_number(self, question_number):
"""Select a specific question by its number."""
print(f"SELECT_QUESTION_BY_NUMBER: Called with {question_number}")
# Handle case where question_number might be None from dropdown reset or initial state
if question_number is None:
print("SELECT_QUESTION_BY_NUMBER: question_number is None")
# Return outputs to clear question area, hide game elements
return (
"", # Clear question text (1)
gr.update(choices=[], value=None, label="Answer Choices", visible=False), # Clear options (2)
gr.update(value="", visible=False), # Clear prompt label (3)
gr.update(visible=False), # Hide question area (4)
gr.update(visible=False), # Hide submit button (5)
gr.update(value=""), # Clear answer output (6)
gr.update(visible=False), # Hide Next Button Row (7)
gr.update(value=None) # Clear dropdown value (8)
)
if not self.current_quiz_data or not self.current_game_pin or not self.player_name:
print("SELECT_QUESTION_BY_NUMBER: No quiz data or player context")
# Return default updates, hide game elements
return (
"Please join a game first.", # (1)
gr.update(choices=[], value=None, label="Answer Choices", visible=False), # (2)
gr.update(value="", visible=False), # (3)
gr.update(visible=False), # (4)
gr.update(visible=False), # (5)
gr.update(value=""), # (6)
gr.update(visible=False), # (7)
gr.update(value=None) # (8)
)
questions = self.current_quiz_data.get('questions', [])
# Convert question_number to integer index
try:
index = int(question_number) - 1
except (ValueError, TypeError):
print("SELECT_QUESTION_BY_NUMBER: Invalid number input")
return (
"Invalid input for question number.", # (1)
gr.update(choices=[], value=None, label="Answer Choices", visible=False), # (2)
gr.update(value="", visible=False), # (3)
gr.update(visible=False), # (4)
gr.update(visible=False), # (5)
gr.update(value=""), # (6)
gr.update(visible=False), # (7)
gr.update(value=None) # (8)
)
if 0 <= index < len(questions):
self.current_question_index = index
question_updates = self.show_question() # 6 outputs
print(f"SELECT_QUESTION_BY_NUMBER: Showing question - index: {self.current_question_index}")
# Append updates for the dropdown (keep value) and keep next button row hidden
return (
*question_updates,
gr.update(visible=False), # Keep Next Button Row hidden (7)
gr.update(value=self.current_question_index + 1) # Keep dropdown value (8)
)
else:
print("SELECT_QUESTION_BY_NUMBER: Invalid question number")
# Return updates for invalid number, hide game elements
return (
"Invalid question number.", # (1)
gr.update(choices=[], value=None, label="Answer Choices", visible=False), # (2)
gr.update(value="", visible=False), # (3)
gr.update(visible=False), # (4)
gr.update(visible=False), # (5)
gr.update(value=""), # (6)
gr.update(visible=False), # (7)
gr.update(value=None) # (8)
)
def submit_answer(self, selected_option):
"""Submit the player's answer for the current question."""
if not self.current_quiz_data or self.current_question_index == -1 or not self.current_game_pin or not self.player_name:
return "Please join a game and select a question first."
# Ensure selected_option is not None if the user submits without selecting
if selected_option is None:
return "Please select an answer."
# Fetch quiz data again to ensure it's fresh before checking answer
quiz = quiz_manager.get_quiz(self.current_game_pin)
if not quiz or 'questions' not in quiz or self.current_question_index < 0 or self.current_question_index >= len(quiz['questions']):
return "Error submitting answer: Could not retrieve current question data."
current_question = quiz['questions'][self.current_question_index]
correct_answer = current_question.get('correct_answer')
# Handle cases where correct_answer might be missing
if correct_answer is None:
return "Error submitting answer: Correct answer not defined for this question."
is_correct = selected_option.strip() == correct_answer.strip()
try:
quiz_manager.record_answer(self.current_game_pin, self.player_name, is_correct)
except Exception as e:
return f"Error recording score: {str(e)}"
return "Correct!" if is_correct else f"Incorrect. The correct answer was: {correct_answer}"
def get_leaderboard(self):
"""Retrieve and display the leaderboard."""
print("GET_LEADERBOARD: Called")
# This method is called by the Refresh button *inside* the leaderboard_area.
# It relies on self.current_game_pin being set by the join_game method.
if not self.current_game_pin:
# This case should ideally not happen if UI flow is correct, but for safety
return "Please join a game first to view the leaderboard."
try:
leaderboard = quiz_manager.get_leaderboard(self.current_game_pin)
if not leaderboard:
print("GET_LEADERBOARD: No scores recorded")
return "No scores recorded yet."
leaderboard_str = "### Leaderboard:\n"
# Sort leaderboard by score in descending order
try:
# get_leaderboard in quiz_manager returns a sorted list of (name, data) tuples
sorted_leaderboard = leaderboard # It's already sorted by quiz_manager
for i, (name, data) in enumerate(sorted_leaderboard, 1):
score = data.get('score', 0) # Safely get score from the dict
leaderboard_str += f"{i}. {name}: {score} points\n"
print("GET_LEADERBOARD: Leaderboard generated")
return leaderboard_str
except (TypeError, IndexError, AttributeError) as e:
# Catch potential errors if the list elements are not in the expected format
print(f"GET_LEADERBOARD: Error processing leaderboard data - {str(e)}")
# Display raw data for debugging
return f"Error displaying leaderboard data: {str(e)}.\nRaw data: {leaderboard}"
except Exception as e:
return f"Error retrieving leaderboard: {str(e)}"
def create_player_interface(self):
"""Create the Gradio interface for players with a step-by-step layout."""
with gr.Blocks() as player_interface:
gr.Markdown("## Quiz Player Dashboard")
# --- Define all Components First ---
# Join Game Section (Visible initially)
# Replaced gr.Box with gr.Group
join_group = gr.Group(visible=True)
with join_group:
gr.Markdown("### Join Game")
with gr.Row():
game_pin_input = gr.Textbox(label="Enter Game Pin", scale=1)
player_name_input = gr.Textbox(label="Your Name", scale=1)
join_btn = gr.Button("Join Game", scale=1)
join_output = gr.Textbox(label="Join Status", interactive=False)
# Game Area (Hidden initially, shown after joining)
game_area = gr.Column(visible=False)
with game_area:
gr.Markdown("### Play Quiz")
# Initial Next Button (to start the quiz) - part of game_area but controlled separately
next_button_row = gr.Row(visible=False) # Controlled visible/hidden
with next_button_row:
next_btn = gr.Button("Start Quiz")
# Question Dropdown (visibility controlled dynamically)
question_dropdown = gr.Dropdown(label="Select Question", choices=[], visible=False, interactive=True)
# Question Display Area (Hidden initially, shown after starting/selecting question)
question_area = gr.Column(visible=False) # Controlled visible/hidden
with question_area:
question_display = gr.Markdown(label="Question Text")
options_prompt_label = gr.Label(label="", visible=False)
options_radio = gr.Radio(label="Answer Choices", visible=False)
submit_btn = gr.Button("Submit Answer", visible=False)
answer_output = gr.Textbox(label="Answer Result", interactive=False)
# Navigation Buttons
with gr.Row():
prev_question_btn = gr.Button("Previous Question")
next_question_btn = gr.Button("Next Question")
# Leaderboard Area (Hidden initially, shown after joining)
leaderboard_area = gr.Column(visible=False) # Controlled visible/hidden
with leaderboard_area:
gr.Markdown("### Current Leaderboard")
leaderboard_output = gr.Markdown(label="Leaderboard Results")
get_leaderboard_btn = gr.Button("Refresh Leaderboard")
# --- Define all Interactions After Components ---
# Join Game interaction
# join_game returns 7 outputs: status, join_group_vis, game_area_vis, leaderboard_area_vis, dropdown_update, next_btn_row_vis, question_display_initial_text
join_btn.click(
fn=self.join_game,
inputs=[game_pin_input, player_name_input],
outputs=[
join_output,
join_group, # Update visibility of join group
game_area, # Update visibility of game area
leaderboard_area, # Update visibility of leaderboard area
question_dropdown, # Update dropdown choices/visibility
next_button_row, # Update visibility of next button row
question_display # Update initial text of question display
]
)
# Start Quiz button interaction (uses next_step to show the first question)
# next_step returns 8 outputs: question_display, options_radio, prompt_label, question_area, submit_btn, answer_output, next_btn_row_vis, dropdown_value
next_btn.click(
fn=self.next_step,
inputs=None, # next_step uses instance variables
outputs=[
question_display,
options_radio,
options_prompt_label,
question_area,
submit_btn,
answer_output,
next_button_row,
question_dropdown # Update dropdown value
]
)
# Question Dropdown interaction
# select_question_by_number returns 8 outputs: question_display, options_radio, prompt_label, question_area, submit_btn, answer_output, next_btn_row_vis, dropdown_value
question_dropdown.change(
fn=self.select_question_by_number,
inputs=question_dropdown, # Pass the selected value (the question number)
outputs=[
question_display,
options_radio,
options_prompt_label,
question_area,
submit_btn,
answer_output,
next_button_row, # Ensure next button row is hidden after selection
question_dropdown # Keep dropdown value updated
]
)
# Next Question button interaction
# next_question returns 8 outputs: question_display, options_radio, prompt_label, question_area, submit_btn, answer_output, next_btn_row_vis, dropdown_value
next_question_btn.click(
fn=self.next_question,
inputs=None, # Uses instance variables
outputs=[
question_display,
options_radio,
options_prompt_label,
question_area,
submit_btn,
answer_output,
next_button_row,
question_dropdown # Update dropdown value
]
)
# Previous Question button interaction
# previous_question returns 8 outputs: question_display, options_radio, prompt_label, question_area, submit_btn, answer_output, next_btn_row_vis, dropdown_value
prev_question_btn.click(
fn=self.previous_question,
inputs=None, # Uses instance variables
outputs=[
question_display,
options_radio,
options_prompt_label,
question_area,
submit_btn,
answer_output,
next_button_row,
question_dropdown # Update dropdown value
]
)
# Submit Answer button interaction
submit_btn.click(
fn=self.submit_answer,
inputs=options_radio, # Input is the selected value from the radio button
outputs=answer_output, # Output is the result of the submission
)
# Refresh Leaderboard button interaction
get_leaderboard_btn.click(
fn=self.get_leaderboard,
inputs=None, # Uses instance variables
outputs=leaderboard_output, # Output is the formatted leaderboard string
)
# No specific load interaction needed here, join_game handles initial visibility.
return player_interface
# --- How to use this class ---
# Create the PlayerInterface instance
player_interface_instance = PlayerInterface()
# To access the Gradio interface object:
# The app.py file will call this method to get the interface block
player_gradio_interface = player_interface_instance.create_player_interface()
# You can now use player_gradio_interface in your main app.py file.