Files
super-auto-pets-board-game/cmd/report/main.go
T
Greyson Parrelli 8ccde03023 Add replayable debug reports.
A report dumps everything needed to understand a game that went wrong:
the full state (decks card by card, shop row and tier deck order,
discards, pending choices, banked Mana/Trumpets/apples), the round's
battles, and the whole event log. Available as JSON, which replays, or
text, which reads — and the text ends with a paste-ready repro test.

Battles now record the randomness they consume alongside what they
started from, so a result can be replayed long after the round cleared
those banks: same lineups, same dice, same events, down to the log text.
Results predating the recording say so rather than quietly re-rolling.

Three ways in: the 🐛 panel's download/copy buttons, GET
/api/debug/report (DEBUG-only, since a report holds both players' hands
and the shop deck order), and `mise run report`, which reads the
database directly so a live game can be dumped without DEBUG.
2026-08-10 10:06:06 -04:00

116 lines
3.2 KiB
Go

// Command report prints a debug report for a saved game: the full state, every
// deck card by card, the round's battles with the dice they rolled, and the
// whole event log — enough to replay the situation in a test (see
// game.DebugReport).
//
// It reads the server's database directly, so it works against a live game
// without the server's DEBUG flag, and it works after the fact — the state is
// persisted on every action.
//
// go run ./cmd/report # list recent games
// go run ./cmd/report QWERT # the report, as text
// go run ./cmd/report -json QWERT # the report, as JSON
// go run ./cmd/report -json -out internal/game/testdata/bug.json QWERT
package main
import (
"flag"
"fmt"
"os"
"strings"
"time"
"github.com/greyson/super-auto-pets-board-game/internal/env"
"github.com/greyson/super-auto-pets-board-game/internal/store"
)
func main() {
asJSON := flag.Bool("json", false, "emit the JSON report (replayable) instead of the text one (readable)")
out := flag.String("out", "", "write to this file instead of stdout")
note := flag.String("note", "", "what looked wrong, recorded in the report")
dataDir := flag.String("data", "", "data directory holding games.db (default: $DATA_DIR or ./data)")
limit := flag.Int("limit", 20, "how many games to list")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "usage: report [flags] [game-code-or-id]\n\n"+
"With no game, lists the most recently played ones.\n\nFlags:\n")
flag.PrintDefaults()
}
flag.Parse()
env.Load(".env")
dir := *dataDir
if dir == "" {
dir = env.Get("DATA_DIR", "data")
}
st, err := store.Open(dir)
if err != nil {
fail("open %s: %v", dir, err)
}
defer st.Close()
if flag.NArg() == 0 {
list(st, *limit)
return
}
g, err := st.LoadAny(flag.Arg(0))
if err != nil {
fail("load %s: %v", flag.Arg(0), err)
}
rep, err := g.DebugReport()
if err != nil {
fail("capture %s: %v", g.Code, err)
}
rep.CapturedAt = time.Now().UTC().Format(time.RFC3339)
rep.Note = *note
var body []byte
if *asJSON {
if body, err = rep.JSON(); err != nil {
fail("render: %v", err)
}
} else {
body = []byte(rep.Text())
}
if *out == "" {
os.Stdout.Write(body)
return
}
if err := os.WriteFile(*out, body, 0o644); err != nil {
fail("write %s: %v", *out, err)
}
fmt.Fprintf(os.Stderr, "wrote %s (%d bytes)\n", *out, len(body))
}
// list prints the games on hand, so you can find the one you mean without
// knowing its code.
func list(st *store.Store, limit int) {
games, err := st.Recent(limit)
if err != nil {
fail("list games: %v", err)
}
if len(games) == 0 {
fmt.Println("no games saved yet")
return
}
fmt.Printf("%-6s %-8s %-5s %-7s %s\n", "CODE", "PHASE", "ROUND", "PLAYERS", "SEATS")
for _, g := range games {
var seats []string
for _, p := range g.Players {
name := p.Name
if p.IsBot {
name += " (bot)"
}
seats = append(seats, name)
}
fmt.Printf("%-6s %-8s %-5d %-7d %s\n",
g.Code, g.Phase, g.Round, len(g.Players), strings.Join(seats, ", "))
}
fmt.Printf("\nrun `report <code>` for any of these\n")
}
func fail(format string, args ...any) {
fmt.Fprintf(os.Stderr, "report: "+format+"\n", args...)
os.Exit(1)
}