Files
testprojekt/docs/superpowers/plans/2026-03-31-retro-arcade-platform.md

31 KiB
Raw Blame History

Retro Arcade Platform Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Erweitere das bestehende Snake-Spiel zu einer Retro-Arcade-Plattform mit vier Spielen (Snake, Tetris, Breakout, Space Invaders), einem gemeinsamen Startmenü und localStorage-Highscores.

Architecture: Shared style.css und scores.js werden von allen Spielen eingebunden. index.html dient als Startmenü mit Spielkacheln und Highscores. Jedes Spiel ist eine eigenständige HTML-Datei. Kein Build-Schritt, kein Server — direkt im Browser öffenbar.

Tech Stack: Plain HTML5, CSS3, JavaScript (Canvas API), localStorage


Dateiübersicht

Datei Aktion Verantwortung
style.css Neu Navy/Teal-Theme, gemeinsame Klassen
scores.js Neu getHighscore / setHighscore via localStorage
index.html Neu Startmenü, 2×2 Spielkacheln mit Highscores
snake.html Anpassen shared assets einbinden, Menü-Link, setHighscore aufrufen
tetris.html Neu Tetris-Implementierung
breakout.html Neu Breakout-Implementierung
spaceinvaders.html Neu Space Invaders-Implementierung

Task 1: Gemeinsames Stylesheet

Files:

  • Create: style.css

  • Schritt 1: style.css erstellen

* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  background: #1a1a2e;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  font-family: 'Courier New', monospace;
  color: #e0e0e0;
}

h1 {
  font-size: 2rem;
  margin-bottom: 12px;
  color: #4ecca3;
  letter-spacing: 4px;
}

.hud {
  display: flex;
  gap: 40px;
  margin-bottom: 12px;
  font-size: 1rem;
  color: #a0a0c0;
}

.hud span { color: #4ecca3; font-weight: bold; }

canvas {
  border: 2px solid #4ecca3;
  box-shadow: 0 0 20px rgba(78, 204, 163, 0.3);
  display: block;
}

.overlay {
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
}

.overlay h2 { color: #4ecca3; font-size: 1.5rem; letter-spacing: 3px; }
.overlay p  { color: #a0a0c0; font-size: 0.9rem; }

.retro-btn {
  background: transparent;
  border: 2px solid #4ecca3;
  color: #4ecca3;
  padding: 10px 24px;
  font-family: 'Courier New', monospace;
  font-size: 0.9rem;
  letter-spacing: 2px;
  cursor: pointer;
  text-transform: uppercase;
}

.retro-btn:hover { background: #4ecca3; color: #1a1a2e; }

.menu-link {
  position: absolute;
  top: 16px;
  left: 16px;
  color: #4ecca3;
  text-decoration: none;
  font-size: 0.85rem;
  letter-spacing: 2px;
  opacity: 0.7;
}

.menu-link:hover { opacity: 1; }
  • Schritt 2: Im Browser prüfen

Erstelle eine temporäre test.html mit <link rel="stylesheet" href="style.css"> und einem <button class="retro-btn">TEST</button>. Öffne im Browser — Button muss transparent mit Teal-Border erscheinen, Hover muss den Hintergrund füllen. Danach test.html löschen.

  • Schritt 3: Commit
git add style.css
git commit -m "feat: add shared retro arcade stylesheet"

Task 2: Highscore-Modul

Files:

  • Create: scores.js

  • Schritt 1: scores.js erstellen

function getHighscore(game) {
  return parseInt(localStorage.getItem('hs_' + game) || '0', 10);
}

function setHighscore(game, score) {
  if (score > getHighscore(game)) {
    localStorage.setItem('hs_' + game, score);
  }
}
  • Schritt 2: Im Browser-Konsole prüfen

Öffne eine beliebige HTML-Datei mit <script src="scores.js"></script>. In der DevTools-Konsole:

setHighscore('test', 100);
getHighscore('test'); // → 100
setHighscore('test', 50);
getHighscore('test'); // → 100 (nicht überschrieben, da niedriger)
setHighscore('test', 200);
getHighscore('test'); // → 200
localStorage.removeItem('hs_test');
  • Schritt 3: Commit
git add scores.js
git commit -m "feat: add localStorage highscore module"

Task 3: Startmenü

Files:

  • Create: index.html

  • Schritt 1: index.html erstellen

<!DOCTYPE html>
<html lang="de">
<head>
  <meta charset="UTF-8">
  <title>Retro Arcade</title>
  <link rel="stylesheet" href="style.css">
  <style>
    .game-grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 20px;
      margin-top: 24px;
    }

    .game-tile {
      border: 2px solid #4ecca3;
      padding: 24px 20px;
      text-align: center;
      text-decoration: none;
      color: inherit;
      transition: background 0.15s;
      display: flex;
      flex-direction: column;
      gap: 8px;
    }

    .game-tile:hover { background: rgba(78, 204, 163, 0.08); }

    .game-title {
      color: #4ecca3;
      font-size: 1rem;
      letter-spacing: 3px;
    }

    .hs-label {
      color: #555;
      font-size: 0.7rem;
      letter-spacing: 2px;
    }

    .hs-value {
      color: #a0a0c0;
      font-size: 0.9rem;
    }
  </style>
</head>
<body>
  <h1>RETRO ARCADE</h1>
  <div class="game-grid">
    <a class="game-tile" href="snake.html">
      <div class="game-title">SNAKE</div>
      <div class="hs-label">HIGHSCORE</div>
      <div class="hs-value" id="hs-snake"></div>
    </a>
    <a class="game-tile" href="tetris.html">
      <div class="game-title">TETRIS</div>
      <div class="hs-label">HIGHSCORE</div>
      <div class="hs-value" id="hs-tetris"></div>
    </a>
    <a class="game-tile" href="breakout.html">
      <div class="game-title">BREAKOUT</div>
      <div class="hs-label">HIGHSCORE</div>
      <div class="hs-value" id="hs-breakout"></div>
    </a>
    <a class="game-tile" href="spaceinvaders.html">
      <div class="game-title">SPACE INV.</div>
      <div class="hs-label">HIGHSCORE</div>
      <div class="hs-value" id="hs-spaceinvaders"></div>
    </a>
  </div>

  <script src="scores.js"></script>
  <script>
    ['snake', 'tetris', 'breakout', 'spaceinvaders'].forEach(game => {
      const hs = getHighscore(game);
      document.getElementById('hs-' + game).textContent = hs > 0 ? hs.toLocaleString('de-DE') : '—';
    });
  </script>
</body>
</html>
  • Schritt 2: Im Browser prüfen

open index.html — Vier Kacheln müssen im 2×2-Grid erscheinen, alle Highscores zeigen „—". Links zu den Spielen müssen vorhanden sein (noch 404, da Spiele noch nicht existieren). Hover-Effekt auf Kacheln muss sichtbar sein.

  • Schritt 3: Commit
git add index.html
git commit -m "feat: add retro arcade start menu"

Task 4: Snake anpassen

Files:

  • Modify: snake.html

Das bestehende snake.html muss folgende Änderungen bekommen:

  1. style.css und scores.js einbinden
  2. Den bestehenden <style>-Block auf spiel-spezifische Styles reduzieren (gemeinsame Styles entfernen)
  3. #hud.hud (ID zu Klasse)
  4. #overlay.overlay (ID zu Klasse — auch im JS anpassen)
  5. ← Menü-Link hinzufügen
  6. setHighscore('snake', score) bei Game Over aufrufen
  • Schritt 1: Head anpassen

Im <head> nach <meta charset> einfügen:

<link rel="stylesheet" href="style.css">

Ganz unten vor </body> vor dem vorhandenen <script>-Tag einfügen:

<script src="scores.js"></script>
  • Schritt 2: Menü-Link hinzufügen

Direkt nach <body> einfügen:

<a class="menu-link" href="index.html">← Menü</a>
  • Schritt 3: Doppelte Styles entfernen

Im <style>-Block des snake.html folgende Regeln entfernen, da sie jetzt in style.css sind:

  • * { margin: 0; ... }
  • body { ... }
  • h1 { ... }
  • #hud { ... } und #hud span { ... }
  • canvas { ... }
  • #overlay { ... }

Alle Button-Styles (.btn, Neustart-Button) auf .retro-btn umstellen oder behalten — je nachdem ob sie identisch sind. Identische Stile entfernen, spiel-spezifische behalten.

  • Schritt 4: IDs zu Klassen ändern

Im HTML:

  • id="hud"class="hud"
  • id="overlay"class="overlay"

Im JavaScript alle Vorkommen von:

  • document.getElementById('hud')document.querySelector('.hud')

  • document.getElementById('overlay')document.querySelector('.overlay')

  • Schritt 5: setHighscore aufrufen

Im JavaScript die Game-Over-Funktion finden (wo der Score zurückgesetzt oder der Overlay gezeigt wird). Direkt vor dem Anzeigen des Overlays einfügen:

setHighscore('snake', score);

score durch den tatsächlichen Score-Variablennamen im bestehenden Code ersetzen.

  • Schritt 6: Im Browser prüfen

open snake.html — Spiel muss funktionieren wie zuvor. „← Menü" oben links muss sichtbar sein und zu index.html navigieren. Nach einem Game Over muss der Highscore in localStorage unter hs_snake gespeichert sein (DevTools → Application → Local Storage prüfen). open index.html — Highscore muss auf der Snake-Kachel erscheinen.

  • Schritt 7: Commit
git add snake.html
git commit -m "feat: integrate snake into arcade platform"

Task 5: Tetris

Files:

  • Create: tetris.html

  • Schritt 1: tetris.html erstellen

<!DOCTYPE html>
<html lang="de">
<head>
  <meta charset="UTF-8">
  <title>Tetris</title>
  <link rel="stylesheet" href="style.css">
  <style>
    #wrapper {
      position: relative;
      display: flex;
      gap: 24px;
      align-items: flex-start;
    }
    #sidebar {
      display: flex;
      flex-direction: column;
      gap: 16px;
      color: #a0a0c0;
      font-size: 0.85rem;
      padding-top: 4px;
      min-width: 80px;
    }
    #sidebar .label { color: #555; font-size: 0.7rem; letter-spacing: 2px; }
    #sidebar .value { color: #4ecca3; font-size: 1.1rem; font-weight: bold; }
    #next-canvas { border: 1px solid #2a2a4a; display: block; }
  </style>
</head>
<body>
  <a class="menu-link" href="index.html">← Menü</a>
  <h1>TETRIS</h1>

  <div id="wrapper">
    <canvas id="canvas" width="300" height="600"></canvas>
    <div id="sidebar">
      <div>
        <div class="label">SCORE</div>
        <div class="value" id="score-display">0</div>
      </div>
      <div>
        <div class="label">LEVEL</div>
        <div class="value" id="level-display">1</div>
      </div>
      <div>
        <div class="label">LINES</div>
        <div class="value" id="lines-display">0</div>
      </div>
      <div>
        <div class="label">BEST</div>
        <div class="value" id="hs-display">0</div>
      </div>
      <div>
        <div class="label">NÄCHSTES</div>
        <canvas id="next-canvas" width="80" height="80"></canvas>
      </div>
    </div>
  </div>

  <div class="overlay" id="overlay">
    <h2>GAME OVER</h2>
    <p id="final-score"></p>
    <button class="retro-btn" onclick="startGame()">NEU STARTEN</button>
    <a class="retro-btn" href="index.html" style="text-decoration:none;text-align:center;">← MENÜ</a>
  </div>

  <script src="scores.js"></script>
  <script>
    const COLS = 10, ROWS = 20, BLOCK = 30;
    const COLORS = ['', '#ff6b6b','#ffd93d','#6bcb77','#4d96ff','#ff9f43','#a29bfe','#fd79a8'];
    const SHAPES = [
      [],
      [[1,1,1,1]],
      [[2,2],[2,2]],
      [[0,3,0],[3,3,3]],
      [[0,4,4],[4,4,0]],
      [[5,5,0],[0,5,5]],
      [[6,0,0],[6,6,6]],
      [[0,0,7],[7,7,7]],
    ];

    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    const nextCanvas = document.getElementById('next-canvas');
    const nextCtx = nextCanvas.getContext('2d');
    const overlay = document.getElementById('overlay');

    let board, piece, nextPiece, score, level, lines, dropInterval, lastTime, animId;

    function createBoard() {
      return Array.from({length: ROWS}, () => Array(COLS).fill(0));
    }

    function randomPiece() {
      const id = Math.ceil(Math.random() * 7);
      const shape = SHAPES[id].map(r => [...r]);
      return { id, shape, x: Math.floor(COLS / 2) - Math.floor(shape[0].length / 2), y: 0 };
    }

    function drawBlock(context, x, y, colorId, size) {
      context.fillStyle = COLORS[colorId];
      context.fillRect(x * size + 1, y * size + 1, size - 2, size - 2);
    }

    function drawBoard() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      board.forEach((row, y) => row.forEach((val, x) => { if (val) drawBlock(ctx, x, y, val, BLOCK); }));
    }

    function drawPiece(p) {
      p.shape.forEach((row, dy) => row.forEach((val, dx) => {
        if (val) drawBlock(ctx, p.x + dx, p.y + dy, val, BLOCK);
      }));
    }

    function drawNext() {
      nextCtx.clearRect(0, 0, nextCanvas.width, nextCanvas.height);
      const size = 20;
      const offX = Math.floor((4 - nextPiece.shape[0].length) / 2);
      const offY = Math.floor((4 - nextPiece.shape.length) / 2);
      nextPiece.shape.forEach((row, dy) => row.forEach((val, dx) => {
        if (val) drawBlock(nextCtx, offX + dx, offY + dy, val, size);
      }));
    }

    function collides(p, board) {
      return p.shape.some((row, dy) => row.some((val, dx) => {
        if (!val) return false;
        const nx = p.x + dx, ny = p.y + dy;
        return nx < 0 || nx >= COLS || ny >= ROWS || (ny >= 0 && board[ny][nx]);
      }));
    }

    function merge(p) {
      p.shape.forEach((row, dy) => row.forEach((val, dx) => {
        if (val) board[p.y + dy][p.x + dx] = val;
      }));
    }

    function clearLines() {
      let cleared = 0;
      for (let y = ROWS - 1; y >= 0; y--) {
        if (board[y].every(v => v)) {
          board.splice(y, 1);
          board.unshift(Array(COLS).fill(0));
          cleared++;
          y++;
        }
      }
      if (cleared) {
        const pts = [0, 100, 300, 500, 800][cleared] * level;
        score += pts;
        lines += cleared;
        level = Math.floor(lines / 10) + 1;
        dropInterval = Math.max(100, 1000 - (level - 1) * 90);
        updateHUD();
      }
    }

    function rotate(shape) {
      return shape[0].map((_, i) => shape.map(row => row[i]).reverse());
    }

    function updateHUD() {
      document.getElementById('score-display').textContent = score.toLocaleString('de-DE');
      document.getElementById('level-display').textContent = level;
      document.getElementById('lines-display').textContent = lines;
      document.getElementById('hs-display').textContent = getHighscore('tetris').toLocaleString('de-DE');
    }

    function gameOver() {
      cancelAnimationFrame(animId);
      setHighscore('tetris', score);
      document.getElementById('final-score').textContent = 'SCORE: ' + score.toLocaleString('de-DE');
      document.getElementById('hs-display').textContent = getHighscore('tetris').toLocaleString('de-DE');
      overlay.style.display = 'flex';
    }

    function startGame() {
      board = createBoard();
      score = 0; level = 1; lines = 0; dropInterval = 1000; lastTime = 0;
      piece = randomPiece();
      nextPiece = randomPiece();
      overlay.style.display = 'none';
      updateHUD();
      drawNext();
      animId = requestAnimationFrame(loop);
    }

    function loop(timestamp) {
      const delta = timestamp - lastTime;
      if (delta > dropInterval) {
        lastTime = timestamp;
        const moved = { ...piece, shape: piece.shape.map(r => [...r]), y: piece.y + 1 };
        if (!collides(moved, board)) {
          piece = moved;
        } else {
          merge(piece);
          clearLines();
          piece = nextPiece;
          nextPiece = randomPiece();
          drawNext();
          if (collides(piece, board)) { gameOver(); return; }
        }
      }
      drawBoard();
      drawPiece(piece);
      animId = requestAnimationFrame(loop);
    }

    document.addEventListener('keydown', e => {
      if (overlay.style.display === 'flex') return;
      if (e.key === 'ArrowLeft') {
        const m = { ...piece, shape: piece.shape.map(r => [...r]), x: piece.x - 1 };
        if (!collides(m, board)) piece = m;
      } else if (e.key === 'ArrowRight') {
        const m = { ...piece, shape: piece.shape.map(r => [...r]), x: piece.x + 1 };
        if (!collides(m, board)) piece = m;
      } else if (e.key === 'ArrowDown') {
        const m = { ...piece, shape: piece.shape.map(r => [...r]), y: piece.y + 1 };
        if (!collides(m, board)) piece = m; else { merge(piece); clearLines(); piece = nextPiece; nextPiece = randomPiece(); drawNext(); if (collides(piece, board)) { gameOver(); return; } }
      } else if (e.key === 'ArrowUp') {
        const rotated = rotate(piece.shape);
        const m = { ...piece, shape: rotated };
        if (!collides(m, board)) piece = m;
      } else if (e.key === ' ') {
        e.preventDefault();
        while (true) {
          const m = { ...piece, shape: piece.shape.map(r => [...r]), y: piece.y + 1 };
          if (collides(m, board)) { merge(piece); clearLines(); piece = nextPiece; nextPiece = randomPiece(); drawNext(); if (collides(piece, board)) gameOver(); break; }
          piece = m;
        }
      }
    });

    startGame();
  </script>
</body>
</html>
  • Schritt 2: Im Browser prüfen

open tetris.html. Prüfen:

  • Blöcke fallen automatisch

  • Pfeiltasten links/rechts bewegen, oben rotiert, unten beschleunigt, Leertaste lässt fallen

  • Vollständige Reihen verschwinden, Score steigt

  • Level steigt nach je 10 Zeilen, Fallgeschwindigkeit nimmt zu

  • Game Over erscheint wenn Stapel die Decke erreicht

  • „← Menü" führt zu index.html

  • Nach Game Over: hs_tetris in localStorage vorhanden (DevTools prüfen)

  • index.html zeigt den Tetris-Highscore

  • Schritt 3: Commit

git add tetris.html
git commit -m "feat: add Tetris game"

Task 6: Breakout

Files:

  • Create: breakout.html

  • Schritt 1: breakout.html erstellen

<!DOCTYPE html>
<html lang="de">
<head>
  <meta charset="UTF-8">
  <title>Breakout</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <a class="menu-link" href="index.html">← Menü</a>
  <h1>BREAKOUT</h1>
  <div class="hud">
    <div>SCORE <span id="score-display">0</span></div>
    <div>LIVES <span id="lives-display">3</span></div>
    <div>BEST <span id="hs-display">0</span></div>
  </div>
  <canvas id="canvas" width="480" height="400"></canvas>

  <div class="overlay" id="overlay">
    <h2 id="overlay-title">GAME OVER</h2>
    <p id="final-score"></p>
    <button class="retro-btn" onclick="startGame()">NEU STARTEN</button>
    <a class="retro-btn" href="index.html" style="text-decoration:none;text-align:center;">← MENÜ</a>
  </div>

  <script src="scores.js"></script>
  <script>
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    const overlay = document.getElementById('overlay');

    const PADDLE_W = 80, PADDLE_H = 10, PADDLE_Y = 370;
    const BALL_R = 8;
    const BRICK_COLS = 10, BRICK_ROWS = 5, BRICK_W = 44, BRICK_H = 18, BRICK_PAD = 4, BRICK_TOP = 40;
    const BRICK_COLORS = ['#ff6b6b','#ffd93d','#6bcb77','#4d96ff','#a29bfe'];

    let paddle, ball, bricks, score, lives, animId, keys;

    function createBricks() {
      const b = [];
      for (let r = 0; r < BRICK_ROWS; r++)
        for (let c = 0; c < BRICK_COLS; c++)
          b.push({ x: c * (BRICK_W + BRICK_PAD) + BRICK_PAD, y: r * (BRICK_H + BRICK_PAD) + BRICK_TOP, alive: true, color: BRICK_COLORS[r] });
      return b;
    }

    function updateHUD() {
      document.getElementById('score-display').textContent = score.toLocaleString('de-DE');
      document.getElementById('lives-display').textContent = lives;
      document.getElementById('hs-display').textContent = getHighscore('breakout').toLocaleString('de-DE');
    }

    function showOverlay(title) {
      cancelAnimationFrame(animId);
      setHighscore('breakout', score);
      document.getElementById('overlay-title').textContent = title;
      document.getElementById('final-score').textContent = 'SCORE: ' + score.toLocaleString('de-DE');
      document.getElementById('hs-display').textContent = getHighscore('breakout').toLocaleString('de-DE');
      overlay.style.display = 'flex';
    }

    function startGame() {
      paddle = { x: 200, w: PADDLE_W };
      ball = { x: 240, y: 340, vx: 3, vy: -4 };
      bricks = createBricks();
      score = 0; lives = 3;
      keys = {};
      overlay.style.display = 'none';
      updateHUD();
      animId = requestAnimationFrame(loop);
    }

    function loop() {
      // Paddle bewegen
      if (keys['ArrowLeft'])  paddle.x = Math.max(0, paddle.x - 5);
      if (keys['ArrowRight']) paddle.x = Math.min(canvas.width - paddle.w, paddle.x + 5);

      // Ball bewegen
      ball.x += ball.vx;
      ball.y += ball.vy;

      // Wände
      if (ball.x - BALL_R < 0)                    { ball.x = BALL_R; ball.vx *= -1; }
      if (ball.x + BALL_R > canvas.width)          { ball.x = canvas.width - BALL_R; ball.vx *= -1; }
      if (ball.y - BALL_R < 0)                     { ball.y = BALL_R; ball.vy *= -1; }

      // Ball verloren
      if (ball.y + BALL_R > canvas.height) {
        lives--;
        updateHUD();
        if (lives <= 0) { showOverlay('GAME OVER'); return; }
        ball = { x: paddle.x + paddle.w / 2, y: PADDLE_Y - BALL_R - 10, vx: 3 * (Math.random() > 0.5 ? 1 : -1), vy: -4 };
      }

      // Paddle-Kollision
      if (ball.y + BALL_R >= PADDLE_Y && ball.y + BALL_R <= PADDLE_Y + PADDLE_H &&
          ball.x >= paddle.x && ball.x <= paddle.x + paddle.w && ball.vy > 0) {
        ball.vy *= -1;
        const offset = (ball.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2);
        ball.vx = offset * 5;
      }

      // Brick-Kollision
      let remaining = 0;
      bricks.forEach(b => {
        if (!b.alive) return;
        remaining++;
        if (ball.x + BALL_R > b.x && ball.x - BALL_R < b.x + BRICK_W &&
            ball.y + BALL_R > b.y && ball.y - BALL_R < b.y + BRICK_H) {
          b.alive = false;
          remaining--;
          ball.vy *= -1;
          score += 10;
          updateHUD();
        }
      });

      if (remaining === 0) { showOverlay('GEWONNEN!'); return; }

      // Zeichnen
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      // Paddle
      ctx.fillStyle = '#4ecca3';
      ctx.fillRect(paddle.x, PADDLE_Y, paddle.w, PADDLE_H);
      // Ball
      ctx.beginPath();
      ctx.arc(ball.x, ball.y, BALL_R, 0, Math.PI * 2);
      ctx.fillStyle = '#e0e0e0';
      ctx.fill();
      // Bricks
      bricks.forEach(b => {
        if (!b.alive) return;
        ctx.fillStyle = b.color;
        ctx.fillRect(b.x, b.y, BRICK_W, BRICK_H);
      });

      animId = requestAnimationFrame(loop);
    }

    document.addEventListener('keydown', e => { keys[e.key] = true; e.preventDefault(); });
    document.addEventListener('keyup',   e => { keys[e.key] = false; });

    document.getElementById('hs-display').textContent = getHighscore('breakout').toLocaleString('de-DE');
    startGame();
  </script>
</body>
</html>
  • Schritt 2: Im Browser prüfen

open breakout.html. Prüfen:

  • Paddle bewegt sich mit Pfeiltasten links/rechts

  • Ball prallt von Wänden, Paddle und Steinen ab

  • Steine verschwinden bei Treffer, Score steigt um 10

  • Bei 3 verlorenen Leben: Game Over-Overlay

  • Wenn alle Steine weg: „GEWONNEN!"-Overlay

  • Highscore in localStorage unter hs_breakout nach Game Over

  • index.html zeigt Breakout-Highscore

  • Schritt 3: Commit

git add breakout.html
git commit -m "feat: add Breakout game"

Task 7: Space Invaders

Files:

  • Create: spaceinvaders.html

  • Schritt 1: spaceinvaders.html erstellen

<!DOCTYPE html>
<html lang="de">
<head>
  <meta charset="UTF-8">
  <title>Space Invaders</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <a class="menu-link" href="index.html">← Menü</a>
  <h1>SPACE INVADERS</h1>
  <div class="hud">
    <div>SCORE <span id="score-display">0</span></div>
    <div>LIVES <span id="lives-display">3</span></div>
    <div>BEST <span id="hs-display">0</span></div>
  </div>
  <canvas id="canvas" width="480" height="480"></canvas>

  <div class="overlay" id="overlay">
    <h2 id="overlay-title">GAME OVER</h2>
    <p id="final-score"></p>
    <button class="retro-btn" onclick="startGame()">NEU STARTEN</button>
    <a class="retro-btn" href="index.html" style="text-decoration:none;text-align:center;">← MENÜ</a>
  </div>

  <script src="scores.js"></script>
  <script>
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    const overlay = document.getElementById('overlay');

    const COLS = 11, ROWS = 5;
    const ALIEN_W = 32, ALIEN_H = 24, ALIEN_PAD_X = 10, ALIEN_PAD_Y = 16;
    const ALIEN_POINTS = [30, 20, 20, 10, 10]; // pro Reihe, oben nach unten
    const PLAYER_W = 36, PLAYER_H = 16, PLAYER_Y = 440, PLAYER_SPEED = 5;
    const BULLET_W = 3, BULLET_H = 12, BULLET_SPEED = 8;
    const ENEMY_BULLET_SPEED = 4;

    let player, bullets, enemyBullets, aliens, alienDir, alienSpeed, alienDropped;
    let score, lives, animId, keys, lastEnemyShot, alienMoveTimer, alienMoveInterval;

    function createAliens() {
      const arr = [];
      for (let r = 0; r < ROWS; r++)
        for (let c = 0; c < COLS; c++)
          arr.push({ x: c * (ALIEN_W + ALIEN_PAD_X) + 40, y: r * (ALIEN_H + ALIEN_PAD_Y) + 60, alive: true, row: r, col: c });
      return arr;
    }

    function updateHUD() {
      document.getElementById('score-display').textContent = score.toLocaleString('de-DE');
      document.getElementById('lives-display').textContent = lives;
      document.getElementById('hs-display').textContent = getHighscore('spaceinvaders').toLocaleString('de-DE');
    }

    function showOverlay(title) {
      cancelAnimationFrame(animId);
      setHighscore('spaceinvaders', score);
      document.getElementById('overlay-title').textContent = title;
      document.getElementById('final-score').textContent = 'SCORE: ' + score.toLocaleString('de-DE');
      document.getElementById('hs-display').textContent = getHighscore('spaceinvaders').toLocaleString('de-DE');
      overlay.style.display = 'flex';
    }

    function startGame() {
      player = { x: 220 };
      bullets = []; enemyBullets = [];
      aliens = createAliens();
      alienDir = 1; alienSpeed = 20; alienDropped = false;
      score = 0; lives = 3;
      keys = {}; lastEnemyShot = 0; alienMoveTimer = 0; alienMoveInterval = 800;
      overlay.style.display = 'none';
      updateHUD();
      animId = requestAnimationFrame(loop);
    }

    function drawAlien(x, y, row) {
      ctx.fillStyle = row === 0 ? '#fd79a8' : row < 3 ? '#a29bfe' : '#4ecca3';
      // Einfaches Alien-Symbol als Rechteck mit "Augen"
      ctx.fillRect(x + 4, y + 4, ALIEN_W - 8, ALIEN_H - 8);
      ctx.fillStyle = '#1a1a2e';
      ctx.fillRect(x + 8, y + 8, 4, 4);
      ctx.fillRect(x + ALIEN_W - 12, y + 8, 4, 4);
    }

    function loop(timestamp) {
      // Spieler bewegen
      if (keys['ArrowLeft'])  player.x = Math.max(0, player.x - PLAYER_SPEED);
      if (keys['ArrowRight']) player.x = Math.min(canvas.width - PLAYER_W, player.x + PLAYER_SPEED);

      // Spieler-Bullets
      bullets.forEach(b => b.y -= BULLET_SPEED);
      bullets = bullets.filter(b => b.y > 0);

      // Alien-Bewegung
      alienMoveTimer += 16;
      if (alienMoveTimer >= alienMoveInterval) {
        alienMoveTimer = 0;
        const alive = aliens.filter(a => a.alive);
        const maxX = Math.max(...alive.map(a => a.x));
        const minX = Math.min(...alive.map(a => a.x));
        if ((alienDir > 0 && maxX + ALIEN_W >= canvas.width - 10) ||
            (alienDir < 0 && minX <= 10)) {
          aliens.forEach(a => { a.y += 20; });
          alienDir *= -1;
          alienMoveInterval = Math.max(100, alienMoveInterval - 30);
        } else {
          aliens.forEach(a => { a.x += alienDir * alienSpeed; });
        }
      }

      // Alien beschießt Spieler zufällig
      if (timestamp - lastEnemyShot > 1200) {
        lastEnemyShot = timestamp;
        const alive = aliens.filter(a => a.alive);
        if (alive.length) {
          const shooter = alive[Math.floor(Math.random() * alive.length)];
          enemyBullets.push({ x: shooter.x + ALIEN_W / 2, y: shooter.y + ALIEN_H });
        }
      }
      enemyBullets.forEach(b => b.y += ENEMY_BULLET_SPEED);
      enemyBullets = enemyBullets.filter(b => b.y < canvas.height);

      // Kollision: Spieler-Bullets vs Aliens
      bullets.forEach(bull => {
        aliens.forEach(a => {
          if (!a.alive) return;
          if (bull.x > a.x && bull.x < a.x + ALIEN_W && bull.y > a.y && bull.y < a.y + ALIEN_H) {
            a.alive = false;
            bull.y = -100;
            score += ALIEN_POINTS[a.row];
            updateHUD();
          }
        });
      });

      // Kollision: Alien-Bullets vs Spieler
      enemyBullets.forEach(b => {
        if (b.x > player.x && b.x < player.x + PLAYER_W && b.y > PLAYER_Y && b.y < PLAYER_Y + PLAYER_H) {
          b.y = canvas.height + 1;
          lives--;
          updateHUD();
        }
      });
      if (lives <= 0) { showOverlay('GAME OVER'); return; }

      // Aliens erreichen Boden
      if (aliens.some(a => a.alive && a.y + ALIEN_H >= PLAYER_Y)) { showOverlay('GAME OVER'); return; }

      // Alle Aliens besiegt
      if (!aliens.some(a => a.alive)) { showOverlay('GEWONNEN!'); return; }

      // Zeichnen
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      // Aliens
      aliens.forEach(a => { if (a.alive) drawAlien(a.x, a.y, a.row); });
      // Spieler (Dreieck als Raumschiff)
      ctx.fillStyle = '#4ecca3';
      ctx.beginPath();
      ctx.moveTo(player.x + PLAYER_W / 2, PLAYER_Y);
      ctx.lineTo(player.x, PLAYER_Y + PLAYER_H);
      ctx.lineTo(player.x + PLAYER_W, PLAYER_Y + PLAYER_H);
      ctx.closePath();
      ctx.fill();
      // Spieler-Bullets
      ctx.fillStyle = '#4ecca3';
      bullets.forEach(b => ctx.fillRect(b.x, b.y, BULLET_W, BULLET_H));
      // Alien-Bullets
      ctx.fillStyle = '#ff6b6b';
      enemyBullets.forEach(b => ctx.fillRect(b.x, b.y, BULLET_W, BULLET_H));

      if (lives > 0) animId = requestAnimationFrame(loop);
    }

    document.addEventListener('keydown', e => {
      keys[e.key] = true;
      if (e.key === ' ' && overlay.style.display !== 'flex') {
        e.preventDefault();
        if (bullets.length < 3) bullets.push({ x: player.x + PLAYER_W / 2 - BULLET_W / 2, y: PLAYER_Y });
      }
    });
    document.addEventListener('keyup', e => { keys[e.key] = false; });

    document.getElementById('hs-display').textContent = getHighscore('spaceinvaders').toLocaleString('de-DE');
    startGame();
  </script>
</body>
</html>
  • Schritt 2: Im Browser prüfen

open spaceinvaders.html. Prüfen:

  • Spieler bewegt sich links/rechts mit Pfeiltasten

  • Leertaste schießt (max. 3 gleichzeitige Bullets)

  • Aliens bewegen sich seitwärts und rücken nach unten wenn sie den Rand erreichen

  • Aliens schießen zufällig zurück

  • Treffer: Alien verschwindet, Score steigt

  • Leben sinkt bei Treffer durch Alien-Bullet oder wenn Aliens den Boden erreichen

  • Game Over / Gewonnen-Overlay erscheint korrekt

  • Highscore in localStorage unter hs_spaceinvaders

  • index.html zeigt Space-Invaders-Highscore

  • Schritt 3: Commit

git add spaceinvaders.html
git commit -m "feat: add Space Invaders game"

Task 8: Abschlusscheck

  • Schritt 1: Alle Spiele von index.html aus starten

open index.html — alle vier Kacheln klickbar, Links funktionieren, Highscores werden angezeigt (oder — wenn noch keine gespielt).

  • Schritt 2: Highscore-Persistenz prüfen

In jedem Spiel ein Game Over provozieren. Dann index.html öffnen und prüfen, ob alle vier Highscores korrekt angezeigt werden.

  • Schritt 3: Finaler Commit
git add .
git commit -m "feat: complete retro arcade platform with 4 games"