Add bot to play against.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user