mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Long expeditions D7-c: -days flag + per-day snapshots in SimResult
cmd/expedition-sim -days N caps runs by synthetic day rollovers (Outcome="day_capped"). SimResult.DaySnapshots traces HP/SU/threat/rooms at start, every Night-camp rollover, and end-of-run — unblocks empirical D5-d retune of phase5BDailyBurnRatePct against per-day SU draws.
This commit is contained in:
@@ -282,7 +282,25 @@ type SimResult struct {
|
||||
// without re-running the matrix. Populated from combat_sessions
|
||||
// rows + their TurnLog at end-of-run.
|
||||
Combats []SimCombatSummary
|
||||
Log []SimLogEntry
|
||||
// DaySnapshots traces HP/SU/threat/rooms at every day rollover
|
||||
// (Night camp) plus the start (Day 0) and the final state. Used by
|
||||
// D7-c long-expedition baselining to see how the trajectory bends
|
||||
// across multi-day runs without scrubbing the log.
|
||||
DaySnapshots []SimDaySnapshot
|
||||
Log []SimLogEntry
|
||||
}
|
||||
|
||||
// SimDaySnapshot is a point-in-time projection of the sim state at a
|
||||
// day-rollover boundary. Day 0 is captured at expedition start; every
|
||||
// subsequent entry lands right after a Night camp lands (CurrentDay
|
||||
// already incremented). A final entry is appended at end-of-run.
|
||||
type SimDaySnapshot struct {
|
||||
Day int
|
||||
HPCurrent int
|
||||
HPMax int
|
||||
Supplies float32
|
||||
Threat int
|
||||
Rooms int // cumulative autopilot rooms walked at snapshot time
|
||||
}
|
||||
|
||||
// SimCombatSummary is a compact per-fight trace: the entry stats, the
|
||||
@@ -366,9 +384,14 @@ const simWalkInterval = autoRunCooldown
|
||||
// walkCap bounds the number of autopilot bursts as a safety net. Each
|
||||
// burst walks up to autopilotRoomCap rooms.
|
||||
//
|
||||
// maxDays, when > 0, stops the run once res.DayTicks reaches that count
|
||||
// — used by D7-c long-expedition baselining to bound multi-day runs by
|
||||
// synthetic day count rather than walk count. 0 leaves the run
|
||||
// unbounded by days (the walkCap safety net still applies).
|
||||
//
|
||||
// Pre-state: uid must own a synthetic character via BuildCharacter and
|
||||
// have a coin balance sufficient for outfitting (caller's responsibility).
|
||||
func (s *SimRunner) RunExpedition(uid id.UserID, zoneID ZoneID, walkCap int) (*SimResult, error) {
|
||||
func (s *SimRunner) RunExpedition(uid id.UserID, zoneID ZoneID, walkCap, maxDays int) (*SimResult, error) {
|
||||
c, err := LoadDnDCharacter(uid)
|
||||
if err != nil || c == nil {
|
||||
return nil, fmt.Errorf("LoadDnDCharacter: %w", err)
|
||||
@@ -392,6 +415,9 @@ func (s *SimRunner) RunExpedition(uid id.UserID, zoneID ZoneID, walkCap int) (*S
|
||||
return res, fmt.Errorf("expedition did not persist after start")
|
||||
}
|
||||
res.SUStart = exp.Supplies.Current
|
||||
// Day-0 baseline so the snapshot stream always opens with a known
|
||||
// starting state, even if the run halts before the first rollover.
|
||||
s.captureDaySnapshot(res, exp, uid)
|
||||
|
||||
// Synthetic clock — anchored on the expedition's real start_date so
|
||||
// nightCampWindow / nightSafetyNet comparisons against LastBriefingAt
|
||||
@@ -500,11 +526,20 @@ func (s *SimRunner) RunExpedition(uid id.UserID, zoneID ZoneID, walkCap int) (*S
|
||||
}
|
||||
if advanced {
|
||||
res.DayTicks++
|
||||
if fresh2, _ := getActiveExpedition(uid); fresh2 != nil {
|
||||
s.captureDaySnapshot(res, fresh2, uid)
|
||||
}
|
||||
}
|
||||
if exp, _ = getActiveExpedition(uid); exp == nil {
|
||||
res.Outcome = "extracted"
|
||||
i = walkCap
|
||||
}
|
||||
if maxDays > 0 && res.DayTicks >= maxDays {
|
||||
if res.Outcome == "" {
|
||||
res.Outcome = "day_capped"
|
||||
}
|
||||
i = walkCap
|
||||
}
|
||||
default:
|
||||
// stopOK / stopPreflight / stopHarvestCombat — soft stops.
|
||||
// Run the same camp scheduler the production autorun fires
|
||||
@@ -526,12 +561,21 @@ func (s *SimRunner) RunExpedition(uid id.UserID, zoneID ZoneID, walkCap int) (*S
|
||||
}
|
||||
if advanced {
|
||||
res.DayTicks++
|
||||
if fresh2, _ := getActiveExpedition(uid); fresh2 != nil {
|
||||
s.captureDaySnapshot(res, fresh2, uid)
|
||||
}
|
||||
}
|
||||
// maybeAutoCamp's drift step may have force-extracted (starvation).
|
||||
if exp, _ = getActiveExpedition(uid); exp == nil {
|
||||
res.Outcome = "extracted"
|
||||
i = walkCap
|
||||
}
|
||||
if maxDays > 0 && res.DayTicks >= maxDays {
|
||||
if res.Outcome == "" {
|
||||
res.Outcome = "day_capped"
|
||||
}
|
||||
i = walkCap
|
||||
}
|
||||
}
|
||||
}
|
||||
if res.Outcome == "" {
|
||||
@@ -557,9 +601,38 @@ func (s *SimRunner) RunExpedition(uid id.UserID, zoneID ZoneID, walkCap int) (*S
|
||||
}
|
||||
res.YieldCount, res.YieldsByName = simMaterialYields(uid)
|
||||
res.Combats = simCombatSummaries(uid)
|
||||
// Final snapshot. Re-read the expedition so closed-run state is
|
||||
// visible (extracted runs return nil from getActiveExpedition; the
|
||||
// row is still on disk via mostRecentExpeditionID). If the
|
||||
// expedition row is gone we synthesize from the cached SU/threat
|
||||
// already on res so the snapshot stream always closes.
|
||||
if exp2, _ := getActiveExpedition(uid); exp2 != nil {
|
||||
s.captureDaySnapshot(res, exp2, uid)
|
||||
} else if past := mostRecentExpeditionID(uid); past != "" {
|
||||
if exp3, _ := getExpedition(past); exp3 != nil {
|
||||
s.captureDaySnapshot(res, exp3, uid)
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// captureDaySnapshot appends a SimDaySnapshot reflecting current state.
|
||||
// HP is read from the live character row; SU/threat/day from the live
|
||||
// expedition. Rooms is the running res.Rooms counter.
|
||||
func (s *SimRunner) captureDaySnapshot(res *SimResult, exp *Expedition, uid id.UserID) {
|
||||
snap := SimDaySnapshot{
|
||||
Day: exp.CurrentDay,
|
||||
Supplies: exp.Supplies.Current,
|
||||
Threat: exp.ThreatLevel,
|
||||
Rooms: res.Rooms,
|
||||
}
|
||||
if c, _ := LoadDnDCharacter(uid); c != nil {
|
||||
snap.HPCurrent = c.HPCurrent
|
||||
snap.HPMax = c.HPMax
|
||||
}
|
||||
res.DaySnapshots = append(res.DaySnapshots, snap)
|
||||
}
|
||||
|
||||
// simCombatSummaries pulls every combat_sessions row for uid and folds
|
||||
// its TurnLog into a SimCombatSummary. AC values are inferred from the
|
||||
// RollAgainst column on attack events (the engine writes the defender's
|
||||
|
||||
@@ -293,3 +293,51 @@ func TestSimRunner_TickDay_EventAnchoredLowSupplies(t *testing.T) {
|
||||
t.Errorf("CurrentDay = %d, want >= 2", got.CurrentDay)
|
||||
}
|
||||
}
|
||||
|
||||
// D7-c: captureDaySnapshot must record the current expedition's day,
|
||||
// supplies, and threat plus the character's live HP into res.DaySnapshots.
|
||||
// Used by RunExpedition to trace state at every day rollover for
|
||||
// long-expedition baselining; verified here in isolation since
|
||||
// RunExpedition end-to-end isn't covered by package tests.
|
||||
func TestSimRunner_CaptureDaySnapshot_PopulatesFields(t *testing.T) {
|
||||
setupZoneRunTestDB(t)
|
||||
uid := id.UserID("@sim-snap:example")
|
||||
campTestCharacter(t, uid, 3)
|
||||
defer cleanupExpeditions(uid)
|
||||
|
||||
exp, err := startExpedition(uid, ZoneGoblinWarrens, "",
|
||||
ExpeditionSupplies{Current: 12, Max: 20, DailyBurn: 2, HarshMod: 1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
exp.CurrentDay = 3
|
||||
exp.ThreatLevel = 7
|
||||
if err := updateSupplies(exp.ID, exp.Supplies); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, _ := LoadDnDCharacter(uid)
|
||||
|
||||
sim := &SimRunner{P: &AdventurePlugin{}}
|
||||
res := &SimResult{Rooms: 9}
|
||||
sim.captureDaySnapshot(res, exp, uid)
|
||||
|
||||
if len(res.DaySnapshots) != 1 {
|
||||
t.Fatalf("DaySnapshots len = %d, want 1", len(res.DaySnapshots))
|
||||
}
|
||||
snap := res.DaySnapshots[0]
|
||||
if snap.Day != 3 {
|
||||
t.Errorf("Day = %d, want 3", snap.Day)
|
||||
}
|
||||
if snap.Supplies != 12 {
|
||||
t.Errorf("Supplies = %v, want 12", snap.Supplies)
|
||||
}
|
||||
if snap.Threat != 7 {
|
||||
t.Errorf("Threat = %d, want 7", snap.Threat)
|
||||
}
|
||||
if snap.Rooms != 9 {
|
||||
t.Errorf("Rooms = %d, want 9", snap.Rooms)
|
||||
}
|
||||
if snap.HPCurrent != c.HPCurrent || snap.HPMax != c.HPMax {
|
||||
t.Errorf("HP = %d/%d, want %d/%d", snap.HPCurrent, snap.HPMax, c.HPCurrent, c.HPMax)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user