Private
Public Access
1
0

bug fixes

This commit is contained in:
Sebastian Unterschütz
2025-11-26 18:56:59 +01:00
parent 6fdad68a9b
commit cf2e6e1c94
13 changed files with 392 additions and 144 deletions

View File

@@ -3,9 +3,11 @@ package main
import ( import (
"encoding/json" "encoding/json"
"html" "html"
"log"
"math/rand" "math/rand"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
@@ -63,7 +65,7 @@ func handleValidate(w http.ResponseWriter, r *http.Request) {
} }
// ---> HIER RUFEN WIR JETZT DIE SIMULATION AUF <--- // ---> HIER RUFEN WIR JETZT DIE SIMULATION AUF <---
isDead, score, obstacles := simulateChunk(req.SessionID, req.Inputs, req.TotalTicks, vals) isDead, score, obstacles, powerUpState, serverTick := simulateChunk(req.SessionID, req.Inputs, req.TotalTicks, vals)
status := "alive" status := "alive"
if isDead { if isDead {
@@ -76,6 +78,8 @@ func handleValidate(w http.ResponseWriter, r *http.Request) {
Status: status, Status: status,
VerifiedScore: score, VerifiedScore: score,
ServerObs: obstacles, ServerObs: obstacles,
PowerUps: powerUpState,
ServerTick: serverTick,
}) })
} }
@@ -86,8 +90,19 @@ func handleSubmitName(w http.ResponseWriter, r *http.Request) {
return return
} }
// Validierung
if len(req.Name) > 4 {
http.Error(w, "Zu lang", 400)
return
}
if containsBadWord(req.Name) {
http.Error(w, "Name verboten", 400)
return
}
safeName := html.EscapeString(req.Name) safeName := html.EscapeString(req.Name)
sessionKey := "session:" + req.SessionID sessionKey := "session:" + req.SessionID
scoreVal, err := rdb.HGet(ctx, sessionKey, "score").Result() scoreVal, err := rdb.HGet(ctx, sessionKey, "score").Result()
if err != nil { if err != nil {
http.Error(w, "Session expired", 404) http.Error(w, "Session expired", 404)
@@ -104,10 +119,13 @@ func handleSubmitName(w http.ResponseWriter, r *http.Request) {
"created_at": timestamp, "created_at": timestamp,
}) })
rdb.ZAdd(ctx, "leaderboard:unverified", redis.Z{ // Leaderboard Eintrag
Score: float64(scoreInt), rdb.ZAdd(ctx, "leaderboard:unverified", redis.Z{Score: float64(scoreInt), Member: req.SessionID})
Member: req.SessionID, rdb.ZAdd(ctx, "leaderboard:public", redis.Z{Score: float64(scoreInt), Member: req.SessionID})
})
rdb.Persist(ctx, sessionKey)
rdb.HDel(ctx, sessionKey, "obstacles", "rng_state", "pos_y", "vel_y", "p_god_lives", "p_has_bat", "p_boot_ticks")
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(SubmitResponse{ClaimCode: claimCode}) json.NewEncoder(w).Encode(SubmitResponse{ClaimCode: claimCode})
@@ -191,6 +209,9 @@ func handleAdminAction(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Bad Request", 400) http.Error(w, "Bad Request", 400)
return return
} }
log.Printf("👮 ADMIN ACTION: %s on %s", req.Action, req.SessionID)
if req.Action == "approve" { if req.Action == "approve" {
score, err := rdb.ZScore(ctx, "leaderboard:unverified", req.SessionID).Result() score, err := rdb.ZScore(ctx, "leaderboard:unverified", req.SessionID).Result()
if err == nil { if err == nil {
@@ -200,7 +221,10 @@ func handleAdminAction(w http.ResponseWriter, r *http.Request) {
} else if req.Action == "delete" { } else if req.Action == "delete" {
rdb.ZRem(ctx, "leaderboard:unverified", req.SessionID) rdb.ZRem(ctx, "leaderboard:unverified", req.SessionID)
rdb.ZRem(ctx, "leaderboard:public", req.SessionID) rdb.ZRem(ctx, "leaderboard:public", req.SessionID)
rdb.Del(ctx, "session:"+req.SessionID)
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
@@ -210,16 +234,28 @@ func handleClaimDelete(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Bad Request", 400) http.Error(w, "Bad Request", 400)
return return
} }
sessionKey := "session:" + req.SessionID sessionKey := "session:" + req.SessionID
realCode, err := rdb.HGet(ctx, sessionKey, "claim_code").Result() realCode, err := rdb.HGet(ctx, sessionKey, "claim_code").Result()
if err != nil || realCode != req.ClaimCode { if err != nil || realCode == "" {
http.Error(w, "Error", 403) http.Error(w, "Not found", 404)
return return
} }
if realCode != req.ClaimCode {
http.Error(w, "Wrong Code", 403)
return
}
log.Printf("🗑️ USER DELETE: Session %s deleted via code", req.SessionID)
// Aus Listen entfernen
rdb.ZRem(ctx, "leaderboard:unverified", req.SessionID) rdb.ZRem(ctx, "leaderboard:unverified", req.SessionID)
rdb.ZRem(ctx, "leaderboard:public", req.SessionID) rdb.ZRem(ctx, "leaderboard:public", req.SessionID)
rdb.HDel(ctx, sessionKey, "name")
rdb.Del(ctx, sessionKey)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
@@ -231,3 +267,37 @@ func generateClaimCode() string {
} }
return string(b) return string(b)
} }
func handleAdminBadwords(w http.ResponseWriter, r *http.Request) {
key := "config:badwords"
// GET: Liste abrufen
if r.Method == http.MethodGet {
words, _ := rdb.SMembers(ctx, key).Result()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(words)
return
}
// POST: Hinzufügen oder Löschen
if r.Method == http.MethodPost {
// Wir nutzen ein einfaches Struct für den Request
type WordReq struct {
Word string `json:"word"`
Action string `json:"action"` // "add" oder "remove"
}
var req WordReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Bad Request", 400)
return
}
if req.Action == "add" && req.Word != "" {
rdb.SAdd(ctx, key, strings.ToLower(req.Word))
} else if req.Action == "remove" && req.Word != "" {
rdb.SRem(ctx, key, strings.ToLower(req.Word))
}
w.WriteHeader(http.StatusOK)
}
}

View File

@@ -30,6 +30,7 @@ func main() {
} }
initGameConfig() initGameConfig()
initBadWords()
fs := http.FileServer(http.Dir("./static")) fs := http.FileServer(http.Dir("./static"))
http.Handle("/", fs) http.Handle("/", fs)
@@ -44,6 +45,7 @@ func main() {
// Admin Routes (Logger + BasicAuth kombinieren) // Admin Routes (Logger + BasicAuth kombinieren)
http.HandleFunc("/admin", Logger(BasicAuth(handleAdminPage))) http.HandleFunc("/admin", Logger(BasicAuth(handleAdminPage)))
http.HandleFunc("/api/admin/badwords", Logger(BasicAuth(handleAdminBadwords)))
http.HandleFunc("/api/admin/list", Logger(BasicAuth(handleAdminList))) http.HandleFunc("/api/admin/list", Logger(BasicAuth(handleAdminList)))
http.HandleFunc("/api/admin/action", Logger(BasicAuth(handleAdminAction))) http.HandleFunc("/api/admin/action", Logger(BasicAuth(handleAdminAction)))

View File

@@ -12,7 +12,7 @@
input[type="text"] { input[type="text"] {
padding: 10px; font-size: 16px; width: 250px; padding: 10px; font-size: 16px; width: 250px;
background: #333; border: 1px solid #666; color: white; background: #333; border: 1px solid #666; color: white; outline: none;
} }
/* Tabs */ /* Tabs */
@@ -22,14 +22,15 @@
} }
.tab-btn.active { background: #4caf50; color: white; } .tab-btn.active { background: #4caf50; color: white; }
.tab-btn#tab-public.active { background: #2196F3; } /* Blau für Public */ .tab-btn#tab-public.active { background: #2196F3; } /* Blau für Public */
.tab-btn#tab-badwords.active { background: #f44336; } /* Rot für Badwords */
/* Liste */ /* Normale Liste */
.entry { .entry {
background: #333; padding: 15px; margin-bottom: 8px; background: #333; padding: 15px; margin-bottom: 8px;
display: flex; justify-content: space-between; align-items: center; display: flex; justify-content: space-between; align-items: center;
border-left: 5px solid #555; border-left: 5px solid #555;
} }
.entry.highlight { border-left-color: #ffeb3b; background: #444; } /* Suchtreffer */ .entry.highlight { border-left-color: #ffeb3b; background: #444; }
.info { font-size: 1.1em; } .info { font-size: 1.1em; }
.meta { font-size: 0.85em; color: #aaa; margin-top: 4px; font-family: monospace; } .meta { font-size: 0.85em; color: #aaa; margin-top: 4px; font-family: monospace; }
@@ -37,10 +38,42 @@
/* Buttons */ /* Buttons */
button { cursor: pointer; padding: 8px 15px; border: none; font-weight: bold; color: white; border-radius: 4px; } button { cursor: pointer; padding: 8px 15px; border: none; font-weight: bold; color: white; border-radius: 4px; }
.btn-approve { background: #4caf50; margin-right: 5px; }
.btn-delete { background: #f44336; } .btn-delete { background: #f44336; }
.btn-delete:hover { background: #d32f2f; } .btn-delete:hover { background: #d32f2f; }
/* BADWORD STYLES (Tags) */
#badword-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
padding: 20px;
background: #333;
border: 1px solid #555;
}
.badword-tag {
background: #222;
padding: 8px 15px;
border-radius: 20px;
display: flex;
align-items: center;
gap: 10px;
border: 1px solid #f44336;
color: #ff8a80;
font-weight: bold;
}
.badword-remove {
cursor: pointer;
color: #f44336;
font-weight: bold;
background: rgba(255,255,255,0.1);
width: 20px; height: 20px;
display: flex; align-items: center; justify-content: center;
border-radius: 50%;
}
.badword-remove:hover { background: #f44336; color: white; }
.empty { text-align: center; padding: 40px; color: #666; font-style: italic; } .empty { text-align: center; padding: 40px; color: #666; font-style: italic; }
</style> </style>
</head> </head>
@@ -52,28 +85,43 @@
</div> </div>
<div class="tabs"> <div class="tabs">
<button id="tab-unverified" class="tab-btn active" onclick="switchTab('unverified')">⏳ Warteschlange (Prüfen)</button> <button id="tab-public" class="tab-btn active" onclick="switchTab('public')">🏆 Bestenliste</button>
<button id="tab-public" class="tab-btn" onclick="switchTab('public')">🏆 Bestenliste (Fertig)</button> <button id="tab-badwords" class="tab-btn" onclick="switchTab('badwords')">🤬 Badwords</button>
</div> </div>
<div id="list">Lade...</div> <div id="list-section">
<div id="list">Lade...</div>
</div>
<div id="badword-section" style="display:none;">
<div style="margin-bottom: 20px; display:flex; gap:10px;">
<input type="text" id="newBadword" placeholder="Neues Wort hinzufügen...">
<button onclick="addBadword()" style="background:#f44336; color:white; border:none; padding:10px 20px; cursor:pointer;">+ Hinzufügen</button>
</div>
<h3>Aktuelle Liste:</h3>
<div id="badword-container">
</div>
</div>
<script> <script>
let currentTab = 'unverified'; let currentTab = 'public';
let allEntries = []; // Speichert die geladenen Daten für die Suche let allEntries = [];
// Initial laden
loadList();
// --- FUNKTIONEN FÜR BESTENLISTE ---
async function loadList() { async function loadList() {
const listEl = document.getElementById('list'); const listEl = document.getElementById('list');
listEl.innerHTML = '<div style="text-align:center">Lade Daten...</div>'; listEl.innerHTML = '<div style="text-align:center">Lade Daten...</div>';
try { try {
// API Aufruf mit Typ (unverified oder public) const res = await fetch('/api/admin/list?type=public');
const res = await fetch('/api/admin/list?type=' + currentTab);
allEntries = await res.json(); allEntries = await res.json();
renderList(allEntries); renderList(allEntries);
} catch(e) { } catch(e) {
listEl.innerHTML = '<div class="empty">Fehler beim Laden der Daten.</div>'; listEl.innerHTML = '<div class="empty">Fehler beim Laden.</div>';
} }
} }
@@ -87,14 +135,6 @@
let html = ""; let html = "";
data.forEach(entry => { data.forEach(entry => {
// Buttons: In der "Public" Liste brauchen wir keinen "Freigeben" Button mehr
let actions = '';
if (currentTab === 'unverified') {
actions += `<button class="btn-approve" onclick="decide('${entry.sessionId}', 'approve')">✔ OK</button>`;
}
// Löschen darf man immer (falls man sich verklickt hat oder Schüler nervt)
actions += `<button class="btn-delete" onclick="decide('${entry.sessionId}', 'delete')">🗑 Weg</button>`;
html += ` html += `
<div class="entry"> <div class="entry">
<div class="info"> <div class="info">
@@ -105,65 +145,104 @@
</div> </div>
</div> </div>
<div class="actions"> <div class="actions">
${actions} <button class="btn-delete" onclick="decide('${entry.sessionId}', 'delete')">🗑 Löschen</button>
</div> </div>
</div>`; </div>`;
}); });
listEl.innerHTML = html; listEl.innerHTML = html;
filterList(); // Suche anwenden falls Text drin steht
// Filter direkt anwenden, falls noch Text in der Suche steht
filterList();
}
// Such-Logik (Client-Side, rasend schnell)
function filterList() {
const term = document.getElementById('searchInput').value.toUpperCase();
const entries = document.querySelectorAll('.entry');
entries.forEach(div => {
// Wir suchen im gesamten Text des Eintrags (Name, Code, Score)
const text = div.innerText.toUpperCase();
if (text.includes(term)) {
div.style.display = "flex";
// Kleines Highlight wenn gesucht wird
if(term.length > 0) div.classList.add("highlight");
else div.classList.remove("highlight");
} else {
div.style.display = "none";
}
});
} }
async function decide(sid, action) { async function decide(sid, action) {
if(!confirm(action === 'approve' ? "Freigeben?" : "Endgültig löschen?")) return; if(!confirm("Eintrag endgültig löschen?")) return;
await fetch('/api/admin/action', { await fetch('/api/admin/action', {
method: 'POST', method: 'POST', headers: {'Content-Type': 'application/json'},
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ sessionId: sid, action: action }) body: JSON.stringify({ sessionId: sid, action: action })
}); });
loadList(); // Neu laden
}
function switchTab(tab) {
currentTab = tab;
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.getElementById('tab-' + tab).classList.add('active');
document.getElementById('searchInput').value = "";
loadList(); loadList();
} }
// Start function filterList() {
loadList(); const term = document.getElementById('searchInput').value.toUpperCase();
const entries = document.querySelectorAll('.entry');
entries.forEach(div => {
if (div.innerText.toUpperCase().includes(term)) {
div.style.display = "flex";
if(term.length > 0) div.classList.add("highlight"); else div.classList.remove("highlight");
} else { div.style.display = "none"; }
});
}
setInterval(() => { // --- FUNKTIONEN FÜR BADWORDS ---
if(currentTab === 'unverified' && document.getElementById('searchInput').value === "") { async function loadBadwords() {
try {
const res = await fetch('/api/admin/badwords');
const words = await res.json(); // Array von Strings
const container = document.getElementById('badword-container');
container.innerHTML = '';
if (!words || words.length === 0) {
container.innerHTML = "<div class='empty'>Keine Badwords definiert.</div>";
return;
}
// Tags generieren
words.forEach(w => {
container.innerHTML += `
<div class="badword-tag">
${w}
<span class="badword-remove" onclick="removeBadword('${w}')" title="Löschen">✖</span>
</div>`;
});
} catch(e) {
document.getElementById('badword-container').innerHTML = "Fehler beim Laden.";
}
}
async function addBadword() {
const input = document.getElementById('newBadword');
const word = input.value.trim();
if(!word) return;
await fetch('/api/admin/badwords', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ action: 'add', word: word })
});
input.value = "";
loadBadwords();
}
async function removeBadword(word) {
if(!confirm(`Das Wort "${word}" von der Liste entfernen?`)) return;
await fetch('/api/admin/badwords', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ action: 'remove', word: word })
});
loadBadwords();
}
// --- TAB UMSCHALTEN ---
function switchTab(tab) {
currentTab = tab;
// Buttons stylen
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.getElementById('tab-' + tab).classList.add('active');
// Ansicht wechseln
if (tab === 'badwords') {
document.getElementById('list-section').style.display = 'none';
document.getElementById('badword-section').style.display = 'block';
document.getElementById('searchInput').style.display = 'none';
loadBadwords();
} else {
document.getElementById('list-section').style.display = 'block';
document.getElementById('badword-section').style.display = 'none';
document.getElementById('searchInput').style.display = 'block';
loadList(); loadList();
} }
}, 5000); }
</script> </script>
</body> </body>
</html> </html>

View File

@@ -8,21 +8,20 @@ import (
"strconv" "strconv"
) )
func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[string]string) (bool, int, []ActiveObstacle) { func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[string]string) (bool, int, []ActiveObstacle, PowerUpState, int) {
// --- 1. STATE LADEN --- // 1. State laden
posY := parseOr(vals["pos_y"], PlayerYBase) posY := parseOr(vals["pos_y"], PlayerYBase)
velY := parseOr(vals["vel_y"], 0.0) velY := parseOr(vals["vel_y"], 0.0)
score := int(parseOr(vals["score"], 0)) score := int(parseOr(vals["score"], 0))
ticksAlive := int(parseOr(vals["total_ticks"], 0)) // Zeit-Basis ticksAlive := int(parseOr(vals["total_ticks"], 0))
rngStateVal, _ := strconv.ParseInt(vals["rng_state"], 10, 64) rngStateVal, _ := strconv.ParseInt(vals["rng_state"], 10, 64)
// Powerups // Powerups laden
godLives := int(parseOr(vals["p_god_lives"], 0)) godLives := int(parseOr(vals["p_god_lives"], 0))
hasBat := vals["p_has_bat"] == "1" hasBat := vals["p_has_bat"] == "1"
bootTicks := int(parseOr(vals["p_boot_ticks"], 0)) bootTicks := int(parseOr(vals["p_boot_ticks"], 0))
// Anti-Cheat State laden (Wichtig für Heuristik über Chunks hinweg) // Anti-Cheat State
lastJumpDist := parseOr(vals["ac_last_dist"], 0.0) lastJumpDist := parseOr(vals["ac_last_dist"], 0.0)
suspicionScore := int(parseOr(vals["ac_suspicion"], 0)) suspicionScore := int(parseOr(vals["ac_suspicion"], 0))
@@ -35,11 +34,12 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
obstacles = []ActiveObstacle{} obstacles = []ActiveObstacle{}
} }
// DEBUG: Start des Chunks // --- DEBUG: Chunk Info ---
// log.Printf("[%s] Simulating Chunk: %d Ticks, Score: %d", sessionID, totalTicks, score) if len(inputs) > 0 {
log.Printf("📦 [%s] Processing Chunk: %d Ticks, %d Inputs", sessionID, totalTicks, len(inputs))
}
// --- ANTI-CHEAT 1: SPAM SCHUTZ --- // --- ANTI-CHEAT: Spam Check ---
// Wer mehr als 10x pro Sekunde springt, ist ein Bot oder nutzt ein Makro
jumpCount := 0 jumpCount := 0
for _, inp := range inputs { for _, inp := range inputs {
if inp.Act == "JUMP" { if inp.Act == "JUMP" {
@@ -47,8 +47,8 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
} }
} }
if jumpCount > 10 { if jumpCount > 10 {
log.Printf("🤖BOT-ALARM [%s]: Spammt Sprünge (%d Inputs)", sessionID, jumpCount) log.Printf("🤖 BOT-ALARM [%s]: Spammt Sprünge (%d)", sessionID, jumpCount)
return true, score, obstacles // Sofort tot return true, score, obstacles, PowerUpState{}, ticksAlive
} }
playerDead := false playerDead := false
@@ -57,24 +57,26 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
for i := 0; i < totalTicks; i++ { for i := 0; i < totalTicks; i++ {
ticksAlive++ ticksAlive++
// Speed Scaling (Zeitbasiert) // 1. Speed (Zeitbasiert)
currentSpeed := BaseSpeed + (float64(ticksAlive)/3000.0)*0.5 currentSpeed := BaseSpeed + (float64(ticksAlive)/3000.0)*0.5
if currentSpeed > 20.0 { if currentSpeed > 12.0 {
currentSpeed = 20.0 currentSpeed = 12.0
} }
// Jump Power (Boots Powerup) // 2. Powerups Timer
currentJumpPower := JumpPower currentJumpPower := JumpPower
if bootTicks > 0 { if bootTicks > 0 {
currentJumpPower = HighJumpPower currentJumpPower = HighJumpPower
bootTicks-- bootTicks--
} }
// Input // 3. Input Verarbeitung (MIT DEBUG LOG)
didJump := false didJump := false
isCrouching := false isCrouching := false
for _, inp := range inputs { for _, inp := range inputs {
if inp.Tick == i { if inp.Tick == i {
log.Printf("🕹️ [%s] ACTION at Tick %d: %s", sessionID, ticksAlive, inp.Act)
if inp.Act == "JUMP" { if inp.Act == "JUMP" {
didJump = true didJump = true
} }
@@ -84,47 +86,38 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
} }
} }
// Physik Status // 4. Physik
isGrounded := posY >= PlayerYBase-1.0 isGrounded := posY >= PlayerYBase-1.0
currentHeight := PlayerHeight currentHeight := PlayerHeight
if isCrouching { if isCrouching {
currentHeight = PlayerHeight / 2 currentHeight = PlayerHeight / 2
if !isGrounded { if !isGrounded {
velY += 2.0 velY += 2.0
} // Fast fall }
} }
// Springen & ANTI-CHEAT 2 (Heuristik)
if didJump && isGrounded && !isCrouching { if didJump && isGrounded && !isCrouching {
velY = currentJumpPower velY = currentJumpPower
// Wir messen den Abstand zum nächsten Hindernis beim Absprung // Heuristik Anti-Cheat (Abstand messen)
var distToObs float64 = -1.0 var distToObs float64 = -1.0
for _, o := range obstacles { for _, o := range obstacles {
if o.X > 50.0 { // Das nächste Hindernis vor uns if o.X > 50.0 {
distToObs = o.X - 50.0 distToObs = o.X - 50.0
break break
} }
} }
// Bot Check: Wenn der Abstand IMMER gleich ist (z.B. exakt 75.5px)
if distToObs > 0 { if distToObs > 0 {
diff := math.Abs(distToObs - lastJumpDist) diff := math.Abs(distToObs - lastJumpDist)
if diff < 1.0 { if diff < 1.0 {
// Verdächtig perfekt wiederholt
suspicionScore++ suspicionScore++
} else { } else if suspicionScore > 0 {
// Menschliche Varianz -> Verdacht senken suspicionScore--
if suspicionScore > 0 {
suspicionScore--
}
} }
lastJumpDist = distToObs lastJumpDist = distToObs
} }
} }
// Physik Anwendung
velY += Gravity velY += Gravity
posY += velY posY += velY
if posY > PlayerYBase { if posY > PlayerYBase {
@@ -137,7 +130,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
hitboxY = posY + (PlayerHeight - currentHeight) hitboxY = posY + (PlayerHeight - currentHeight)
} }
// Hindernisse bewegen & Kollision // 5. Hindernisse & Kollision
nextObstacles := []ActiveObstacle{} nextObstacles := []ActiveObstacle{}
rightmostX := 0.0 rightmostX := 0.0
@@ -148,7 +141,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
continue continue
} }
// Passed Check (Verhindert "Geister-Kollision" von hinten) // Passed Check
paddingX := 10.0 paddingX := 10.0
if obs.X+obs.Width-paddingX < 55.0 { if obs.X+obs.Width-paddingX < 55.0 {
nextObstacles = append(nextObstacles, obs) nextObstacles = append(nextObstacles, obs)
@@ -186,7 +179,6 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
} }
continue continue
} else { } else {
// Kollision mit Gegner
if hasBat && obs.Type == "teacher" { if hasBat && obs.Type == "teacher" {
hasBat = false hasBat = false
log.Printf("[%s] ⚾ Bat used on %s", sessionID, obs.ID) log.Printf("[%s] ⚾ Bat used on %s", sessionID, obs.ID)
@@ -194,10 +186,9 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
} }
if godLives > 0 { if godLives > 0 {
godLives-- godLives--
log.Printf("[%s] 🛡️ Godmode saved life (%d left)", sessionID, godLives) log.Printf("[%s] 🛡️ Godmode saved life", sessionID)
continue continue
} }
log.Printf("💀 DEATH [%s]: Hit %s at Tick %d", sessionID, obs.ID, ticksAlive) log.Printf("💀 DEATH [%s]: Hit %s at Tick %d", sessionID, obs.ID, ticksAlive)
playerDead = true playerDead = true
} }
@@ -210,7 +201,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
} }
obstacles = nextObstacles obstacles = nextObstacles
// Spawning // 6. Spawning
if rightmostX < GameWidth-10.0 { if rightmostX < GameWidth-10.0 {
gap := float64(int(400.0 + rng.NextRange(0, 500))) gap := float64(int(400.0 + rng.NextRange(0, 500)))
spawnX := rightmostX + gap spawnX := rightmostX + gap
@@ -252,7 +243,12 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
if def != nil { if def != nil {
spawnY := GroundY - def.Height - def.YOffset spawnY := GroundY - def.Height - def.YOffset
obstacles = append(obstacles, ActiveObstacle{ obstacles = append(obstacles, ActiveObstacle{
ID: def.ID, Type: def.Type, X: spawnX, Y: spawnY, Width: def.Width, Height: def.Height, ID: def.ID,
Type: def.Type,
X: spawnX,
Y: spawnY,
Width: def.Width,
Height: def.Height,
}) })
} }
} }
@@ -265,13 +261,11 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
} }
} }
// --- ANTI-CHEAT CHECK (Ergebnis) ---
if suspicionScore > 10 { if suspicionScore > 10 {
log.Printf("🤖BOT-ALARM [%s]: Zu perfekte Sprünge (Heuristik Fail)", sessionID) log.Printf("🤖 BOT-ALARM [%s]: Zu perfekte Sprünge (Heuristik)", sessionID)
playerDead = true playerDead = true
} }
// --- SPEICHERN ---
obsJson, _ := json.Marshal(obstacles) obsJson, _ := json.Marshal(obstacles)
batStr := "0" batStr := "0"
if hasBat { if hasBat {
@@ -288,12 +282,18 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
"p_god_lives": godLives, "p_god_lives": godLives,
"p_has_bat": batStr, "p_has_bat": batStr,
"p_boot_ticks": bootTicks, "p_boot_ticks": bootTicks,
// AC Daten speichern für nächsten Chunk
"ac_last_dist": fmt.Sprintf("%f", lastJumpDist), "ac_last_dist": fmt.Sprintf("%f", lastJumpDist),
"ac_suspicion": suspicionScore, "ac_suspicion": suspicionScore,
}) })
return playerDead, score, obstacles // Return PowerUp State
pState := PowerUpState{
GodLives: godLives,
HasBat: hasBat,
BootTicks: bootTicks,
}
return playerDead, score, obstacles, pState, ticksAlive
} }
func parseOr(s string, def float64) float64 { func parseOr(s string, def float64) float64 {

View File

@@ -69,7 +69,7 @@
<p>Dein Score: <span id="finalScore" style="color:yellow; font-size: 24px;">0</span></p> <p>Dein Score: <span id="finalScore" style="color:yellow; font-size: 24px;">0</span></p>
<div id="inputSection"> <div id="inputSection">
<input type="text" id="playerNameInput" placeholder="Dein Name" maxlength="10"> <input type="text" id="playerNameInput" placeholder="NAME" maxlength="4" style="text-transform:uppercase;">
<button id="submitBtn" onclick="submitScore()">EINTRAGEN</button> <button id="submitBtn" onclick="submitScore()">EINTRAGEN</button>
</div> </div>

View File

@@ -10,7 +10,7 @@ const CHUNK_SIZE = 60;
const TARGET_FPS = 60; const TARGET_FPS = 60;
const MS_PER_TICK = 1000 / TARGET_FPS; const MS_PER_TICK = 1000 / TARGET_FPS;
const DEBUG_SYNC = true; const DEBUG_SYNC = false;
const SYNC_TOLERANCE = 5.0; const SYNC_TOLERANCE = 5.0;
// RNG Klasse // RNG Klasse

View File

@@ -53,7 +53,9 @@ function updateGameLogic() {
else if (obs.def.type === "powerup") { else if (obs.def.type === "powerup") {
if (obs.def.id === "p_god") godModeLives = 3; if (obs.def.id === "p_god") godModeLives = 3;
if (obs.def.id === "p_bat") hasBat = true; if (obs.def.id === "p_bat") hasBat = true;
if (obs.def.id === "p_boot") bootTicks = 600; // ca. 10 Sekunden if (obs.def.id === "p_boot") bootTicks = 600;
lastPowerupTick = currentTick;
continue; // Entfernen (Eingesammelt) continue; // Entfernen (Eingesammelt)
} }
// C. GEGNER / HINDERNIS // C. GEGNER / HINDERNIS

View File

@@ -50,7 +50,7 @@ window.startGameClick = async function() {
sessionID = sData.sessionId; sessionID = sData.sessionId;
rng = new PseudoRNG(sData.seed); rng = new PseudoRNG(sData.seed);
isGameRunning = true; isGameRunning = true;
// Wir resetten die Zeit, damit es keinen Sprung gibt maxRawBgIndex = 0;
lastTime = performance.now(); lastTime = performance.now();
resize(); resize();
} catch(e) { } catch(e) {
@@ -100,32 +100,48 @@ window.submitScore = async function() {
// ========================================== // ==========================================
window.showMyCodes = function() { window.showMyCodes = function() {
if(window.openModal) window.openModal('codes'); if(window.openModal) window.openModal('codes');
const listEl = document.getElementById('codesList'); const listEl = document.getElementById('codesList');
if(!listEl) return; if(!listEl) return;
const claims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
if (claims.length === 0) {
const rawClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
if (rawClaims.length === 0) {
listEl.innerHTML = "<div style='padding:10px; text-align:center; color:#666;'>Keine Codes gespeichert.</div>"; listEl.innerHTML = "<div style='padding:10px; text-align:center; color:#666;'>Keine Codes gespeichert.</div>";
return; return;
} }
const sortedClaims = rawClaims
.map((item, index) => ({ ...item, originalIndex: index }))
.sort((a, b) => b.score - a.score);
let html = ""; let html = "";
for (let i = claims.length - 1; i >= 0; i--) {
const c = claims[i];
sortedClaims.forEach(c => {
const canDelete = c.sessionId ? true : false; const canDelete = c.sessionId ? true : false;
const btnStyle = canDelete ? "cursor:pointer; color:#ff4444; border-color:#ff4444;" : "cursor:not-allowed; color:gray; border-color:gray;"; const btnStyle = canDelete ? "cursor:pointer; color:#ff4444; border-color:#ff4444;" : "cursor:not-allowed; color:gray; border-color:gray;";
const btnAttr = canDelete ? `onclick="deleteClaim(${i}, '${c.sessionId}', '${c.code}')"` : "disabled"; const btnAttr = canDelete ? `onclick="deleteClaim(${c.originalIndex}, '${c.sessionId}', '${c.code}')"` : "disabled";
let rankIcon = "📄";
if (c.score >= 10000) rankIcon = "🔥";
if (c.score >= 5000) rankIcon = "⭐";
html += ` html += `
<div style="border-bottom:1px solid #444; padding:8px 0; display:flex; justify-content:space-between; align-items:center;"> <div style="border-bottom:1px solid #444; padding:8px 0; display:flex; justify-content:space-between; align-items:center;">
<div style="text-align:left;"> <div style="text-align:left;">
<span style="color:#00e5ff; font-weight:bold; font-size:12px;">${c.code}</span> <span style="color:#00e5ff; font-weight:bold; font-size:12px;">${rankIcon} ${c.code}</span>
<span style="color:#ffcc00;">(${c.score} Pkt)</span><br> <span style="color:#ffcc00; font-weight:bold;">(${c.score} Pkt)</span><br>
<span style="color:#aaa; font-size:9px;">${c.name}${c.date}</span> <span style="color:#aaa; font-size:9px;">${c.name}${c.date}</span>
</div> </div>
<button ${btnAttr} style="background:transparent; border:1px solid; padding:5px; font-size:9px; margin:0; ${btnStyle}">LÖSCHEN</button> <button ${btnAttr}
style="background:transparent; border:1px solid; padding:5px; font-size:9px; margin:0; ${btnStyle}">
LÖSCHEN
</button>
</div>`; </div>`;
} });
listEl.innerHTML = html; listEl.innerHTML = html;
}; };
@@ -150,13 +166,38 @@ async function loadLeaderboard() {
try { try {
const res = await fetch(`/api/leaderboard?sessionId=${sessionID}`); const res = await fetch(`/api/leaderboard?sessionId=${sessionID}`);
const entries = await res.json(); const entries = await res.json();
let html = "<h3>BESTENLISTE</h3>";
let html = "<h3 style='margin-bottom:5px'>BESTENLISTE</h3>";
entries.forEach(e => { entries.forEach(e => {
const color = e.isMe ? "yellow" : "white"; const color = e.isMe ? "yellow" : "white";
html += `<div style="display:flex; justify-content:space-between; color:${color}; margin-bottom:5px;"><span>#${e.rank} ${e.name}</span><span>${Math.floor(e.score/10)}</span></div>`; const bgStyle = e.isMe ? "background:rgba(255,255,0,0.1);" : "";
const betterThanMe = e.rank - 1;
let infoText = "";
if (e.isMe && betterThanMe > 0) {
infoText = `<div style='font-size:8px; color:#aaa;'>(${betterThanMe} waren besser)</div>`;
} else if (e.isMe && betterThanMe === 0) {
infoText = `<div style='font-size:8px; color:#ffcc00;'>👑 NIEMAND ist besser!</div>`;
}
html += `
<div style="border-bottom:1px dotted #444; padding:5px; ${bgStyle} margin-bottom:2px;">
<div style="display:flex; justify-content:space-between; color:${color};">
<span>#${e.rank} ${e.name.toUpperCase()}</span>
<span>${Math.floor(e.score/10)}</span>
</div>
${infoText}
</div>`;
if(e.rank === 3 && entries.length > 3 && !entries[3].isMe) {
html += "<div style='text-align:center; color:gray; font-size:8px;'>...</div>";
}
}); });
document.getElementById('leaderboard').innerHTML = html; document.getElementById('leaderboard').innerHTML = html;
} catch(e) {} } catch(e) { console.error(e); }
} }
async function loadStartScreenLeaderboard() { async function loadStartScreenLeaderboard() {

View File

@@ -27,11 +27,21 @@ async function sendChunk() {
if (data.serverObs) { if (data.serverObs) {
serverObstacles = data.serverObs; serverObstacles = data.serverObs;
// --- NEU: DEBUG MODUS VERGLEICH ---
if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) { if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) {
compareState(snapshotobstacles, data.serverObs); compareState(snapshotobstacles, data.serverObs);
} }
// ----------------------------------
if (data.powerups) {
const sTick = data.serverTick;
if (lastPowerupTick > sTick) {
} else {
godModeLives = data.powerups.godLives;
hasBat = data.powerups.hasBat;
bootTicks = data.powerups.bootTicks;
}
}
} }
if (data.status === "dead") { if (data.status === "dead") {

View File

@@ -44,29 +44,29 @@ resize();
// --- DRAWING --- // --- DRAWING ---
function drawGame() { function drawGame() {
// 1. Alles löschen
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT); ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
// --- HINTERGRUND (Level-Wechsel) ---
let currentBg = null; let currentBg = null;
// Haben wir Hintergründe geladen?
if (bgSprites.length > 0) { if (bgSprites.length > 0) {
// Wechsel alle 2000 Punkte (Server-Score) = 200 Punkte (Anzeige)
const changeInterval = 10000; const changeInterval = 10000;
// Berechne Index: 0-1999 -> 0, 2000-3999 -> 1, etc. const currentRawIndex = Math.floor(score / changeInterval);
// Das % (Modulo) sorgt dafür, dass es wieder von vorne anfängt, wenn die Bilder ausgehen
const bgIndex = Math.floor(score / changeInterval) % bgSprites.length; if (currentRawIndex > maxRawBgIndex) {
maxRawBgIndex = currentRawIndex;
}
const bgIndex = maxRawBgIndex % bgSprites.length;
currentBg = bgSprites[bgIndex]; currentBg = bgSprites[bgIndex];
} }
if (currentBg && currentBg.complete && currentBg.naturalHeight !== 0) { if (currentBg && currentBg.complete && currentBg.naturalHeight !== 0) {
ctx.drawImage(currentBg, 0, 0, GAME_WIDTH, GAME_HEIGHT); ctx.drawImage(currentBg, 0, 0, GAME_WIDTH, GAME_HEIGHT);
} else { } else {
// Fallback: Hellgrau, falls Bild fehlt // Fallback
ctx.fillStyle = "#f0f0f0"; ctx.fillStyle = "#f0f0f0";
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT); ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
} }
@@ -127,7 +127,7 @@ function drawGame() {
if(bootTicks > 0) statusText += `👟 ${(bootTicks/60).toFixed(1)}s`; if(bootTicks > 0) statusText += `👟 ${(bootTicks/60).toFixed(1)}s`;
// Drift Info (nur wenn Objekte da sind) // Drift Info (nur wenn Objekte da sind)
if (obstacles.length > 0 && serverObstacles.length > 0) { if (DEBUG_SYNC == true && length > 0 && serverObstacles.length > 0) {
const drift = Math.abs(obstacles[0].x - serverObstacles[0].x).toFixed(1); const drift = Math.abs(obstacles[0].x - serverObstacles[0].x).toFixed(1);
statusText += ` | Drift: ${drift}px`; // Einkommentieren für Debugging statusText += ` | Drift: ${drift}px`; // Einkommentieren für Debugging
} }

View File

@@ -19,10 +19,12 @@ let bootTicks = 0;
// Hintergrund // Hintergrund
let currentBgIndex = 0; let currentBgIndex = 0;
let maxRawBgIndex = 0;
// Tick Time // Tick Time
let lastTime = 0; let lastTime = 0;
let accumulator = 0; let accumulator = 0;
let lastPowerupTick = -9999;
// Grafiken // Grafiken
let sprites = {}; let sprites = {};

View File

@@ -39,11 +39,18 @@ type ValidateRequest struct {
TotalTicks int `json:"totalTicks"` TotalTicks int `json:"totalTicks"`
} }
type PowerUpState struct {
GodLives int `json:"godLives"`
HasBat bool `json:"hasBat"`
BootTicks int `json:"bootTicks"`
}
type ValidateResponse struct { type ValidateResponse struct {
Status string `json:"status"` Status string `json:"status"`
VerifiedScore int `json:"verifiedScore"` VerifiedScore int `json:"verifiedScore"`
ServerObs []ActiveObstacle `json:"serverObs"` ServerObs []ActiveObstacle `json:"serverObs"`
ActivePowerup string `json:"activePowerup"` PowerUps PowerUpState `json:"powerups"`
ServerTick int `json:"serverTick"`
} }
type StartResponse struct { type StartResponse struct {

35
utils.go Normal file
View File

@@ -0,0 +1,35 @@
package main
import (
"log"
"strings"
)
func initBadWords() {
key := "config:badwords"
count, _ := rdb.SCard(ctx, key).Result()
if count == 0 {
log.Println("Lade Default Badwords...")
defaults := []string{"admin", "root", "hitler", "nazi", "sex", "fuck", "shit", "ass"}
for _, w := range defaults {
rdb.SAdd(ctx, key, w)
}
}
}
func containsBadWord(name string) bool {
badwords, _ := rdb.SMembers(ctx, "config:badwords").Result()
lowerName := strings.ToLower(name)
for _, word := range badwords {
if word == "" {
continue
}
if strings.Contains(lowerName, strings.ToLower(word)) {
return true
}
}
return false
}