// 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 ` for any of these\n") } func fail(format string, args ...any) { fmt.Fprintf(os.Stderr, "report: "+format+"\n", args...) os.Exit(1) }