package ai import ( "fmt" "math/rand/v2" "github.com/greyson/super-auto-pets-board-game/internal/game" ) // ctx is per-decision scratch state: the view, the memory, and caches shared // by every candidate evaluated in the same decision. type ctx struct { v *game.View m *Memory me *game.PlayerView oppSeat int pools map[int][]game.Card // unseen cards per tier, for hidden-card guesses simID int } func newCtx(v *game.View, m *Memory) *ctx { cx := &ctx{v: v, m: m, me: &v.Players[v.YouSeat], oppSeat: m.Opp.Seat, pools: map[int][]game.Card{}} if cx.oppSeat == cx.me.Seat || cx.oppSeat < 0 { // Memory hasn't observed yet (shouldn't happen in practice). for _, p := range v.Players { if p.Seat != v.YouSeat { cx.oppSeat = p.Seat } } } return cx } func (cx *ctx) nextSimID() string { cx.simID++ return fmt.Sprintf("sim-%d", cx.simID) } // unseenPool lists the printed cards of a tier that the bot cannot account // for anywhere it can see — its own deck, the opponent model, the shop row. // Hidden opponent cards are drawn from this pool, so the bot's guesses // respect card counting without peeking at the real decks. func (cx *ctx) unseenPool(tier int) []game.Card { if pool, ok := cx.pools[tier]; ok { return pool } seen := map[string]int{} note := func(c game.Card) { if c.Tier == tier { seen[c.Name]++ } } for _, c := range cx.me.Deck { note(c) } for _, c := range cx.m.Opp.Known { note(c) } for _, c := range cx.v.ShopRow { note(c) } var pool []game.Card for _, c := range game.TierContents(tier) { if seen[c.Name] > 0 { seen[c.Name]-- continue } pool = append(pool, c) } cx.pools[tier] = pool return pool } // sampleOppDeck instantiates one concrete guess at the opponent's deck: // known cards as-is, hidden cards drawn from the unseen pool of their tier // (or their named template, when a pick was later revealed). func (cx *ctx) sampleOppDeck() []game.Card { deck := append([]game.Card(nil), cx.m.Opp.Known...) for _, h := range cx.m.Opp.Hidden { var c game.Card if t, ok := templateByName(h.Name); ok { c = t } else if pool := cx.unseenPool(h.Tier); len(pool) > 0 { c = pool[rand.IntN(len(pool))] } else { continue // tier exhausted and fully accounted for; nothing to guess } c.ID = cx.nextSimID() deck = append(deck, c) } return deck } // oppArrangements produces n independent guesses of what the opponent will // field: each is a sampled deck put in a plausible order by the same // heuristic the bot itself uses, with enough noise that the bot prepares for // a range of opponent plans rather than assuming one. The first guess is the // noise-free "book" ordering. func (cx *ctx) oppArrangements(n int) [][]game.Card { out := make([][]game.Card, 0, n) for i := range n { temp := 0.8 if i == 0 { temp = 0 } out = append(out, heuristicOrder(cx.sampleOppDeck(), temp)) } return out }