Initial commit - Science Bowl buzzer system

This commit is contained in:
2026-01-20 23:00:07 -06:00
commit e4f1e17edb
8 changed files with 885 additions and 0 deletions

BIN
.gitignore vendored Normal file

Binary file not shown.

22
config.js Normal file
View File

@@ -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);

90
display.html Normal file
View File

@@ -0,0 +1,90 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Science Bowl - Moderator Display</title>
<link rel="stylesheet" href="styles.css">
</head>
<body class="display-body">
<div class="display-container">
<div class="scoreboard">
<div class="player-score player-1">
<h3>🔴 Player 1</h3>
<div class="score" id="score1">0</div>
</div>
<div class="player-score player-2">
<h3>🔵 Player 2</h3>
<div class="score" id="score2">0</div>
</div>
<div class="player-score player-3">
<h3>🟢 Player 3</h3>
<div class="score" id="score3">0</div>
</div>
<div class="player-score player-4">
<h3>🟡 Player 4</h3>
<div class="score" id="score4">0</div>
</div>
</div>
<div class="buzzer-indicator" id="buzzerIndicator">
<div class="buzzer-lights">
<div class="light player-1" id="light1"></div>
<div class="light player-2" id="light2"></div>
<div class="light player-3" id="light3"></div>
<div class="light player-4" id="light4"></div>
</div>
<div class="buzzed-player" id="buzzedPlayer"></div>
</div>
<div class="question-section">
<div class="question-header">
<span class="question-category" id="questionCategory">CATEGORY</span>
<span class="question-number">Question <span id="questionNum">1</span>/25</span>
</div>
<div class="question-text" id="questionText">
Click "New Question" to start
</div>
</div>
<div class="answer-section" id="answerSection" style="display: none;">
<h3>Player Answer:</h3>
<div class="player-answer" id="playerAnswer"></div>
<h3>Correct Answer:</h3>
<div class="correct-answer" id="correctAnswer"></div>
</div>
<div class="controls">
<button id="newQuestion" class="control-btn primary">New Question</button>
<button id="activateBuzzer" class="control-btn" disabled>Activate Buzzers</button>
<button id="markCorrect" class="control-btn success" style="display: none;">✓ Correct (+4)</button>
<button id="markIncorrect" class="control-btn danger" style="display: none;">✗ Incorrect (-4)</button>
<button id="showAnswer" class="control-btn" style="display: none;">Show Answer</button>
<button id="resetGame" class="control-btn warning">Reset Game</button>
</div>
<div class="category-filter">
<label>
<input type="checkbox" class="category-checkbox" value="PHYSICS" checked> Physics
</label>
<label>
<input type="checkbox" class="category-checkbox" value="CHEMISTRY" checked> Chemistry
</label>
<label>
<input type="checkbox" class="category-checkbox" value="BIOLOGY" checked> Biology
</label>
<label>
<input type="checkbox" class="category-checkbox" value="MATH" checked> Math
</label>
<label>
<input type="checkbox" class="category-checkbox" value="EARTH SCIENCE" checked> Earth Science
</label>
</div>
</div>
<script src="https://www.gstatic.com/firebasejs/9.17.1/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.17.1/firebase-database-compat.js"></script>
<script src="config.js"></script>
<script src="display.js"></script>
</body>
</html>

199
display.js Normal file
View File

@@ -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;
}

47
index.html Normal file
View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Science Bowl Buzzer System</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<h1>Science Bowl Buzzer System</h1>
<div class="role-selection">
<h2>Select Your Role:</h2>
<a href="display.html" class="role-btn moderator-btn">
<h3>📺 Moderator Display</h3>
<p>Main screen showing questions and scores</p>
</a>
<div class="player-buttons">
<a href="player.html?id=1" class="role-btn player-btn player-1">
<h3>🔴 Player 1</h3>
</a>
<a href="player.html?id=2" class="role-btn player-btn player-2">
<h3>🔵 Player 2</h3>
</a>
<a href="player.html?id=3" class="role-btn player-btn player-3">
<h3>🟢 Player 3</h3>
</a>
<a href="player.html?id=4" class="role-btn player-btn player-4">
<h3>🟡 Player 4</h3>
</a>
</div>
</div>
<div class="instructions">
<h3>Setup Instructions:</h3>
<ol>
<li>Open the Moderator Display on your main screen</li>
<li>Each player opens their assigned player page on their laptop</li>
<li>Press spacebar or click the buzzer to buzz in!</li>
</ol>
</div>
</div>
</body>
</html>

42
player.html Normal file
View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Science Bowl - Player</title>
<link rel="stylesheet" href="styles.css">
</head>
<body class="player-body">
<div class="player-container">
<div class="player-header">
<h1 id="playerName">Player</h1>
<div class="score-display">
Score: <span id="playerScore">0</span>
</div>
</div>
<div class="status-message" id="statusMessage">
Waiting for question...
</div>
<div class="buzzer-container">
<button id="buzzer" class="buzzer-btn">
<span class="buzzer-text">BUZZ</span>
<span class="buzzer-hint">Press SPACE</span>
</button>
</div>
<div class="answer-container" id="answerContainer" style="display: none;">
<input type="text" id="answerInput" placeholder="Type your answer...">
<button id="submitAnswer" class="submit-btn">Submit Answer</button>
</div>
<div class="question-display" id="questionDisplay"></div>
</div>
<script src="https://www.gstatic.com/firebasejs/9.17.1/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.17.1/firebase-database-compat.js"></script>
<script src="config.js"></script>
<script src="player.js"></script>
</body>
</html>

110
player.js Normal file
View File

@@ -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...';
}

375
styles.css Normal file
View File

@@ -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;
}