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,59 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user