110 lines
2.9 KiB
Go
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.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
|
|
}
|