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:
@@ -1,5 +1,7 @@
|
||||
package game
|
||||
|
||||
import "fmt"
|
||||
|
||||
// SimulateBattle resolves a hypothetical battle between the given arranged
|
||||
// decks (top of deck first) and returns the result. deckA sits at seat 0 and
|
||||
// deckB at seat 1; firstSeat (0 or 1) is the one holding priority. It runs on
|
||||
@@ -24,6 +26,52 @@ func SimulateBattle(round, firstSeat int, deckA, deckB []Card, rollDie func() in
|
||||
return g.runBattle(0, 1)
|
||||
}
|
||||
|
||||
// ReplayResult re-runs a recorded battle from the result it produced. A
|
||||
// BattleResult carries everything the fight started from — both lineups, each
|
||||
// side's banked Mana/Trumpets/apples, and the tape of every die it rolled — so
|
||||
// replaying one reproduces it move for move. That is what makes a debug report
|
||||
// reproducible: drop the report in a test, replay the battle, and step through
|
||||
// the same fight the player saw.
|
||||
//
|
||||
// The two sides keep their real seat numbers, so WinnerSeat means what it did
|
||||
// in the original. Player names are not part of a result, though, so they come
|
||||
// back as "Seat N" and every event's Text reads accordingly — compare
|
||||
// structure, not prose. DebugReport.ReplayBattle, which has the real game to
|
||||
// replay against, reproduces the text too. Returns nil for a malformed result.
|
||||
func ReplayResult(res *BattleResult) *BattleResult {
|
||||
if res == nil || len(res.Seats) != 2 || len(res.Lineups) != 2 {
|
||||
return nil
|
||||
}
|
||||
g := &Game{Round: res.Round, NextCardID: res.StartCardID,
|
||||
drawReplay: append([]int(nil), res.Draws...)}
|
||||
if g.NextCardID == 0 {
|
||||
g.NextCardID = 1_000_000
|
||||
}
|
||||
// Seat the fighters where they really sat: at a bigger table the two sides
|
||||
// of one battle are not seats 0 and 1, and WinnerSeat is a table seat.
|
||||
for seat := range max(res.Seats[0], res.Seats[1]) + 1 {
|
||||
g.Players = append(g.Players, &Player{Name: fmt.Sprintf("Seat %d", seat), Seat: seat})
|
||||
}
|
||||
applyBattleInputs(g, res)
|
||||
return g.runBattle(res.Seats[0], res.Seats[1])
|
||||
}
|
||||
|
||||
// applyBattleInputs stages a game's players for a replay of res: each side's
|
||||
// lineup and the banked resources it fought with.
|
||||
func applyBattleInputs(g *Game, res *BattleResult) {
|
||||
for side, seat := range res.Seats {
|
||||
if seat < 0 || seat >= len(g.Players) {
|
||||
continue
|
||||
}
|
||||
p := g.Players[seat]
|
||||
p.Deck = append([]Card(nil), res.Lineups[side]...)
|
||||
if side < len(res.Inputs) {
|
||||
in := res.Inputs[side]
|
||||
p.Mana, p.PendingTrumpets, p.PendingApplesInPlay = in.Mana, in.Trumpets, in.ApplesInPlay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TierContents returns the full printed contents of a tier's shop deck for the
|
||||
// default pack. Cards carry placeholder IDs; they are reference data, not live
|
||||
// instances.
|
||||
|
||||
Reference in New Issue
Block a user