diff --git a/simulation.go b/simulation.go index aa543d4..91e0eb7 100644 --- a/simulation.go +++ b/simulation.go @@ -3,23 +3,29 @@ package main import ( "encoding/json" "fmt" + "log" + "math" "strconv" ) func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[string]string) (bool, int, []ActiveObstacle) { - // State laden + // --- 1. STATE LADEN --- posY := parseOr(vals["pos_y"], PlayerYBase) velY := parseOr(vals["vel_y"], 0.0) score := int(parseOr(vals["score"], 0)) - - // NEU: Wir laden die bisher vergangene Zeit (Ticks) - ticksAlive := int(parseOr(vals["total_ticks"], 0)) + ticksAlive := int(parseOr(vals["total_ticks"], 0)) // Zeit-Basis rngStateVal, _ := strconv.ParseInt(vals["rng_state"], 10, 64) + + // Powerups godLives := int(parseOr(vals["p_god_lives"], 0)) hasBat := vals["p_has_bat"] == "1" bootTicks := int(parseOr(vals["p_boot_ticks"], 0)) + // Anti-Cheat State laden (Wichtig für Heuristik über Chunks hinweg) + lastJumpDist := parseOr(vals["ac_last_dist"], 0.0) + suspicionScore := int(parseOr(vals["ac_suspicion"], 0)) + rng := NewRNG(rngStateVal) var obstacles []ActiveObstacle @@ -29,26 +35,42 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st obstacles = []ActiveObstacle{} } + // DEBUG: Start des Chunks + // log.Printf("[%s] Simulating Chunk: %d Ticks, Score: %d", sessionID, totalTicks, score) + + // --- ANTI-CHEAT 1: SPAM SCHUTZ --- + // Wer mehr als 10x pro Sekunde springt, ist ein Bot oder nutzt ein Makro + jumpCount := 0 + for _, inp := range inputs { + if inp.Act == "JUMP" { + jumpCount++ + } + } + if jumpCount > 10 { + log.Printf("🤖BOT-ALARM [%s]: Spammt Sprünge (%d Inputs)", sessionID, jumpCount) + return true, score, obstacles // Sofort tot + } + playerDead := false + // --- SIMULATION LOOP --- for i := 0; i < totalTicks; i++ { - // WICHTIG: Wir erhöhen die Zeit ticksAlive++ - // LOGIK FIX: Geschwindigkeit basiert jetzt auf ZEIT (Ticks), nicht Score! - // 3000 Ticks = ca. 50 Sekunden. Da wird es schneller. + // Speed Scaling (Zeitbasiert) currentSpeed := BaseSpeed + (float64(ticksAlive)/3000.0)*0.5 - if currentSpeed > 12.0 { - currentSpeed = 12.0 + if currentSpeed > 20.0 { + currentSpeed = 20.0 } + // Jump Power (Boots Powerup) currentJumpPower := JumpPower if bootTicks > 0 { currentJumpPower = HighJumpPower bootTicks-- } - // Input Verarbeitung + // Input didJump := false isCrouching := false for _, inp := range inputs { @@ -62,20 +84,47 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st } } - // Physik + // Physik Status isGrounded := posY >= PlayerYBase-1.0 + currentHeight := PlayerHeight if isCrouching { currentHeight = PlayerHeight / 2 if !isGrounded { velY += 2.0 + } // Fast fall + } + + // Springen & ANTI-CHEAT 2 (Heuristik) + if didJump && isGrounded && !isCrouching { + velY = currentJumpPower + + // Wir messen den Abstand zum nächsten Hindernis beim Absprung + var distToObs float64 = -1.0 + for _, o := range obstacles { + if o.X > 50.0 { // Das nächste Hindernis vor uns + distToObs = o.X - 50.0 + break + } + } + + // Bot Check: Wenn der Abstand IMMER gleich ist (z.B. exakt 75.5px) + if distToObs > 0 { + diff := math.Abs(distToObs - lastJumpDist) + if diff < 1.0 { + // Verdächtig perfekt wiederholt + suspicionScore++ + } else { + // Menschliche Varianz -> Verdacht senken + if suspicionScore > 0 { + suspicionScore-- + } + } + lastJumpDist = distToObs } } - if didJump && isGrounded && !isCrouching { - velY = currentJumpPower - } - + // Physik Anwendung velY += Gravity posY += velY if posY > PlayerYBase { @@ -88,7 +137,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st hitboxY = posY + (PlayerHeight - currentHeight) } - // Obstacles + // Hindernisse bewegen & Kollision nextObstacles := []ActiveObstacle{} rightmostX := 0.0 @@ -99,10 +148,9 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st continue } - // Passed Check (Verhindert Hängenbleiben an "toten" Objekten) + // Passed Check (Verhindert "Geister-Kollision" von hinten) paddingX := 10.0 if obs.X+obs.Width-paddingX < 55.0 { - // Schon vorbei -> Keine Kollision mehr prüfen nextObstacles = append(nextObstacles, obs) if obs.X+obs.Width > rightmostX { rightmostX = obs.X + obs.Width @@ -124,7 +172,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st if isCollision { if obs.Type == "coin" { - score += 2000 // Score Bonus macht das Spiel nicht mehr kaputt! + score += 2000 continue } else if obs.Type == "powerup" { if obs.ID == "p_god" { @@ -138,14 +186,19 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st } continue } else { + // Kollision mit Gegner if hasBat && obs.Type == "teacher" { hasBat = false + log.Printf("[%s] ⚾ Bat used on %s", sessionID, obs.ID) continue } if godLives > 0 { godLives-- + log.Printf("[%s] 🛡️ Godmode saved life (%d left)", sessionID, godLives) continue } + + log.Printf("💀 DEATH [%s]: Hit %s at Tick %d", sessionID, obs.ID, ticksAlive) playerDead = true } } @@ -165,10 +218,9 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st spawnX = GameWidth } - // LOGIK FIX: Boss Phase basiert auf ZEIT (Ticks) isBossPhase := (ticksAlive % 1500) > 1200 - var possibleDefs []ObstacleDef + for _, d := range defaultConfig.Obstacles { if isBossPhase { if d.ID == "principal" || d.ID == "trashcan" { @@ -178,7 +230,6 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st if d.ID == "principal" { continue } - // Eraser kommt ab Tick 3000 (ca. 50 sekunden) if d.ID == "eraser" && ticksAlive < 3000 { continue } @@ -197,6 +248,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st if def.Type == "powerup" && rng.NextFloat() > 0.1 { def = nil } + if def != nil { spawnY := GroundY - def.Height - def.YOffset obstacles = append(obstacles, ActiveObstacle{ @@ -207,12 +259,19 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st } if !playerDead { - score++ // Basis-Score läuft normal weiter + score++ } else { break } } + // --- ANTI-CHEAT CHECK (Ergebnis) --- + if suspicionScore > 10 { + log.Printf("🤖BOT-ALARM [%s]: Zu perfekte Sprünge (Heuristik Fail)", sessionID) + playerDead = true + } + + // --- SPEICHERN --- obsJson, _ := json.Marshal(obstacles) batStr := "0" if hasBat { @@ -221,7 +280,7 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st rdb.HSet(ctx, "session:"+sessionID, map[string]interface{}{ "score": score, - "total_ticks": ticksAlive, // NEU: Speichern + "total_ticks": ticksAlive, "pos_y": fmt.Sprintf("%f", posY), "vel_y": fmt.Sprintf("%f", velY), "rng_state": rng.State, @@ -229,6 +288,9 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st "p_god_lives": godLives, "p_has_bat": batStr, "p_boot_ticks": bootTicks, + // AC Daten speichern für nächsten Chunk + "ac_last_dist": fmt.Sprintf("%f", lastJumpDist), + "ac_suspicion": suspicionScore, }) return playerDead, score, obstacles diff --git a/static/js/logic.js b/static/js/logic.js index 3a051ca..8dcef70 100644 --- a/static/js/logic.js +++ b/static/js/logic.js @@ -7,7 +7,7 @@ function updateGameLogic() { // 2. Geschwindigkeit (Basiert auf ZEIT/Ticks, nicht Score!) // Formel: Start bei 5, erhöht sich alle 3000 Ticks (ca. 50 Sek) um 0.5 let currentSpeed = 5 + (currentTick / 3000.0) * 0.5; - if (currentSpeed > 12.0) currentSpeed = 12.0; // Max Speed Cap + if (currentSpeed > 20.0) currentSpeed = 20.0; // Max Speed Cap // 3. Spieler Physik & Größe const originalHeight = 50;