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.PlayerView(v.YouSeat), oppSeat: v.YourOpponent, pools: map[int][]game.Card{}} // The round's pairing says exactly who the bot is preparing for, so it // plans against that one rival even at a six-player table. Falling back to // any other seat keeps a malformed view from wedging the bot. if cx.oppSeat == cx.me.Seat || cx.oppSeat < 0 { for _, p := range v.Players { if p.Seat != v.YouSeat { cx.oppSeat = p.Seat break } } } return cx } // opp is the model of the opponent this round's battle is against. func (cx *ctx) opp() *OppModel { return cx.m.Opp(cx.oppSeat) } 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, every 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. It counts against // the whole table's known cards, and against the combined contents of every // pack in play, which is what a human counting cards would be working from. 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 _, opp := range cx.m.Opps { for _, c := range opp.Known { note(c) } } for _, c := range cx.v.ShopRow { note(c) } var pool []game.Card for _, c := range game.TierContentsForPacks(cx.v.Packs, 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 { opp := cx.opp() deck := append([]game.Card(nil), opp.Known...) for _, h := range opp.Hidden { var c game.Card if t, ok := templateByName(cx.v.Packs, 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 }