chore: initial project state with snake and design docs
This commit is contained in:
19
CLAUDE.md
Normal file
19
CLAUDE.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
Regeln
|
||||
Du darfst auf keinen Fall aus dem Projektordner ausbrechen!!!
|
||||
|
||||
## Project
|
||||
|
||||
This is a single-file browser game (`snake.html`) — a self-contained Snake implementation in plain HTML, CSS, and JavaScript with no build step, no dependencies, and no package manager.
|
||||
|
||||
## Running the game
|
||||
|
||||
Open directly in a browser:
|
||||
```
|
||||
open snake.html
|
||||
```
|
||||
|
||||
No server, compilation, or installation required.
|
||||
BIN
docs/.DS_Store
vendored
Normal file
BIN
docs/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
docs/superpowers/.DS_Store
vendored
Normal file
BIN
docs/superpowers/.DS_Store
vendored
Normal file
Binary file not shown.
1022
docs/superpowers/plans/2026-03-31-retro-arcade-platform.md
Normal file
1022
docs/superpowers/plans/2026-03-31-retro-arcade-platform.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
||||
# Retro Arcade Platform — Design Spec
|
||||
|
||||
**Date:** 2026-03-31
|
||||
|
||||
## Overview
|
||||
|
||||
Erweiterung des bestehenden Snake-Spiels zu einer kleinen Retro-Arcade-Plattform mit vier Spielen. Alle Dateien sind plain HTML/CSS/JS, kein Build-Schritt, kein Server, kein Package Manager — direkt im Browser öffenbar.
|
||||
|
||||
## Spiele
|
||||
|
||||
1. **Snake** — bestehendes `snake.html`, minimal angepasst
|
||||
2. **Tetris** — neu
|
||||
3. **Breakout** — neu
|
||||
4. **Space Invaders** — neu
|
||||
|
||||
## Dateistruktur
|
||||
|
||||
```
|
||||
testprojekt/
|
||||
├── index.html ← Startmenü (neu)
|
||||
├── style.css ← gemeinsame Styles (neu)
|
||||
├── scores.js ← Highscore-Logik (neu)
|
||||
├── snake.html ← angepasst (shared assets einbinden)
|
||||
├── tetris.html ← neu
|
||||
├── breakout.html ← neu
|
||||
└── spaceinvaders.html ← neu
|
||||
```
|
||||
|
||||
## Architektur
|
||||
|
||||
### Gemeinsame Assets
|
||||
|
||||
**`style.css`**
|
||||
- Navy/Teal-Theme: Hintergrund `#1a1a2e`, Akzent `#4ecca3`, Schrift `Courier New`
|
||||
- Gemeinsame Klassen: Body-Layout, Überschriften, HUD, Canvas-Border, Overlay, Buttons, Game-Kacheln
|
||||
- Wird von allen HTML-Dateien via `<link rel="stylesheet" href="style.css">` eingebunden
|
||||
|
||||
**`scores.js`**
|
||||
- `getHighscore(game)` — liest aus `localStorage` (`hs_snake`, `hs_tetris`, `hs_breakout`, `hs_spaceinvaders`), gibt `0` zurück wenn kein Eintrag
|
||||
- `setHighscore(game, score)` — speichert nur wenn `score > getHighscore(game)`
|
||||
- Wird via `<script src="scores.js"></script>` eingebunden
|
||||
|
||||
### Startmenü (`index.html`)
|
||||
|
||||
- Titel: "RETRO ARCADE" in Teal, zentriert
|
||||
- 2×2 Grid mit einer Kachel pro Spiel
|
||||
- Jede Kachel zeigt: Spielname + aktuellen Highscore (gelesen beim Seitenload via `scores.js`)
|
||||
- Klick auf Kachel → öffnet die jeweilige Spieldatei (`href="snake.html"` etc.)
|
||||
- Highscores werden beim Öffnen von `index.html` frisch aus `localStorage` gelesen — immer aktuell
|
||||
|
||||
### Einzelne Spiele
|
||||
|
||||
Jedes Spiel bekommt:
|
||||
- `<link rel="stylesheet" href="style.css">` und `<script src="scores.js"></script>` im Head
|
||||
- "← Menü"-Button oben links, verlinkt auf `index.html`
|
||||
- Beim Game Over: `setHighscore('tetris', score)` (bzw. jeweiliger Spielname) aufrufen, bevor der Overlay angezeigt wird
|
||||
- Spielspezifische Styles (z.B. Tetris-Blockfarben) inline im jeweiligen `<style>`-Block
|
||||
|
||||
### Snake-Anpassung
|
||||
|
||||
`snake.html` wird minimal geändert:
|
||||
- Inline-CSS für gemeinsame Stile auf `style.css` auslagern
|
||||
- `scores.js` einbinden
|
||||
- "← Menü"-Button hinzufügen
|
||||
- `setHighscore('snake', score)` bei Game Over aufrufen
|
||||
|
||||
## Highscore-Speicherung
|
||||
|
||||
| localStorage-Key | Spiel |
|
||||
|---------------------|---------------|
|
||||
| `hs_snake` | Snake |
|
||||
| `hs_tetris` | Tetris |
|
||||
| `hs_breakout` | Breakout |
|
||||
| `hs_spaceinvaders` | Space Invaders|
|
||||
|
||||
Ein Highscore wird nur überschrieben wenn der neue Score höher ist.
|
||||
|
||||
## Visueller Stil
|
||||
|
||||
Einheitliches Navy/Teal-Theme, abgeleitet vom bestehenden Snake-Design:
|
||||
|
||||
- Background: `#1a1a2e`
|
||||
- Akzentfarbe: `#4ecca3`
|
||||
- Text: `#e0e0e0`
|
||||
- Sekundärtext: `#a0a0c0`
|
||||
- Schrift: `'Courier New', monospace`
|
||||
- Canvas/Kachel-Border: `2px solid #4ecca3`, Glow via `box-shadow`
|
||||
|
||||
Jedes Spiel behält seine eigenen Spielmechanik-spezifischen Farben (z.B. Tetris-Blockfarben), aber das Rahmenlayout (HUD, Overlay, Menübutton) ist identisch.
|
||||
|
||||
## Nicht im Scope
|
||||
|
||||
- Sound / Musik
|
||||
- Mobile-Touch-Steuerung
|
||||
- Globale Ranglisten / Server
|
||||
- Animierte Übergänge zwischen Spielen
|
||||
- Schwierigkeitsstufen
|
||||
275
snake.html
Normal file
275
snake.html
Normal file
@@ -0,0 +1,275 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Snake</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
background: #1a1a2e;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
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 {
|
||||
font-size: 2.5rem;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
#overlay p {
|
||||
font-size: 1rem;
|
||||
color: #a0a0c0;
|
||||
}
|
||||
|
||||
#overlay.hidden { display: none; }
|
||||
|
||||
button {
|
||||
margin-top: 8px;
|
||||
padding: 10px 28px;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
background: #4ecca3;
|
||||
color: #1a1a2e;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
letter-spacing: 2px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
button:hover { background: #38b28a; }
|
||||
|
||||
#controls {
|
||||
margin-top: 12px;
|
||||
font-size: 0.75rem;
|
||||
color: #606080;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>SNAKE</h1>
|
||||
<div id="hud">
|
||||
Score: <span id="score">0</span> Highscore: <span id="highscore">0</span>
|
||||
</div>
|
||||
|
||||
<div style="position:relative;">
|
||||
<canvas id="canvas" width="400" height="400"></canvas>
|
||||
<div id="overlay">
|
||||
<h2 id="overlay-title">SNAKE</h2>
|
||||
<p id="overlay-msg">Steuere die Schlange mit den Pfeiltasten oder WASD</p>
|
||||
<button id="btn">STARTEN</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="controls">Pfeiltasten / WASD | P = Pause</div>
|
||||
|
||||
<script>
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const overlay = document.getElementById('overlay');
|
||||
const overlayTitle = document.getElementById('overlay-title');
|
||||
const overlayMsg = document.getElementById('overlay-msg');
|
||||
const btn = document.getElementById('btn');
|
||||
const scoreEl = document.getElementById('score');
|
||||
const highscoreEl = document.getElementById('highscore');
|
||||
|
||||
const COLS = 20, ROWS = 20;
|
||||
const CELL = canvas.width / COLS;
|
||||
|
||||
let snake, dir, nextDir, food, score, highscore, running, paused, animId;
|
||||
|
||||
highscore = parseInt(localStorage.getItem('snake_hs') || '0');
|
||||
highscoreEl.textContent = highscore;
|
||||
|
||||
function init() {
|
||||
snake = [
|
||||
{ x: 10, y: 10 },
|
||||
{ x: 9, y: 10 },
|
||||
{ x: 8, y: 10 },
|
||||
];
|
||||
dir = { x: 1, y: 0 };
|
||||
nextDir = { x: 1, y: 0 };
|
||||
score = 0;
|
||||
scoreEl.textContent = 0;
|
||||
spawnFood();
|
||||
}
|
||||
|
||||
function spawnFood() {
|
||||
let pos;
|
||||
do {
|
||||
pos = { x: Math.floor(Math.random() * COLS), y: Math.floor(Math.random() * ROWS) };
|
||||
} while (snake.some(s => s.x === pos.x && s.y === pos.y));
|
||||
food = pos;
|
||||
}
|
||||
|
||||
function step() {
|
||||
dir = nextDir;
|
||||
const head = { x: snake[0].x + dir.x, y: snake[0].y + dir.y };
|
||||
|
||||
// Wall collision
|
||||
if (head.x < 0 || head.x >= COLS || head.y < 0 || head.y >= ROWS) return gameOver();
|
||||
// Self collision
|
||||
if (snake.some(s => s.x === head.x && s.y === head.y)) return gameOver();
|
||||
|
||||
snake.unshift(head);
|
||||
|
||||
if (head.x === food.x && head.y === food.y) {
|
||||
score++;
|
||||
scoreEl.textContent = score;
|
||||
if (score > highscore) {
|
||||
highscore = score;
|
||||
highscoreEl.textContent = highscore;
|
||||
localStorage.setItem('snake_hs', highscore);
|
||||
}
|
||||
spawnFood();
|
||||
} else {
|
||||
snake.pop();
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// Background
|
||||
ctx.fillStyle = '#0f0f1a';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Grid (subtle)
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.03)';
|
||||
ctx.lineWidth = 0.5;
|
||||
for (let x = 0; x <= COLS; x++) {
|
||||
ctx.beginPath(); ctx.moveTo(x * CELL, 0); ctx.lineTo(x * CELL, canvas.height); ctx.stroke();
|
||||
}
|
||||
for (let y = 0; y <= ROWS; y++) {
|
||||
ctx.beginPath(); ctx.moveTo(0, y * CELL); ctx.lineTo(canvas.width, y * CELL); ctx.stroke();
|
||||
}
|
||||
|
||||
// Food
|
||||
const fx = food.x * CELL, fy = food.y * CELL;
|
||||
ctx.fillStyle = '#ff6b6b';
|
||||
ctx.beginPath();
|
||||
ctx.arc(fx + CELL / 2, fy + CELL / 2, CELL / 2 - 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Snake
|
||||
snake.forEach((seg, i) => {
|
||||
const t = i / snake.length;
|
||||
const r = Math.round(78 + (30 - 78) * t);
|
||||
const g = Math.round(204 + (150 - 204) * t);
|
||||
const b = Math.round(163 + (80 - 163) * t);
|
||||
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
||||
const pad = i === 0 ? 1 : 2;
|
||||
ctx.fillRect(seg.x * CELL + pad, seg.y * CELL + pad, CELL - pad * 2, CELL - pad * 2);
|
||||
|
||||
// Eyes on head
|
||||
if (i === 0) {
|
||||
ctx.fillStyle = '#1a1a2e';
|
||||
const ex = dir.y !== 0 ? [CELL * 0.3, CELL * 0.7] : [CELL * 0.6, CELL * 0.6];
|
||||
const ey = dir.x !== 0 ? [CELL * 0.3, CELL * 0.7] : [CELL * 0.6, CELL * 0.6];
|
||||
const ox = dir.x === -1 ? CELL * 0.2 : 0;
|
||||
const oy = dir.y === -1 ? CELL * 0.2 : 0;
|
||||
ctx.beginPath();
|
||||
ctx.arc(seg.x * CELL + ex[0] + ox, seg.y * CELL + ey[0] + oy, 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(seg.x * CELL + ex[1] + ox, seg.y * CELL + ey[1] + oy, 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let lastTime = 0;
|
||||
const SPEED = 120; // ms per step
|
||||
|
||||
function loop(ts) {
|
||||
if (!running || paused) return;
|
||||
if (ts - lastTime >= SPEED) {
|
||||
step();
|
||||
lastTime = ts;
|
||||
}
|
||||
if (running) draw();
|
||||
animId = requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
function startGame() {
|
||||
init();
|
||||
running = true;
|
||||
paused = false;
|
||||
overlay.classList.add('hidden');
|
||||
lastTime = 0;
|
||||
animId = requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
function gameOver() {
|
||||
running = false;
|
||||
cancelAnimationFrame(animId);
|
||||
overlayTitle.textContent = 'GAME OVER';
|
||||
overlayTitle.style.color = '#ff6b6b';
|
||||
overlayMsg.textContent = `Punkte: ${score}`;
|
||||
btn.textContent = 'NEU STARTEN';
|
||||
overlay.classList.remove('hidden');
|
||||
}
|
||||
|
||||
btn.addEventListener('click', startGame);
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
const keys = {
|
||||
ArrowUp: { x: 0, y: -1 }, w: { x: 0, y: -1 }, W: { x: 0, y: -1 },
|
||||
ArrowDown: { x: 0, y: 1 }, s: { x: 0, y: 1 }, S: { x: 0, y: 1 },
|
||||
ArrowLeft: { x: -1, y: 0 }, a: { x: -1, y: 0 }, A: { x: -1, y: 0 },
|
||||
ArrowRight: { x: 1, y: 0 }, d: { x: 1, y: 0 }, D: { x: 1, y: 0 },
|
||||
};
|
||||
if (keys[e.key]) {
|
||||
const d = keys[e.key];
|
||||
// Prevent 180 turn
|
||||
if (d.x !== -dir.x || d.y !== -dir.y) nextDir = d;
|
||||
e.preventDefault();
|
||||
}
|
||||
if (e.key === 'p' || e.key === 'P') {
|
||||
if (!running) return;
|
||||
paused = !paused;
|
||||
if (!paused) animId = requestAnimationFrame(loop);
|
||||
}
|
||||
});
|
||||
|
||||
// Initial draw
|
||||
init();
|
||||
draw();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user