You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

120 lines
3.6 KiB
Go

package game
import (
"math/rand"
"testing"
"time"
"github.com/denisovdennis/autohero/internal/model"
"github.com/denisovdennis/autohero/internal/tuning"
)
func testGraphTownTour(t *testing.T) *RoadGraph {
t.Helper()
g := testGraph()
g.Towns[1].Radius = 35
npc := TownNPC{ID: 101, Name: "Merchant", NameKey: "npc.merchant.test", Type: "merchant", OffsetX: 2, OffsetY: 1}
g.TownNPCs[1] = []TownNPC{npc}
g.NPCByID[101] = npc
return g
}
func heroInTown(id int64, townID int64) *model.Hero {
return &model.Hero{
ID: id, Level: 5, HP: 900, MaxHP: 1000,
Attack: 50, Defense: 30, Speed: 1.0,
Strength: 10, Constitution: 10, Agility: 10, Luck: 5,
State: model.StateInTown,
CurrentTownID: &townID,
PositionX: 1, PositionY: 1,
}
}
func TestTownTour_WelcomeTimeoutReturnsToWander(t *testing.T) {
graph := testGraphTownTour(t)
hero := heroInTown(1, 1)
now := time.Now()
hm := NewHeroMovement(hero, graph, now)
hm.CurrentTownID = 1
hm.CurrentX = 1
hm.CurrentY = 1
hm.LastMoveTick = now
hm.Excursion = model.ExcursionSession{
Kind: model.ExcursionKindTown,
Phase: model.ExcursionWild,
TownTourPhase: string(model.TownTourPhaseNpcWelcome),
TownWelcomeUntil: now.Add(-time.Second),
TownTourNpcID: 101,
TownTourStandX: 3,
TownTourStandY: 2,
AttractorSet: true,
AttractorX: 3,
AttractorY: 2,
}
ProcessSingleHeroMovementTick(hero.ID, hm, graph, now.Add(50*time.Millisecond), nil, nil, nil, nil, nil, nil)
if hm.Excursion.TownTourPhase != string(model.TownTourPhaseWander) {
t.Fatalf("expected wander after welcome timeout, got %q", hm.Excursion.TownTourPhase)
}
}
func TestTownTour_DialogClosedFromWelcomeLeavesWelcome(t *testing.T) {
graph := testGraphTownTour(t)
hero := heroInTown(1, 1)
now := time.Now()
hm := NewHeroMovement(hero, graph, now)
hm.CurrentTownID = 1
hm.Excursion = model.ExcursionSession{
Kind: model.ExcursionKindTown,
Phase: model.ExcursionWild,
TownTourPhase: string(model.TownTourPhaseNpcWelcome),
TownWelcomeUntil: now.Add(time.Hour),
TownTourNpcID: 101,
}
hm.townTourNPCDialogClosed(now, graph)
if model.TownTourPhase(hm.Excursion.TownTourPhase) == model.TownTourPhaseNpcWelcome {
t.Fatal("still in npc_welcome after dialog closed")
}
if !hm.Excursion.TownWelcomeUntil.IsZero() {
t.Fatal("expected TownWelcomeUntil cleared")
}
}
// TestTownTour_DefaultPNpc_AtLeastOneNpcOpportunity uses default tuning to estimate P(≥1 NPC attractor pick)
// over a synthetic town stay (retarget cadence vs tour length). Target from design: ≥ 0.6.
func TestTownTour_DefaultPNpc_AtLeastOneNpcOpportunity(t *testing.T) {
cfg := tuning.DefaultValues()
pNpc := cfg.TownTourNpcAttractorChance
minRT := cfg.TownTourWanderRetargetMinMs
maxRT := cfg.TownTourWanderRetargetMaxMs
minStay := cfg.TownRestMinMs
maxStay := cfg.TownRestMaxMs
if minRT <= 0 || maxRT < minRT || minStay <= 0 || maxStay < minStay {
t.Fatal("invalid default town tour / rest durations")
}
const trials = 8000
rng := rand.New(rand.NewSource(42))
hits := 0
for i := 0; i < trials; i++ {
stayMs := minStay + rng.Int63n(maxStay-minStay+1)
anyNpc := false
for elapsed := int64(0); elapsed < stayMs; {
if rng.Float64() < pNpc {
anyNpc = true
break
}
step := minRT + rng.Int63n(maxRT-minRT+1)
if step < 1 {
step = 1
}
elapsed += step
}
if anyNpc {
hits++
}
}
rate := float64(hits) / float64(trials)
if rate < 0.6 {
t.Fatalf("Monte Carlo P(≥1 NPC retarget)=%.3f with defaults; want >= 0.6 (townTourNpcAttractorChance=%.3f)", rate, pNpc)
}
}