add music, better sync, particles
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 2m18s
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 2m18s
This commit is contained in:
712
simulation.go
712
simulation.go
@@ -8,273 +8,491 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[string]string) (bool, int, []ActiveObstacle, PowerUpState, int, int) {
|
||||
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))
|
||||
rngStateVal, _ := strconv.ParseInt(vals["rng_state"], 10, 64)
|
||||
// --- INTERNE STATE STRUKTUR ---
|
||||
type SimState struct {
|
||||
SessionID string
|
||||
Score int
|
||||
Ticks int
|
||||
PosY float64
|
||||
VelY float64
|
||||
IsDead bool
|
||||
|
||||
nextSpawnTick := int(parseOr(vals["next_spawn_tick"], 0))
|
||||
// Objekte
|
||||
Obstacles []ActiveObstacle
|
||||
Platforms []ActivePlatform
|
||||
|
||||
godLives := int(parseOr(vals["p_god_lives"], 0))
|
||||
hasBat := vals["p_has_bat"] == "1"
|
||||
bootTicks := int(parseOr(vals["p_boot_ticks"], 0))
|
||||
// Powerups
|
||||
GodLives int
|
||||
HasBat bool
|
||||
BootTicks int
|
||||
|
||||
lastJumpDist := parseOr(vals["ac_last_dist"], 0.0)
|
||||
suspicionScore := int(parseOr(vals["ac_suspicion"], 0))
|
||||
// Spawning & RNG
|
||||
NextSpawnTick int
|
||||
RNG *PseudoRNG
|
||||
|
||||
rng := NewRNG(rngStateVal)
|
||||
// Anti-Cheat
|
||||
LastJumpDist float64
|
||||
SuspicionScore int
|
||||
}
|
||||
|
||||
var obstacles []ActiveObstacle
|
||||
if val, ok := vals["obstacles"]; ok && val != "" {
|
||||
json.Unmarshal([]byte(val), &obstacles)
|
||||
} else {
|
||||
obstacles = []ActiveObstacle{}
|
||||
// ============================================================================
|
||||
// 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) {
|
||||
jumpPower := JumpPower
|
||||
if s.BootTicks > 0 {
|
||||
jumpPower = HighJumpPower
|
||||
s.BootTicks--
|
||||
}
|
||||
|
||||
isGrounded := checkGrounded(s)
|
||||
|
||||
// Fehler behoben: "currentHeight declared but not used" entfernt.
|
||||
// Wir brauchen es hier nicht, da checkPlatformLanding mit fixen 50.0 rechnet.
|
||||
// Die Hitbox-Änderung passiert nur in checkCollisions.
|
||||
|
||||
if isCrouching && !isGrounded {
|
||||
s.VelY += 2.0 // Fast Fall
|
||||
}
|
||||
|
||||
if didJump && isGrounded && !isCrouching {
|
||||
s.VelY = jumpPower
|
||||
isGrounded = false
|
||||
checkJumpSuspicion(s)
|
||||
}
|
||||
|
||||
s.VelY += Gravity
|
||||
oldY := s.PosY
|
||||
newY := s.PosY + s.VelY
|
||||
|
||||
landed := false
|
||||
|
||||
// A. Plattform Landung (One-Way Logic)
|
||||
if s.VelY > 0 {
|
||||
for _, p := range s.Platforms {
|
||||
hit, landY := checkPlatformLanding(p.X, p.Y, p.Width, 50.0, oldY, newY, s.VelY)
|
||||
if hit {
|
||||
newY = landY
|
||||
s.VelY = 0
|
||||
landed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// B. Boden Landung
|
||||
if !landed {
|
||||
if newY >= PlayerYBase {
|
||||
newY = PlayerYBase
|
||||
s.VelY = 0
|
||||
}
|
||||
}
|
||||
|
||||
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++
|
||||
}
|
||||
}
|
||||
if jumpCount > 10 {
|
||||
log.Printf("🤖 BOT-ALARM [%s]: Spammt Sprünge (%d)", sessionID, jumpCount)
|
||||
return true, score, obstacles, PowerUpState{}, ticksAlive, nextSpawnTick
|
||||
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
|
||||
}
|
||||
|
||||
playerDead := false
|
||||
func calculateSpeed(ticks int) float64 {
|
||||
speed := BaseSpeed + (float64(ticks)/1000.0)*1.5
|
||||
if speed > 36.0 {
|
||||
return 36.0
|
||||
}
|
||||
return speed
|
||||
}
|
||||
|
||||
for i := 0; i < totalTicks; i++ {
|
||||
ticksAlive++
|
||||
|
||||
currentSpeed := BaseSpeed + (float64(ticksAlive)/3000.0)*0.5
|
||||
if currentSpeed > 12.0 {
|
||||
currentSpeed = 12.0
|
||||
}
|
||||
|
||||
currentJumpPower := JumpPower
|
||||
if bootTicks > 0 {
|
||||
currentJumpPower = HighJumpPower
|
||||
bootTicks--
|
||||
}
|
||||
|
||||
didJump := false
|
||||
isCrouching := false
|
||||
for _, inp := range inputs {
|
||||
if inp.Tick == i {
|
||||
if inp.Act == "JUMP" {
|
||||
didJump = true
|
||||
}
|
||||
if inp.Act == "DUCK" {
|
||||
isCrouching = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isGrounded := posY >= PlayerYBase-1.0
|
||||
currentHeight := PlayerHeight
|
||||
if isCrouching {
|
||||
currentHeight = PlayerHeight / 2
|
||||
if !isGrounded {
|
||||
velY += 2.0
|
||||
}
|
||||
}
|
||||
|
||||
if didJump && isGrounded && !isCrouching {
|
||||
velY = currentJumpPower
|
||||
|
||||
var distToObs float64 = -1.0
|
||||
for _, o := range obstacles {
|
||||
if o.X > 50.0 {
|
||||
distToObs = o.X - 50.0
|
||||
break
|
||||
}
|
||||
}
|
||||
if distToObs > 0 {
|
||||
diff := math.Abs(distToObs - lastJumpDist)
|
||||
if diff < 1.0 {
|
||||
suspicionScore++
|
||||
} else if suspicionScore > 0 {
|
||||
suspicionScore--
|
||||
}
|
||||
lastJumpDist = distToObs
|
||||
}
|
||||
}
|
||||
|
||||
velY += Gravity
|
||||
posY += velY
|
||||
if posY > PlayerYBase {
|
||||
posY = PlayerYBase
|
||||
velY = 0
|
||||
}
|
||||
|
||||
hitboxY := posY
|
||||
if isCrouching {
|
||||
hitboxY = posY + (PlayerHeight - currentHeight)
|
||||
}
|
||||
|
||||
nextObstacles := []ActiveObstacle{}
|
||||
|
||||
for _, obs := range obstacles {
|
||||
obs.X -= currentSpeed
|
||||
|
||||
if obs.X+obs.Width < -50.0 {
|
||||
continue
|
||||
}
|
||||
|
||||
paddingX := 10.0
|
||||
realRightEdge := obs.X + obs.Width - paddingX
|
||||
|
||||
if realRightEdge < 55.0 {
|
||||
nextObstacles = append(nextObstacles, obs)
|
||||
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 {
|
||||
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", sessionID)
|
||||
continue
|
||||
}
|
||||
log.Printf("💀 DEATH [%s]: Hit %s at Tick %d", sessionID, obs.ID, ticksAlive)
|
||||
playerDead = true
|
||||
}
|
||||
}
|
||||
|
||||
nextObstacles = append(nextObstacles, obs)
|
||||
}
|
||||
obstacles = nextObstacles
|
||||
|
||||
if nextSpawnTick == 0 {
|
||||
nextSpawnTick = ticksAlive + 50
|
||||
}
|
||||
|
||||
if ticksAlive >= nextSpawnTick {
|
||||
gapPixel := 400 + int(rng.NextRange(0, 500))
|
||||
ticksToWait := int(float64(gapPixel) / currentSpeed)
|
||||
nextSpawnTick = ticksAlive + ticksToWait
|
||||
|
||||
spawnX := GameWidth + 50.0
|
||||
|
||||
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 {
|
||||
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 suspicionScore > 15 {
|
||||
log.Printf("🤖 BOT-ALARM [%s]: Zu perfekte Sprünge (Heuristik)", sessionID)
|
||||
playerDead = true
|
||||
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
|
||||
}
|
||||
|
||||
obsJson, _ := json.Marshal(obstacles)
|
||||
batStr := "0"
|
||||
if hasBat {
|
||||
batStr = "1"
|
||||
}
|
||||
|
||||
rdb.HSet(ctx, "session:"+sessionID, map[string]interface{}{
|
||||
"score": score,
|
||||
"total_ticks": ticksAlive,
|
||||
"next_spawn_tick": nextSpawnTick,
|
||||
"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_last_dist": fmt.Sprintf("%f", lastJumpDist),
|
||||
"ac_suspicion": suspicionScore,
|
||||
})
|
||||
|
||||
pState := PowerUpState{
|
||||
GodLives: godLives,
|
||||
HasBat: hasBat,
|
||||
BootTicks: bootTicks,
|
||||
}
|
||||
|
||||
return playerDead, score, obstacles, pState, ticksAlive, nextSpawnTick
|
||||
}
|
||||
|
||||
func parseOr(s string, def float64) float64 {
|
||||
|
||||
Reference in New Issue
Block a user