package main import ( "encoding/json" "fmt" "log" "math" "strconv" ) func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[string]string) (bool, int, []ActiveObstacle) { // --- 1. STATE LADEN --- posY := parseOr(vals["pos_y"], PlayerYBase) velY := parseOr(vals["vel_y"], 0.0) score := int(parseOr(vals["score"], 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 if val, ok := vals["obstacles"]; ok && val != "" { json.Unmarshal([]byte(val), &obstacles) } else { 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++ { ticksAlive++ // Speed Scaling (Zeitbasiert) currentSpeed := BaseSpeed + (float64(ticksAlive)/3000.0)*0.5 if currentSpeed > 20.0 { currentSpeed = 20.0 } // Jump Power (Boots Powerup) currentJumpPower := JumpPower if bootTicks > 0 { currentJumpPower = HighJumpPower bootTicks-- } // Input didJump := false isCrouching := false for _, inp := range inputs { if inp.Tick == i { if inp.Act == "JUMP" { didJump = true } if inp.Act == "DUCK" { isCrouching = true } } } // 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 } } // Physik Anwendung velY += Gravity posY += velY if posY > PlayerYBase { posY = PlayerYBase velY = 0 } hitboxY := posY if isCrouching { hitboxY = posY + (PlayerHeight - currentHeight) } // Hindernisse bewegen & Kollision nextObstacles := []ActiveObstacle{} rightmostX := 0.0 for _, obs := range obstacles { obs.X -= currentSpeed if obs.X+obs.Width < -50.0 { continue } // Passed Check (Verhindert "Geister-Kollision" von hinten) paddingX := 10.0 if obs.X+obs.Width-paddingX < 55.0 { nextObstacles = append(nextObstacles, obs) if obs.X+obs.Width > rightmostX { rightmostX = obs.X + obs.Width } continue } paddingY_Top := 10.0 if obs.Type == "teacher" { paddingY_Top = 25.0 } pLeft, pRight := 50.0+paddingX, 50.0+30.0-paddingX pTop, pBottom := hitboxY+paddingY_Top, hitboxY+currentHeight-5.0 oLeft, oRight := obs.X+paddingX, obs.X+obs.Width-paddingX oTop, oBottom := obs.Y+paddingY_Top, obs.Y+obs.Height-5.0 isCollision := pRight > oLeft && pLeft < oRight && pBottom > oTop && pTop < oBottom if isCollision { if obs.Type == "coin" { score += 2000 continue } else if obs.Type == "powerup" { if obs.ID == "p_god" { godLives = 3 } if obs.ID == "p_bat" { hasBat = true } if obs.ID == "p_boot" { bootTicks = 600 } 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 } } nextObstacles = append(nextObstacles, obs) if obs.X+obs.Width > rightmostX { rightmostX = obs.X + obs.Width } } obstacles = nextObstacles // Spawning if rightmostX < GameWidth-10.0 { gap := float64(int(400.0 + rng.NextRange(0, 500))) spawnX := rightmostX + gap if spawnX < GameWidth { spawnX = GameWidth } isBossPhase := (ticksAlive % 1500) > 1200 var possibleDefs []ObstacleDef for _, d := range defaultConfig.Obstacles { if isBossPhase { if d.ID == "principal" || d.ID == "trashcan" { possibleDefs = append(possibleDefs, d) } } else { if d.ID == "principal" { continue } if d.ID == "eraser" && ticksAlive < 3000 { continue } possibleDefs = append(possibleDefs, d) } } def := rng.PickDef(possibleDefs) if def != nil && def.CanTalk { if rng.NextFloat() > 0.7 { rng.NextFloat() } } if def != nil { if def.Type == "powerup" && rng.NextFloat() > 0.1 { def = nil } if def != nil { spawnY := GroundY - def.Height - def.YOffset obstacles = append(obstacles, ActiveObstacle{ ID: def.ID, Type: def.Type, X: spawnX, Y: spawnY, Width: def.Width, Height: def.Height, }) } } } if !playerDead { 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 { batStr = "1" } rdb.HSet(ctx, "session:"+sessionID, map[string]interface{}{ "score": score, "total_ticks": ticksAlive, "pos_y": fmt.Sprintf("%f", posY), "vel_y": fmt.Sprintf("%f", velY), "rng_state": rng.State, "obstacles": string(obsJson), "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 } func parseOr(s string, def float64) float64 { if s == "" { return def } v, err := strconv.ParseFloat(s, 64) if err != nil { return def } return v }