Private
Public Access
1
0

add a lot debug
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 55s

This commit is contained in:
Sebastian Unterschütz
2025-11-24 23:23:43 +01:00
parent 613a37ec09
commit 7f66e4ca1b

181
main.go
View File

@@ -16,16 +16,23 @@ import (
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
) )
// ==========================================
// 1. KONSTANTEN
// ==========================================
const ( const (
Gravity = 0.6 Gravity = 0.6
JumpPower = -12.0 JumpPower = -12.0
GroundY = 350.0 GroundY = 350.0
PlayerHeight = 50.0 PlayerHeight = 50.0
PlayerYBase = GroundY - PlayerHeight PlayerYBase = GroundY - PlayerHeight // 300.0
GameSpeed = 5.0 GameSpeed = 5.0
GameWidth = 800.0 GameWidth = 800.0
) )
// ==========================================
// 2. GLOBALE VARIABLEN
// ==========================================
var ( var (
ctx = context.Background() ctx = context.Background()
rdb *redis.Client rdb *redis.Client
@@ -34,6 +41,10 @@ var (
adminPass string adminPass string
) )
// ==========================================
// 3. STRUCTS
// ==========================================
type ObstacleDef struct { type ObstacleDef struct {
ID string `json:"id"` ID string `json:"id"`
Width float64 `json:"width"` Width float64 `json:"width"`
@@ -114,11 +125,16 @@ type ClaimDeleteRequest struct {
ClaimCode string `json:"claimCode"` ClaimCode string `json:"claimCode"`
} }
// ==========================================
// 4. PSEUDO RNG
// ==========================================
type PseudoRNG struct { type PseudoRNG struct {
State uint32 State uint32
} }
func NewRNG(seed int64) *PseudoRNG { func NewRNG(seed int64) *PseudoRNG {
// DEBUG: RNG Init
// log.Printf("[RNG] Init mit Seed: %d", seed)
return &PseudoRNG{State: uint32(seed)} return &PseudoRNG{State: uint32(seed)}
} }
@@ -140,6 +156,9 @@ func (r *PseudoRNG) PickDef(defs []ObstacleDef) *ObstacleDef {
return &defs[idx] return &defs[idx]
} }
// ==========================================
// 5. HELPER
// ==========================================
func getEnv(key, fallback string) string { func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok { if value, ok := os.LookupEnv(key); ok {
return value return value
@@ -147,6 +166,27 @@ func getEnv(key, fallback string) string {
return fallback return fallback
} }
func parseOr(s string, def float64) float64 {
if s == "" {
return def
}
v, err := strconv.ParseFloat(s, 64)
if err != nil {
return def
}
return v
}
func generateClaimCode() string {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, 8)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}
// Middleware für Basic Auth
func BasicAuth(next http.HandlerFunc) http.HandlerFunc { func BasicAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth() user, pass, ok := r.BasicAuth()
@@ -159,45 +199,60 @@ func BasicAuth(next http.HandlerFunc) http.HandlerFunc {
} }
} }
// Middleware für Request Logging (DAMIT DU SIEHST OB REQUESTS ANKOMMEN)
func LogRequest(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("➡ [REQ] %s %s von %s", r.Method, r.URL.Path, r.RemoteAddr)
next(w, r)
log.Printf("⬅ [RES] %s %s fertig in %v", r.Method, r.URL.Path, time.Since(start))
}
}
// ==========================================
// 6. MAIN
// ==========================================
func main() { func main() {
// Config laden
redisAddr := getEnv("REDIS_ADDR", "localhost:6379") redisAddr := getEnv("REDIS_ADDR", "localhost:6379")
adminUser = getEnv("ADMIN_USER", "lehrer") adminUser = getEnv("ADMIN_USER", "lehrer")
adminPass = getEnv("ADMIN_PASS", "geheim123") adminPass = getEnv("ADMIN_PASS", "geheim123")
log.Printf("🔧 CONFIG: Redis=%s | AdminUser=%s", redisAddr, adminUser)
// Redis verbinden
log.Println("🔌 Verbinde zu Redis...")
rdb = redis.NewClient(&redis.Options{Addr: redisAddr}) rdb = redis.NewClient(&redis.Options{Addr: redisAddr})
if _, err := rdb.Ping(ctx).Result(); err != nil {
log.Fatal(err) pong, err := rdb.Ping(ctx).Result()
if err != nil {
log.Fatalf("❌ REDIS ERROR: %v", err)
} }
log.Printf("✅ Redis verbunden: %s", pong)
initGameConfig() initGameConfig()
// File Server
fs := http.FileServer(http.Dir("./static")) fs := http.FileServer(http.Dir("./static"))
http.Handle("/", fs) http.Handle("/", fs)
http.HandleFunc("/api/config", handleConfig) // API Routes (mit Logging Wrapper)
http.HandleFunc("/api/start", handleStart) http.HandleFunc("/api/config", LogRequest(handleConfig))
http.HandleFunc("/api/validate", handleValidate) http.HandleFunc("/api/start", LogRequest(handleStart))
http.HandleFunc("/api/submit-name", handleSubmitName) http.HandleFunc("/api/validate", LogRequest(handleValidate)) // Hier passiert die Magie
http.HandleFunc("/api/leaderboard", handleLeaderboard) http.HandleFunc("/api/submit-name", LogRequest(handleSubmitName))
http.HandleFunc("/api/claim/delete", handleClaimDelete) http.HandleFunc("/api/leaderboard", LogRequest(handleLeaderboard))
http.HandleFunc("/api/claim/delete", LogRequest(handleClaimDelete))
// Admin Routes
http.HandleFunc("/admin", BasicAuth(handleAdminPage)) http.HandleFunc("/admin", BasicAuth(handleAdminPage))
http.HandleFunc("/api/admin/list", BasicAuth(handleAdminList)) http.HandleFunc("/api/admin/list", BasicAuth(handleAdminList))
http.HandleFunc("/api/admin/action", BasicAuth(handleAdminAction)) http.HandleFunc("/api/admin/action", BasicAuth(handleAdminAction))
log.Println("Server läuft auf Port 8080") log.Println("🦖 Server läuft auf Port 8080")
log.Fatal(http.ListenAndServe(":8080", nil)) log.Fatal(http.ListenAndServe(":8080", nil))
} }
func generateClaimCode() string {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, 8)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}
func initGameConfig() { func initGameConfig() {
defaultConfig = GameConfig{ defaultConfig = GameConfig{
Obstacles: []ObstacleDef{ Obstacles: []ObstacleDef{
@@ -206,10 +261,15 @@ func initGameConfig() {
{ID: "trashcan", Width: 25, Height: 35, Color: "#555", Image: "trash.png"}, {ID: "trashcan", Width: 25, Height: 35, Color: "#555", Image: "trash.png"},
{ID: "eraser", Width: 30, Height: 20, Color: "#fff", Image: "eraser.png", YOffset: 45.0}, {ID: "eraser", Width: 30, Height: 20, Color: "#fff", Image: "eraser.png", YOffset: 45.0},
}, },
Backgrounds: []string{"background.jpg"}, Backgrounds: []string{"background.png"},
} }
log.Println("✅ Game Config initialisiert")
} }
// ==========================================
// 7. HANDLER (MIT DEBUG LOGS)
// ==========================================
func handleConfig(w http.ResponseWriter, r *http.Request) { func handleConfig(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(defaultConfig) json.NewEncoder(w).Encode(defaultConfig)
@@ -217,25 +277,34 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
func handleStart(w http.ResponseWriter, r *http.Request) { func handleStart(w http.ResponseWriter, r *http.Request) {
sessionID := uuid.New().String() sessionID := uuid.New().String()
// Seed erstellen (32-Bit sicher)
rawSeed := time.Now().UnixNano() rawSeed := time.Now().UnixNano()
seed32 := uint32(rawSeed) seed32 := uint32(rawSeed)
// Initiale Hindernisse (leer)
emptyObs, _ := json.Marshal([]ActiveObstacle{}) emptyObs, _ := json.Marshal([]ActiveObstacle{})
log.Printf("🆕 START SESSION: %s | Seed: %d", sessionID, seed32)
// In Redis speichern
err := rdb.HSet(ctx, "session:"+sessionID, map[string]interface{}{ err := rdb.HSet(ctx, "session:"+sessionID, map[string]interface{}{
"seed": seed32, "seed": seed32,
"rng_state": seed32, "rng_state": seed32,
"score": 0, "score": 0,
"is_dead": 0, "is_dead": 0,
"pos_y": PlayerYBase, "pos_y": PlayerYBase, // WICHTIG: Start auf dem Boden (300.0)
"vel_y": 0.0, "vel_y": 0.0,
"obstacles": string(emptyObs), "obstacles": string(emptyObs),
}).Err() }).Err()
if err != nil { if err != nil {
log.Printf("❌ REDIS WRITE ERROR bei Start: %v", err)
http.Error(w, "DB Error", 500) http.Error(w, "DB Error", 500)
return return
} }
// Expire setzen
rdb.Expire(ctx, "session:"+sessionID, 1*time.Hour) rdb.Expire(ctx, "session:"+sessionID, 1*time.Hour)
json.NewEncoder(w).Encode(StartResponse{SessionID: sessionID, Seed: seed32}) json.NewEncoder(w).Encode(StartResponse{SessionID: sessionID, Seed: seed32})
@@ -244,29 +313,38 @@ func handleStart(w http.ResponseWriter, r *http.Request) {
func handleValidate(w http.ResponseWriter, r *http.Request) { func handleValidate(w http.ResponseWriter, r *http.Request) {
var req ValidateRequest var req ValidateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
log.Printf("⚠️ Validate Decode Error: %v", err)
http.Error(w, "Bad Request", 400) http.Error(w, "Bad Request", 400)
return return
} }
key := "session:" + req.SessionID key := "session:" + req.SessionID
// Alle Daten holen
vals, err := rdb.HGetAll(ctx, key).Result() vals, err := rdb.HGetAll(ctx, key).Result()
if err != nil || len(vals) == 0 { if err != nil || len(vals) == 0 {
log.Printf("⚠️ Session nicht gefunden oder leer: %s", req.SessionID)
http.Error(w, "Session invalid", 401) http.Error(w, "Session invalid", 401)
return return
} }
// Check ob schon tot
if vals["is_dead"] == "1" { if vals["is_dead"] == "1" {
// log.Printf("💀 Validierung abgelehnt: Spieler %s ist bereits tot.", req.SessionID)
json.NewEncoder(w).Encode(ValidateResponse{Status: "dead", VerifiedScore: 0}) json.NewEncoder(w).Encode(ValidateResponse{Status: "dead", VerifiedScore: 0})
return return
} }
// State parsen
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))
rngStateVal, _ := strconv.ParseInt(vals["rng_state"], 10, 64) rngStateVal, _ := strconv.ParseInt(vals["rng_state"], 10, 64)
// RNG initialisieren
rng := NewRNG(rngStateVal) rng := NewRNG(rngStateVal)
// Hindernisse laden
var obstacles []ActiveObstacle var obstacles []ActiveObstacle
if val, ok := vals["obstacles"]; ok && val != "" { if val, ok := vals["obstacles"]; ok && val != "" {
json.Unmarshal([]byte(val), &obstacles) json.Unmarshal([]byte(val), &obstacles)
@@ -274,12 +352,16 @@ func handleValidate(w http.ResponseWriter, r *http.Request) {
obstacles = []ActiveObstacle{} obstacles = []ActiveObstacle{}
} }
// log.Printf("🎮 SIMULATION START (%s) | Ticks: %d | PosY: %.2f | Obstacles: %d", req.SessionID, req.TotalTicks, posY, len(obstacles))
playerDead := false playerDead := false
// --- SIMULATION LOOP ---
for i := 0; i < req.TotalTicks; i++ { for i := 0; i < req.TotalTicks; i++ {
// A. INPUT
didJump := false didJump := false
isCrouching := false isCrouching := false
for _, inp := range req.Inputs { for _, inp := range req.Inputs {
if inp.Tick == i { if inp.Tick == i {
if inp.Act == "JUMP" { if inp.Act == "JUMP" {
@@ -291,6 +373,8 @@ func handleValidate(w http.ResponseWriter, r *http.Request) {
} }
} }
// B. PHYSIK
// Toleranz 1.0px
isGrounded := posY >= PlayerYBase-1.0 isGrounded := posY >= PlayerYBase-1.0
currentHeight := PlayerHeight currentHeight := PlayerHeight
@@ -298,7 +382,7 @@ func handleValidate(w http.ResponseWriter, r *http.Request) {
currentHeight = PlayerHeight / 2 currentHeight = PlayerHeight / 2
if !isGrounded { if !isGrounded {
velY += 2.0 velY += 2.0
} } // Fast fall
} }
if didJump && isGrounded && !isCrouching { if didJump && isGrounded && !isCrouching {
@@ -313,21 +397,26 @@ func handleValidate(w http.ResponseWriter, r *http.Request) {
velY = 0 velY = 0
} }
// Hitbox Y berechnen
hitboxY := posY hitboxY := posY
if isCrouching { if isCrouching {
hitboxY = posY + (PlayerHeight - currentHeight) hitboxY = posY + (PlayerHeight - currentHeight)
} }
// C. OBSTACLES & KOLLISION
nextObstacles := []ActiveObstacle{} nextObstacles := []ActiveObstacle{}
rightmostX := 0.0 rightmostX := 0.0
for _, obs := range obstacles { for _, obs := range obstacles {
obs.X -= GameSpeed obs.X -= GameSpeed
// Ist Hindernis schon vorbei? (Hinter Spieler)
if obs.X+obs.Width < 50.0 { if obs.X+obs.Width < 50.0 {
continue // log.Printf("🗑️ Obstacle %s passed safely", obs.ID)
continue // Nicht in nextObstacles aufnehmen -> wird gelöscht
} }
// Kollisions-Logik
paddingX := 10.0 paddingX := 10.0
paddingY_Top := 25.0 paddingY_Top := 25.0
paddingY_Bottom := 5.0 paddingY_Bottom := 5.0
@@ -339,9 +428,12 @@ func handleValidate(w http.ResponseWriter, r *http.Request) {
oTop, oBottom := obs.Y+paddingY_Top, obs.Y+obs.Height-paddingY_Bottom oTop, oBottom := obs.Y+paddingY_Top, obs.Y+obs.Height-paddingY_Bottom
if pRight > oLeft && pLeft < oRight && pBottom > oTop && pTop < oBottom { if pRight > oLeft && pLeft < oRight && pBottom > oTop && pTop < oBottom {
log.Printf("💥 KOLLISION! Session: %s | Obs: %s at %.2f | PlayerY: %.2f | Crouch: %v", req.SessionID, obs.ID, obs.X, posY, isCrouching)
playerDead = true playerDead = true
} }
// Behalten wenn noch relevant (sichtbar oder kurz davor)
// Wir behalten es etwas länger, damit Client Synchronisation nicht springt
if obs.X+obs.Width > -100 { if obs.X+obs.Width > -100 {
nextObstacles = append(nextObstacles, obs) nextObstacles = append(nextObstacles, obs)
if obs.X+obs.Width > rightmostX { if obs.X+obs.Width > rightmostX {
@@ -351,6 +443,7 @@ func handleValidate(w http.ResponseWriter, r *http.Request) {
} }
obstacles = nextObstacles obstacles = nextObstacles
// D. SPAWNING
if rightmostX < GameWidth-10.0 { if rightmostX < GameWidth-10.0 {
rawGap := 400.0 + rng.NextRange(0, 500) rawGap := 400.0 + rng.NextRange(0, 500)
gap := float64(int(rawGap)) gap := float64(int(rawGap))
@@ -380,6 +473,8 @@ func handleValidate(w http.ResponseWriter, r *http.Request) {
if def != nil { if def != nil {
spawnY := GroundY - def.Height - def.YOffset spawnY := GroundY - def.Height - def.YOffset
// log.Printf("✨ SPAWN: %s at %.2f (Y: %.2f)", def.ID, spawnX, spawnY)
obstacles = append(obstacles, ActiveObstacle{ obstacles = append(obstacles, ActiveObstacle{
ID: def.ID, ID: def.ID,
X: spawnX, X: spawnX,
@@ -392,9 +487,15 @@ func handleValidate(w http.ResponseWriter, r *http.Request) {
if !playerDead { if !playerDead {
score++ score++
} else {
// Wenn tot, brechen wir die Loop ab, um Rechenzeit zu sparen
// und senden den Dead-Status zurück
break
} }
} }
// --- SPEICHERN ---
status := "alive" status := "alive"
if playerDead { if playerDead {
status = "dead" status = "dead"
@@ -403,13 +504,18 @@ func handleValidate(w http.ResponseWriter, r *http.Request) {
obsJson, _ := json.Marshal(obstacles) obsJson, _ := json.Marshal(obstacles)
rdb.HSet(ctx, key, map[string]interface{}{ err = rdb.HSet(ctx, key, map[string]interface{}{
"score": score, "score": score,
"pos_y": fmt.Sprintf("%f", posY), "pos_y": fmt.Sprintf("%f", posY),
"vel_y": fmt.Sprintf("%f", velY), "vel_y": fmt.Sprintf("%f", velY),
"rng_state": rng.State, "rng_state": rng.State,
"obstacles": string(obsJson), "obstacles": string(obsJson),
}) }).Err()
if err != nil {
log.Printf("❌ REDIS SAVE ERROR: %v", err)
}
rdb.Expire(ctx, key, 1*time.Hour) rdb.Expire(ctx, key, 1*time.Hour)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@@ -427,11 +533,14 @@ func handleSubmitName(w http.ResponseWriter, r *http.Request) {
return return
} }
safeName := html.EscapeString(req.Name) log.Printf("📝 SUBMIT NAME: %s für Session %s", req.Name, req.SessionID)
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 {
log.Printf("⚠️ Submit fehlgeschlagen: Session nicht gefunden %s", req.SessionID)
http.Error(w, "Session expired", 404) http.Error(w, "Session expired", 404)
return return
} }
@@ -547,6 +656,8 @@ func handleAdminAction(w http.ResponseWriter, r *http.Request) {
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 {
@@ -575,29 +686,21 @@ func handleClaimDelete(w http.ResponseWriter, r *http.Request) {
realCode, err := rdb.HGet(ctx, sessionKey, "claim_code").Result() realCode, err := rdb.HGet(ctx, sessionKey, "claim_code").Result()
if err != nil || realCode == "" { if err != nil || realCode == "" {
log.Printf("⚠️ Claim Delete Failed: Session/Code missing %s", req.SessionID)
http.Error(w, "Not found", 404) http.Error(w, "Not found", 404)
return return
} }
if realCode != req.ClaimCode { if realCode != req.ClaimCode {
log.Printf("⛔ Claim Delete Denied: Wrong Code %s vs %s", req.ClaimCode, realCode)
http.Error(w, "Wrong Code", 403) http.Error(w, "Wrong Code", 403)
return return
} }
log.Printf("🗑️ USER DELETE: Session %s deleted via code", req.SessionID)
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.HDel(ctx, sessionKey, "name")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
func parseOr(s string, def float64) float64 {
if s == "" {
return def
}
v, err := strconv.ParseFloat(s, 64)
if err != nil {
return def
}
return v
}