package main import "math/rand" // durakNamePool — имена ботов в Дураке. Отдельный пул от Тонка // (там прозвища, здесь — обычные русские имена), но по той же // логике: уровень игры бота нигде не отражается в имени, а он тут // и всего один (DurakBot{}) — градации сложности нет. var durakNamePool = []string{ "Иван", "Пётр", "Алексей", "Дмитрий", "Сергей", "Николай", "Андрей", "Михаил", } // durakNamePoolEN — англоязычная версия того же пула. var durakNamePoolEN = []string{ "John", "Peter", "Alex", "James", "Simon", "Nicholas", "Andrew", "Michael", } func activeDurakNamePool() []string { if CurrentLang() == LangEN { return durakNamePoolEN } return durakNamePool } // pickDurakBotName выбирает случайное имя из активного (для // текущего языка) пула, по возможности не повторяющее уже занятые // (used). func pickDurakBotName(used map[string]bool) string { pool := activeDurakNamePool() 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))] } // newDurakBotNames возвращает n случайных (без повторов, если // хватает пула) имён ботов для Дурака. func newDurakBotNames(n int) []string { used := make(map[string]bool, n) names := make([]string, n) for i := 0; i < n; i++ { name := pickDurakBotName(used) used[name] = true names[i] = name } return names } // DurakBot — единственная (без градации уровней) стратегия бота // для Дурака: разумная, но не идеальная игра. Бережёт козыри, // старается не подставляться и подкидывает только тогда, когда это // низкорисково. type DurakBot struct{} // DecideAttack решает, какой картой открыть раунд. Вызывается // только в фазе DurakPhaseAttack — это обязательный первый ход, // отказаться нельзя, поэтому второе возвращаемое значение всегда // true, если на руке вообще есть карты (иначе бот не должен был // стать атакующим в принципе). func (DurakBot) DecideAttack(g *DurakGameState) (Card, bool) { hand := g.attacker().Hand if len(hand) == 0 { return Card{}, false } return weakestCard(hand, g.TrumpSuit, true), true } // DecideThrowOrPass решает, что делать игроку, чья сейчас очередь в // фазе DurakPhaseThrowIn: подкинуть карту подходящего ранга или // пасовать. Возвращает (card, true), если нужно подкинуть card, // иначе (_, false), если нужно пасовать (вызывающий код вызывает // g.Pass()). func (DurakBot) DecideThrowOrPass(g *DurakGameState) (Card, bool) { throwerIdx := g.CurrentThrower() if throwerIdx == -1 { return Card{}, false } hand := g.Players[throwerIdx].Hand if len(g.Table) >= g.attackCap { return Card{}, false } var candidates []Card for _, c := range hand { if g.tableHasRank(c.Rank) { candidates = append(candidates, c) } } if len(candidates) == 0 { return Card{}, false } return weakestCard(candidates, g.TrumpSuit, true), true } // DecideDefend решает, чем отбиваться от последней неотбитой карты // на столе. Возвращает (card, true), если нужно отбить card, иначе // (_, false), если нужно забрать стол (вызывающий код вызывает // g.Take()). Вызывать имеет смысл только когда Phase == // DurakPhaseDefend. func (DurakBot) DecideDefend(g *DurakGameState) (Card, bool) { if len(g.Table) == 0 { return Card{}, false } attackCard := g.Table[len(g.Table)-1].Attack hand := g.defender().Hand var best *Card for i, c := range hand { if !durakBeats(c, attackCard, g.TrumpSuit) { continue } if best == nil || durakDefendCost(c, g.TrumpSuit) < durakDefendCost(*best, g.TrumpSuit) { cc := hand[i] best = &cc } } if best == nil { return Card{}, false } return *best, true } // weakestCard находит самую "дешёвую" карту в наборе: если // preferNonTrump=true и есть некозырные карты, ищет минимум среди // них; иначе ищет минимум по всему набору. "Минимум" — по // старшинству в Дураке (durakRankOrder). func weakestCard(cards []Card, trump Suit, preferNonTrump bool) Card { pool := cards if preferNonTrump { var nonTrump []Card for _, c := range cards { if c.Suit != trump { nonTrump = append(nonTrump, c) } } if len(nonTrump) > 0 { pool = nonTrump } } best := pool[0] for _, c := range pool[1:] { if durakRankOrder[c.Rank] < durakRankOrder[best.Rank] { best = c } } return best } // durakDefendCost оценивает "стоимость" использования карты для // защиты: некозырные карты дешевле козырей (козыри — ценный // ресурс, тратить их на отбой лишний раз не хочется), а внутри // каждой категории дешевле карты младше. func durakDefendCost(c Card, trump Suit) int { base := durakRankOrder[c.Rank] if c.Suit == trump { return base + 100 // козырь всегда "дороже" любой некозырной карты } return base } // PlayFullTurn исполняет одно решение бота: подкинуть/отбиться/ // пасовать/забрать стол — используется при автоматической прокрутке // партии. Возвращает ошибку только если что-то пошло не так на // уровне игровой логики (не должно случаться при корректной работе // решающих функций). func (b DurakBot) PlayFullTurn(g *DurakGameState) error { switch g.Phase { case DurakPhaseAttack: if card, ok := b.DecideAttack(g); ok { return g.ThrowIn(card) } return nil // не должно происходить: у атакующего не может не быть карт case DurakPhaseDefend: if card, ok := b.DecideDefend(g); ok { return g.Defend(card) } return g.Take() case DurakPhaseThrowIn: if card, ok := b.DecideThrowOrPass(g); ok { return g.ThrowIn(card) } return g.Pass() } return nil }