Lade dir unsere ultimative Multiplayer Version des klassischen Snake Spiels herunter, komplett umgesetzt in PHP, Ajax und JavaScript in einer einzigen Datei. Professionell gestaltet wie von einer Marketing-Agentur entwickelt, bietet dieses Script mehr Features als das Original:
Features:
- Benutzerkonten mit Registrierung, E-Mail-Verifikation und Passwort zurücksetzen
- Highscore-System pro Benutzer mit separatem Leaderboard
- Mehrere Spielmodi: Überleben oder Zeitlimit
- Optische Animationen, Soundeffekte und responsive UI
- PWA-fähig: auf Smartphone installierbar
- Moderationspanel mit Benutzerverwaltung, Bann-Funktion und Score-Löschung
- PHPMailer-Integration für zuverlässige E-Mail-Kommunikation
- Sicherheitsfunktionen: reCAPTCHA, IP-Sperre und verschlüsseltes Admin-Login
Installation:
- Einfach die Datei auf deinen Server hochladen
- Berechtigungen für den Datenordner prüfen, SQLite-Datenbank wird automatisch erstellt
- SMTP-Daten für E-Mail-Funktion konfigurieren (optional)
- Zugriff über den Browser – sofort spielbar
<?php
session_start();
date_default_timezone_set('Europe/Berlin');
/*
Features
- SQLite Datenbank initialisierung
- Registrierung mit E Mail Verifikation
- Passwort zurücksetzen per E Mail
- PHPMailer Integration optional
- Moderations UI und Endpunkte
- Spiel API bereits vorhanden
- Alles in einer Datei
- Ein Dreamcodes.NET Spiel
*/
/* ========== Konfiguration ========== */
define('DATA_DIR', __DIR__ . '/data_snake');
define('DB_FILE', DATA_DIR . '/snake.sqlite');
define('SITE_FROM_EMAIL', 'no-reply@example.com'); // Absender E Mail
define('SITE_FROM_NAME', 'Snake Dreamcodes');
define('SITE_BASE_URL', (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . dirname($_SERVER['SCRIPT_NAME']));
if(substr(SITE_BASE_URL, -1) === '/') SITE_BASE_URL = rtrim(SITE_BASE_URL, '/'); // optional trim
// SMTP Konfiguration für PHPMailer
// Wenn du PHPMailer nicht verwenden willst dann wird mail() als Fallback eingesetzt
$smtp = [
'enabled' => true, // true um PHPMailer zu nutzen wenn verfügbar
'host' => 'smtp.example.com',
'port' => 587,
'username' => 'smtp_user@example.com',
'password' => 'smtp_password',
'secure' => 'tls' // tls oder ssl
];
// Admin Zugangs Passwort zum Moderationspanel bitte ändern
define('ADMIN_PASSWORD', 'ChangeMeAdminPass123!');
// reCAPTCHA optional platzhalter
define('RECAPTCHA_SITE_KEY', '');
define('RECAPTCHA_SECRET_KEY', '');
/* ========== Initialisierung ========== */
if(!is_dir(DATA_DIR)) mkdir(DATA_DIR,0755,true);
if(!file_exists(DB_FILE)){
$pdo = new PDO('sqlite:' . DB_FILE);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL,
pass_hash TEXT NOT NULL,
created INTEGER NOT NULL,
verified INTEGER DEFAULT 0,
verify_token TEXT DEFAULT NULL,
reset_token TEXT DEFAULT NULL,
reset_expires INTEGER DEFAULT NULL,
is_banned INTEGER DEFAULT 0
);
CREATE TABLE scores (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
name TEXT,
score INTEGER NOT NULL,
mode TEXT,
created INTEGER NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id)
);
CREATE INDEX idx_scores_user ON scores(user_id);
");
$pdo = null;
}
/* ========== Hilfsfunktionen ========== */
function get_db(){
$pdo = new PDO('sqlite:' . DB_FILE);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
return $pdo;
}
function json_response($data){
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data);
exit;
}
function read_input_json(){
$s = file_get_contents('php://input');
if(!$s) return [];
$data = json_decode($s, true);
return is_array($data) ? $data : [];
}
function current_user(){
if(!empty($_SESSION['user_id'])){
$pdo = get_db();
$st = $pdo->prepare('SELECT id, username, email, verified, is_banned FROM users WHERE id=:id LIMIT 1');
$st->execute([':id'=>$_SESSION['user_id']]);
$row = $st->fetch(PDO::FETCH_ASSOC);
if($row) return $row;
}
return null;
}
function send_mail($to_email, $to_name, $subject, $body_html, $body_text=''){
global $smtp;
// Versuch PHPMailer wenn aktiviert und verfügbar
if($smtp['enabled'] && class_exists('PHPMailer\PHPMailer\PHPMailer')){
try{
$mail = new PHPMailer\PHPMailer\PHPMailer(true);
$mail->isSMTP();
$mail->Host = $smtp['host'];
$mail->SMTPAuth = true;
$mail->Username = $smtp['username'];
$mail->Password = $smtp['password'];
$mail->SMTPSecure = $smtp['secure'] === 'ssl' ? PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS : PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = $smtp['port'];
$mail->setFrom(SITE_FROM_EMAIL, SITE_FROM_NAME);
$mail->addAddress($to_email, $to_name);
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $body_html;
if($body_text) $mail->AltBody = $body_text;
$mail->send();
return true;
} catch(Exception $e){
error_log('PHPMailer error: ' . $e->getMessage());
// fallback to mail
}
}
// fallback simple mail
$headers = "From: " . SITE_FROM_NAME . " <" . SITE_FROM_EMAIL . ">\r\n";
$headers .= "MIME-Version: 1.0\r\n";
$headers .= "Content-type: text/html; charset=utf-8\r\n";
return mail($to_email, $subject, $body_html, $headers);
}
function random_token($len=64){
return bin2hex(random_bytes((int)($len/2)));
}
function require_admin(){
if(empty($_SESSION['is_admin']) || !$_SESSION['is_admin']) json_response(['ok'=>false,'error'=>'admin_required']);
}
/* Optional PHPMailer Autoload if vorhanden in vendor */
if(file_exists(__DIR__ . '/vendor/autoload.php')){
require_once __DIR__ . '/vendor/autoload.php';
}
/* ========== Service Worker und Manifest wie vorher ========== */
if(isset($_GET['sw']) && $_GET['sw']=='1'){
header('Content-Type: application/javascript; charset=utf-8');
echo "self.addEventListener('install', e=>{e.waitUntil(caches.open('snake-v1').then(c=>c.addAll(['/?manifest=1','/snake.php'])))});\n";
echo "self.addEventListener('fetch', e=>{e.respondWith(caches.match(e.request).then(r=>r||fetch(e.request)))});\n";
exit;
}
if(isset($_GET['manifest']) && $_GET['manifest']=='1'){
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
"name" => "Snake Spiel Dreamcodes",
"short_name" => "Snake",
"start_url" => "/snake.php",
"display" => "standalone",
"background_color" => "#071025",
"theme_color" => "#06b6d4",
"icons" => [
["src"=>"/favicon-192.png","sizes"=>"192x192","type"=>"image/png"],
["src"=>"/favicon-512.png","sizes"=>"512x512","type"=>"image/png"]
]
]);
exit;
}
/* ========== AJAX Endpunkte ========== */
if(isset($_GET['action'])){
$action = $_GET['action'];
// Registrieren mit Verifikation
if($action === 'register' && $_SERVER['REQUEST_METHOD'] === 'POST'){
$data = read_input_json();
$username = trim($data['username'] ?? '');
$email = trim($data['email'] ?? '');
$password = trim($data['password'] ?? '');
if(!$username || !$email || !$password) json_response(['ok'=>false,'error'=>'Bitte alle Felder ausfüllen']);
// optional reCAPTCHA prüfen
if(RECAPTCHA_SECRET_KEY && !empty($data['recaptcha_token'])){
$resp = file_get_contents('https://www.google.com/recaptcha/api/siteverify?secret=' . urlencode(RECAPTCHA_SECRET_KEY) . '&response=' . urlencode($data['recaptcha_token']));
$j = json_decode($resp, true);
if(empty($j['success']) || $j['score'] < 0.3) json_response(['ok'=>false,'error'=>'reCAPTCHA Prüfung fehlgeschlagen']);
}
try{
$pdo = get_db();
$st = $pdo->prepare('SELECT id FROM users WHERE username=:u OR email=:e LIMIT 1');
$st->execute([':u'=>$username,':e'=>$email]);
if($st->fetch()) json_response(['ok'=>false,'error'=>'Benutzername oder E Mail bereits vorhanden']);
$token = random_token(64);
$pass_hash = password_hash($password, PASSWORD_DEFAULT);
$ins = $pdo->prepare('INSERT INTO users (username, email, pass_hash, created, verify_token, verified) VALUES (:u,:e,:p,:t,:vt,0)');
$ins->execute([':u'=>$username,':e'=>$email,':p'=>$pass_hash,':t'=>time(),':vt'=>$token]);
$uid = $pdo->lastInsertId();
// Verifikationsmail senden
$link = SITE_BASE_URL . '/snake.php?action=verify&token=' . $token;
$subject = 'Bitte E Mail bestätigen';
$body = "<p>Hallo " . htmlspecialchars($username) . "</p><p>Bitte bestätige deine E Mail Adresse mit diesem Link</p><p><a href=\"" . htmlspecialchars($link) . "\">E Mail bestätigen</a></p>";
send_mail($email, $username, $subject, $body);
// auto login optional nicht bevor verifiziert
json_response(['ok'=>true,'message'=>'Registriert bitte prüfe dein Postfach zum Bestätigen']);
} catch(Exception $e){
json_response(['ok'=>false,'error'=>'Fehler bei Registrierung']);
}
}
// E Mail Verifikation Link
if($action === 'verify' && isset($_GET['token'])){
$token = $_GET['token'];
if(!$token) { echo 'Ungültiger Token'; exit; }
$pdo = get_db();
$st = $pdo->prepare('SELECT id, verified FROM users WHERE verify_token=:t LIMIT 1');
$st->execute([':t'=>$token]);
$row = $st->fetch(PDO::FETCH_ASSOC);
if(!$row) { echo 'Token ungültig oder bereits verwendet'; exit; }
$upd = $pdo->prepare('UPDATE users SET verified=1, verify_token=NULL WHERE id=:id');
$upd->execute([':id'=>$row['id']]);
echo '<!doctype html><html><body><h2>Bestätigung erfolgreich</h2><p>Dein Account wurde aktiviert</p><p><a href="snake.php">Zurück zum Spiel</a></p></body></html>';
exit;
}
// Login
if($action === 'login' && $_SERVER['REQUEST_METHOD'] === 'POST'){
$data = read_input_json();
$user = trim($data['username'] ?? '');
$pass = trim($data['password'] ?? '');
if(!$user || !$pass) json_response(['ok'=>false,'error'=>'Bitte ausfüllen']);
$pdo = get_db();
$st = $pdo->prepare('SELECT id, username, pass_hash, verified, is_banned FROM users WHERE username=:u LIMIT 1');
$st->execute([':u'=>$user]);
$row = $st->fetch(PDO::FETCH_ASSOC);
if(!$row) json_response(['ok'=>false,'error'=>'Ungültige Zugangsdaten']);
if($row['is_banned']) json_response(['ok'=>false,'error'=>'Account gesperrt']);
if(!password_verify($pass, $row['pass_hash'])) json_response(['ok'=>false,'error'=>'Ungültige Zugangsdaten']);
if(!$row['verified']) json_response(['ok'=>false,'error'=>'E Mail noch nicht bestätigt']);
$_SESSION['user_id'] = $row['id'];
json_response(['ok'=>true,'user'=>['id'=>$row['id'],'username'=>$row['username']]]);
}
// Logout
if($action === 'logout'){
session_unset();
session_destroy();
json_response(['ok'=>true]);
}
// request password reset
if($action === 'request_reset' && $_SERVER['REQUEST_METHOD'] === 'POST'){
$data = read_input_json();
$email = trim($data['email'] ?? '');
if(!$email) json_response(['ok'=>false,'error'=>'E Mail fehlt']);
$pdo = get_db();
$st = $pdo->prepare('SELECT id, username FROM users WHERE email=:e LIMIT 1');
$st->execute([':e'=>$email]);
$row = $st->fetch(PDO::FETCH_ASSOC);
if(!$row) json_response(['ok'=>false,'error'=>'Keine passenden Daten gefunden']);
$token = random_token(48);
$expires = time() + 3600; // 1 Stunde gültig
$upd = $pdo->prepare('UPDATE users SET reset_token=:rt, reset_expires=:re WHERE id=:id');
$upd->execute([':rt'=>$token, ':re'=>$expires, ':id'=>$row['id']]);
$link = SITE_BASE_URL . '/snake.php?action=do_reset&token=' . $token;
$subject = 'Passwort zurücksetzen';
$body = "<p>Hallo " . htmlspecialchars($row['username']) . "</p><p>Klicke den Link um dein Passwort zurückzusetzen</p><p><a href=\"" . htmlspecialchars($link) . "\">Passwort zurücksetzen</a></p><p>Link gültig 1 Stunde</p>";
send_mail($email, $row['username'], $subject, $body);
json_response(['ok'=>true,'message'=>'Reset Link gesendet']);
}
// perform reset form oder direkte API post
if($action === 'do_reset'){
if($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['token'])){
// zeige einfach ein kleines Formular
$token = $_GET['token'];
echo "<!doctype html><html><body><h3>Passwort neu setzen</h3>
<form method='POST' action='?action=do_reset'>
<input type='hidden' name='token' value='" . htmlspecialchars($token) . "' />
<div><label>Neues Passwort</label><br><input type='password' name='password' /></div>
<div><button type='submit'>Speichern</button></div>
</form></body></html>";
exit;
} elseif($_SERVER['REQUEST_METHOD'] === 'POST'){
// handle both form urlencoded and json
$token = $_POST['token'] ?? null;
$password = $_POST['password'] ?? null;
if(empty($token)){
$j = read_input_json();
$token = $j['token'] ?? $token;
$password = $j['password'] ?? $password;
}
if(!$token || !$password) json_response(['ok'=>false,'error'=>'Token oder Passwort fehlt']);
$pdo = get_db();
$st = $pdo->prepare('SELECT id, reset_expires FROM users WHERE reset_token=:rt LIMIT 1');
$st->execute([':rt'=>$token]);
$row = $st->fetch(PDO::FETCH_ASSOC);
if(!$row) json_response(['ok'=>false,'error'=>'Ungültiger Token']);
if(time() > $row['reset_expires']) json_response(['ok'=>false,'error'=>'Token abgelaufen']);
$pass_hash = password_hash($password, PASSWORD_DEFAULT);
$upd = $pdo->prepare('UPDATE users SET pass_hash=:ph, reset_token=NULL, reset_expires=NULL WHERE id=:id');
$upd->execute([':ph'=>$pass_hash, ':id'=>$row['id']]);
json_response(['ok'=>true,'message'=>'Passwort geändert']);
}
}
// get current user details
if($action === 'get_user'){
$u = current_user();
json_response(['ok'=>true,'user'=>$u]);
}
// save score like before
if($action === 'save_score' && $_SERVER['REQUEST_METHOD'] === 'POST'){
$payload = read_input_json();
$name = trim(substr(strip_tags($payload['name'] ?? 'Spieler'), 0, 64));
$score = intval($payload['score'] ?? 0);
$mode = in_array($payload['mode'] ?? '', ['survival','timed']) ? $payload['mode'] : 'survival';
$time = time();
$pdo = get_db();
$user = current_user();
$uid = $user ? $user['id'] : null;
$ins = $pdo->prepare('INSERT INTO scores (user_id, name, score, mode, created) VALUES (:uid, :n, :s, :m, :t)');
$ins->execute([':uid'=>$uid, ':n'=>$name, ':s'=>$score, ':m'=>$mode, ':t'=>$time]);
// respond top 20
$top = $pdo->prepare('SELECT s.score, COALESCE(u.username, s.name) as name, s.mode FROM scores s LEFT JOIN users u ON u.id = s.user_id ORDER BY s.score DESC LIMIT 20');
$top->execute();
$toplist = $top->fetchAll(PDO::FETCH_ASSOC);
json_response(['ok'=>true,'leaderboard'=>$toplist]);
}
// get leaderboard
if($action === 'get_leaderboard'){
$pdo = get_db();
$top = $pdo->prepare('SELECT s.score, COALESCE(u.username, s.name) as name, s.mode FROM scores s LEFT JOIN users u ON u.id = s.user_id ORDER BY s.score DESC LIMIT 50');
$top->execute();
json_response(['ok'=>true,'leaderboard'=>$top->fetchAll(PDO::FETCH_ASSOC)]);
}
// Moderation Endpunkte
if($action === 'admin_login' && $_SERVER['REQUEST_METHOD'] === 'POST'){
$data = read_input_json();
$pass = $data['password'] ?? '';
if($pass === ADMIN_PASSWORD){
$_SESSION['is_admin'] = true;
json_response(['ok'=>true]);
} else json_response(['ok'=>false,'error'=>'invalid']);
}
if($action === 'admin_logout'){
unset($_SESSION['is_admin']);
json_response(['ok'=>true]);
}
if($action === 'admin_list_users'){
require_admin();
$pdo = get_db();
$st = $pdo->query('SELECT id, username, email, created, verified, is_banned FROM users ORDER BY created DESC LIMIT 200');
$rows = $st->fetchAll(PDO::FETCH_ASSOC);
json_response(['ok'=>true,'users'=>$rows]);
}
if($action === 'admin_ban_user' && $_SERVER['REQUEST_METHOD'] === 'POST'){
require_admin();
$d = read_input_json();
$uid = intval($d['user_id'] ?? 0);
$ban = !empty($d['ban']) ? 1 : 0;
$pdo = get_db();
$st = $pdo->prepare('UPDATE users SET is_banned=:b WHERE id=:id');
$st->execute([':b'=>$ban,':id'=>$uid]);
json_response(['ok'=>true]);
}
if($action === 'admin_delete_score' && $_SERVER['REQUEST_METHOD'] === 'POST'){
require_admin();
$d = read_input_json();
$sid = intval($d['score_id'] ?? 0);
$pdo = get_db();
$st = $pdo->prepare('DELETE FROM scores WHERE id=:id');
$st->execute([':id'=>$sid]);
json_response(['ok'=>true]);
}
json_response(['ok'=>false,'error'=>'unknown_action']);
}
/* ========== Frontend HTML und JS ========== */
/* Aus Platzgründen ist der UI Teil kompakter gehalten und baut auf dem vorherigen Script auf.
Er enthält nun UI für Verifikation Reset Moderation */
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Snake Spiel – Dreamcodes</title>
<link rel="manifest" href="?manifest=1">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<style>
:root{--bg:#071025;--card:#0f1724;--muted:#9fb4d6;--accent:#06b6d4;--accent2:#7c3aed}
html,body{height:100%;margin:0;font-family:Inter,system-ui,Arial;color:#e6f0fb;background:linear-gradient(180deg,var(--bg),#021226)}
.wrap{max-width:1100px;margin:28px auto;padding:20px}
header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:18px}
h1{font-size:20px;margin:0;font-weight:800}
.sub{color:var(--muted);font-size:13px}
.grid{display:grid;grid-template-columns:1fr 420px;gap:18px}
@media(max-width:1020px){.grid{grid-template-columns:1fr}}
.card{background:linear-gradient(180deg,var(--card),#071428);border-radius:12px;padding:18px;box-shadow:0 12px 40px rgba(2,6,23,0.6);border:1px solid rgba(255,255,255,0.03)}
.controls{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
.btn{background:var(--accent);color:#012;padding:10px 12px;border-radius:10px;border:none;font-weight:700;cursor:pointer}
.btn.alt{background:transparent;border:1px solid rgba(255,255,255,0.04);color:var(--muted)}
.smalltxt{color:var(--muted);font-size:13px}
#gameCanvas{background:#fff;border-radius:10px;box-shadow:0 10px 30px rgba(2,6,23,0.45);display:block;max-width:560px;width:100%}
.auth input{background:transparent;border:1px solid rgba(255,255,255,0.04);padding:8px;border-radius:8px;color:inherit;margin-right:6px}
.leaderboard li{padding:8px;border-radius:8px;margin-bottom:6px;background:linear-gradient(90deg, rgba(255,255,255,0.01), rgba(255,255,255,0.008));display:flex;justify-content:space-between;align-items:center;font-weight:600}
footer{max-width:1100px;margin:28px auto;color:var(--muted);text-align:center;font-size:13px}
footer a{color:var(--accent2);text-decoration:none}
</style>
</head>
<body>
<div class="wrap">
<header>
<div>
<h1>Snake Spiel</h1>
<div class="sub">Agentur Style mit Account E Mail Verifikation und Moderation</div>
</div>
<div id="userArea" class="sub"></div>
</header>
<main class="grid">
<section class="card">
<div style="display:flex;justify-content:space-between;align-items:center">
<div><strong>Spielmodi</strong><div class="smalltxt">Wähle Modus</div></div>
<div><select id="modeSelect"><option value="survival">Überleben</option><option value="timed">Zeitlimit 60s</option></select></div>
</div>
<div style="margin-top:12px;display:flex;flex-direction:column;align-items:center;gap:10px">
<canvas id="gameCanvas" width="560" height="560"></canvas>
<div class="controls">
<button id="btnStart" class="btn">Start</button>
<button id="btnPause" class="btn alt">Pause</button>
<button id="btnReset" class="btn alt">Neu</button>
<select id="speedSelect" class="btn alt"><option value="140">Leicht</option><option value="100" selected>Normal</option><option value="70">Schwer</option></select>
<label style="margin-left:auto" class="smalltxt">Pfeiltasten oder W A S D</label>
</div>
</div>
<div style="display:flex;gap:18px;margin-top:12px;align-items:center;flex-wrap:wrap">
<div><div class="smalltxt">Aktuelle Punktzahl</div><div id="currentScore" style="font-size:28px;font-weight:800;color:var(--accent)">0</div></div>
<div><div class="smalltxt">Highscore</div><div id="bestScore" style="font-size:28px;font-weight:800">0</div></div>
<div><div class="smalltxt">Spiele gesamt</div><div id="gamesTotal" style="font-size:24px;color:var(--muted)">0</div></div>
</div>
<div class="smalltxt" style="margin-top:12px">Bei Spielende kannst du speichern wenn du eingeloggt bist</div>
<div style="margin-top:8px" id="msgArea"></div>
</section>
<aside class="card">
<h3 style="margin:0 0 8px 0">Leaderboard</h3>
<div style="padding:12px;max-height:320px;overflow:auto">
<ol id="leaderList"></ol>
</div>
<div style="margin-top:12px">
<h4 style="margin:0 0 6px 0">Konto</h4>
<div id="authArea">
<div id="regBox" style="display:block" class="auth">
<input id="regUser" placeholder="Benutzername" type="text">
<input id="regEmail" placeholder="E Mail" type="text">
<input id="regPass" placeholder="Passwort" type="password">
<button id="btnRegister" class="btn alt">Registrieren</button>
</div>
<div style="margin-top:8px" class="auth">
<input id="loginUser" placeholder="Benutzername" type="text">
<input id="loginPass" placeholder="Passwort" type="password">
<button id="btnLogin" class="btn alt">Login</button>
</div>
<div style="margin-top:8px" id="loggedInBox" style="display:none">
<div class="smalltxt">Eingeloggt als <strong id="loggedUser"></strong></div>
<div style="display:flex;gap:8px;margin-top:8px">
<input id="playerName" placeholder="Name für Leaderboard" style="flex:1;padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:transparent;color:inherit">
<button id="btnSaveScore" class="btn">Speichern</button>
</div>
<div style="margin-top:8px">
<button id="btnLogout" class="btn alt">Logout</button>
</div>
</div>
<div style="margin-top:8px">
<a href="#" id="forgotLink" class="smalltxt">Passwort vergessen</a>
</div>
</div>
</div>
<div style="margin-top:16px" id="adminPanelWrap">
<h4 style="margin:0 0 6px 0">Moderation</h4>
<div id="adminLoginBox" style="display:block">
<input id="adminPass" placeholder="Admin Passwort" type="password"><button id="adminLoginBtn" class="btn alt">Admin Login</button>
</div>
<div id="adminPanel" style="display:none">
<div style="margin-top:8px" class="smalltxt">Admin angemeldet</div>
<div style="margin-top:8px"><button id="adminFetchUsers" class="btn small">Nutzer Liste</button> <button id="adminLogoutBtn" class="btn alt small">Logout Admin</button></div>
<div id="adminUsers" style="margin-top:8px;max-height:200px;overflow:auto"></div>
</div>
</div>
</aside>
</main>
<footer>© <?=date('Y')?> <a href="https://www.dreamcodes.net" target="_blank" rel="noopener noreferrer">Dreamcodes</a> — Snake Spiel</footer>
</div>
<script>
// grundlegende helper und API wrapper
const api = async (action, method='GET', body=null) => {
const url = '?action=' + action;
const opts = { method, credentials: 'same-origin' };
if(body){ opts.headers = {'Content-Type':'application/json'}; opts.body = JSON.stringify(body); }
const r = await fetch(url, opts);
return await r.json();
};
function el(id){ return document.getElementById(id); }
function showMsg(t){ el('msgArea').textContent = t; setTimeout(()=>el('msgArea').textContent = '',4000); }
// Auth flow
async function refreshUser(){
const j = await api('get_user');
const user = j.user;
if(user){
el('regBox').style.display='none';
el('loggedInBox').style.display='block';
el('loggedUser').textContent = user.username;
} else {
el('regBox').style.display='block';
el('loggedInBox').style.display='none';
}
}
el('btnRegister').addEventListener('click', async ()=>{
const u = el('regUser').value.trim();
const e = el('regEmail').value.trim();
const p = el('regPass').value.trim();
if(!u||!e||!p){ alert('Bitte ausfüllen'); return; }
const r = await api('register','POST',{username:u,email:e,password:p});
if(r.ok){ alert('Registriert bitte E Mail prüfen zum Aktivieren'); } else alert(r.error || 'Fehler');
});
el('btnLogin').addEventListener('click', async ()=>{
const u = el('loginUser').value.trim();
const p = el('loginPass').value.trim();
if(!u||!p) { alert('Bitte ausfüllen'); return; }
const r = await api('login','POST',{username:u,password:p});
if(r.ok){ refreshUser(); showMsg('Login erfolgreich'); } else alert(r.error || 'Fehler');
});
el('btnLogout').addEventListener('click', async ()=>{
await api('logout'); refreshUser();
});
el('forgotLink').addEventListener('click', async (e)=>{
e.preventDefault();
const email = prompt('Deine E Mail zum Zurücksetzen eingeben');
if(!email) return;
const r = await api('request_reset','POST',{email});
if(r.ok) alert('Falls die E Mail existiert wurde ein Link gesendet');
else alert(r.error || 'Fehler');
});
// Spiel logik verkürzt hier wieder integriert
(function(){
const canvas = el('gameCanvas');
const ctx = canvas.getContext('2d');
const grid = 28;
let cell = canvas.width / grid;
let snake = [{x:12,y:12}];
let dir = {x:0,y:-1};
let food = null;
let alive = false;
let score = 0;
let best = 0;
function randCell(){ return {x: Math.floor(Math.random()*grid), y: Math.floor(Math.random()*grid)}; }
function placeFood(){ let f = randCell(); while(snake.some(s=>s.x===f.x&&s.y===f.y)) f=randCell(); food=f; }
function draw(){
ctx.clearRect(0,0,canvas.width,canvas.height);
if(food){ ctx.fillStyle='#ef4444'; ctx.fillRect(food.x*cell+cell*0.12, food.y*cell+cell*0.12, cell*0.76, cell*0.76); }
for(let i=0;i<snake.length;i++){
ctx.fillStyle = i===0 ? '#06b6d4' : '#7c3aed';
const s = snake[i];
ctx.fillRect(s.x*cell+1, s.y*cell+1, cell-2, cell-2);
}
el('currentScore').textContent = score;
el('bestScore').textContent = best;
}
function step(){
if(!alive) return;
const head = {x: snake[0].x + dir.x, y: snake[0].y + dir.y};
if(head.x<0) head.x=grid-1; if(head.x>=grid) head.x=0;
if(head.y<0) head.y=grid-1; if(head.y>=grid) head.y=0;
if(snake.some((p,i)=>i>0 && p.x===head.x && p.y===head.y)){ gameOver(); return; }
snake.unshift(head);
if(food && head.x===food.x && head.y===food.y){ score += 10; placeFood(); } else snake.pop();
draw();
}
function start(){ if(alive) return; alive=true; if(!food) placeFood(); timer = setInterval(step, parseInt(el('speedSelect').value)); }
function pause(){ alive=false; clearInterval(window.timer); }
function reset(){ alive=false; clearInterval(window.timer); snake=[{x:12,y:12}]; dir={x:0,y:-1}; placeFood(); score=0; draw(); }
function gameOver(){ alive=false; clearInterval(window.timer); if(score>best) best=score; showMsg('Spielende Punktzahl ' + score); }
document.getElementById('btnStart').addEventListener('click', start);
document.getElementById('btnPause').addEventListener('click', ()=>{ if(alive) pause(); else start(); });
document.getElementById('btnReset').addEventListener('click', reset);
document.getElementById('speedSelect').addEventListener('change', ()=>{ if(alive){ clearInterval(window.timer); window.timer=setInterval(step, parseInt(el('speedSelect').value)); } });
window.addEventListener('keydown', e=>{
const map = {'ArrowUp':[0,-1],'ArrowDown':[0,1],'ArrowLeft':[-1,0],'ArrowRight':[1,0],'w':[0,-1],'s':[0,1],'a':[-1,0],'d':[1,0]};
const k = e.key;
if(map[k]){ const [x,y]=map[k]; if(snake.length>1 && snake[1].x===snake[0].x+x && snake[1].y===snake[0].y+y) return; dir={x,y}; e.preventDefault(); }
});
el('btnSaveScore').addEventListener('click', async ()=>{
const name = (el('playerName').value || 'Spieler').substring(0,64);
const res = await fetch('?action=save_score', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name,score,mode:el('modeSelect').value})});
const j = await res.json();
if(j.ok){ renderLeaderboard(j.leaderboard); showMsg('Erfolgreich gespeichert'); } else showMsg('Fehler beim speichern');
});
function renderLeaderboard(list){
const ol = el('leaderList'); ol.innerHTML='';
list.forEach((it,i)=>{
const li = document.createElement('li');
li.innerHTML = `<span>${i+1}. ${escapeHtml(it.name)}</span><strong>${it.score}</strong>`;
ol.appendChild(li);
});
}
function escapeHtml(s){ return String(s).replace(/[&<>"']/g, m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''})[m]); }
// initial
placeFood();
draw();
(async ()=>{ const r = await api('get_leaderboard'); if(r.ok) renderLeaderboard(r.leaderboard); const s = await api('get_user'); if(s.ok) refreshUser(); })();
})();
// Admin Aktionen
el('adminLoginBtn').addEventListener('click', async ()=>{
const p = el('adminPass').value;
const r = await api('admin_login','POST',{password:p});
if(r.ok){ el('adminPanel').style.display='block'; el('adminLoginBox').style.display='none'; showMsg('Admin angemeldet'); } else alert('Admin login fehlgeschlagen');
});
el('adminLogoutBtn').addEventListener('click', async ()=>{ await api('admin_logout'); el('adminPanel').style.display='none'; el('adminLoginBox').style.display='block'; });
el('adminFetchUsers').addEventListener('click', async ()=>{
const r = await api('admin_list_users');
if(r.ok){
const wrap = el('adminUsers'); wrap.innerHTML='';
r.users.forEach(u=>{
const div = document.createElement('div');
div.style.display='flex'; div.style.justifyContent='space-between'; div.style.alignItems='center'; div.style.padding='6px';
div.innerHTML = `<div><strong>${escapeHtml(u.username)}</strong><div class="smalltxt">${escapeHtml(u.email)} ${u.verified?'<span style="color:lime">verified</span>':''} ${u.is_banned?'<span style="color:crimson">banned</span>':''}</div></div>
<div>
<button class="btn small" data-id="${u.id}" data-action="ban">${u.is_banned?'Unban':'Ban'}</button>
</div>`;
wrap.appendChild(div);
});
wrap.querySelectorAll('button[data-action="ban"]').forEach(b=>{
b.addEventListener('click', async ()=>{
const uid = b.dataset.id; const ban = b.textContent.trim() !== 'Unban';
await api('admin_ban_user','POST',{user_id:uid,ban: ban});
b.textContent = ban ? 'Unban' : 'Ban';
});
});
} else alert('Fehler');
});
async function escapeHtml(s){ return String(s).replace(/[&<>"']/g, function(m){ return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]; }); }
</script>
</body>
</html>
