290 lines
6.3 KiB
Go
290 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"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)
|
|
|
|
nextSpawnTick := int(parseOr(vals["next_spawn_tick"], 0))
|
|
|
|
godLives := int(parseOr(vals["p_god_lives"], 0))
|
|
hasBat := vals["p_has_bat"] == "1"
|
|
bootTicks := int(parseOr(vals["p_boot_ticks"], 0))
|
|
|
|
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{}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
playerDead := false
|
|
|
|
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 {
|
|
break
|
|
}
|
|
}
|
|
|
|
if suspicionScore > 15 {
|
|
log.Printf("🤖 BOT-ALARM [%s]: Zu perfekte Sprünge (Heuristik)", sessionID)
|
|
playerDead = true
|
|
}
|
|
|
|
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 {
|
|
if s == "" {
|
|
return def
|
|
}
|
|
v, err := strconv.ParseFloat(s, 64)
|
|
if err != nil {
|
|
return def
|
|
}
|
|
return v
|
|
}
|