Projekt: Quiz-Game mit JavaScript
Projekt: Quiz-Game mit JavaScript
In diesem Projekt baust du ein komplettes Quiz-Spiel mit:
- Dynamische Fragen laden
- Punktestand & Timer
- Highscore-System mit LocalStorage
- Animationen & Sound-Effekte
- Responsive Design
Das Endergebnis
+------------------------------------------+
| 🎯 JS Quiz |
+------------------------------------------+
| Frage 3/10 ⏱️ 0:15 |
+------------------------------------------+
| |
| Was gibt typeof null zurück? |
| |
| [A] "null" |
| [B] "undefined" |
| [C] "object" ← richtig |
| [D] "boolean" |
| |
+------------------------------------------+
| Punkte: 200 |
+------------------------------------------+
Projekt-Setup
quiz-game/
├── index.html
├── style.css
├── app.js
└── questions.js
questions.js - Fragen-Datenbank
const questions = [
{
question: "Was gibt typeof null in JavaScript zurück?",
answers: [
{ text: '"null"', correct: false },
{ text: '"undefined"', correct: false },
{ text: '"object"', correct: true },
{ text: '"boolean"', correct: false }
],
explanation: "Das ist ein historischer Bug in JavaScript. typeof null gibt 'object' zurück."
},
{
question: "Welche Methode fügt ein Element am Ende eines Arrays hinzu?",
answers: [
{ text: "push()", correct: true },
{ text: "pop()", correct: false },
{ text: "shift()", correct: false },
{ text: "unshift()", correct: false }
],
explanation: "push() fügt Elemente am Ende hinzu, pop() entfernt das letzte Element."
},
{
question: "Was ist der Unterschied zwischen == und ===?",
answers: [
{ text: "Kein Unterschied", correct: false },
{ text: "=== vergleicht auch den Typ", correct: true },
{ text: "== ist schneller", correct: false },
{ text: "=== ist veraltet", correct: false }
],
explanation: "=== (strict equality) prüft Wert UND Typ, == konvertiert erst."
},
{
question: "Wie deklariert man eine Konstante in JavaScript?",
answers: [
{ text: "var", correct: false },
{ text: "let", correct: false },
{ text: "const", correct: true },
{ text: "constant", correct: false }
],
explanation: "const deklariert eine Konstante, deren Referenz nicht geändert werden kann."
},
{
question: "Was ist eine Closure?",
answers: [
{ text: "Eine geschlossene Funktion", correct: false },
{ text: "Eine Funktion mit Zugriff auf äußeren Scope", correct: true },
{ text: "Eine CSS-Eigenschaft", correct: false },
{ text: "Ein HTML-Tag", correct: false }
],
explanation: "Eine Closure ist eine Funktion, die sich an ihre lexikalische Umgebung erinnert."
},
{
question: "Welches Keyword erstellt eine asynchrone Funktion?",
answers: [
{ text: "await", correct: false },
{ text: "async", correct: true },
{ text: "promise", correct: false },
{ text: "defer", correct: false }
],
explanation: "async vor einer Funktion macht sie asynchron und ermöglicht await darin."
},
{
question: "Was gibt [1, 2, 3].map(x => x * 2) zurück?",
answers: [
{ text: "[1, 2, 3]", correct: false },
{ text: "[2, 4, 6]", correct: true },
{ text: "6", correct: false },
{ text: "undefined", correct: false }
],
explanation: "map() erstellt ein neues Array mit transformierten Elementen."
},
{
question: "Wie greift man auf das letzte Element eines Arrays zu?",
answers: [
{ text: "arr[arr.length]", correct: false },
{ text: "arr[-1]", correct: false },
{ text: "arr[arr.length - 1]", correct: true },
{ text: "arr.last()", correct: false }
],
explanation: "Arrays sind 0-indexiert, das letzte Element ist bei length - 1."
},
{
question: "Was macht event.preventDefault()?",
answers: [
{ text: "Stoppt Event-Bubbling", correct: false },
{ text: "Verhindert Standard-Aktion", correct: true },
{ text: "Löscht den Event-Listener", correct: false },
{ text: "Erstellt ein neues Event", correct: false }
],
explanation: "preventDefault() verhindert z.B. Formular-Submit oder Link-Navigation."
},
{
question: "Welche Aussage über let ist korrekt?",
answers: [
{ text: "let ist function-scoped", correct: false },
{ text: "let ist block-scoped", correct: true },
{ text: "let kann nicht re-assigned werden", correct: false },
{ text: "let ist veraltet", correct: false }
],
explanation: "let ist block-scoped, var ist function-scoped."
}
];
// Fragen mischen
function shuffleArray(array) {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
function getQuestions(count = 10) {
const shuffled = shuffleArray(questions);
return shuffled.slice(0, count).map(q => ({
...q,
answers: shuffleArray(q.answers)
}));
}
HTML-Struktur
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JavaScript Quiz</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="app">
<!-- Start Screen -->
<div class="screen" id="start-screen">
<h1>🎯 JavaScript Quiz</h1>
<p class="subtitle">Teste dein JavaScript-Wissen!</p>
<div class="stats-preview">
<div class="stat">
<span class="stat-value">10</span>
<span class="stat-label">Fragen</span>
</div>
<div class="stat">
<span class="stat-value">20s</span>
<span class="stat-label">pro Frage</span>
</div>
</div>
<div class="highscore-display" id="highscore-display">
<span>🏆 Highscore: </span>
<span id="highscore-value">0</span>
</div>
<button class="btn btn-primary btn-lg" id="start-btn">
Quiz starten
</button>
</div>
<!-- Quiz Screen -->
<div class="screen hidden" id="quiz-screen">
<div class="quiz-header">
<div class="progress">
<span id="question-count">Frage 1/10</span>
<div class="progress-bar">
<div class="progress-fill" id="progress-fill"></div>
</div>
</div>
<div class="timer" id="timer">
<span class="timer-icon">⏱️</span>
<span id="timer-value">20</span>
</div>
</div>
<div class="quiz-body">
<h2 class="question" id="question"></h2>
<div class="answers" id="answers"></div>
<div class="explanation hidden" id="explanation">
<p id="explanation-text"></p>
</div>
</div>
<div class="quiz-footer">
<div class="score">
Punkte: <span id="score">0</span>
</div>
<button class="btn btn-primary hidden" id="next-btn">
Nächste Frage →
</button>
</div>
</div>
<!-- Result Screen -->
<div class="screen hidden" id="result-screen">
<div class="result-emoji" id="result-emoji">🎉</div>
<h2 class="result-title" id="result-title">Quiz beendet!</h2>
<div class="result-stats">
<div class="result-stat">
<span class="result-value" id="final-score">0</span>
<span class="result-label">Punkte</span>
</div>
<div class="result-stat">
<span class="result-value" id="correct-count">0/10</span>
<span class="result-label">Richtig</span>
</div>
<div class="result-stat">
<span class="result-value" id="accuracy">0%</span>
<span class="result-label">Genauigkeit</span>
</div>
</div>
<div class="new-highscore hidden" id="new-highscore">
🏆 Neuer Highscore!
</div>
<div class="result-actions">
<button class="btn btn-primary" id="restart-btn">
Nochmal spielen
</button>
<button class="btn btn-secondary" id="home-btn">
Zum Start
</button>
</div>
</div>
</div>
<script src="questions.js"></script>
<script src="app.js"></script>
</body>
</html>
CSS-Styling
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #6366f1;
--primary-dark: #4f46e5;
--success: #22c55e;
--error: #ef4444;
--warning: #f59e0b;
--bg: #0f172a;
--card: #1e293b;
--border: #334155;
--text: #f1f5f9;
--text-muted: #94a3b8;
}
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: linear-gradient(135deg, var(--bg) 0%, #1e1b4b 100%);
color: var(--text);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
}
.app {
width: 100%;
max-width: 600px;
}
.screen {
background: var(--card);
border-radius: 24px;
padding: 2rem;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.hidden {
display: none !important;
}
/* Start Screen */
#start-screen {
text-align: center;
}
#start-screen h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.subtitle {
color: var(--text-muted);
margin-bottom: 2rem;
}
.stats-preview {
display: flex;
justify-content: center;
gap: 3rem;
margin-bottom: 2rem;
}
.stat {
display: flex;
flex-direction: column;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: var(--primary);
}
.stat-label {
font-size: 0.875rem;
color: var(--text-muted);
}
.highscore-display {
margin-bottom: 2rem;
padding: 1rem;
background: var(--bg);
border-radius: 12px;
display: inline-block;
}
#highscore-value {
font-weight: 700;
color: var(--warning);
}
/* Buttons */
.btn {
padding: 0.875rem 1.5rem;
border: none;
border-radius: 12px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--primary-dark);
transform: translateY(-2px);
}
.btn-secondary {
background: var(--bg);
color: var(--text);
border: 1px solid var(--border);
}
.btn-lg {
padding: 1rem 2rem;
font-size: 1.125rem;
}
/* Quiz Screen */
.quiz-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.progress {
flex: 1;
margin-right: 1rem;
}
#question-count {
font-size: 0.875rem;
color: var(--text-muted);
}
.progress-bar {
height: 8px;
background: var(--bg);
border-radius: 4px;
margin-top: 0.5rem;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary), #8b5cf6);
border-radius: 4px;
transition: width 0.3s ease;
}
.timer {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: var(--bg);
border-radius: 10px;
}
.timer.warning {
background: rgba(245, 158, 11, 0.2);
color: var(--warning);
}
.timer.danger {
background: rgba(239, 68, 68, 0.2);
color: var(--error);
animation: pulse 0.5s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.question {
font-size: 1.25rem;
margin-bottom: 1.5rem;
line-height: 1.5;
}
.answers {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.answer-btn {
padding: 1rem 1.25rem;
background: var(--bg);
border: 2px solid var(--border);
border-radius: 12px;
color: var(--text);
font-size: 1rem;
text-align: left;
cursor: pointer;
transition: all 0.2s;
}
.answer-btn:hover:not(.disabled) {
border-color: var(--primary);
background: rgba(99, 102, 241, 0.1);
}
.answer-btn.correct {
border-color: var(--success);
background: rgba(34, 197, 94, 0.2);
}
.answer-btn.incorrect {
border-color: var(--error);
background: rgba(239, 68, 68, 0.2);
}
.answer-btn.disabled {
cursor: not-allowed;
opacity: 0.7;
}
.explanation {
margin-top: 1.5rem;
padding: 1rem;
background: rgba(99, 102, 241, 0.1);
border-radius: 12px;
border-left: 4px solid var(--primary);
}
.quiz-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border);
}
.score {
font-size: 1.125rem;
}
.score span {
font-weight: 700;
color: var(--primary);
}
/* Result Screen */
#result-screen {
text-align: center;
}
.result-emoji {
font-size: 4rem;
margin-bottom: 1rem;
}
.result-title {
font-size: 1.75rem;
margin-bottom: 2rem;
}
.result-stats {
display: flex;
justify-content: center;
gap: 2rem;
margin-bottom: 2rem;
}
.result-stat {
display: flex;
flex-direction: column;
padding: 1rem;
background: var(--bg);
border-radius: 12px;
min-width: 100px;
}
.result-value {
font-size: 1.75rem;
font-weight: 700;
color: var(--primary);
}
.result-label {
font-size: 0.875rem;
color: var(--text-muted);
}
.new-highscore {
padding: 1rem;
background: rgba(245, 158, 11, 0.2);
border-radius: 12px;
color: var(--warning);
font-weight: 600;
margin-bottom: 2rem;
animation: bounce 0.5s ease infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
.result-actions {
display: flex;
gap: 1rem;
justify-content: center;
}
/* Responsive */
@media (max-width: 480px) {
.screen {
padding: 1.5rem;
}
.stats-preview {
gap: 1.5rem;
}
.result-stats {
flex-direction: column;
gap: 1rem;
}
.result-actions {
flex-direction: column;
}
}
JavaScript - Game Logic
// === Game State ===
const state = {
questions: [],
currentIndex: 0,
score: 0,
correctAnswers: 0,
timer: null,
timeLeft: 20,
isAnswered: false
};
const POINTS_PER_CORRECT = 100;
const TIME_BONUS_MULTIPLIER = 5;
const TIME_PER_QUESTION = 20;
// === DOM Elements ===
const screens = {
start: document.getElementById("start-screen"),
quiz: document.getElementById("quiz-screen"),
result: document.getElementById("result-screen")
};
const elements = {
startBtn: document.getElementById("start-btn"),
restartBtn: document.getElementById("restart-btn"),
homeBtn: document.getElementById("home-btn"),
nextBtn: document.getElementById("next-btn"),
questionCount: document.getElementById("question-count"),
progressFill: document.getElementById("progress-fill"),
timerValue: document.getElementById("timer-value"),
timer: document.getElementById("timer"),
question: document.getElementById("question"),
answers: document.getElementById("answers"),
explanation: document.getElementById("explanation"),
explanationText: document.getElementById("explanation-text"),
score: document.getElementById("score"),
highscoreValue: document.getElementById("highscore-value"),
finalScore: document.getElementById("final-score"),
correctCount: document.getElementById("correct-count"),
accuracy: document.getElementById("accuracy"),
resultEmoji: document.getElementById("result-emoji"),
resultTitle: document.getElementById("result-title"),
newHighscore: document.getElementById("new-highscore")
};
// === Screen Management ===
function showScreen(screenName) {
Object.values(screens).forEach(screen => screen.classList.add("hidden"));
screens[screenName].classList.remove("hidden");
}
// === Highscore ===
function getHighscore() {
return parseInt(localStorage.getItem("quizHighscore") || "0");
}
function saveHighscore(score) {
const current = getHighscore();
if (score > current) {
localStorage.setItem("quizHighscore", score.toString());
return true;
}
return false;
}
function updateHighscoreDisplay() {
elements.highscoreValue.textContent = getHighscore();
}
// === Timer ===
function startTimer() {
state.timeLeft = TIME_PER_QUESTION;
updateTimerDisplay();
state.timer = setInterval(() => {
state.timeLeft--;
updateTimerDisplay();
if (state.timeLeft <= 0) {
clearInterval(state.timer);
handleTimeout();
}
}, 1000);
}
function stopTimer() {
clearInterval(state.timer);
}
function updateTimerDisplay() {
elements.timerValue.textContent = state.timeLeft;
// Styling based on time
elements.timer.classList.remove("warning", "danger");
if (state.timeLeft <= 5) {
elements.timer.classList.add("danger");
} else if (state.timeLeft <= 10) {
elements.timer.classList.add("warning");
}
}
function handleTimeout() {
if (state.isAnswered) return;
state.isAnswered = true;
showCorrectAnswer();
showExplanation();
elements.nextBtn.classList.remove("hidden");
}
// === Quiz Logic ===
function startQuiz() {
state.questions = getQuestions(10);
state.currentIndex = 0;
state.score = 0;
state.correctAnswers = 0;
elements.score.textContent = "0";
showScreen("quiz");
loadQuestion();
}
function loadQuestion() {
state.isAnswered = false;
const question = state.questions[state.currentIndex];
// Update UI
elements.questionCount.textContent = `Frage ${state.currentIndex + 1}/${state.questions.length}`;
elements.progressFill.style.width = `${((state.currentIndex) / state.questions.length) * 100}%`;
elements.question.textContent = question.question;
// Clear and create answers
elements.answers.textContent = "";
question.answers.forEach((answer, index) => {
const btn = document.createElement("button");
btn.className = "answer-btn";
btn.textContent = answer.text;
btn.addEventListener("click", () => handleAnswer(index));
elements.answers.appendChild(btn);
});
// Hide elements
elements.explanation.classList.add("hidden");
elements.nextBtn.classList.add("hidden");
// Start timer
startTimer();
}
function handleAnswer(selectedIndex) {
if (state.isAnswered) return;
state.isAnswered = true;
stopTimer();
const question = state.questions[state.currentIndex];
const buttons = elements.answers.querySelectorAll(".answer-btn");
const isCorrect = question.answers[selectedIndex].correct;
// Disable all buttons
buttons.forEach(btn => btn.classList.add("disabled"));
// Show correct/incorrect
buttons.forEach((btn, index) => {
if (question.answers[index].correct) {
btn.classList.add("correct");
} else if (index === selectedIndex) {
btn.classList.add("incorrect");
}
});
// Update score
if (isCorrect) {
state.correctAnswers++;
const timeBonus = state.timeLeft * TIME_BONUS_MULTIPLIER;
const points = POINTS_PER_CORRECT + timeBonus;
state.score += points;
elements.score.textContent = state.score;
}
showExplanation();
elements.nextBtn.classList.remove("hidden");
}
function showCorrectAnswer() {
const question = state.questions[state.currentIndex];
const buttons = elements.answers.querySelectorAll(".answer-btn");
buttons.forEach((btn, index) => {
btn.classList.add("disabled");
if (question.answers[index].correct) {
btn.classList.add("correct");
}
});
}
function showExplanation() {
const question = state.questions[state.currentIndex];
elements.explanationText.textContent = question.explanation;
elements.explanation.classList.remove("hidden");
}
function nextQuestion() {
state.currentIndex++;
if (state.currentIndex >= state.questions.length) {
endQuiz();
} else {
loadQuestion();
}
}
function endQuiz() {
const isNewHighscore = saveHighscore(state.score);
const accuracy = Math.round((state.correctAnswers / state.questions.length) * 100);
// Update result screen
elements.finalScore.textContent = state.score;
elements.correctCount.textContent = `${state.correctAnswers}/${state.questions.length}`;
elements.accuracy.textContent = `${accuracy}%`;
// Emoji based on performance
if (accuracy >= 90) {
elements.resultEmoji.textContent = "🏆";
elements.resultTitle.textContent = "Ausgezeichnet!";
} else if (accuracy >= 70) {
elements.resultEmoji.textContent = "🎉";
elements.resultTitle.textContent = "Gut gemacht!";
} else if (accuracy >= 50) {
elements.resultEmoji.textContent = "👍";
elements.resultTitle.textContent = "Nicht schlecht!";
} else {
elements.resultEmoji.textContent = "📚";
elements.resultTitle.textContent = "Weiter üben!";
}
// Show highscore badge
if (isNewHighscore) {
elements.newHighscore.classList.remove("hidden");
} else {
elements.newHighscore.classList.add("hidden");
}
showScreen("result");
updateHighscoreDisplay();
}
// === Event Listeners ===
elements.startBtn.addEventListener("click", startQuiz);
elements.nextBtn.addEventListener("click", nextQuestion);
elements.restartBtn.addEventListener("click", startQuiz);
elements.homeBtn.addEventListener("click", () => {
showScreen("start");
});
// === Initialize ===
function init() {
updateHighscoreDisplay();
showScreen("start");
}
init();
Was du gelernt hast
-
Game State Management
- Zentralen State verwalten
- State-Updates und UI-Sync
-
Timer & Intervals
- setInterval/clearInterval
- Countdown-Logik
-
LocalStorage
- Highscores speichern
- Daten persistieren
-
DOM-Manipulation
- Dynamische UI-Updates
- Event Handling
-
Array-Methoden
- Arrays mischen
- Daten transformieren
-
Code-Organisation
- Module/Dateien trennen
- Saubere Struktur
Challenge: Erweitere das Quiz um Kategorien (HTML, CSS, JS), verschiedene Schwierigkeitsgrade und eine Bestenliste mit mehreren Spielern!