Files

121 lines
3.4 KiB
Go

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
}