Dieses Premium Script ist exklusiv für unsere Newsletter Abonnenten!
Gebe deine eMail Adresse zum kostenlosen Freischalten ein!
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>