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.
60 lines
1.8 KiB
Go
60 lines
1.8 KiB
Go
package ai
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/greyson/super-auto-pets-board-game/internal/game"
|
|
)
|
|
|
|
// A debug report is only worth taking if replaying it reproduces the battle it
|
|
// captured, and the situations worth reporting are the messy ones — six seats,
|
|
// three packs shuffled together, Mana and Trumpets banked, Komodo shuffling
|
|
// apples into a deck mid-fight. This test lives in the ai package because that
|
|
// is where full games get played: it hands the bots a table, then replays every
|
|
// battle of the last round out of the report and demands the identical result.
|
|
//
|
|
// If it ever fails, the engine has grown a source of randomness (or a piece of
|
|
// battle input) that the result doesn't record, and reports of that battle are
|
|
// no longer reproducible.
|
|
func TestDebugReportReplaysRealGames(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
packs []string
|
|
levels []float64
|
|
}{
|
|
{"two seats, one pack", []string{"turtle"}, []float64{0.9, 0.9}},
|
|
{"six seats, three packs", []string{"turtle", "golden", "unicorn"},
|
|
[]float64{0.9, 0.9, 0.9, 0.9, 0.9, 0.9}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
g := playBotTable(t, tc.packs, tc.levels...)
|
|
rep, err := g.DebugReport()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(g.Battles) == 0 {
|
|
t.Fatal("a finished game should have battles to replay")
|
|
}
|
|
for i, recorded := range g.Battles {
|
|
replayed, err := rep.ReplayBattle(i)
|
|
if err != nil {
|
|
t.Fatalf("battle %d: %v", i, err)
|
|
}
|
|
if got, want := marshal(t, replayed), marshal(t, recorded); got != want {
|
|
t.Fatalf("battle %d replayed differently:\nrecorded %s\nreplayed %s", i, want, got)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func marshal(t *testing.T, res *game.BattleResult) string {
|
|
t.Helper()
|
|
b, err := json.Marshal(res)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(b)
|
|
}
|