From 7cf610354489a74bb5a1d97d673e5703d3d052a0 Mon Sep 17 00:00:00 2001 From: Admin Date: Mon, 21 Sep 2026 01:52:46 +0000 Subject: [PATCH] Add server-authoritative PvP main_test.go --- server/main_test.go | 106 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 100 insertions(+), 6 deletions(-) diff --git a/server/main_test.go b/server/main_test.go index 8c2d70d..7b062cc 100644 --- a/server/main_test.go +++ b/server/main_test.go @@ -3,6 +3,7 @@ package main import ( "math" "testing" + "time" ) func TestTicketSingleUse(t *testing.T) { @@ -20,21 +21,18 @@ func TestTicketSingleUse(t *testing.T) { } func TestTransformSanitizers(t *testing.T) { - if got := clampFloat(-5, 0, 3600); got != 0 { + if got := clampFloat(-5, 0, worldSize); got != 0 { t.Fatalf("clamp low=%v", got) } - if got := clampFloat(4000, 0, 3600); got != 3600 { + if got := clampFloat(4000, 0, worldSize); got != worldSize { t.Fatalf("clamp high=%v", got) } - if got := clampFloat(math.NaN(), 0, 3600); got != 0 { + if got := clampFloat(math.NaN(), 0, worldSize); got != 0 { t.Fatalf("nan=%v", got) } if got := normalizeAngle(-90); got != 270 { t.Fatalf("angle=%v", got) } - if got := clampInt(900, 0, 500); got != 500 { - t.Fatalf("hull=%v", got) - } } func TestAudienceMatch(t *testing.T) { @@ -48,3 +46,99 @@ func TestAudienceMatch(t *testing.T) { t.Fatal("wrong client must not match") } } + +func TestServerMovementUsesInputNotClientTransform(t *testing.T) { + state := newPlayerState("player-1", "Dave", 0, 0) + startX, startY := state.X, state.Y + input := inputState{Throttle: 1, Steer: 0.4, Seq: 7} + now := time.Unix(100, 0) + for i := 0; i < 30; i++ { + stepPlayer(&state, input, now.Add(time.Duration(i)*simTick), simTick.Seconds()) + } + if state.InputSeq != 7 { + t.Fatalf("input seq=%d", state.InputSeq) + } + if state.Speed <= 0 || state.Speed > playerMaxSpeed { + t.Fatalf("speed=%v", state.Speed) + } + if state.X == startX && state.Y == startY { + t.Fatal("authoritative simulation did not move player") + } + if state.Angle == 0 { + t.Fatal("steer input did not change angle") + } +} + +func TestDockedInputFreezesAuthoritativeShip(t *testing.T) { + state := newPlayerState("player-1", "Dave", 0, 0) + state.Speed = 150 + x, y := state.X, state.Y + stepPlayer(&state, inputState{Docked: true, Seq: 1}, time.Now(), simTick.Seconds()) + if state.Speed != 0 || state.X != x || state.Y != y { + t.Fatalf("docked ship moved: speed=%v x=%v y=%v", state.Speed, state.X, state.Y) + } +} + +func TestBroadsideSpawnsThreeServerProjectilesAndEnforcesReload(t *testing.T) { + h := newHub("TEST") + c := &client{state: newPlayerState("one", "One", 0, 0), lastFire: map[string]time.Time{}} + h.clients[c.state.ID] = c + now := time.Unix(100, 0) + if !h.fire("one", "port", now) { + t.Fatal("first fire should succeed") + } + if len(h.projectiles) != 3 { + t.Fatalf("projectiles=%d", len(h.projectiles)) + } + if h.fire("one", "port", now.Add(100*time.Millisecond)) { + t.Fatal("reload should reject early second broadside") + } + if !h.fire("one", "port", now.Add(broadsideReload)) { + t.Fatal("broadside should fire after reload") + } + if len(h.projectiles) != 6 { + t.Fatalf("projectiles after reload=%d", len(h.projectiles)) + } +} + +func TestServerProjectileAppliesDamageDeathAndKill(t *testing.T) { + h := newHub("TEST") + shooter := &client{state: newPlayerState("one", "One", 0, 0), lastFire: map[string]time.Time{}} + target := &client{state: newPlayerState("two", "Two", 0, 0), lastFire: map[string]time.Time{}} + shooter.state.X, shooter.state.Y = 1000, 1000 + target.state.X, target.state.Y = 1050, 1000 + target.state.Hull = shotDamage + h.clients[shooter.state.ID] = shooter + h.clients[target.state.ID] = target + now := time.Unix(100, 0) + h.projectiles["shot"] = projectileState{ + ID: "shot", OwnerID: shooter.state.ID, X: target.state.X, Y: target.state.Y, + Damage: shotDamage, bornAt: now, + } + h.step(now.Add(simTick), simTick.Seconds()) + if target.state.Alive { + t.Fatal("target should be dead") + } + if target.state.Deaths != 1 || shooter.state.Kills != 1 { + t.Fatalf("kills/deaths=%d/%d", shooter.state.Kills, target.state.Deaths) + } + if target.state.LastKiller != "One" { + t.Fatalf("killer=%q", target.state.LastKiller) + } + if _, ok := h.projectiles["shot"]; ok { + t.Fatal("hit projectile should be removed") + } +} + +func TestServerRespawnsDestroyedPlayer(t *testing.T) { + state := newPlayerState("player-1", "Dave", 0, 1) + now := time.Unix(100, 0) + state.Alive = false + state.Hull = 0 + state.Speed = 120 + state.RespawnAt = now.Add(respawnDelay).UnixMilli() + stepPlayer(&state, inputState{}, now.Add(respawnDelay+time.Millisecond), simTick.Seconds()) + if !state.Alive || state.Hull != playerMaxHull || state.Speed != 0 { + t.Fatalf("bad respawn: %+v", state) + } +}