package main import ( "encoding/json" "fmt" "log" "math" "strconv" ) type SimState struct { SessionID string Score int Ticks int PosY float64 VelY float64 IsDead bool // Objekte Obstacles []ActiveObstacle Platforms []ActivePlatform // Powerups GodLives int HasBat bool BootTicks int // Spawning & RNG NextSpawnTick int RNG *PseudoRNG // Anti-Cheat LastJumpDist float64 SuspicionScore int Chunks []ChunkDef } func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[string]string) (bool, int, []ActiveObstacle, []ActivePlatform, PowerUpState, int, int, uint32) { state := loadSimState(sessionID, vals) if isBotSpamming(inputs) { log.Printf("🤖 BOT-ALARM [%s]: Spammt Sprünge", sessionID) state.IsDead = true return packResponse(&state) } // 3. Game Loop for i := 0; i < totalTicks; i++ { state.Ticks++ currentSpeed := calculateSpeed(state.Ticks) didJump, isCrouching := parseInput(inputs, i) updatePhysics(&state, didJump, isCrouching, currentSpeed) checkCollisions(&state, isCrouching, currentSpeed) if state.IsDead { break } moveWorld(&state, currentSpeed) handleSpawning(&state, currentSpeed) state.Score++ } if state.SuspicionScore > 15 { log.Printf("🤖 BOT-ALARM [%s]: Zu perfekte Sprünge (Heuristik)", sessionID) state.IsDead = true } saveSimState(&state) return packResponse(&state) } func updatePhysics(s *SimState, didJump, isCrouching bool, currentSpeed float64) { jumpPower := JumpPower if s.BootTicks > 0 { jumpPower = HighJumpPower s.BootTicks-- } isGrounded := checkGrounded(s) if isCrouching { if !isGrounded { s.VelY += 2.0 } } if didJump && isGrounded && !isCrouching { s.VelY = jumpPower isGrounded = false } // 5. Schwerkraft anwenden s.VelY += Gravity oldY := s.PosY newY := s.PosY + s.VelY landed := false if s.VelY > 0 { playerFeetOld := oldY + PlayerHeight playerFeetNew := newY + PlayerHeight pLeft := 50.0 pRight := 50.0 + 30.0 for _, p := range s.Platforms { if (pRight-5.0 > p.X) && (pLeft+5.0 < p.X+p.Width) { if playerFeetOld <= p.Y && playerFeetNew >= p.Y { newY = p.Y - PlayerHeight s.VelY = 0 landed = true isGrounded = true break } } } } if !landed { if newY >= PlayerYBase { newY = PlayerYBase s.VelY = 0 isGrounded = true } } s.PosY = newY } func checkCollisions(s *SimState, isCrouching bool, currentSpeed float64) { hitboxH := PlayerHeight hitboxY := s.PosY if isCrouching { hitboxH = PlayerHeight / 2 hitboxY = s.PosY + (PlayerHeight - hitboxH) } activeObs := []ActiveObstacle{} for _, obs := range s.Obstacles { paddingX := 10.0 if obs.X+obs.Width-paddingX < 55.0 { activeObs = append(activeObs, obs) continue } pLeft, pRight := 50.0+paddingX, 50.0+30.0-paddingX pTop, pBottom := hitboxY+10.0, hitboxY+hitboxH-5.0 if obs.Type == "teacher" { pTop = hitboxY + 25.0 } oLeft, oRight := obs.X+paddingX, obs.X+obs.Width-paddingX oTop, oBottom := obs.Y+10.0, obs.Y+obs.Height-5.0 isHit := pRight > oLeft && pLeft < oRight && pBottom > oTop && pTop < oBottom if isHit { if obs.Type == "coin" { s.Score += 2000 continue } if obs.Type == "powerup" { applyPowerup(s, obs.ID) continue } if s.HasBat && obs.Type == "teacher" { s.HasBat = false log.Printf("[%s] ⚾ Bat used on %s", s.SessionID, obs.ID) continue } if s.GodLives > 0 { s.GodLives-- log.Printf("[%s] 🛡️ Godmode saved life", s.SessionID) continue } _, pRight := 50.0+10.0, 50.0+30.0-10.0 // Player X ist fest // Player Y/Height ausrechnen (für Log) pH := PlayerHeight if isCrouching { pH = PlayerHeight / 2 } pTopLog := s.PosY + 10.0 pBottomLog := s.PosY + pH - 5.0 log.Printf("\n💀 --- DEATH REPORT [%s] ---\n"+ "⏱️ Tick: %d (Speed: %.2f)\n"+ "🏃 Player: Y=%.2f (Top: %.2f, Bottom: %.2f) | VelY=%.2f | Duck=%v\n"+ "🧱 Killer: ID='%s' (Type=%s)\n"+ "📍 Object: X=%.2f (Left: %.2f, Right: %.2f)\n"+ " Y=%.2f (Top: %.2f, Bottom: %.2f)\n"+ "💥 Overlap: X-Diff=%.2f, Y-Diff=%.2f\n"+ "------------------------------------------\n", s.SessionID, s.Ticks, currentSpeed, s.PosY, pTopLog, pBottomLog, s.VelY, isCrouching, obs.ID, obs.Type, obs.X, oLeft, oRight, obs.Y, oTop, oBottom, (oLeft - pRight), (oTop - pBottomLog), // Wenn negativ, überlappen sie ) log.Printf("💀 DEATH [%s]: Hit %s at Tick %d", s.SessionID, obs.ID, s.Ticks) s.IsDead = true } activeObs = append(activeObs, obs) } s.Obstacles = activeObs } func moveWorld(s *SimState, speed float64) { nextObs := []ActiveObstacle{} for _, o := range s.Obstacles { o.X -= speed if o.X+o.Width > -200.0 { nextObs = append(nextObs, o) } } s.Obstacles = nextObs nextPlats := []ActivePlatform{} for _, p := range s.Platforms { p.X -= speed if p.X+p.Width > -200.0 { nextPlats = append(nextPlats, p) } } s.Platforms = nextPlats } func handleSpawning(s *SimState, speed float64) { if s.NextSpawnTick == 0 { s.NextSpawnTick = s.Ticks + 50 } if s.Ticks >= s.NextSpawnTick { spawnX := GameWidth + 3200.0 chunkCount := len(defaultConfig.Chunks) if chunkCount > 0 && s.RNG.NextFloat() > 0.8 { idx := int(s.RNG.NextRange(0, float64(chunkCount))) chunk := defaultConfig.Chunks[idx] for _, p := range chunk.Platforms { s.Platforms = append(s.Platforms, ActivePlatform{ X: spawnX + p.X, Y: p.Y, Width: p.Width, Height: p.Height, }) } for _, o := range chunk.Obstacles { s.Obstacles = append(s.Obstacles, ActiveObstacle{ ID: o.ID, Type: o.Type, X: spawnX + o.X, Y: o.Y, Width: o.Width, Height: o.Height, }) } width := chunk.TotalWidth if width == 0 { width = 2000 } s.NextSpawnTick = s.Ticks + int(float64(width)/speed) } else { spawnRandomObstacle(s, speed, spawnX) } } } func loadSimState(sid string, vals map[string]string) SimState { rngState, _ := strconv.ParseInt(vals["rng_state"], 10, 64) s := SimState{ SessionID: sid, Score: int(parseOr(vals["score"], 0)), Ticks: int(parseOr(vals["total_ticks"], 0)), PosY: parseOr(vals["pos_y"], PlayerYBase), VelY: parseOr(vals["vel_y"], 0.0), NextSpawnTick: int(parseOr(vals["next_spawn_tick"], 0)), GodLives: int(parseOr(vals["p_god_lives"], 0)), BootTicks: int(parseOr(vals["p_boot_ticks"], 0)), HasBat: vals["p_has_bat"] == "1", LastJumpDist: parseOr(vals["ac_last_dist"], 0.0), SuspicionScore: int(parseOr(vals["ac_suspicion"], 0)), RNG: NewRNG(rngState), } if v, ok := vals["obstacles"]; ok && v != "" { json.Unmarshal([]byte(v), &s.Obstacles) } if v, ok := vals["platforms"]; ok && v != "" { json.Unmarshal([]byte(v), &s.Platforms) } return s } func saveSimState(s *SimState) { obsJson, _ := json.Marshal(s.Obstacles) platJson, _ := json.Marshal(s.Platforms) batStr := "0" if s.HasBat { batStr = "1" } rdb.HSet(ctx, "session:"+s.SessionID, map[string]interface{}{ "score": s.Score, "total_ticks": s.Ticks, "next_spawn_tick": s.NextSpawnTick, "pos_y": fmt.Sprintf("%f", s.PosY), "vel_y": fmt.Sprintf("%f", s.VelY), "rng_state": s.RNG.State, "obstacles": string(obsJson), "platforms": string(platJson), "p_god_lives": s.GodLives, "p_has_bat": batStr, "p_boot_ticks": s.BootTicks, "ac_last_dist": fmt.Sprintf("%f", s.LastJumpDist), "ac_suspicion": s.SuspicionScore, }) } func packResponse(s *SimState) (bool, int, []ActiveObstacle, []ActivePlatform, PowerUpState, int, int, uint32) { pState := PowerUpState{ GodLives: s.GodLives, HasBat: s.HasBat, BootTicks: s.BootTicks, } return s.IsDead, s.Score, s.Obstacles, s.Platforms, pState, s.Ticks, s.NextSpawnTick, s.RNG.State } func checkPlatformLanding(pX, pY, pW, playerX, oldPlayerY, newPlayerY, velY float64) (bool, float64) { if velY < 0 { return false, 0 } const pTolerance = 10.0 playerW := 30.0 if (playerX+playerW-pTolerance > pX) && (playerX+pTolerance < pX+pW) { playerFeetOld := oldPlayerY + PlayerHeight playerFeetNew := newPlayerY + PlayerHeight if playerFeetOld <= pY && playerFeetNew >= pY { return true, pY - PlayerHeight } } return false, 0 } func spawnRandomObstacle(s *SimState, speed, spawnX float64) { gapPixel := 400 + int(s.RNG.NextRange(0, 500)) ticksToWait := int(float64(gapPixel) / speed) s.NextSpawnTick = s.Ticks + ticksToWait isBossPhase := (s.Ticks % 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" && s.Ticks < 3000 { continue } possibleDefs = append(possibleDefs, d) } } def := s.RNG.PickDef(possibleDefs) if def != nil && def.CanTalk { if s.RNG.NextFloat() > 0.7 { s.RNG.NextFloat() } } if def != nil && def.Type == "powerup" { if s.RNG.NextFloat() > 0.1 { def = nil } } if def != nil { spawnY := GroundY - def.Height - def.YOffset s.Obstacles = append(s.Obstacles, ActiveObstacle{ ID: def.ID, Type: def.Type, X: spawnX, Y: spawnY, Width: def.Width, Height: def.Height, }) } } func applyPowerup(s *SimState, id string) { if id == "p_god" { s.GodLives = 3 } if id == "p_bat" { s.HasBat = true } if id == "p_boot" { s.BootTicks = 600 } } func checkGrounded(s *SimState) bool { if s.VelY != 0 { return false } if s.PosY >= PlayerYBase-0.1 { return true } for _, p := range s.Platforms { if math.Abs((s.PosY+PlayerHeight)-p.Y) < 0.5 { if 50+30 > p.X && 50 < p.X+p.Width { return true } } } return false } func isBotSpamming(inputs []Input) bool { jumpCount := 0 for _, inp := range inputs { if inp.Act == "JUMP" { jumpCount++ } } return jumpCount > 10 } func parseInput(inputs []Input, currentTick int) (bool, bool) { jump := false duck := false for _, inp := range inputs { if inp.Tick == currentTick { if inp.Act == "JUMP" { jump = true } if inp.Act == "DUCK" { duck = true } } } return jump, duck } func calculateSpeed(ticks int) float64 { speed := BaseSpeed + (float64(ticks)/1000.0)*1.5 if speed > 36.0 { return 36.0 } return speed } func checkJumpSuspicion(s *SimState) { var distToObs float64 = -1.0 for _, o := range s.Obstacles { if o.X > 50.0 { distToObs = o.X - 50.0 break } } if distToObs > 0 { diff := math.Abs(distToObs - s.LastJumpDist) if diff < 1.0 { s.SuspicionScore++ } else if s.SuspicionScore > 0 { s.SuspicionScore-- } s.LastJumpDist = distToObs } } func parseOr(s string, def float64) float64 { if s == "" { return def } v, err := strconv.ParseFloat(s, 64) if err != nil { return def } return v }