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:
@@ -211,6 +211,33 @@ type BattleResult struct {
|
||||
// is apples each side banked for next round's hand (Skeleton Dog).
|
||||
ManaAfter []int `json:"manaAfter,omitempty"`
|
||||
NextRoundApples []int `json:"nextRoundApples,omitempty"`
|
||||
// Draws is every random draw this battle made, in order: rock die faces,
|
||||
// Komodo's apple shuffle, random target picks. It's a recording, not an
|
||||
// input — feed it back through ReplayResult and the identical battle plays
|
||||
// out, which is how a debug report reproduces a fight that went wrong.
|
||||
// Public like the rest of the result: the dice are rolled in the open.
|
||||
Draws []int `json:"draws,omitempty"`
|
||||
// Inputs is the rest of what the battle started from, per side, and
|
||||
// StartCardID is the card-id counter it began minting apples at. Recorded
|
||||
// because the round clears those banks the moment the battles end, so
|
||||
// without them a result can't be replayed after the fact. Nothing here is
|
||||
// private: the battle already announces each of them in its own events.
|
||||
Inputs []BattleInputs `json:"inputs,omitempty"`
|
||||
StartCardID int `json:"startCardId,omitempty"`
|
||||
}
|
||||
|
||||
// Replayable reports whether this result carries the recording a faithful
|
||||
// replay needs. Results saved before the engine recorded battles don't: their
|
||||
// lineups are still on file, so the fight can be re-run, but the dice will fall
|
||||
// where they may and the outcome may differ from what the player saw.
|
||||
func (r *BattleResult) Replayable() bool { return r.StartCardID > 0 }
|
||||
|
||||
// BattleInputs is the persistent player state one side brought into a battle —
|
||||
// everything runBattle reads off the Player besides the arranged lineup.
|
||||
type BattleInputs struct {
|
||||
Mana int `json:"mana,omitempty"` // Player.Mana (Unicorn)
|
||||
Trumpets int `json:"trumpets,omitempty"` // Player.PendingTrumpets (Golden)
|
||||
ApplesInPlay int `json:"applesInPlay,omitempty"` // Player.PendingApplesInPlay (Golden)
|
||||
}
|
||||
|
||||
// Side returns the battle-side index (0 or 1) for a seat at the table, or -1
|
||||
@@ -458,6 +485,13 @@ func (g *Game) runBattle(first, second int) *BattleResult {
|
||||
seats := []int{first, second}
|
||||
res := &BattleResult{Round: g.Round, WinnerSeat: -1, Seats: seats,
|
||||
StackSizes: make([]int, n), Lineups: make([][]Card, n)}
|
||||
// Every die this battle rolls lands on the tape and ships with the result,
|
||||
// so a debug report can replay the fight exactly (see debug.go). The tape
|
||||
// belongs to one battle: start it empty and hand it over on the way out.
|
||||
g.drawTape = nil
|
||||
defer func() { res.Draws, g.drawTape, g.drawReplay = g.drawTape, nil, nil }()
|
||||
res.StartCardID = g.NextCardID
|
||||
res.Inputs = make([]BattleInputs, n)
|
||||
sides := make([]*battleSide, n)
|
||||
emit := func(ev BattleEvent) { res.Events = append(res.Events, ev) }
|
||||
// pname is the owning player's display name for a side, for log text.
|
||||
@@ -472,6 +506,12 @@ func (g *Game) runBattle(first, second int) *BattleResult {
|
||||
sides[side] = s
|
||||
res.StackSizes[side] = len(p.Deck)
|
||||
res.Lineups[side] = append([]Card(nil), p.Deck...)
|
||||
// Everything else runBattle reads off the Player, banked for the replay.
|
||||
res.Inputs[side] = BattleInputs{
|
||||
Mana: p.Mana,
|
||||
Trumpets: p.PendingTrumpets,
|
||||
ApplesInPlay: p.PendingApplesInPlay,
|
||||
}
|
||||
}
|
||||
// enemyOf returns the opposing side.
|
||||
enemyOf := func(seat int) *battleSide { return sides[(seat+1)%n] }
|
||||
|
||||
Reference in New Issue
Block a user