31 KiB
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.csserstellen
* { 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.jserstellen
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.htmlerstellen
<!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:
style.cssundscores.jseinbinden- Den bestehenden
<style>-Block auf spiel-spezifische Styles reduzieren (gemeinsame Styles entfernen) #hud→.hud(ID zu Klasse)#overlay→.overlay(ID zu Klasse — auch im JS anpassen)← Menü-Link hinzufügensetHighscore('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.htmlerstellen
<!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_tetrisin 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.htmlerstellen
<!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_breakoutnach 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.htmlerstellen
<!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"