Fix shop action costs and pass semantics.
Only buying costs gold; selling and trading (Triple) are free. Pass is now a final action, legal only at or under the pet limit: it forfeits remaining gold and ends that player's shopping for the round, with the shop closing once everyone has passed. That makes the separate cleanup phase unreachable (you sell down in-shop before passing), so it is removed. The client asks for confirmation before passing, and the bot knows buys are the only coin sink, when passing is legal, and that it must sell down before it can pass.
This commit is contained in:
+41
-58
@@ -46,10 +46,11 @@ func (cx *ctx) simApple() game.Card {
|
||||
}
|
||||
}
|
||||
|
||||
// previewCleanup applies the forced end-of-shop sale to a hypothetical deck:
|
||||
// while over the pet limit, the lowest-value pet is sold for an apple. This
|
||||
// lets the bot buy a sixth pet on purpose, knowing what it will cost.
|
||||
func (cx *ctx) previewCleanup(deck []game.Card) []game.Card {
|
||||
// previewSellDown applies the sell-down a pass would eventually force onto a
|
||||
// hypothetical deck: while over the pet limit, the lowest-value pet is sold
|
||||
// for an apple. This lets the bot buy a sixth pet on purpose, knowing what
|
||||
// it will cost.
|
||||
func (cx *ctx) previewSellDown(deck []game.Card) []game.Card {
|
||||
for {
|
||||
pets := 0
|
||||
worst, worstVal := -1, 0.0
|
||||
@@ -92,37 +93,50 @@ func (b *Bot) score(cx *ctx, cands []candidate) {
|
||||
}
|
||||
|
||||
// decideShop picks one shop action: buy a row card, sell some own cards,
|
||||
// trade in a suit triple, or pass.
|
||||
// trade in a suit triple, or pass. Only buying costs gold; selling and
|
||||
// trading are free, and passing (the only way to end the round's shopping)
|
||||
// is legal only at or under the pet limit.
|
||||
func (b *Bot) decideShop(v *game.View, mem *Memory) *Action {
|
||||
cx := newCtx(v, mem)
|
||||
deck := cx.me.Deck
|
||||
var cands []candidate
|
||||
|
||||
// Passing forfeits the bot's remaining coins; it is the baseline every
|
||||
// other option must beat, with a nudge because spending is usually right.
|
||||
cands = append(cands, candidate{
|
||||
act: &Action{Type: "pass"},
|
||||
decks: [][]game.Card{slices.Clone(deck)},
|
||||
bias: -0.02,
|
||||
})
|
||||
|
||||
for i, c := range v.ShopRow {
|
||||
if c.ID == "" {
|
||||
continue
|
||||
// Passing ends the bot's shopping for the round; it is the baseline every
|
||||
// other option must beat, nudged down while unspent coins remain because
|
||||
// spending them is usually right. Illegal over the pet limit — the sell
|
||||
// and trade candidates below always exist then, so the bot works its way
|
||||
// back under.
|
||||
if cx.me.PetCount <= v.MaxPets {
|
||||
bias := 0.0
|
||||
if cx.me.Coins > 0 {
|
||||
bias = -0.02
|
||||
}
|
||||
nd := append(slices.Clone(deck), c)
|
||||
nd = cx.applyTemplateShopEffects(nd, c, game.TriggerBuy)
|
||||
nd = cx.previewCleanup(nd)
|
||||
cands = append(cands, candidate{
|
||||
act: &Action{Type: "buy", Row: i},
|
||||
decks: [][]game.Card{nd},
|
||||
act: &Action{Type: "pass"},
|
||||
decks: [][]game.Card{slices.Clone(deck)},
|
||||
bias: bias,
|
||||
})
|
||||
}
|
||||
|
||||
// Sell candidates: the worst 1, 2, or 3 keepers. One gold sells any
|
||||
// number of cards, so bulk-dumping junk before a battle is one action.
|
||||
// Temporary cards are excluded — selling an apple for an apple is a pure
|
||||
// waste of gold.
|
||||
if cx.me.Coins > 0 {
|
||||
for i, c := range v.ShopRow {
|
||||
if c.ID == "" {
|
||||
continue
|
||||
}
|
||||
nd := append(slices.Clone(deck), c)
|
||||
nd = cx.applyTemplateShopEffects(nd, c, game.TriggerBuy)
|
||||
nd = cx.previewSellDown(nd)
|
||||
cands = append(cands, candidate{
|
||||
act: &Action{Type: "buy", Row: i},
|
||||
decks: [][]game.Card{nd},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sell candidates: the worst 1, 2, or 3 keepers. Selling is free and
|
||||
// takes any number of cards, so bulk-dumping junk before a battle is one
|
||||
// action. Temporary cards are excluded — selling an apple for an apple
|
||||
// does nothing.
|
||||
sellable := slices.Clone(deck)
|
||||
sellable = slices.DeleteFunc(sellable, func(c game.Card) bool { return c.Temporary })
|
||||
slices.SortStableFunc(sellable, func(a, b game.Card) int {
|
||||
@@ -195,7 +209,7 @@ func (b *Bot) decideShop(v *game.View, mem *Memory) *Action {
|
||||
reward.ID = cx.nextSimID()
|
||||
nd := append(slices.Clone(base), reward)
|
||||
nd = cx.applyTemplateShopEffects(nd, reward, game.TriggerBuy)
|
||||
nd = cx.previewCleanup(nd)
|
||||
nd = cx.previewSellDown(nd)
|
||||
decks = append(decks, nd)
|
||||
}
|
||||
cands = append(cands, candidate{
|
||||
@@ -217,7 +231,7 @@ func (b *Bot) decideTradeChoose(v *game.View, mem *Memory) *Action {
|
||||
for pick, c := range v.Pending.Options {
|
||||
nd := append(slices.Clone(cx.me.Deck), c)
|
||||
nd = cx.applyTemplateShopEffects(nd, c, game.TriggerBuy)
|
||||
nd = cx.previewCleanup(nd)
|
||||
nd = cx.previewSellDown(nd)
|
||||
cands = append(cands, candidate{
|
||||
act: &Action{Type: "tradeChoose", Pick: pick},
|
||||
decks: [][]game.Card{nd},
|
||||
@@ -227,34 +241,3 @@ func (b *Bot) decideTradeChoose(v *game.View, mem *Memory) *Action {
|
||||
return b.pick(cands).act
|
||||
}
|
||||
|
||||
// decideCleanup performs the forced sale down to the pet limit, dumping the
|
||||
// lowest-value pets. This one is deterministic at every difficulty — even a
|
||||
// weak player doesn't discard their best pet by accident.
|
||||
func (b *Bot) decideCleanup(v *game.View, mem *Memory) *Action {
|
||||
cx := newCtx(v, mem)
|
||||
excess := cx.me.PetCount - v.MaxPets
|
||||
if excess <= 0 {
|
||||
return nil
|
||||
}
|
||||
pets := make([]game.Card, 0, cx.me.PetCount)
|
||||
for _, c := range cx.me.Deck {
|
||||
if c.IsPet() {
|
||||
pets = append(pets, c)
|
||||
}
|
||||
}
|
||||
slices.SortStableFunc(pets, func(a, b game.Card) int {
|
||||
av, bv := keepValue(a), keepValue(b)
|
||||
switch {
|
||||
case av < bv:
|
||||
return -1
|
||||
case av > bv:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
ids := make([]string, 0, excess)
|
||||
for _, p := range pets[:excess] {
|
||||
ids = append(ids, p.ID)
|
||||
}
|
||||
return &Action{Type: "sell", Cards: ids}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user