1023 lines
31 KiB
Markdown
1023 lines
31 KiB
Markdown
# 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"
|
||
```
|