mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 00:52:40 +00:00
J3 D8-prereq: split compact flag so sim drives the picker
Adds inlineBossCombat alongside compact in runAutopilotWalk and advanceOnceWithOpts. Production background autorun keeps both true (inline auto-resolve), foreground stays both false (manual !fight), the sim now uses compact=true + inlineBossCombat=false so the boss/elite doorway returns stopBoss/stopElite after the safety gate — autoResolveCombat + simPickCombatAction / simPickSpell drive the fight via the turn-based engine. The picker (and D8-b upcasting) has been dead since D3's compact-inline boss rooms; this re-wires it. n=50/cell L10 smoke vs d7d (zones T1-T3): bard forest_shadows 61 → 100 (+39) bard manor_blackspire 10 → 34 (+24) cleric forest_shadows 15 → 96 (+81) cleric manor_blackspire 0 → 54 (+54) fighter T1-T3 100 (unchanged) Also parallelizes matrix mode via subprocess workers (each child has its own SQLite — db package globals preclude in-process parallelism). New -jobs flag, defaults to runtime.NumCPU(). 8 workers gave ~7x speedup on the smoke matrix.
This commit is contained in:
@@ -17,8 +17,11 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"gogobee/internal/plugin"
|
"gogobee/internal/plugin"
|
||||||
|
|
||||||
@@ -46,6 +49,8 @@ func main() {
|
|||||||
trace = flag.Bool("trace", false, "include raw per-round CombatEvent stream on the LAST combat of each expedition (boss room) — for J2 diagnostic sweeps")
|
trace = flag.Bool("trace", false, "include raw per-round CombatEvent stream on the LAST combat of each expedition (boss room) — for J2 diagnostic sweeps")
|
||||||
|
|
||||||
petLevel = flag.Int("pet-level", 0, "attach a base housing pet at this level (1-10) to every sim character; 0 = no pet (default, matches prod char-creation)")
|
petLevel = flag.Int("pet-level", 0, "attach a base housing pet at this level (1-10) to every sim character; 0 = no pet (default, matches prod char-creation)")
|
||||||
|
|
||||||
|
jobs = flag.Int("jobs", 0, "matrix mode — concurrent worker count (each worker is a subprocess so it gets its own sqlite). 0 = runtime.NumCPU()")
|
||||||
)
|
)
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@@ -66,7 +71,7 @@ func main() {
|
|||||||
includeLog = *logFlag
|
includeLog = *logFlag
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
runMatrix(*classes, *levels, *zones, *runs, *bank, *cap, *days, includeLog)
|
runMatrix(*classes, *levels, *zones, *runs, *bank, *cap, *days, includeLog, *jobs, *trace, *petLevel)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,45 +105,108 @@ func runSingle(class string, level int, zone, userTag, dataDir string, bank floa
|
|||||||
emitIndented(res)
|
emitIndented(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runMatrix(classes, levels, zones string, runs int, bank float64, cap, days int, includeLog bool) {
|
// matrixJob is one (class, level, zone, replicate-index) cell of the
|
||||||
|
// matrix sweep. Each job is run by a worker as a single-run subprocess so
|
||||||
|
// it gets its own SQLite handle — the plugin package's db.* globals
|
||||||
|
// preclude in-process parallelism.
|
||||||
|
type matrixJob struct {
|
||||||
|
class string
|
||||||
|
level int
|
||||||
|
zone string
|
||||||
|
rep int
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMatrix(classes, levels, zones string, runs int, bank float64, cap, days int, includeLog bool, jobs int, trace bool, petLevel int) {
|
||||||
cs := splitNonEmpty(classes)
|
cs := splitNonEmpty(classes)
|
||||||
ls := parseLevels(levels)
|
ls := parseLevels(levels)
|
||||||
zs := splitNonEmpty(zones)
|
zs := splitNonEmpty(zones)
|
||||||
if len(cs) == 0 || len(ls) == 0 || len(zs) == 0 || runs <= 0 {
|
if len(cs) == 0 || len(ls) == 0 || len(zs) == 0 || runs <= 0 {
|
||||||
fail("matrix mode requires non-empty -classes, -levels, -zones and runs > 0")
|
fail("matrix mode requires non-empty -classes, -levels, -zones and runs > 0")
|
||||||
}
|
}
|
||||||
enc := json.NewEncoder(os.Stdout)
|
if jobs <= 0 {
|
||||||
|
jobs = runtime.NumCPU()
|
||||||
|
}
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
fail("os.Executable:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
work := make([]matrixJob, 0, len(cs)*len(ls)*len(zs)*runs)
|
||||||
for _, c := range cs {
|
for _, c := range cs {
|
||||||
for _, lv := range ls {
|
for _, lv := range ls {
|
||||||
for _, z := range zs {
|
for _, z := range zs {
|
||||||
for r := 0; r < runs; r++ {
|
for r := 0; r < runs; r++ {
|
||||||
|
work = append(work, matrixJob{class: c, level: lv, zone: z, rep: r})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
workCh := make(chan matrixJob)
|
||||||
|
resCh := make(chan *plugin.SimResult, len(work))
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for i := 0; i < jobs; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go matrixWorker(exe, workCh, resCh, &wg, bank, cap, days, includeLog, trace, petLevel)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
for _, j := range work {
|
||||||
|
workCh <- j
|
||||||
|
}
|
||||||
|
close(workCh)
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(resCh)
|
||||||
|
}()
|
||||||
|
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
for r := range resCh {
|
||||||
|
_ = enc.Encode(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func matrixWorker(exe string, in <-chan matrixJob, out chan<- *plugin.SimResult, wg *sync.WaitGroup, bank float64, cap, days int, includeLog, trace bool, petLevel int) {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := range in {
|
||||||
|
uid := fmt.Sprintf("@sim:%s-l%d-%s-%d", j.class, j.level, j.zone, j.rep)
|
||||||
dir, err := os.MkdirTemp("", "expedition-sim-")
|
dir, err := os.MkdirTemp("", "expedition-sim-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail("mkdir temp:", err)
|
out <- &plugin.SimResult{UserID: uid, Class: j.class, Level: j.level, Zone: j.zone, Outcome: "halted"}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
uid := id.UserID(fmt.Sprintf("@sim:%s-l%d-%s-%d", c, lv, z, r))
|
args := []string{
|
||||||
res, runErr := runOne(dir, uid, plugin.DnDClass(c), lv, plugin.ZoneID(z), bank, cap, days)
|
"-class", j.class,
|
||||||
if res != nil && !includeLog {
|
"-level", strconv.Itoa(j.level),
|
||||||
|
"-zone", j.zone,
|
||||||
|
"-bank", strconv.FormatFloat(bank, 'f', -1, 64),
|
||||||
|
"-cap", strconv.Itoa(cap),
|
||||||
|
"-days", strconv.Itoa(days),
|
||||||
|
"-data", dir,
|
||||||
|
"-user", uid,
|
||||||
|
fmt.Sprintf("-log=%t", includeLog),
|
||||||
|
fmt.Sprintf("-pet-level=%d", petLevel),
|
||||||
|
}
|
||||||
|
if trace {
|
||||||
|
args = append(args, "-trace")
|
||||||
|
}
|
||||||
|
cmd := exec.Command(exe, args...)
|
||||||
|
stdout, runErr := cmd.Output()
|
||||||
|
var res plugin.SimResult
|
||||||
|
if jerr := json.Unmarshal(stdout, &res); jerr != nil {
|
||||||
|
res = plugin.SimResult{UserID: uid, Class: j.class, Level: j.level, Zone: j.zone, Outcome: "halted"}
|
||||||
|
}
|
||||||
|
if runErr != nil && res.Outcome == "" {
|
||||||
|
res.Outcome = "halted"
|
||||||
|
}
|
||||||
|
if !includeLog {
|
||||||
res.Log = nil
|
res.Log = nil
|
||||||
}
|
}
|
||||||
if runErr != nil && res == nil {
|
out <- &res
|
||||||
// Synthesize a row so the corpus has one line per
|
|
||||||
// cell regardless of init failures.
|
|
||||||
res = &plugin.SimResult{
|
|
||||||
UserID: string(uid),
|
|
||||||
Class: c,
|
|
||||||
Level: lv,
|
|
||||||
Zone: z,
|
|
||||||
Outcome: "halted",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ = enc.Encode(res)
|
|
||||||
_ = os.RemoveAll(dir)
|
_ = os.RemoveAll(dir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runOne(dataDir string, uid id.UserID, class plugin.DnDClass, level int, zone plugin.ZoneID, bank float64, cap, days int) (*plugin.SimResult, error) {
|
func runOne(dataDir string, uid id.UserID, class plugin.DnDClass, level int, zone plugin.ZoneID, bank float64, cap, days int) (*plugin.SimResult, error) {
|
||||||
runner, err := plugin.NewSimRunner(dataDir)
|
runner, err := plugin.NewSimRunner(dataDir)
|
||||||
|
|||||||
@@ -642,7 +642,7 @@ type autopilotWalkResult struct {
|
|||||||
// combat already auto-resolves inside resolveCombatRoom; elite/boss
|
// combat already auto-resolves inside resolveCombatRoom; elite/boss
|
||||||
// doorways stop here so the player can choose !fight on their own terms.
|
// doorways stop here so the player can choose !fight on their own terms.
|
||||||
func (p *AdventurePlugin) expeditionCmdRun(ctx MessageContext) error {
|
func (p *AdventurePlugin) expeditionCmdRun(ctx MessageContext) error {
|
||||||
r := p.runAutopilotWalk(ctx, autopilotRoomCap, false)
|
r := p.runAutopilotWalk(ctx, autopilotRoomCap, false, false)
|
||||||
if r.initErr != "" {
|
if r.initErr != "" {
|
||||||
return p.SendDM(ctx.Sender, r.initErr)
|
return p.SendDM(ctx.Sender, r.initErr)
|
||||||
}
|
}
|
||||||
@@ -667,7 +667,7 @@ func (p *AdventurePlugin) expeditionCmdRun(ctx MessageContext) error {
|
|||||||
// run graph / harvest tally / supplies / threat — same as before, just
|
// run graph / harvest tally / supplies / threat — same as before, just
|
||||||
// no streamFlow here. compact==true switches the underlying combat
|
// no streamFlow here. compact==true switches the underlying combat
|
||||||
// narration into terse mode and auto-resolves elite (not boss) rooms.
|
// narration into terse mode and auto-resolves elite (not boss) rooms.
|
||||||
func (p *AdventurePlugin) runAutopilotWalk(ctx MessageContext, maxRooms int, compact bool) autopilotWalkResult {
|
func (p *AdventurePlugin) runAutopilotWalk(ctx MessageContext, maxRooms int, compact, inlineBossCombat bool) autopilotWalkResult {
|
||||||
exp, err := getActiveExpedition(ctx.Sender)
|
exp, err := getActiveExpedition(ctx.Sender)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return autopilotWalkResult{initErr: "Couldn't read expedition state: " + err.Error()}
|
return autopilotWalkResult{initErr: "Couldn't read expedition state: " + err.Error()}
|
||||||
@@ -717,7 +717,7 @@ func (p *AdventurePlugin) runAutopilotWalk(ctx MessageContext, maxRooms int, com
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res, aerr := p.advanceOnceWithOpts(ctx, compact)
|
res, aerr := p.advanceOnceWithOpts(ctx, compact, inlineBossCombat)
|
||||||
if aerr != nil {
|
if aerr != nil {
|
||||||
return autopilotWalkResult{initErr: aerr.Error()}
|
return autopilotWalkResult{initErr: aerr.Error()}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -472,10 +472,17 @@ func (p *AdventurePlugin) zoneCmdAdvance(ctx MessageContext) error {
|
|||||||
// doorways (boss still stops; boss is the climax beat). Foreground
|
// doorways (boss still stops; boss is the climax beat). Foreground
|
||||||
// `!expedition run` / `!zone advance` always pass false.
|
// `!expedition run` / `!zone advance` always pass false.
|
||||||
func (p *AdventurePlugin) advanceOnce(ctx MessageContext) (advanceResult, error) {
|
func (p *AdventurePlugin) advanceOnce(ctx MessageContext) (advanceResult, error) {
|
||||||
return p.advanceOnceWithOpts(ctx, false)
|
return p.advanceOnceWithOpts(ctx, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AdventurePlugin) advanceOnceWithOpts(ctx MessageContext, compact bool) (advanceResult, error) {
|
// inlineBossCombat (only consulted when compact==true) selects between the
|
||||||
|
// two background combat paths at a boss/elite doorway. true keeps the
|
||||||
|
// long-expedition D3 inline auto-resolve (production autorun). false
|
||||||
|
// returns stopBoss/stopElite after the safety gate so a turn-based driver
|
||||||
|
// — currently only the headless sim's autoResolveCombat / simPickCombatAction
|
||||||
|
// — handles the fight via the regular !fight / !attack engine. The sim
|
||||||
|
// uses false so simPickSpell actually fires; D8-prereq re-wired this seam.
|
||||||
|
func (p *AdventurePlugin) advanceOnceWithOpts(ctx MessageContext, compact, inlineBossCombat bool) (advanceResult, error) {
|
||||||
run, err := getActiveZoneRun(ctx.Sender)
|
run, err := getActiveZoneRun(ctx.Sender)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return advanceResult{}, fmt.Errorf("Couldn't read run state: %s", err.Error())
|
return advanceResult{}, fmt.Errorf("Couldn't read run state: %s", err.Error())
|
||||||
@@ -560,6 +567,17 @@ func (p *AdventurePlugin) advanceOnceWithOpts(ctx MessageContext, compact bool)
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !inlineBossCombat {
|
||||||
|
// Background caller wants to drive the fight itself (sim's
|
||||||
|
// autoResolveCombat / simPickCombatAction). Surface the
|
||||||
|
// doorway like the foreground path does, after the safety
|
||||||
|
// gate has had a chance to defer the engagement.
|
||||||
|
r := stopElite
|
||||||
|
if prev == RoomBoss {
|
||||||
|
r = stopBoss
|
||||||
|
}
|
||||||
|
return advanceResult{reason: r}, nil
|
||||||
|
}
|
||||||
// Compact-mode elite/boss auto-resolve. resolveCombatRoom
|
// Compact-mode elite/boss auto-resolve. resolveCombatRoom
|
||||||
// selects monster + label by run.CurrentRoomType().
|
// selects monster + label by run.CurrentRoomType().
|
||||||
ai, ap, ao, aended, aerr := p.resolveCombatRoom(ctx.Sender, run, zone, prev == RoomElite, true)
|
ai, ap, ao, aended, aerr := p.resolveCombatRoom(ctx.Sender, run, zone, prev == RoomElite, true)
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ func (p *AdventurePlugin) tryAutoRun(e *Expedition, now time.Time) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := id.UserID(e.UserID)
|
uid := id.UserID(e.UserID)
|
||||||
r := p.runAutopilotWalk(MessageContext{Sender: uid}, autoRunRoomCap, true)
|
r := p.runAutopilotWalk(MessageContext{Sender: uid}, autoRunRoomCap, true, true)
|
||||||
if r.initErr != "" {
|
if r.initErr != "" {
|
||||||
// "no expedition" / "no run" — race with abandon/extract. Silent.
|
// "no expedition" / "no run" — race with abandon/extract. Silent.
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -429,7 +429,7 @@ func (s *SimRunner) RunExpedition(uid id.UserID, zoneID ZoneID, walkCap, maxDays
|
|||||||
|
|
||||||
for i := 0; i < walkCap; i++ {
|
for i := 0; i < walkCap; i++ {
|
||||||
simNow = simNow.Add(simWalkInterval)
|
simNow = simNow.Add(simWalkInterval)
|
||||||
walk := s.P.runAutopilotWalk(ctx, autopilotRoomCap, true)
|
walk := s.P.runAutopilotWalk(ctx, autopilotRoomCap, true, false)
|
||||||
if walk.initErr != "" {
|
if walk.initErr != "" {
|
||||||
res.Outcome = "halted"
|
res.Outcome = "halted"
|
||||||
res.StopCode = "init:" + walk.initErr
|
res.StopCode = "init:" + walk.initErr
|
||||||
|
|||||||
Reference in New Issue
Block a user