mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
expedition-sim: matrix mode for batch corpora
-matrix sweeps the cartesian product of -classes × -levels × -zones with -runs replicates per cell, fresh sqlite DB per run, emitting one JSON object per stdout line. Log field is suppressed by default in matrix mode (override with -log=true) since a single matrix cell's log can easily dwarf the per-run summary. Default -cap shrunk from 200 to 50 — RunExpedition now counts autopilot bursts (not !expedition run invocations), and 50 bursts is enough to clear a tier-1 zone with headroom.
This commit is contained in:
@@ -2,12 +2,14 @@
|
|||||||
// analysis. Re-uses production plugin paths against a fresh sqlite DB so
|
// analysis. Re-uses production plugin paths against a fresh sqlite DB so
|
||||||
// outcomes mirror what live players hit.
|
// outcomes mirror what live players hit.
|
||||||
//
|
//
|
||||||
// Usage: expedition-sim [-class fighter] [-level 5] [-zone goblin_warrens]
|
// Single run:
|
||||||
// [-bank 1000] [-cap 200] [-data DIR]
|
// expedition-sim [-class fighter] [-level 5] [-zone goblin_warrens]
|
||||||
|
// [-bank 1000] [-cap 50] [-log] [-data DIR]
|
||||||
//
|
//
|
||||||
// Defaults run one L5 fighter through goblin_warrens and print a JSON
|
// Matrix mode (cartesian sweep over classes × levels × zones × N runs,
|
||||||
// SimResult to stdout. Per-room logs and matrix-mode batches land in a
|
// one JSON object per stdout line, log suppressed by default):
|
||||||
// follow-up; this binary is the scaffold the analysis stack hangs off.
|
// expedition-sim -matrix -classes fighter,mage -levels 5,10 \
|
||||||
|
// -zones goblin_warrens,wolf_den -runs 20
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -15,6 +17,8 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gogobee/internal/plugin"
|
"gogobee/internal/plugin"
|
||||||
|
|
||||||
@@ -23,17 +27,42 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var (
|
var (
|
||||||
class = flag.String("class", "fighter", "DnD class id (fighter, mage, cleric, …)")
|
class = flag.String("class", "fighter", "DnD class id (single-run mode)")
|
||||||
level = flag.Int("level", 5, "character level")
|
level = flag.Int("level", 5, "character level (single-run mode)")
|
||||||
zone = flag.String("zone", "goblin_warrens", "zone id (see internal/plugin/dnd_zone_registry.go)")
|
zone = flag.String("zone", "goblin_warrens", "zone id (single-run mode)")
|
||||||
bank = flag.Float64("bank", 1000, "starting coin balance — must cover outfitting")
|
bank = flag.Float64("bank", 1000, "starting coin balance — must cover outfitting")
|
||||||
cap = flag.Int("cap", 200, "max !expedition run invocations per expedition")
|
cap = flag.Int("cap", 50, "max autopilot bursts per expedition (each = up to autopilotRoomCap rooms)")
|
||||||
dataDir = flag.String("data", "", "data dir for the temp sqlite db (default: OS tempdir)")
|
dataDir = flag.String("data", "", "data dir for the temp sqlite db (default: OS tempdir; ignored in matrix mode)")
|
||||||
userTag = flag.String("user", "@sim:expedition", "synthetic user id")
|
userTag = flag.String("user", "@sim:expedition", "synthetic user id (single-run mode)")
|
||||||
|
logFlag = flag.Bool("log", true, "include per-row expedition log in output (single-run default true; matrix default false)")
|
||||||
|
|
||||||
|
matrix = flag.Bool("matrix", false, "matrix mode — sweep over classes × levels × zones × runs")
|
||||||
|
classes = flag.String("classes", "", "comma-separated class ids (matrix mode)")
|
||||||
|
levels = flag.String("levels", "", "comma-separated levels (matrix mode)")
|
||||||
|
zones = flag.String("zones", "", "comma-separated zone ids (matrix mode)")
|
||||||
|
runs = flag.Int("runs", 1, "replicates per (class,level,zone) cell (matrix mode)")
|
||||||
)
|
)
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
dir := *dataDir
|
if *matrix {
|
||||||
|
// Matrix default: drop log to keep stdout manageable; explicit
|
||||||
|
// -log=true overrides.
|
||||||
|
includeLog := false
|
||||||
|
// Flag.Lookup tells us whether the user explicitly set -log.
|
||||||
|
flag.Visit(func(f *flag.Flag) {
|
||||||
|
if f.Name == "log" {
|
||||||
|
includeLog = *logFlag
|
||||||
|
}
|
||||||
|
})
|
||||||
|
runMatrix(*classes, *levels, *zones, *runs, *bank, *cap, includeLog)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
runSingle(*class, *level, *zone, *userTag, *dataDir, *bank, *cap, *logFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSingle(class string, level int, zone, userTag, dataDir string, bank float64, cap int, includeLog bool) {
|
||||||
|
dir := dataDir
|
||||||
if dir == "" {
|
if dir == "" {
|
||||||
var err error
|
var err error
|
||||||
dir, err = os.MkdirTemp("", "expedition-sim-")
|
dir, err = os.MkdirTemp("", "expedition-sim-")
|
||||||
@@ -43,37 +72,105 @@ func main() {
|
|||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
runner, err := plugin.NewSimRunner(dir)
|
res, err := runOne(dir, id.UserID(userTag), plugin.DnDClass(class), level, plugin.ZoneID(zone), bank, cap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail("init runner:", err)
|
if res != nil {
|
||||||
|
if !includeLog {
|
||||||
|
res.Log = nil
|
||||||
|
}
|
||||||
|
emitIndented(res)
|
||||||
|
}
|
||||||
|
fail("run:", err)
|
||||||
|
}
|
||||||
|
if !includeLog {
|
||||||
|
res.Log = nil
|
||||||
|
}
|
||||||
|
emitIndented(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMatrix(classes, levels, zones string, runs int, bank float64, cap int, includeLog bool) {
|
||||||
|
cs := splitNonEmpty(classes)
|
||||||
|
ls := parseLevels(levels)
|
||||||
|
zs := splitNonEmpty(zones)
|
||||||
|
if len(cs) == 0 || len(ls) == 0 || len(zs) == 0 || runs <= 0 {
|
||||||
|
fail("matrix mode requires non-empty -classes, -levels, -zones and runs > 0")
|
||||||
|
}
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
for _, c := range cs {
|
||||||
|
for _, lv := range ls {
|
||||||
|
for _, z := range zs {
|
||||||
|
for r := 0; r < runs; r++ {
|
||||||
|
dir, err := os.MkdirTemp("", "expedition-sim-")
|
||||||
|
if err != nil {
|
||||||
|
fail("mkdir temp:", err)
|
||||||
|
}
|
||||||
|
uid := id.UserID(fmt.Sprintf("@sim:%s-l%d-%s-%d", c, lv, z, r))
|
||||||
|
res, runErr := runOne(dir, uid, plugin.DnDClass(c), lv, plugin.ZoneID(z), bank, cap)
|
||||||
|
if res != nil && !includeLog {
|
||||||
|
res.Log = nil
|
||||||
|
}
|
||||||
|
if runErr != nil && res == nil {
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runOne(dataDir string, uid id.UserID, class plugin.DnDClass, level int, zone plugin.ZoneID, bank float64, cap int) (*plugin.SimResult, error) {
|
||||||
|
runner, err := plugin.NewSimRunner(dataDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("init runner: %w", err)
|
||||||
}
|
}
|
||||||
defer runner.Close()
|
defer runner.Close()
|
||||||
|
|
||||||
uid := id.UserID(*userTag)
|
if _, err := runner.BuildCharacter(uid, class, level); err != nil {
|
||||||
if _, err := runner.BuildCharacter(uid, plugin.DnDClass(*class), *level); err != nil {
|
return nil, fmt.Errorf("build character: %w", err)
|
||||||
fail("build character:", err)
|
|
||||||
}
|
}
|
||||||
|
runner.Euro.Credit(uid, bank, "expedition-sim bankroll")
|
||||||
runner.Euro.Credit(uid, *bank, "expedition-sim bankroll")
|
return runner.RunExpedition(uid, zone, cap)
|
||||||
|
|
||||||
res, err := runner.RunExpedition(uid, plugin.ZoneID(*zone), *cap)
|
|
||||||
if err != nil {
|
|
||||||
// RunExpedition returns a partial result alongside the error so the
|
|
||||||
// outcome of a partial run is still observable.
|
|
||||||
if res != nil {
|
|
||||||
emit(res)
|
|
||||||
}
|
|
||||||
fail("run expedition:", err)
|
|
||||||
}
|
|
||||||
emit(res)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func emit(res *plugin.SimResult) {
|
func emitIndented(res *plugin.SimResult) {
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(os.Stdout)
|
||||||
enc.SetIndent("", " ")
|
enc.SetIndent("", " ")
|
||||||
_ = enc.Encode(res)
|
_ = enc.Encode(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func splitNonEmpty(s string) []string {
|
||||||
|
parts := strings.Split(s, ",")
|
||||||
|
out := parts[:0]
|
||||||
|
for _, p := range parts {
|
||||||
|
if p = strings.TrimSpace(p); p != "" {
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLevels(s string) []int {
|
||||||
|
var out []int
|
||||||
|
for _, p := range splitNonEmpty(s) {
|
||||||
|
n, err := strconv.Atoi(p)
|
||||||
|
if err != nil {
|
||||||
|
fail("bad level:", p)
|
||||||
|
}
|
||||||
|
out = append(out, n)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func fail(args ...interface{}) {
|
func fail(args ...interface{}) {
|
||||||
fmt.Fprintln(os.Stderr, args...)
|
fmt.Fprintln(os.Stderr, args...)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user