mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 01:39:40 +02:00
Rename games/mana-games/ to games/arcade/, update all package names (@mana-games/* → @arcade/*), appIds, display names, docker-compose service, root scripts, and documentation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
662 lines
No EOL
27 KiB
HTML
662 lines
No EOL
27 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Snake Spiel</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
background: #000;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
min-height: 100vh;
|
|
font-family: 'Courier New', monospace;
|
|
color: #00ffff;
|
|
user-select: none;
|
|
}
|
|
|
|
.game-container {
|
|
text-align: center;
|
|
}
|
|
|
|
.score {
|
|
font-size: 16px;
|
|
margin-bottom: 5px;
|
|
letter-spacing: 2px;
|
|
}
|
|
|
|
canvas {
|
|
border: 2px solid #00ffff;
|
|
background: #000;
|
|
display: block;
|
|
image-rendering: pixelated;
|
|
image-rendering: -moz-crisp-edges;
|
|
image-rendering: crisp-edges;
|
|
}
|
|
|
|
.game-over {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
background: rgba(0, 0, 0, 0.95);
|
|
border: 2px solid #00ffff;
|
|
padding: 20px;
|
|
display: none;
|
|
}
|
|
|
|
.restart-btn {
|
|
background: #000;
|
|
color: #00ffff;
|
|
border: 1px solid #00ffff;
|
|
padding: 8px 16px;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.restart-btn:hover {
|
|
background: #00ffff;
|
|
color: #000;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="game-container">
|
|
<div class="score">SCORE: <span id="score">0</span></div>
|
|
<canvas id="gameCanvas" width="400" height="400"></canvas>
|
|
<div class="game-over" id="gameOver">
|
|
<div>GAME OVER</div>
|
|
<div>SCORE: <span id="finalScore">0</span></div>
|
|
<button class="restart-btn" onclick="restartGame()">RESTART</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// ======================== CANVAS UND UI ELEMENTE ========================
|
|
// Hole die Canvas und 2D Kontext für das Zeichnen des Spiels
|
|
const canvas = document.getElementById('gameCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// UI Elemente für Score-Anzeige und Game Over Screen
|
|
const scoreElement = document.getElementById('score');
|
|
const gameOverElement = document.getElementById('gameOver');
|
|
const finalScoreElement = document.getElementById('finalScore');
|
|
|
|
// ======================== SPIEL-KONSTANTEN ========================
|
|
// Größe eines einzelnen Feldes/Kachel in Pixeln
|
|
const gridSize = 20;
|
|
// Anzahl der Kacheln in jeder Richtung (20x20 Grid bei 400px Canvas)
|
|
const tileCount = canvas.width / gridSize;
|
|
|
|
// ======================== SPIEL-ZUSTAND ========================
|
|
// Snake Array - jedes Element ist ein Objekt mit x,y Koordinaten
|
|
// Index 0 ist der Kopf der Schlange
|
|
let snake = [{x: 10, y: 10}];
|
|
|
|
// Position des Essens als Objekt mit x,y Koordinaten
|
|
let food = {};
|
|
|
|
// Bewegungsrichtung der Schlange (-1, 0, 1 für jede Achse)
|
|
let dx = 0; // Horizontale Bewegung: -1 = links, 0 = keine, 1 = rechts
|
|
let dy = 0; // Vertikale Bewegung: -1 = oben, 0 = keine, 1 = unten
|
|
|
|
// Aktueller Punktestand
|
|
let score = 0;
|
|
|
|
// Game ID für Statistiken
|
|
const GAME_ID = 'snake';
|
|
|
|
// Flag ob das Spiel läuft oder pausiert/beendet ist
|
|
let gameRunning = true;
|
|
|
|
// Zeit-Management für konstante Bewegungsgeschwindigkeit
|
|
let lastMoveTime = 0; // Zeitstempel der letzten Bewegung
|
|
let moveInterval = 120; // Millisekunden zwischen Bewegungen (Start-Geschwindigkeit)
|
|
|
|
// ======================== INPUT STEUERUNG ========================
|
|
// Queue für Tasteneingaben - ermöglicht schnelle Richtungswechsel ohne Verlust
|
|
// Maximal 3 Eingaben werden gespeichert
|
|
let inputQueue = [];
|
|
// Letzte tatsächliche Bewegungsrichtung (verhindert 180° Drehungen)
|
|
let lastDirection = { dx: 0, dy: 0 };
|
|
|
|
// ======================== GAME OVER ANIMATION ========================
|
|
// Flag ob die Explosions-Animation läuft
|
|
let gameOverAnimation = false;
|
|
// Array mit Partikeln für die Explosion
|
|
let explosionParticles = [];
|
|
// Startzeit der Animation für Timing
|
|
let animationStartTime = 0;
|
|
|
|
// ======================== BESUCHTE FELDER TRACKING ========================
|
|
// 2D Array das speichert, wie oft jedes Feld besucht wurde
|
|
// 0 = unbesucht (schwarz)
|
|
// 1 = 1x besucht (blau)
|
|
// 2 = 2x besucht (rot mit Streifen)
|
|
// 3 = 3x besucht (magenta mit Kreuz) - tödlich!
|
|
let visitedGrid = Array(tileCount).fill().map(() => Array(tileCount).fill(0));
|
|
|
|
// ======================== FARB-PALETTE ========================
|
|
// Zentrale Definition aller Farben für konsistentes Design
|
|
// und bessere Performance (weniger String-Allokationen)
|
|
const COLORS = {
|
|
background: '#000', // Schwarzer Hintergrund
|
|
snakeHead: '#00ffff', // Cyan für Schlangenkopf
|
|
snakeBody: '#0088aa', // Dunkleres Cyan für Körper
|
|
food: '#ffff00', // Gelb für Essen
|
|
border: '#ffffff', // Weiße Ränder
|
|
visited1: '#4444aa', // Blau für 1x besuchte Felder
|
|
visited2: '#aa4444', // Rot für 2x besuchte Felder
|
|
visited3: '#aa44aa', // Magenta für 3x besuchte (tödliche) Felder
|
|
pattern: '#ffffff' // Weiß für Muster auf besuchten Feldern
|
|
};
|
|
|
|
// ======================== ESSEN GENERATION ========================
|
|
/**
|
|
* Generiert eine neue zufällige Position für das Essen.
|
|
* Stellt sicher, dass das Essen nicht auf der Schlange erscheint.
|
|
*/
|
|
function generateFood() {
|
|
// Wiederhole bis eine freie Position gefunden wird
|
|
do {
|
|
food = {
|
|
x: Math.floor(Math.random() * tileCount),
|
|
y: Math.floor(Math.random() * tileCount)
|
|
};
|
|
// Prüfe ob irgendein Schlangen-Segment auf dieser Position ist
|
|
} while (snake.some(segment => segment.x === food.x && segment.y === food.y));
|
|
}
|
|
|
|
// ======================== HAUPT-GAME-LOOP ========================
|
|
/**
|
|
* Die zentrale Game Loop die kontinuierlich läuft.
|
|
* Wird von requestAnimationFrame aufgerufen für 60 FPS.
|
|
*
|
|
* @param {number} currentTime - Aktuelle Zeit in Millisekunden
|
|
*/
|
|
function gameLoop(currentTime) {
|
|
// Spezialbehandlung während der Game Over Animation
|
|
if (gameOverAnimation) {
|
|
updateExplosion(currentTime); // Bewege Explosions-Partikel
|
|
drawGame(); // Zeichne normales Spielfeld
|
|
drawExplosion(currentTime); // Zeichne Explosion darüber
|
|
requestAnimationFrame(gameLoop);
|
|
return;
|
|
}
|
|
|
|
// Beende Loop wenn Spiel nicht läuft
|
|
if (!gameRunning) return;
|
|
|
|
// Verarbeite gespeicherte Tasteneingaben
|
|
processInputQueue();
|
|
|
|
// Bewegung nur in festgelegten Intervallen (nicht jeden Frame)
|
|
// Dies erzeugt die klassische "ruckartige" Snake-Bewegung
|
|
if (currentTime - lastMoveTime >= moveInterval) {
|
|
moveSnake();
|
|
lastMoveTime = currentTime;
|
|
}
|
|
|
|
// Zeichne jeden Frame für flüssige Darstellung
|
|
// (auch wenn Bewegung nur alle 120ms erfolgt)
|
|
drawGame();
|
|
|
|
// Nächsten Frame anfordern
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
// ======================== INPUT VERARBEITUNG ========================
|
|
/**
|
|
* Verarbeitet die nächste Eingabe aus der Input-Queue.
|
|
* Verhindert 180° Drehungen (Rückwärtsbewegung in sich selbst).
|
|
*/
|
|
function processInputQueue() {
|
|
// Keine Eingaben vorhanden
|
|
if (inputQueue.length === 0) return;
|
|
|
|
// Hole und entferne erste Eingabe aus Queue
|
|
const nextMove = inputQueue.shift();
|
|
|
|
// Prüfe ob die Bewegung gültig ist (keine 180° Drehung)
|
|
// Beispiel: Wenn Schlange nach rechts (dx=1) läuft,
|
|
// ist links (dx=-1) nicht erlaubt
|
|
if ((nextMove.dx !== 0 && lastDirection.dx !== -nextMove.dx) ||
|
|
(nextMove.dy !== 0 && lastDirection.dy !== -nextMove.dy)) {
|
|
// Setze neue Bewegungsrichtung
|
|
dx = nextMove.dx;
|
|
dy = nextMove.dy;
|
|
// Speichere als letzte Richtung für nächste Prüfung
|
|
lastDirection = { dx, dy };
|
|
}
|
|
}
|
|
|
|
// ======================== HAUPT-ZEICHENFUNKTION ========================
|
|
/**
|
|
* Zeichnet das gesamte Spiel.
|
|
* Reihenfolge ist wichtig für korrekte Überlagerung.
|
|
*/
|
|
function drawGame() {
|
|
clearCanvas(); // 1. Lösche alles und zeichne Hintergrund + besuchte Felder
|
|
drawFood(); // 2. Zeichne Essen
|
|
drawSnake(); // 3. Zeichne Schlange (über allem anderen)
|
|
checkCollision(); // 4. Prüfe Kollisionen
|
|
}
|
|
|
|
// ======================== CANVAS LÖSCHEN UND HINTERGRUND ========================
|
|
/**
|
|
* Löscht das Canvas und zeichnet den Hintergrund inklusive besuchter Felder.
|
|
* Optimiert durch Batch-Rendering gleicher Farben.
|
|
*/
|
|
function clearCanvas() {
|
|
// Lösche gesamtes Canvas mit schwarzem Hintergrund
|
|
ctx.fillStyle = COLORS.background;
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Zeichne alle besuchten Felder nach Level gruppiert
|
|
// Dies minimiert ctx.fillStyle Änderungen für bessere Performance
|
|
for (let level = 1; level <= 3; level++) {
|
|
// Setze Farbe für aktuelles Level
|
|
ctx.fillStyle = level === 1 ? COLORS.visited1 :
|
|
level === 2 ? COLORS.visited2 : COLORS.visited3;
|
|
|
|
// Durchlaufe gesamtes Grid
|
|
for (let x = 0; x < tileCount; x++) {
|
|
for (let y = 0; y < tileCount; y++) {
|
|
// Zeichne nur wenn Feld dieses Level hat
|
|
if (visitedGrid[x][y] === level) {
|
|
// Prüfe ob Schlange auf diesem Feld ist
|
|
const isSnakeField = snake.some(segment => segment.x === x && segment.y === y);
|
|
// Zeichne nur wenn Schlange NICHT auf dem Feld ist
|
|
if (!isSnakeField) {
|
|
ctx.fillRect(x * gridSize, y * gridSize, gridSize, gridSize);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Zeichne Muster auf besuchten Feldern (Level 2 und 3)
|
|
// Alle Muster verwenden die gleiche Farbe für Performance
|
|
ctx.fillStyle = COLORS.pattern;
|
|
for (let x = 0; x < tileCount; x++) {
|
|
for (let y = 0; y < tileCount; y++) {
|
|
const level = visitedGrid[x][y];
|
|
const isSnakeField = snake.some(segment => segment.x === x && segment.y === y);
|
|
|
|
// Nur Muster für Level 2 und 3, nicht wo Schlange ist
|
|
if (!isSnakeField && level > 1) {
|
|
// Berechne Pixel-Position des Feldes
|
|
const baseX = x * gridSize;
|
|
const baseY = y * gridSize;
|
|
|
|
if (level === 2) {
|
|
// Vertikale Streifen für Level 2
|
|
ctx.fillRect(baseX + 5, baseY, 2, gridSize); // Linker Streifen
|
|
ctx.fillRect(baseX + 13, baseY, 2, gridSize); // Rechter Streifen
|
|
} else if (level === 3) {
|
|
// Kreuz-Muster für Level 3 (tödliche Felder)
|
|
ctx.fillRect(baseX + 8, baseY, 4, gridSize); // Vertikaler Balken
|
|
ctx.fillRect(baseX, baseY + 8, gridSize, 4); // Horizontaler Balken
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ======================== SCHLANGE ZEICHNEN ========================
|
|
/**
|
|
* Zeichnet die Schlange mit unterschiedlichen Farben für Kopf und Körper.
|
|
* Fügt weiße Ränder für bessere Sichtbarkeit hinzu.
|
|
*/
|
|
function drawSnake() {
|
|
// Verstecke Schlange während Explosions-Animation
|
|
if (gameOverAnimation) return;
|
|
|
|
// Zeichne alle Segmente der Schlange
|
|
snake.forEach((segment, index) => {
|
|
// Kopf (index 0) ist heller als Körper
|
|
ctx.fillStyle = index === 0 ? COLORS.snakeHead : COLORS.snakeBody;
|
|
ctx.fillRect(segment.x * gridSize, segment.y * gridSize, gridSize, gridSize);
|
|
});
|
|
|
|
// Zeichne Ränder für alle Segmente in einem Durchgang
|
|
// (Performance-Optimierung: nur einmal Stil setzen)
|
|
ctx.strokeStyle = COLORS.border;
|
|
ctx.lineWidth = 1;
|
|
snake.forEach(segment => {
|
|
ctx.strokeRect(segment.x * gridSize, segment.y * gridSize, gridSize, gridSize);
|
|
});
|
|
}
|
|
|
|
// ======================== ESSEN ZEICHNEN ========================
|
|
/**
|
|
* Zeichnet das Essen als gelbes Quadrat mit weißem Rand.
|
|
*/
|
|
function drawFood() {
|
|
// Gelbes Quadrat für das Essen
|
|
ctx.fillStyle = COLORS.food;
|
|
ctx.fillRect(food.x * gridSize, food.y * gridSize, gridSize, gridSize);
|
|
|
|
// Weißer Rand für bessere Sichtbarkeit
|
|
ctx.strokeStyle = COLORS.border;
|
|
ctx.lineWidth = 1;
|
|
ctx.strokeRect(food.x * gridSize, food.y * gridSize, gridSize, gridSize);
|
|
}
|
|
|
|
// ======================== SCHLANGEN-BEWEGUNG ========================
|
|
/**
|
|
* Bewegt die Schlange in die aktuelle Richtung.
|
|
* Behandelt Wrap-Around an den Rändern, Essen-Aufnahme und Kollisionen.
|
|
*/
|
|
function moveSnake() {
|
|
// Keine Bewegung wenn Schlange stillsteht (Spielstart)
|
|
if (dx === 0 && dy === 0) return;
|
|
|
|
// Berechne neue Kopfposition
|
|
let head = {x: snake[0].x + dx, y: snake[0].y + dy};
|
|
|
|
// Wrap-Around: Schlange erscheint auf der anderen Seite
|
|
if (head.x < 0) head.x = tileCount - 1; // Links raus -> rechts rein
|
|
if (head.x >= tileCount) head.x = 0; // Rechts raus -> links rein
|
|
if (head.y < 0) head.y = tileCount - 1; // Oben raus -> unten rein
|
|
if (head.y >= tileCount) head.y = 0; // Unten raus -> oben rein
|
|
|
|
// Prüfe ob neues Feld tödlich ist (3x besucht = rot mit Kreuz)
|
|
if (visitedGrid[head.x][head.y] === 3) {
|
|
gameOver();
|
|
return;
|
|
}
|
|
|
|
// Füge neuen Kopf am Anfang des Arrays hinzu
|
|
snake.unshift(head);
|
|
|
|
// Prüfe ob Essen gegessen wurde
|
|
if (head.x === food.x && head.y === food.y) {
|
|
// Essen aufgenommen: Score erhöhen, neues Essen generieren
|
|
score += 10;
|
|
scoreElement.textContent = score;
|
|
|
|
// Sende Score Update für Statistiken
|
|
window.parent.postMessage({
|
|
type: 'GAME_EVENT',
|
|
gameId: GAME_ID,
|
|
event: 'SCORE_UPDATE',
|
|
data: { score: score }
|
|
}, '*');
|
|
|
|
generateFood();
|
|
// Spiel wird schneller (min. 80ms zwischen Bewegungen)
|
|
moveInterval = Math.max(80, moveInterval - 1);
|
|
// Schwanz wird NICHT entfernt -> Schlange wächst
|
|
} else {
|
|
// Kein Essen: Entferne Schwanz (Schlange bleibt gleich lang)
|
|
const tail = snake.pop();
|
|
// Erhöhe Besuchszähler für verlassenes Feld (max. 3)
|
|
visitedGrid[tail.x][tail.y] = Math.min(3, visitedGrid[tail.x][tail.y] + 1);
|
|
}
|
|
}
|
|
|
|
// ======================== KOLLISIONSPRÜFUNG ========================
|
|
/**
|
|
* Prüft ob die Schlange mit sich selbst kollidiert.
|
|
* Wandkollisionen gibt es nicht (Wrap-Around).
|
|
*/
|
|
function checkCollision() {
|
|
const head = snake[0];
|
|
// Prüfe Kollision mit jedem Körpersegment (nicht mit Kopf selbst)
|
|
for (let i = 1; i < snake.length; i++) {
|
|
if (head.x === snake[i].x && head.y === snake[i].y) {
|
|
// Schlange hat sich selbst gebissen
|
|
gameOver();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ======================== GAME OVER BEHANDLUNG ========================
|
|
/**
|
|
* Beendet das Spiel und startet die Explosions-Animation.
|
|
* Erstellt Partikel für jeden Teil der Schlange.
|
|
*/
|
|
function gameOver() {
|
|
// Stoppe Spiellogik
|
|
gameRunning = false;
|
|
|
|
// Sende Game Over Event mit Score für Statistiken
|
|
window.parent.postMessage({
|
|
type: 'GAME_EVENT',
|
|
gameId: GAME_ID,
|
|
event: 'GAME_OVER',
|
|
data: { score: score }
|
|
}, '*');
|
|
|
|
// Achievement prüfen
|
|
if (score >= 500) {
|
|
window.parent.postMessage({
|
|
type: 'GAME_EVENT',
|
|
gameId: GAME_ID,
|
|
event: 'ACHIEVEMENT_UNLOCKED',
|
|
data: {
|
|
achievement: {
|
|
id: 'snake-master',
|
|
name: 'Snake Meister',
|
|
description: '500 Punkte in einem Spiel erreicht!'
|
|
}
|
|
}
|
|
}, '*');
|
|
}
|
|
|
|
if (score >= 1000) {
|
|
window.parent.postMessage({
|
|
type: 'GAME_EVENT',
|
|
gameId: GAME_ID,
|
|
event: 'ACHIEVEMENT_UNLOCKED',
|
|
data: {
|
|
achievement: {
|
|
id: 'snake-legend',
|
|
name: 'Snake Legende',
|
|
description: '1000 Punkte in einem Spiel erreicht!'
|
|
}
|
|
}
|
|
}, '*');
|
|
}
|
|
|
|
// Starte Explosions-Animation
|
|
gameOverAnimation = true;
|
|
animationStartTime = performance.now();
|
|
|
|
// Erstelle Explosions-Partikel für jedes Schlangen-Segment
|
|
explosionParticles = [];
|
|
snake.forEach((segment, index) => {
|
|
// Berechne Zentrum des Segments in Pixeln
|
|
const centerX = segment.x * gridSize + gridSize / 2;
|
|
const centerY = segment.y * gridSize + gridSize / 2;
|
|
const isHead = index === 0;
|
|
|
|
// Erstelle 4 Partikel pro Segment (in 4 Richtungen)
|
|
for (let i = 0; i < 4; i++) {
|
|
// Berechne Richtung mit leichter Zufälligkeit
|
|
const angle = (Math.PI * 2 * i) / 4 + Math.random() * 0.3;
|
|
const speed = Math.random() * 2 + 2;
|
|
|
|
// Erstelle Partikel-Objekt
|
|
explosionParticles.push({
|
|
x: centerX, // Start-Position X
|
|
y: centerY, // Start-Position Y
|
|
vx: Math.cos(angle) * speed, // Geschwindigkeit X
|
|
vy: Math.sin(angle) * speed, // Geschwindigkeit Y
|
|
size: gridSize / 4, // Größe des Partikels
|
|
life: 1.0, // Lebenszeit (1.0 = 100%)
|
|
color: isHead ? COLORS.snakeHead : COLORS.snakeBody // Farbe
|
|
});
|
|
}
|
|
});
|
|
|
|
// Setze finalen Score
|
|
finalScoreElement.textContent = score;
|
|
|
|
// Zeige Game Over Dialog nach 1 Sekunde Animation
|
|
setTimeout(() => {
|
|
gameOverElement.style.display = 'block';
|
|
gameOverAnimation = false;
|
|
}, 1000);
|
|
}
|
|
|
|
// ======================== EXPLOSIONS-UPDATE ========================
|
|
/**
|
|
* Aktualisiert die Positionen und Eigenschaften der Explosions-Partikel.
|
|
* Wird jeden Frame während der Game Over Animation aufgerufen.
|
|
*
|
|
* @param {number} currentTime - Aktuelle Zeit (wird hier nicht verwendet)
|
|
*/
|
|
function updateExplosion(currentTime) {
|
|
// Aktualisiere jeden Partikel
|
|
explosionParticles.forEach(particle => {
|
|
// Bewege Partikel basierend auf Geschwindigkeit
|
|
particle.x += particle.vx;
|
|
particle.y += particle.vy;
|
|
|
|
// Reibung: Verlangsame Partikel (5% pro Frame)
|
|
particle.vx *= 0.95;
|
|
particle.vy *= 0.95;
|
|
|
|
// Reduziere Lebenszeit (2% pro Frame)
|
|
particle.life -= 0.02;
|
|
});
|
|
|
|
// Entferne "tote" Partikel (life <= 0) aus dem Array
|
|
explosionParticles = explosionParticles.filter(particle => particle.life > 0);
|
|
}
|
|
|
|
// ======================== EXPLOSIONS-ZEICHNUNG ========================
|
|
/**
|
|
* Zeichnet alle Explosions-Partikel.
|
|
* Verwendet Alpha-Transparenz für Fade-Out Effekt.
|
|
*
|
|
* @param {number} currentTime - Aktuelle Zeit (wird hier nicht verwendet)
|
|
*/
|
|
function drawExplosion(currentTime) {
|
|
// Zeichne jeden Partikel
|
|
explosionParticles.forEach(particle => {
|
|
// Setze Transparenz basierend auf Lebenszeit (fade out)
|
|
ctx.globalAlpha = Math.max(0, particle.life);
|
|
ctx.fillStyle = particle.color;
|
|
|
|
// Zeichne Partikel als Quadrat (zentriert um Position)
|
|
ctx.fillRect(
|
|
particle.x - particle.size/2, // X-Position (zentriert)
|
|
particle.y - particle.size/2, // Y-Position (zentriert)
|
|
particle.size, // Breite
|
|
particle.size // Höhe
|
|
);
|
|
});
|
|
|
|
// Setze Alpha auf Standard zurück für nächste Zeichenoperationen
|
|
ctx.globalAlpha = 1.0;
|
|
}
|
|
|
|
// ======================== SPIEL NEUSTART ========================
|
|
/**
|
|
* Setzt alle Spielvariablen zurück und startet ein neues Spiel.
|
|
* Wird vom Restart-Button aufgerufen.
|
|
*/
|
|
function restartGame() {
|
|
// Setze Schlange auf Startposition zurück
|
|
snake = [{x: 10, y: 10}];
|
|
|
|
// Keine Bewegung zu Beginn
|
|
dx = 0;
|
|
dy = 0;
|
|
|
|
// Reset Score und Geschwindigkeit
|
|
score = 0;
|
|
moveInterval = 120;
|
|
|
|
// Leere Input-Queue und Richtungs-Tracking
|
|
inputQueue = [];
|
|
lastDirection = { dx: 0, dy: 0 };
|
|
|
|
// Beende Animationen
|
|
gameOverAnimation = false;
|
|
explosionParticles = [];
|
|
|
|
// Update UI
|
|
scoreElement.textContent = score;
|
|
gameRunning = true;
|
|
gameOverElement.style.display = 'none';
|
|
|
|
// Reset besuchte Felder (alle auf 0)
|
|
visitedGrid = Array(tileCount).fill().map(() => Array(tileCount).fill(0));
|
|
|
|
// Generiere neues Essen und starte Game Loop
|
|
generateFood();
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
// ======================== TASTATUR-STEUERUNG ========================
|
|
/**
|
|
* Event-Listener für Tastatureingaben.
|
|
* Unterstützt Pfeiltasten und WASD.
|
|
* Verwendet eine Queue für responsive Steuerung bei schnellen Eingaben.
|
|
*/
|
|
document.addEventListener('keydown', (e) => {
|
|
// Ignoriere Eingaben wenn Spiel nicht läuft
|
|
if (!gameRunning) return;
|
|
|
|
let newMove = null;
|
|
|
|
// Mappe Tasten zu Bewegungsrichtungen
|
|
switch(e.key) {
|
|
case 'ArrowUp': // Pfeil nach oben
|
|
case 'w': // W-Taste
|
|
case 'W':
|
|
newMove = {dx: 0, dy: -1}; // Nach oben
|
|
break;
|
|
case 'ArrowDown': // Pfeil nach unten
|
|
case 's': // S-Taste
|
|
case 'S':
|
|
newMove = {dx: 0, dy: 1}; // Nach unten
|
|
break;
|
|
case 'ArrowLeft': // Pfeil nach links
|
|
case 'a': // A-Taste
|
|
case 'A':
|
|
newMove = {dx: -1, dy: 0}; // Nach links
|
|
break;
|
|
case 'ArrowRight': // Pfeil nach rechts
|
|
case 'd': // D-Taste
|
|
case 'D':
|
|
newMove = {dx: 1, dy: 0}; // Nach rechts
|
|
break;
|
|
}
|
|
|
|
// Füge gültige Bewegung zur Queue hinzu
|
|
if (newMove && inputQueue.length < 3) { // Max. 3 Eingaben speichern
|
|
// Verhindere identische aufeinanderfolgende Eingaben
|
|
const lastInQueue = inputQueue[inputQueue.length - 1];
|
|
if (!lastInQueue || lastInQueue.dx !== newMove.dx || lastInQueue.dy !== newMove.dy) {
|
|
inputQueue.push(newMove);
|
|
}
|
|
// Verhindere Standard-Scrolling bei Pfeiltasten
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
|
|
// ======================== SPIEL INITIALISIERUNG ========================
|
|
// Sende Game Loaded Event für Statistiken
|
|
window.parent.postMessage({
|
|
type: 'GAME_LOADED',
|
|
gameId: GAME_ID
|
|
}, '*');
|
|
|
|
// Generiere erstes Essen
|
|
generateFood();
|
|
// Starte Game Loop
|
|
requestAnimationFrame(gameLoop);
|
|
</script>
|
|
</body>
|
|
</html> |