Private
Public Access
1
0
Files
it232Abschied/simulation.go
Sebastian Unterschütz 61e82e0dba
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m34s
fix Sync
2025-11-30 18:49:00 +01:00

544 lines
13 KiB
Go

package main
import (
"encoding/json"
"fmt"
"log"
"math"
"strconv"
)
// --- INTERNE STATE STRUKTUR ---
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
}
// ============================================================================
// HAUPTFUNKTION
// ============================================================================
func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[string]string) (bool, int, []ActiveObstacle, []ActivePlatform, PowerUpState, int, int, uint32) {
// 1. State laden
state := loadSimState(sessionID, vals)
// 2. Bot-Check
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++
}
// 4. Anti-Cheat Heuristik
if state.SuspicionScore > 15 {
log.Printf("🤖 BOT-ALARM [%s]: Zu perfekte Sprünge (Heuristik)", sessionID)
state.IsDead = true
}
// 5. Speichern
saveSimState(&state)
return packResponse(&state)
}
// ============================================================================
// LOGIK & PHYSIK FUNKTIONEN
// ============================================================================
func updatePhysics(s *SimState, didJump, isCrouching bool, currentSpeed float64) {
// 1. Powerup Logik (Jump Boots)
jumpPower := JumpPower
if s.BootTicks > 0 {
jumpPower = HighJumpPower
s.BootTicks--
}
// 2. Sind wir am Boden?
isGrounded := checkGrounded(s)
// 3. Ducken / Fast Fall
// (Variable 'currentHeight' entfernt, da sie hier nicht gebraucht wird)
if isCrouching {
// Wenn man in der Luft duckt, fällt man schneller ("Fast Fall")
if !isGrounded {
s.VelY += 2.0
}
}
// 4. Springen
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
// ============================================================
// PLATTFORM KOLLISION (MIT VERTICAL SWEEP)
// ============================================================
if s.VelY > 0 { // Nur wenn wir fallen
// Wir nutzen hier die Standard-Höhe für die Füße.
// Auch beim Ducken bleiben die Füße meist unten (oder ziehen hoch?),
// aber für die Landung auf Plattformen ist die Standard-Box sicherer.
playerFeetOld := oldY + PlayerHeight
playerFeetNew := newY + PlayerHeight
// Player X ist fest bei 50, Breite 30
pLeft := 50.0
pRight := 50.0 + 30.0
for _, p := range s.Platforms {
// 1. Horizontal Check (Großzügig!)
// Toleranz an den Rändern (-5 / +5), damit man nicht abrutscht
if (pRight-5.0 > p.X) && (pLeft+5.0 < p.X+p.Width) {
// 2. Vertikaler Sweep (Durchsprung-Schutz)
// Check: Füße waren vorher <= Plattform-Oberkante
// UND Füße sind jetzt >= Plattform-Oberkante
if playerFeetOld <= p.Y && playerFeetNew >= p.Y {
// Korrektur: Wir setzen den Spieler exakt AUF die Plattform
newY = p.Y - PlayerHeight
s.VelY = 0
landed = true
isGrounded = true
break // Landung erfolgreich
}
}
}
}
// ============================================================
// BODEN KOLLISION
// ============================================================
if !landed {
if newY >= PlayerYBase {
newY = PlayerYBase
s.VelY = 0
isGrounded = true
}
}
// Neue Position setzen
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 {
// Passed Check
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
// --- OPTION A: CUSTOM CHUNK (20% Chance) ---
chunkCount := len(defaultConfig.Chunks)
if chunkCount > 0 && s.RNG.NextFloat() > 0.8 {
idx := int(s.RNG.NextRange(0, float64(chunkCount)))
chunk := defaultConfig.Chunks[idx]
// Objekte spawnen
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 {
// Fehler behoben: Zugriff auf o.X, o.Y jetzt möglich dank neuem Types-Struct
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
}
// Fehler behoben: Mismatched Types (int vs float64)
s.NextSpawnTick = s.Ticks + int(float64(width)/speed)
} else {
// --- OPTION B: RANDOM GENERATION ---
spawnRandomObstacle(s, speed, spawnX)
}
}
}
// ============================================================================
// HELPER
// ============================================================================
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
}