mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
J3 D8-f #2: T4 difficulty lift (leaders-define-band) + feywild fork1 soft-lock fix
T4 monster tuning so martial leaders land in the 60-75% band (underdark 59/80, feywild 65/84 at L10/L12, n=50); casters trail (class-side gap that monster tuning provably can't compress -- Pass 1 showed casters pinned at 0% while martials moved). T5 deferred (walls everyone at its L12 floor; needs an L15-16 corpus). - dnd_bestiary.go: underdark elite/boss HP+AC up + caster-lethal proc cuts (mind_flayer/drow_mage/roper); feywild HP+AC up. - bestiary_srd.go: feywild multiattack profiles (fomorian/night_hag/green_hag) + Thornmother 2->3 lashes -- HP/AC alone didn't move feywild's low-damage roster; multiattack is what pulls facerolling martials into band. - zone_graph_feywild_crossing.go: free fork1's marsh edge. fork1 was the only fork in the game with every exit skill-locked (CHA+Perception, no LockNone) and deterministic no-retry rolls -- a prod SOFT-LOCK stranding ~60% of players (low CHA+WIS). Kept grove's CHA bargain as a bonus route. - expedition_sim.go: firstUnlockedForkChoice -- sim fork policy picks the first unlocked option instead of blind 'go 1' (which looped forever on locked forks); halts fork_all_locked if none. - tests: graph-wide TestZoneGraphs_NoSoftLockedFork + fork1 free-path guard. Writeup: sim_results/d8f_findings.md
This commit is contained in:
@@ -506,9 +506,22 @@ func (s *SimRunner) RunExpedition(uid id.UserID, zoneID ZoneID, walkCap, maxDays
|
||||
// Boss kill closes the run via combat resolution → continue
|
||||
// the loop so the next walk picks up the cleared-run state.
|
||||
case stopFork:
|
||||
// Deterministic sim policy: always take path 1. Real players
|
||||
// pick based on intent; the sim just needs to make progress.
|
||||
if err := s.P.handleDnDExpeditionCmd(ctx, "go 1"); err != nil {
|
||||
// Deterministic sim policy: take the first UNLOCKED path. The
|
||||
// old blind "go 1" stalled forever on all-skill-check forks
|
||||
// (feywild fork1) — resolveForkChoice rejects a locked edge but
|
||||
// zoneCmdGo swallows it as a sent-DM with a nil return, so the
|
||||
// run never advanced and burned every walk at the same node. A
|
||||
// real player reads the menu and picks a passable path; mirror
|
||||
// that. choice==0 means every edge is locked (a graph soft-lock
|
||||
// the author must fix) — halt loudly rather than spin.
|
||||
choice := s.firstUnlockedForkChoice(ctx.Sender)
|
||||
if choice == 0 {
|
||||
res.Outcome = "halted"
|
||||
res.StopCode = "fork_all_locked"
|
||||
i = walkCap
|
||||
break
|
||||
}
|
||||
if err := s.P.handleDnDExpeditionCmd(ctx, fmt.Sprintf("go %d", choice)); err != nil {
|
||||
res.Outcome = "halted"
|
||||
res.StopCode = "fork:" + err.Error()
|
||||
i = walkCap
|
||||
@@ -626,6 +639,26 @@ func (s *SimRunner) RunExpedition(uid id.UserID, zoneID ZoneID, walkCap, maxDays
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// firstUnlockedForkChoice returns the 1-based index of the first
|
||||
// traversable option at the pending fork, or 0 if every edge is locked
|
||||
// (a graph soft-lock — see feywild fork1, which had no LockNone exit).
|
||||
func (s *SimRunner) firstUnlockedForkChoice(uid id.UserID) int {
|
||||
run, err := getActiveZoneRun(uid)
|
||||
if err != nil || run == nil {
|
||||
return 1
|
||||
}
|
||||
pf, err := decodePendingFork(run.NodeChoices)
|
||||
if err != nil || pf == nil {
|
||||
return 1
|
||||
}
|
||||
for _, o := range pf.Options {
|
||||
if o.Unlocked {
|
||||
return o.Index
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
Reference in New Issue
Block a user