go_games_collection/durak_bot.go
2026-07-04 13:51:04 +03:00

177 lines
6.6 KiB
Go
Raw 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"
// durakNamePool — имена ботов в Дураке. Отдельный пул от Тонка
// (там прозвища, здесь — обычные русские имена), но по той же
// логике: уровень игры бота нигде не отражается в имени, а он тут
// и всего один (DurakBot{}) — градации сложности нет.
var durakNamePool = []string{
"Иван", "Пётр", "Алексей", "Дмитрий", "Сергей", "Николай", "Андрей", "Михаил",
}
// pickDurakBotName выбирает случайное имя из durakNamePool, по
// возможности не повторяющее уже занятые (used).
func pickDurakBotName(used map[string]bool) string {
free := make([]string, 0, len(durakNamePool))
for _, name := range durakNamePool {
if !used[name] {
free = append(free, name)
}
}
if len(free) == 0 {
free = durakNamePool
}
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
}