JavaScript Fortgeschritten

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

  1. Game State Management

    • Zentralen State verwalten
    • State-Updates und UI-Sync
  2. Timer & Intervals

    • setInterval/clearInterval
    • Countdown-Logik
  3. LocalStorage

    • Highscores speichern
    • Daten persistieren
  4. DOM-Manipulation

    • Dynamische UI-Updates
    • Event Handling
  5. Array-Methoden

    • Arrays mischen
    • Daten transformieren
  6. Code-Organisation

    • Module/Dateien trennen
    • Saubere Struktur

Challenge: Erweitere das Quiz um Kategorien (HTML, CSS, JS), verschiedene Schwierigkeitsgrade und eine Bestenliste mit mehreren Spielern!