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] }