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.
93 lines
3.7 KiB
Go
93 lines
3.7 KiB
Go
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
|
|
// a scratch game and awards nothing, so it never touches real state — callers
|
|
// (notably the AI player) can roll out as many what-if battles as they like.
|
|
// Dice rolls are random unless rollDie is non-nil.
|
|
func SimulateBattle(round, firstSeat int, deckA, deckB []Card, rollDie func() int) *BattleResult {
|
|
g := &Game{
|
|
Round: round,
|
|
RollDie: rollDie,
|
|
// Cards minted during the simulation (apples, bees) get IDs far away
|
|
// from real ones, purely to avoid confusion when reading results.
|
|
NextCardID: 1_000_000,
|
|
Players: []*Player{
|
|
{Name: "A", Seat: 0, Deck: append([]Card(nil), deckA...)},
|
|
{Name: "B", Seat: 1, Deck: append([]Card(nil), deckB...)},
|
|
},
|
|
}
|
|
if firstSeat == 1 {
|
|
return g.runBattle(1, 0)
|
|
}
|
|
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.
|
|
func TierContents(tier int) []Card {
|
|
return TierContentsForPacks([]string{DefaultPack}, tier)
|
|
}
|
|
|
|
// TierContentsForPacks returns the printed tier contents of a pack selection,
|
|
// combined the way the shop decks combine them — public information from the
|
|
// boxes. Used by the AI, which decides from a View that names its packs.
|
|
func TierContentsForPacks(packs []string, tier int) []Card {
|
|
scratch := &Game{Packs: packs}
|
|
scratch.buildShopDecks()
|
|
if tier < 1 || tier > len(scratch.ShopDecks) {
|
|
return nil
|
|
}
|
|
return scratch.ShopDecks[tier-1]
|
|
}
|