package main import ( "math" "strings" "testing" "time" ) func TestTicketSingleUse(t *testing.T) { store := newTicketStore() value, err := store.issue("player-1", "Dave") if err != nil { t.Fatal(err) } if item, ok := store.consume(value); !ok || item.Subject != "player-1" { t.Fatalf("first consume failed: %#v %v", item, ok) } if _, ok := store.consume(value); ok { t.Fatal("ticket must be one-time") } } func TestTransformSanitizers(t *testing.T) { if got := clampFloat(-5, 0, worldSize); got != 0 { t.Fatalf("clamp low=%v", got) } if got := clampFloat(6000, 0, worldSize); got != worldSize { t.Fatalf("clamp high=%v", got) } 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) } } func TestAudienceMatch(t *testing.T) { if !audienceMatches(claims{AuthorizedParty: "airship-web"}, "airship-web") { t.Fatal("azp should match") } if !audienceMatches(claims{Audience: []interface{}{"account", "airship-web"}}, "airship-web") { t.Fatal("aud should match") } if audienceMatches(claims{AuthorizedParty: "other"}, "airship-web") { 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) } } func TestFakeCaptainsJoinSnapshotsWithoutSocketRecipients(t *testing.T) { h := newHub("TEST", 3) recipients, players, _ := h.snapshot() if len(recipients) != 0 { t.Fatalf("fake captains must not be websocket recipients: %d", len(recipients)) } if len(players) != 3 { t.Fatalf("fake captain count=%d", len(players)) } for _, player := range players { if !player.Bot || !strings.HasPrefix(player.ID, "bot:") { t.Fatalf("expected bot snapshot state: %+v", player) } } } func TestFakeCaptainEngagesNearbyHumanWithAuthoritativeBroadside(t *testing.T) { h := newHub("TEST", 1) var bot *client for _, candidate := range h.clients { if candidate.state.Bot { bot = candidate break } } if bot == nil { t.Fatal("missing fake captain") } human := &client{ state: newPlayerState("human", "Human", 0, 0), lastFire: map[string]time.Time{}, } bot.state.X, bot.state.Y, bot.state.Angle = 1000, 1000, 0 human.state.X, human.state.Y = 1300, 1000 h.clients[human.state.ID] = human now := time.Unix(200, 0) h.step(now, simTick.Seconds()) if len(h.projectiles) != 3 { t.Fatalf("expected fake captain broadside, projectiles=%d", len(h.projectiles)) } if bot.input.Seq == 0 { t.Fatal("fake captain input sequence did not advance") } } func TestExpandedWorldAndSmallerShipCombatScale(t *testing.T) { if worldSize != 5400 { t.Fatalf("worldSize=%v", worldSize) } if shipHitRadius != 34 { t.Fatalf("shipHitRadius=%v", shipHitRadius) } } func TestFakeCaptainDoesNotAggroHumanAcrossWholeWorld(t *testing.T) { h := newHub("TEST", 1) var bot *client for _, candidate := range h.clients { if candidate.state.Bot { bot = candidate break } } human := &client{ state: newPlayerState("human-far", "Human", 0, 0), lastFire: map[string]time.Time{}, } bot.state.X, bot.state.Y = 500, 500 human.state.X, human.state.Y = 5000, 5000 h.clients[human.state.ID] = human input, fireSide := h.fakeCaptainDecisionLocked(bot, time.Unix(300, 0)) if fireSide != "" { t.Fatalf("far human should not trigger broadside: %s", fireSide) } if input.Throttle <= 0 { t.Fatalf("bot should patrol when human is far: %+v", input) } }