commit e4f1e17edbe7ae0f8983559ba2b4dcef4e687daa Author: SanabhiG Date: Tue Jan 20 23:00:07 2026 -0600 Initial commit - Science Bowl buzzer system diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..640cc9f Binary files /dev/null and b/.gitignore differ diff --git a/config.js b/config.js new file mode 100644 index 0000000..fcbe3c0 --- /dev/null +++ b/config.js @@ -0,0 +1,22 @@ +// Import the functions you need from the SDKs you need +import { initializeApp } from "firebase/app"; +import { getAnalytics } from "firebase/analytics"; +// TODO: Add SDKs for Firebase products that you want to use +// https://firebase.google.com/docs/web/setup#available-libraries + +// Your web app's Firebase configuration +// For Firebase JS SDK v7.20.0 and later, measurementId is optional +const firebaseConfig = { + apiKey: "AIzaSyDNk5EGWDPBr8MkUFNdfvhP1NvnDxWERq8", + authDomain: "science-bowl-practice-8800a.firebaseapp.com", + databaseURL: "https://science-bowl-practice-8800a-default-rtdb.firebaseio.com", + projectId: "science-bowl-practice-8800a", + storageBucket: "science-bowl-practice-8800a.firebasestorage.app", + messagingSenderId: "240054855565", + appId: "1:240054855565:web:2897ab544b9f1c1b3d3fc4", + measurementId: "G-4TD0W788X5" +}; + +// Initialize Firebase +const app = initializeApp(firebaseConfig); +const analytics = getAnalytics(app); \ No newline at end of file diff --git a/display.html b/display.html new file mode 100644 index 0000000..bfd8e79 --- /dev/null +++ b/display.html @@ -0,0 +1,90 @@ + + + + + + Science Bowl - Moderator Display + + + +
+
+
+

🔴 Player 1

+
0
+
+
+

🔵 Player 2

+
0
+
+
+

🟢 Player 3

+
0
+
+
+

🟡 Player 4

+
0
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+ CATEGORY + Question 1/25 +
+
+ Click "New Question" to start +
+
+ + + +
+ + + + + + +
+ +
+ + + + + +
+
+ + + + + + + \ No newline at end of file diff --git a/display.js b/display.js new file mode 100644 index 0000000..117fb7d --- /dev/null +++ b/display.js @@ -0,0 +1,199 @@ +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 playerAnswer = document.getElementById('playerAnswer'); +const answerSection = document.getElementById('answerSection'); +const buzzedPlayer = document.getElementById('buzzedPlayer'); + +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'); + +let currentQuestion = null; +let currentQuestionNumber = 1; +let currentBuzzedPlayer = null; + +// Initialize game state +database.ref('gameState').set({ + buzzerActive: false, + currentQuestion: null, + scores: { + player1: 0, + player2: 0, + player3: 0, + player4: 0 + }, + questionNumber: 1 +}); + +// 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; + + // Light up the buzzer + document.querySelectorAll('.light').forEach(l => l.classList.remove('active')); + document.getElementById(`light${playerNum}`).classList.add('active'); + + buzzedPlayer.textContent = `Player ${playerNum} buzzed in!`; + + // Show grading buttons + markCorrectBtn.style.display = 'inline-block'; + markIncorrectBtn.style.display = 'inline-block'; + showAnswerBtn.style.display = 'inline-block'; + } + + // Update player answer + if (state.playerAnswer) { + playerAnswer.textContent = state.playerAnswer.answer; + answerSection.style.display = 'block'; + } +}); + +// Fetch new question +newQuestionBtn.addEventListener('click', async () => { + const categories = Array.from(document.querySelectorAll('.category-checkbox:checked')) + .map(cb => cb.value); + + try { + const response = await fetch(API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ categories }) + }); + + const data = await response.json(); + currentQuestion = data; + + questionText.textContent = currentQuestion.tossup_question; + questionCategory.textContent = currentQuestion.category; + correctAnswer.textContent = currentQuestion.tossup_answer; + questionNum.textContent = currentQuestionNumber; + + // Update Firebase + database.ref('gameState').update({ + currentQuestion: currentQuestion, + buzzerActive: false, + buzzer: null, + playerAnswer: null, + questionNumber: currentQuestionNumber + }); + + // Reset UI + answerSection.style.display = 'none'; + buzzedPlayer.textContent = ''; + document.querySelectorAll('.light').forEach(l => l.classList.remove('active')); + markCorrectBtn.style.display = 'none'; + markIncorrectBtn.style.display = 'none'; + showAnswerBtn.style.display = 'none'; + + activateBuzzerBtn.disabled = false; + currentQuestionNumber++; + + } catch (error) { + console.error('Error fetching question:', error); + alert('Error loading question. Check console.'); + } +}); + +// Activate buzzers +activateBuzzerBtn.addEventListener('click', () => { + database.ref('gameState').update({ + buzzerActive: true, + buzzer: null, + playerAnswer: null + }); + activateBuzzerBtn.disabled = true; + answerSection.style.display = 'none'; +}); + +// Mark correct +markCorrectBtn.addEventListener('click', () => { + if (!currentBuzzedPlayer) return; + + database.ref(`gameState/scores/${currentBuzzedPlayer}`).transaction((score) => { + return (score || 0) + 4; + }); + + resetForNextQuestion(); +}); + +// Mark incorrect +markIncorrectBtn.addEventListener('click', () => { + if (!currentBuzzedPlayer) return; + + database.ref(`gameState/scores/${currentBuzzedPlayer}`).transaction((score) => { + return (score || 0) - 4; + }); + + // Reactivate buzzers for other players + database.ref('gameState').update({ + buzzerActive: true, + buzzer: null, + playerAnswer: null + }); + + answerSection.style.display = 'none'; + markCorrectBtn.style.display = 'none'; + markIncorrectBtn.style.display = 'none'; + showAnswerBtn.style.display = 'none'; +}); + +// Show answer +showAnswerBtn.addEventListener('click', () => { + answerSection.style.display = 'block'; +}); + +// Reset game +resetGameBtn.addEventListener('click', () => { + if (confirm('Reset all scores and start over?')) { + currentQuestionNumber = 1; + database.ref('gameState').set({ + buzzerActive: false, + currentQuestion: null, + scores: { + player1: 0, + player2: 0, + player3: 0, + player4: 0 + }, + questionNumber: 1 + }); + questionText.textContent = 'Click "New Question" to start'; + answerSection.style.display = 'none'; + } +}); + +function resetForNextQuestion() { + database.ref('gameState').update({ + buzzerActive: false, + buzzer: null, + playerAnswer: null + }); + + 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'; + currentBuzzedPlayer = null; +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..2c5484a --- /dev/null +++ b/index.html @@ -0,0 +1,47 @@ + + + + + + Science Bowl Buzzer System + + + +
+

Science Bowl Buzzer System

+ + + +
+

Setup Instructions:

+
    +
  1. Open the Moderator Display on your main screen
  2. +
  3. Each player opens their assigned player page on their laptop
  4. +
  5. Press spacebar or click the buzzer to buzz in!
  6. +
+
+
+ + \ No newline at end of file diff --git a/player.html b/player.html new file mode 100644 index 0000000..0158560 --- /dev/null +++ b/player.html @@ -0,0 +1,42 @@ + + + + + + Science Bowl - Player + + + +
+
+

Player

+
+ Score: 0 +
+
+ +
+ Waiting for question... +
+ +
+ +
+ + + +
+
+ + + + + + + \ No newline at end of file diff --git a/player.js b/player.js new file mode 100644 index 0000000..0f05527 --- /dev/null +++ b/player.js @@ -0,0 +1,110 @@ +// Get player ID from URL +const urlParams = new URLSearchParams(window.location.search); +const playerId = urlParams.get('id') || '1'; +const playerColors = ['#ff4444', '#4444ff', '#44ff44', '#ffff44']; +const playerNames = ['Player 1', 'Player 2', 'Player 3', 'Player 4']; + +// DOM elements +const playerName = document.getElementById('playerName'); +const playerScore = document.getElementById('playerScore'); +const statusMessage = document.getElementById('statusMessage'); +const buzzer = document.getElementById('buzzer'); +const answerContainer = document.getElementById('answerContainer'); +const answerInput = document.getElementById('answerInput'); +const submitAnswer = document.getElementById('submitAnswer'); +const questionDisplay = document.getElementById('questionDisplay'); + +// Set player identity +playerName.textContent = playerNames[playerId - 1]; +playerName.style.color = playerColors[playerId - 1]; +document.body.style.setProperty('--player-color', playerColors[playerId - 1]); + +// Game state +let canBuzz = false; +let hasBuzzed = false; + +// Listen to game state +database.ref('gameState').on('value', (snapshot) => { + const state = snapshot.val(); + if (!state) return; + + // Update score + if (state.scores && state.scores[`player${playerId}`] !== undefined) { + playerScore.textContent = state.scores[`player${playerId}`]; + } + + // Update question + if (state.currentQuestion) { + questionDisplay.textContent = state.currentQuestion.tossup_question; + } + + // Update buzzer state + if (state.buzzerActive) { + canBuzz = true; + hasBuzzed = false; + buzzer.disabled = false; + buzzer.classList.remove('locked'); + statusMessage.textContent = 'Ready to buzz!'; + answerContainer.style.display = 'none'; + } else { + canBuzz = false; + buzzer.disabled = true; + buzzer.classList.add('locked'); + } + + // Check if this player buzzed in + if (state.buzzer && state.buzzer.playerId === `player${playerId}`) { + statusMessage.textContent = 'You buzzed in! Answer now:'; + answerContainer.style.display = 'block'; + answerInput.focus(); + } else if (state.buzzer && state.buzzer.playerId) { + statusMessage.textContent = `${state.buzzer.playerId} buzzed in`; + buzzer.classList.add('locked'); + } +}); + +// Buzzer click +buzzer.addEventListener('click', buzzIn); + +// Spacebar to buzz +document.addEventListener('keydown', (e) => { + if (e.code === 'Space' && canBuzz && !hasBuzzed) { + e.preventDefault(); + buzzIn(); + } +}); + +function buzzIn() { + if (!canBuzz || hasBuzzed) return; + + hasBuzzed = true; + database.ref('gameState/buzzer').set({ + playerId: `player${playerId}`, + timestamp: Date.now() + }); + + // Visual feedback + buzzer.classList.add('buzzed'); + setTimeout(() => buzzer.classList.remove('buzzed'), 300); +} + +// Submit answer +submitAnswer.addEventListener('click', submitPlayerAnswer); +answerInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') submitPlayerAnswer(); +}); + +function submitPlayerAnswer() { + const answer = answerInput.value.trim(); + if (!answer) return; + + database.ref('gameState/playerAnswer').set({ + playerId: `player${playerId}`, + answer: answer, + timestamp: Date.now() + }); + + answerInput.value = ''; + answerContainer.style.display = 'none'; + statusMessage.textContent = 'Answer submitted! Waiting for moderator...'; +} \ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..3c7f601 --- /dev/null +++ b/styles.css @@ -0,0 +1,375 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 20px; +} + +.container { + max-width: 800px; + margin: 0 auto; + background: white; + padding: 40px; + border-radius: 20px; + box-shadow: 0 20px 60px rgba(0,0,0,0.3); +} + +h1 { + text-align: center; + color: #333; + margin-bottom: 30px; +} + +.role-selection { + margin: 30px 0; +} + +.role-btn { + display: block; + padding: 20px; + margin: 15px 0; + background: #f8f9fa; + border: 3px solid #dee2e6; + border-radius: 10px; + text-decoration: none; + color: #333; + transition: all 0.3s; +} + +.role-btn:hover { + transform: translateY(-5px); + box-shadow: 0 10px 25px rgba(0,0,0,0.15); +} + +.moderator-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; +} + +.player-buttons { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; + margin-top: 20px; +} + +.player-1 { border-color: #ff4444; } +.player-2 { border-color: #4444ff; } +.player-3 { border-color: #44ff44; } +.player-4 { border-color: #ffff44; } + +/* Player Screen */ +.player-body { + background: var(--player-color, #667eea); +} + +.player-container { + max-width: 600px; + margin: 0 auto; + text-align: center; +} + +.player-header { + background: white; + padding: 20px; + border-radius: 15px; + margin-bottom: 30px; + box-shadow: 0 10px 30px rgba(0,0,0,0.2); +} + +.score-display { + font-size: 2em; + font-weight: bold; + color: var(--player-color); + margin-top: 10px; +} + +.status-message { + background: white; + padding: 15px; + border-radius: 10px; + font-size: 1.2em; + margin-bottom: 30px; + box-shadow: 0 5px 15px rgba(0,0,0,0.1); +} + +.buzzer-container { + margin: 50px 0; +} + +.buzzer-btn { + width: 300px; + height: 300px; + border-radius: 50%; + border: 10px solid white; + background: linear-gradient(135deg, #ff6b6b, #ee5a6f); + color: white; + font-size: 3em; + font-weight: bold; + cursor: pointer; + transition: all 0.2s; + box-shadow: 0 20px 40px rgba(0,0,0,0.3); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.buzzer-btn:hover:not(:disabled) { + transform: scale(1.05); + box-shadow: 0 25px 50px rgba(0,0,0,0.4); +} + +.buzzer-btn:active:not(:disabled) { + transform: scale(0.95); +} + +.buzzer-btn.buzzed { + animation: buzz 0.3s; +} + +@keyframes buzz { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.1); } +} + +.buzzer-btn.locked { + background: #ccc; + cursor: not-allowed; + opacity: 0.5; +} + +.buzzer-hint { + display: block; + font-size: 0.3em; + margin-top: 10px; + opacity: 0.8; +} + +.answer-container { + background: white; + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0,0,0,0.2); +} + +#answerInput { + width: 100%; + padding: 15px; + font-size: 1.2em; + border: 3px solid var(--player-color); + border-radius: 10px; + margin-bottom: 15px; +} + +.submit-btn { + padding: 15px 40px; + font-size: 1.2em; + background: var(--player-color); + color: white; + border: none; + border-radius: 10px; + cursor: pointer; + transition: all 0.3s; +} + +.submit-btn:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0,0,0,0.3); +} + +.question-display { + background: white; + padding: 20px; + border-radius: 10px; + margin-top: 30px; + font-size: 1.1em; + box-shadow: 0 5px 15px rgba(0,0,0,0.1); +} + +/* Display Screen */ +.display-body { + background: #1a1a2e; + color: white; +} + +.display-container { + max-width: 1400px; + margin: 0 auto; +} + +.scoreboard { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 20px; + margin-bottom: 30px; +} + +.player-score { + background: rgba(255,255,255,0.1); + padding: 20px; + border-radius: 15px; + text-align: center; + border: 3px solid; +} + +.player-score.player-1 { border-color: #ff4444; } +.player-score.player-2 { border-color: #4444ff; } +.player-score.player-3 { border-color: #44ff44; } +.player-score.player-4 { border-color: #ffff44; } + +.score { + font-size: 3em; + font-weight: bold; + margin-top: 10px; +} + +.buzzer-indicator { + background: rgba(255,255,255,0.1); + padding: 30px; + border-radius: 15px; + margin-bottom: 30px; + text-align: center; +} + +.buzzer-lights { + display: flex; + justify-content: center; + gap: 30px; + margin-bottom: 20px; +} + +.light { + width: 80px; + height: 80px; + border-radius: 50%; + background: rgba(255,255,255,0.2); + transition: all 0.3s; +} + +.light.player-1 { border: 5px solid #ff4444; } +.light.player-2 { border: 5px solid #4444ff; } +.light.player-3 { border: 5px solid #44ff44; } +.light.player-4 { border: 5px solid #ffff44; } + +.light.active { + animation: pulse 1s infinite; + box-shadow: 0 0 30px currentColor; +} + +.light.player-1.active { background: #ff4444; } +.light.player-2.active { background: #4444ff; } +.light.player-3.active { background: #44ff44; } +.light.player-4.active { background: #ffff44; } + +@keyframes pulse { + 0%, 100% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.1); opacity: 0.8; } +} + +.buzzed-player { + font-size: 2em; + font-weight: bold; + color: #ffff44; +} + +.question-section { + background: white; + color: #333; + padding: 40px; + border-radius: 15px; + margin-bottom: 30px; + box-shadow: 0 10px 30px rgba(0,0,0,0.3); +} + +.question-header { + display: flex; + justify-content: space-between; + margin-bottom: 20px; + font-weight: bold; + color: #667eea; +} + +.question-text { + font-size: 1.5em; + line-height: 1.6; +} + +.answer-section { + background: rgba(255,255,255,0.1); + padding: 30px; + border-radius: 15px; + margin-bottom: 30px; +} + +.player-answer, .correct-answer { + background: rgba(255,255,255,0.2); + padding: 20px; + border-radius: 10px; + font-size: 1.3em; + margin-top: 10px; +} + +.controls { + display: flex; + gap: 15px; + flex-wrap: wrap; + margin-bottom: 30px; +} + +.control-btn { + padding: 15px 30px; + font-size: 1.1em; + border: none; + border-radius: 10px; + cursor: pointer; + transition: all 0.3s; + font-weight: bold; +} + +.control-btn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0,0,0,0.3); +} +.control-btn:disabled { +opacity: 0.5; +cursor: not-allowed; +} +.primary { background: #667eea; color: white; } +.success { background: #44ff44; color: #333; } +.danger { background: #ff4444; color: white; } +.warning { background: #ffaa00; color: #333; } +.category-filter { +background: rgba(255,255,255,0.1); +padding: 20px; +border-radius: 10px; +display: flex; +gap: 20px; +flex-wrap: wrap; +} +.category-filter label { +display: flex; +align-items: center; +gap: 8px; +cursor: pointer; +} +.instructions { +margin-top: 40px; +padding: 20px; +background: #f8f9fa; +border-radius: 10px; +} +.instructions ol { +margin-left: 20px; +margin-top: 15px; +} +.instructions li { +margin: 10px 0; +} \ No newline at end of file