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

1023 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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**
```css
* { 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**
```bash
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**
```js
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:
```js
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**
```bash
git add scores.js
git commit -m "feat: add localStorage highscore module"
```
---
### Task 3: Startmenü
**Files:**
- Create: `index.html`
- [ ] **Schritt 1: `index.html` erstellen**
```html
<!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**
```bash
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:
```html
<link rel="stylesheet" href="style.css">
```
Ganz unten vor `</body>` vor dem vorhandenen `<script>`-Tag einfügen:
```html
<script src="scores.js"></script>
```
- [ ] **Schritt 2: Menü-Link hinzufügen**
Direkt nach `<body>` einfügen:
```html
<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:
```js
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**
```bash
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**
```html
<!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**
```bash
git add tetris.html
git commit -m "feat: add Tetris game"
```
---
### Task 6: Breakout
**Files:**
- Create: `breakout.html`
- [ ] **Schritt 1: `breakout.html` erstellen**
```html
<!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**
```bash
git add breakout.html
git commit -m "feat: add Breakout game"
```
---
### Task 7: Space Invaders
**Files:**
- Create: `spaceinvaders.html`
- [ ] **Schritt 1: `spaceinvaders.html` erstellen**
```html
<!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**
```bash
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**
```bash
git add .
git commit -m "feat: complete retro arcade platform with 4 games"
```