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.
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user