Files
super-auto-pets-board-game/internal/ai/predict.go
T

110 lines
2.9 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.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.TierContentsForPack(cx.v.Pack, 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(cx.v.Pack, 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
}