go_games_collection/thousand_bot.go

234 lines
7.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import "math/rand"
// thousandNamePool — имена ботов в 1000. Отдельный пул — мужские
// имена для разнообразия среди игр коллекции.
var thousandNamePool = []string{
"Виктор", "Роман", "Максим", "Артём", "Кирилл", "Егор", "Денис", "Павел",
}
// thousandNamePoolEN — англоязычная версия того же пула.
var thousandNamePoolEN = []string{
"Victor", "Roman", "Max", "Arthur", "Cyril", "Eric", "Dennis", "Paul",
}
func activeThousandNamePool() []string {
if CurrentLang() == LangEN {
return thousandNamePoolEN
}
return thousandNamePool
}
func pickThousandBotName(used map[string]bool) string {
pool := activeThousandNamePool()
free := make([]string, 0, len(pool))
for _, name := range pool {
if !used[name] {
free = append(free, name)
}
}
if len(free) == 0 {
free = pool
}
return free[rand.Intn(len(free))]
}
func newThousandBotNames(n int) []string {
used := make(map[string]bool, n)
names := make([]string, n)
for i := 0; i < n; i++ {
name := pickThousandBotName(used)
used[name] = true
names[i] = name
}
return names
}
// ThousandBot — единственная (без градации уровней) стратегия
// бота для 1000: оценивает силу руки для торгов, сбрасывает
// заведомо слабые карты, старается объявлять марьяжи и играть
// дёшево там, где нельзя выиграть взятку.
type ThousandBot struct{}
// estimateHandStrength грубо оценивает, сколько очков реалистично
// набрать с такой рукой: сумма очков карт на руке плюс стоимость
// лучшего марьяжа (марьяж считается один раз, даже если их
// несколько, — консервативная оценка).
func estimateHandStrength(hand []Card) int {
sum := 0
for _, c := range hand {
sum += thousandCardValue(c.Rank)
}
return sum + bestMarriageValue(hand)
}
// DecideBid решает, поднимать ли ставку в текущих торгах и до
// какой суммы, либо пасовать. Возвращает (amount, true), если нужно
// вызвать g.Bid(amount), иначе (_, false) для g.Pass().
func (ThousandBot) DecideBid(g *ThousandGameState) (int, bool) {
p := g.current()
estimate := estimateHandStrength(p.Hand)
capAmt := g.maxBidFor(g.CurrentBidderIdx)
minRequired := g.HighBid + 5
mandatory := g.bidsMade == 0 && g.CurrentBidderIdx == g.firstBidderIdx
if mandatory {
minRequired = 100
}
if minRequired > capAmt {
if mandatory {
return 100, true
}
return 0, false
}
if estimate+10 < minRequired && !mandatory {
return 0, false
}
bid := minRequired
if bid > capAmt {
bid = capAmt
}
return bid, true
}
// DecideDiscards выбирает 2 карты для сноса сопернику: самые
// дешёвые карты руки, по возможности не разбивая марьяж.
func (ThousandBot) DecideDiscards(hand []Card) [2]Card {
type scored struct {
card Card
cost int
}
list := make([]scored, len(hand))
for i, c := range hand {
cost := thousandCardValue(c.Rank)
if (c.Rank == King || c.Rank == Queen) && hasMarriage(hand, c.Suit) {
cost += 1000
}
list[i] = scored{card: c, cost: cost}
}
for i := 0; i < len(list); i++ {
for j := i + 1; j < len(list); j++ {
if list[j].cost < list[i].cost {
list[i], list[j] = list[j], list[i]
}
}
}
return [2]Card{list[0].card, list[1].card}
}
// DecidePlay решает, какую карту сыграть в текущей взятке: если
// ходит первым и есть необъявленный марьяж (не на первой взятке
// раздачи) — предпочитает объявить самый дорогой; иначе играет
// самую дешёвую из разрешённых карт (по возможности выигрышную
// дёшево, а если выиграть нельзя — минимальные потери).
func (ThousandBot) DecidePlay(g *ThousandGameState) Card {
actor := g.currentTrickActor()
hand := g.Players[actor].Hand
leading := len(g.CurrentTrick) == 0
if leading && g.TricksPlayed > 0 {
bestSuit := Suit(-1)
bestValue := -1
for s := Clubs; s <= Spades; s++ {
if hasMarriage(hand, s) {
if v := thousandMarriageValue[s]; v > bestValue {
bestValue = v
bestSuit = s
}
}
}
if bestValue >= 0 {
return Card{Suit: bestSuit, Rank: King}
}
}
var legal []Card
for _, c := range hand {
if leading || g.isValidPlay(hand, c) {
legal = append(legal, c)
}
}
if len(legal) == 0 {
legal = hand
}
if leading {
return weakestByThousandRank(legal)
}
led, _ := g.ledSuit()
currentBest := g.CurrentTrick[0]
bestIsTrump := g.TrumpSet && currentBest.Card.Suit == g.TrumpSuit
for _, play := range g.CurrentTrick[1:] {
isTrump := g.TrumpSet && play.Card.Suit == g.TrumpSuit
if (isTrump && !bestIsTrump) ||
(isTrump == bestIsTrump && play.Card.Suit == currentBest.Card.Suit &&
thousandRankOrder[play.Card.Rank] > thousandRankOrder[currentBest.Card.Rank]) {
currentBest = play
bestIsTrump = isTrump
}
}
var winners []Card
for _, c := range legal {
isTrump := g.TrumpSet && c.Suit == g.TrumpSuit
wins := false
switch {
case isTrump && !bestIsTrump:
wins = true
case isTrump == bestIsTrump && c.Suit == currentBest.Card.Suit &&
thousandRankOrder[c.Rank] > thousandRankOrder[currentBest.Card.Rank]:
wins = true
case !bestIsTrump && c.Suit == led && currentBest.Card.Suit != led:
wins = true
}
if wins {
winners = append(winners, c)
}
}
if len(winners) > 0 {
return weakestByThousandRank(winners)
}
return weakestByThousandRank(legal)
}
// weakestByThousandRank находит карту с наименьшим старшинством в
// наборе (при равенстве — с меньшей очковой стоимостью).
func weakestByThousandRank(cards []Card) Card {
best := cards[0]
for _, c := range cards[1:] {
if thousandRankOrder[c.Rank] < thousandRankOrder[best.Rank] ||
(thousandRankOrder[c.Rank] == thousandRankOrder[best.Rank] && thousandCardValue(c.Rank) < thousandCardValue(best.Rank)) {
best = c
}
}
return best
}
// PlayFullTurn исполняет одно решение бота (в контексте 1000 —
// один шаг торгов, снос, или один ход во взятке) — используется
// при автоматической прокрутке партии.
func (b ThousandBot) PlayFullTurn(g *ThousandGameState) error {
switch g.Phase {
case ThousandPhaseBidding:
if amount, ok := b.DecideBid(g); ok {
return g.Bid(amount)
}
return g.Pass()
case ThousandPhaseDiscard:
discards := b.DecideDiscards(g.Players[g.DeclarerIdx].Hand)
if err := g.DiscardCard(discards[0]); err != nil {
return err
}
if g.Phase != ThousandPhaseDiscard {
return nil
}
return g.DiscardCard(discards[1])
case ThousandPhaseTrick:
card := b.DecidePlay(g)
return g.PlayCard(card)
}
return nil
}