Files
science-bowl-practice/display.js

475 lines
15 KiB
JavaScript

// Use CORS proxy to bypass restrictions
const CORS_PROXY = 'https://corsproxy.io/?';
const API_URL = 'https://scibowldb.com/api/questions/random';
// DOM elements
const questionText = document.getElementById('questionText');
const questionCategory = document.getElementById('questionCategory');
const questionNum = document.getElementById('questionNum');
const correctAnswer = document.getElementById('correctAnswer');
const answerSection = document.getElementById('answerSection');
const buzzedPlayer = document.getElementById('buzzedPlayer');
const timerDisplay = document.getElementById('timerDisplay');
const newQuestionBtn = document.getElementById('newQuestion');
const activateBuzzerBtn = document.getElementById('activateBuzzer');
const markCorrectBtn = document.getElementById('markCorrect');
const markIncorrectBtn = document.getElementById('markIncorrect');
const showAnswerBtn = document.getElementById('showAnswer');
const resetGameBtn = document.getElementById('resetGame');
const readQuestionBtn = document.getElementById('readQuestion');
let currentQuestion = null;
let currentQuestionNumber = 1;
let currentBuzzedPlayer = null;
let speechSynthesis = window.speechSynthesis;
let currentUtterance = null;
let voicesLoaded = false;
let isShowingBonus = false;
let timerInterval = null;
let timeRemaining = 0;
// Track used questions to avoid repeats
let usedQuestionIds = new Set();
let allQuestions = [];
let questionsLoaded = false;
// Load voices
function loadVoices() {
return new Promise((resolve) => {
let voices = speechSynthesis.getVoices();
if (voices.length > 0) {
voicesLoaded = true;
resolve(voices);
} else {
speechSynthesis.onvoiceschanged = () => {
voices = speechSynthesis.getVoices();
voicesLoaded = true;
resolve(voices);
};
}
});
}
loadVoices().then(() => {
console.log('Voices loaded:', speechSynthesis.getVoices().length);
});
// Load all questions once at startup
async function loadAllQuestions() {
if (questionsLoaded) return;
try {
console.log('Loading all questions from database...');
const response = await fetch(CORS_PROXY + encodeURIComponent('https://scibowldb.com/api/questions'), {
method: 'GET'
});
if (!response.ok) {
throw new Error('Failed to load questions');
}
const data = await response.json();
allQuestions = data.questions || data;
questionsLoaded = true;
console.log(`Loaded ${allQuestions.length} questions!`);
} catch (error) {
console.error('Error loading questions:', error);
alert('Could not load question database. Will use random API instead (may repeat questions).');
}
}
// Start loading questions immediately
loadAllQuestions();
// Initialize game state
database.ref('gameState').set({
buzzerActive: false,
currentQuestion: null,
scores: {
player1: 0,
player2: 0,
player3: 0,
player4: 0
},
questionNumber: 1,
isReading: false
});
// Listen for game state changes
database.ref('gameState').on('value', (snapshot) => {
const state = snapshot.val();
if (!state) return;
// Update scores
document.getElementById('score1').textContent = state.scores.player1 || 0;
document.getElementById('score2').textContent = state.scores.player2 || 0;
document.getElementById('score3').textContent = state.scores.player3 || 0;
document.getElementById('score4').textContent = state.scores.player4 || 0;
// Update buzzer lights
if (state.buzzer && state.buzzer.playerId) {
const playerNum = state.buzzer.playerId.replace('player', '');
currentBuzzedPlayer = state.buzzer.playerId;
document.querySelectorAll('.light').forEach(l => l.classList.remove('active'));
document.getElementById(`light${playerNum}`).classList.add('active');
buzzedPlayer.textContent = `Player ${playerNum} buzzed in!`;
// Stop reading and timer when someone buzzes
if (currentUtterance && speechSynthesis.speaking) {
speechSynthesis.cancel();
readQuestionBtn.textContent = '🔊 Read Question';
readQuestionBtn.disabled = false;
}
stopTimer();
markCorrectBtn.style.display = 'inline-block';
markIncorrectBtn.style.display = 'inline-block';
showAnswerBtn.style.display = 'inline-block';
}
});
// Timer functions
function startTimer(seconds) {
stopTimer();
timeRemaining = seconds;
timerDisplay.textContent = `Time: ${seconds}s`;
timerDisplay.style.display = 'block';
timerInterval = setInterval(() => {
timeRemaining--;
timerDisplay.textContent = `Time: ${timeRemaining}s`;
if (timeRemaining <= 0) {
stopTimer();
timerDisplay.textContent = 'Time\'s up!';
setTimeout(() => {
timerDisplay.style.display = 'none';
}, 2000);
}
}, 1000);
}
function stopTimer() {
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
}
// Get filtered questions based on user selection
function getFilteredQuestions() {
const categories = Array.from(document.querySelectorAll('.category-checkbox:checked'))
.map(cb => cb.value);
const source = document.getElementById('sourceSelect').value;
const roundNum = document.getElementById('roundNumber').value;
let filtered = allQuestions.filter(q => {
// Filter by category
if (categories.length > 0 && !categories.includes(q.category)) {
return false;
}
// Filter by source
if (source && !q.source.startsWith(source)) {
return false;
}
// Filter by round number
if (roundNum && !q.source.includes(`round${roundNum}`)) {
return false;
}
// Exclude already used questions
if (usedQuestionIds.has(q.id)) {
return false;
}
return true;
});
return filtered;
}
// Fetch new question with filtering
newQuestionBtn.addEventListener('click', async () => {
const categories = Array.from(document.querySelectorAll('.category-checkbox:checked'))
.map(cb => cb.value);
if (categories.length === 0) {
alert('Please select at least one category!');
return;
}
// Wait for questions to load if not ready
if (!questionsLoaded) {
newQuestionBtn.textContent = 'Loading questions...';
newQuestionBtn.disabled = true;
await loadAllQuestions();
newQuestionBtn.textContent = 'New Question';
newQuestionBtn.disabled = false;
}
try {
const availableQuestions = getFilteredQuestions();
if (availableQuestions.length === 0) {
const shouldReset = confirm('No more unique questions available with these filters! Reset used questions?');
if (shouldReset) {
usedQuestionIds.clear();
newQuestionBtn.click();
return;
} else {
alert('Try changing your filters or reset the game.');
return;
}
}
// Pick a random question from available ones
const randomIndex = Math.floor(Math.random() * availableQuestions.length);
currentQuestion = availableQuestions[randomIndex];
// Mark this question as used
usedQuestionIds.add(currentQuestion.id);
isShowingBonus = false;
console.log('Selected question:', currentQuestion);
console.log('Questions remaining:', availableQuestions.length - 1);
questionText.textContent = currentQuestion.tossup_question;
questionCategory.textContent = `${currentQuestion.category} - TOSSUP`;
correctAnswer.textContent = currentQuestion.tossup_answer;
questionNum.textContent = currentQuestionNumber;
// Show source info
if (currentQuestion.source) {
questionCategory.textContent = `${currentQuestion.category} - TOSSUP (${currentQuestion.source})`;
}
await database.ref('gameState').update({
currentQuestion: currentQuestion,
buzzerActive: false,
buzzer: null,
questionNumber: currentQuestionNumber,
isReading: false
});
// Reset UI
answerSection.style.display = 'none';
buzzedPlayer.textContent = '';
timerDisplay.style.display = 'none';
document.querySelectorAll('.light').forEach(l => l.classList.remove('active'));
markCorrectBtn.style.display = 'none';
markIncorrectBtn.style.display = 'none';
showAnswerBtn.style.display = 'none';
activateBuzzerBtn.disabled = false;
readQuestionBtn.disabled = false;
currentQuestionNumber++;
console.log('Question loaded successfully!');
} catch (error) {
console.error('Error fetching question:', error);
alert('Error loading question. Try refreshing the page.');
}
});
// Read question aloud
readQuestionBtn.addEventListener('click', async () => {
if (!currentQuestion) {
alert('Load a question first!');
return;
}
if (!voicesLoaded) {
await loadVoices();
}
if (speechSynthesis.speaking) {
speechSynthesis.cancel();
await new Promise(resolve => setTimeout(resolve, 100));
}
const textToRead = isShowingBonus ? currentQuestion.bonus_question : currentQuestion.tossup_question;
currentUtterance = new SpeechSynthesisUtterance(textToRead);
// Select best voice
const voices = speechSynthesis.getVoices();
const preferredVoice = voices.find(v => v.name.includes('Google') && v.lang.startsWith('en')) ||
voices.find(v => v.lang === 'en-US' && v.name.includes('Natural')) ||
voices.find(v => v.lang === 'en-US') ||
voices[0];
if (preferredVoice) {
currentUtterance.voice = preferredVoice;
}
currentUtterance.rate = 0.9;
currentUtterance.pitch = 1.0;
currentUtterance.volume = 1.0;
console.log('Reading with voice:', preferredVoice?.name);
database.ref('gameState/isReading').set(true);
currentUtterance.onstart = () => {
readQuestionBtn.textContent = '🔊 Reading...';
readQuestionBtn.disabled = true;
};
currentUtterance.onend = () => {
database.ref('gameState/isReading').set(false);
readQuestionBtn.textContent = '🔊 Read Question';
readQuestionBtn.disabled = false;
// Start timer after reading
const timerDuration = isShowingBonus ? 20 : 5;
startTimer(timerDuration);
};
currentUtterance.onerror = (event) => {
console.error('Speech error:', event);
database.ref('gameState/isReading').set(false);
readQuestionBtn.textContent = '🔊 Read Question';
readQuestionBtn.disabled = false;
};
speechSynthesis.speak(currentUtterance);
});
// Activate buzzers
activateBuzzerBtn.addEventListener('click', () => {
database.ref('gameState').update({
buzzerActive: true,
buzzer: null
});
activateBuzzerBtn.disabled = true;
answerSection.style.display = 'none';
});
// Mark correct
markCorrectBtn.addEventListener('click', () => {
if (!currentBuzzedPlayer) return;
const points = isShowingBonus ? 10 : 4;
database.ref(`gameState/scores/${currentBuzzedPlayer}`).transaction((score) => {
return (score || 0) + points;
});
// If tossup was correct, show bonus
if (!isShowingBonus && currentQuestion.bonus_question) {
showBonus();
} else {
resetForNextQuestion();
}
});
// Show bonus question
function showBonus() {
isShowingBonus = true;
questionText.textContent = currentQuestion.bonus_question;
questionCategory.textContent = currentQuestion.category + ' - BONUS';
correctAnswer.textContent = currentQuestion.bonus_answer;
// Reset UI for bonus
buzzedPlayer.textContent = '';
document.querySelectorAll('.light').forEach(l => l.classList.remove('active'));
markCorrectBtn.style.display = 'none';
markIncorrectBtn.style.display = 'none';
showAnswerBtn.style.display = 'none';
answerSection.style.display = 'none';
readQuestionBtn.disabled = false;
alert('Tossup correct! Now reading the BONUS question for the same team.');
}
// Mark incorrect
markIncorrectBtn.addEventListener('click', () => {
if (!currentBuzzedPlayer) return;
const points = isShowingBonus ? 0 : -4;
if (points !== 0) {
database.ref(`gameState/scores/${currentBuzzedPlayer}`).transaction((score) => {
return (score || 0) + points;
});
}
if (!isShowingBonus) {
// Reactivate buzzers for tossup
database.ref('gameState').update({
buzzerActive: true,
buzzer: null
});
answerSection.style.display = 'none';
markCorrectBtn.style.display = 'none';
markIncorrectBtn.style.display = 'none';
showAnswerBtn.style.display = 'none';
} else {
resetForNextQuestion();
}
});
// Show answer
showAnswerBtn.addEventListener('click', () => {
answerSection.style.display = 'block';
});
// Reset game
resetGameBtn.addEventListener('click', () => {
if (confirm('Reset all scores and used questions?')) {
currentQuestionNumber = 1;
usedQuestionIds.clear();
speechSynthesis.cancel();
stopTimer();
database.ref('gameState').set({
buzzerActive: false,
currentQuestion: null,
scores: {
player1: 0,
player2: 0,
player3: 0,
player4: 0
},
questionNumber: 1,
isReading: false
});
questionText.textContent = 'Click "New Question" to start';
answerSection.style.display = 'none';
readQuestionBtn.disabled = true;
console.log('Game reset! All questions available again.');
}
});
function resetForNextQuestion() {
if (speechSynthesis.speaking) {
speechSynthesis.cancel();
}
stopTimer();
database.ref('gameState').update({
buzzerActive: false,
buzzer: null,
isReading: false
});
buzzedPlayer.textContent = '';
document.querySelectorAll('.light').forEach(l => l.classList.remove('active'));
markCorrectBtn.style.display = 'none';
markIncorrectBtn.style.display = 'none';
showAnswerBtn.style.display = 'none';
answerSection.style.display = 'none';
readQuestionBtn.textContent = '🔊 Read Question';
readQuestionBtn.disabled = false;
currentBuzzedPlayer = null;
isShowingBonus = false;
}