191 lines
7 KiB
Go
191 lines
7 KiB
Go
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
|
||
}
|