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 }