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.
116 lines
3.2 KiB
Go
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)
|
|
}
|