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
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)
|
|
}
|
|
}
|