From e4f1e17edbe7ae0f8983559ba2b4dcef4e687daa Mon Sep 17 00:00:00 2001 From: SanabhiG Date: Tue, 20 Jan 2026 23:00:07 -0600 Subject: [PATCH] Initial commit - Science Bowl buzzer system --- .gitignore | Bin 0 -> 98 bytes config.js | 22 +++ display.html | 90 +++++++++++++ display.js | 199 +++++++++++++++++++++++++++ index.html | 47 +++++++ player.html | 42 ++++++ player.js | 110 +++++++++++++++ styles.css | 375 +++++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 885 insertions(+) create mode 100644 .gitignore create mode 100644 config.js create mode 100644 display.html create mode 100644 display.js create mode 100644 index.html create mode 100644 player.html create mode 100644 player.js create mode 100644 styles.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..640cc9f9edd4ab0fe05ce2aa4223bda9704935a9 GIT binary patch literal 98 zcmezWFOMOgA%!88A)X-@%qnHb0rH9&^ci>=xEKl;5*d;ivVmkeP*fKvmk;LaF=PSN M + + + + + 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