package main import "math/rand" // BotStrategy — стратегия одного полного хода бота: взять карту, // при необходимости выложить/подложить комбинации, сбросить карту // (либо объявить Тонк, если это входит в стратегию). type BotStrategy interface { PlayTurn(g *GameState) error } // dropRiskThreshold — вес руки, ниже которого боты готовы рискнуть // дропом. Строже порога Тонка (см. DefaultTonkThreshold), потому что // у дропа, в отличие от Тонка, нет встроенной защиты от ошибки: // объявить его может каждый в начале своего хода, и при ошибке // придётся доплатить штраф. Бот при этом решает только по своей // руке — он не подглядывает в карты соперников, как и человек. const dropRiskThreshold = 3 // maybeBotDrop проверяет, стоит ли боту рискнуть дропом прямо перед // взятием карты (единственный момент, когда дроп разрешён). Если да // — объявляет его и возвращает true (ход на этом закончен). func maybeBotDrop(g *GameState) (bool, error) { if g.Phase != PhaseDraw { return false, nil } if HandValue(g.current().Hand) > dropRiskThreshold { return false, nil } _, err := g.Drop() return true, err } // botLevel — запись в реестре доступных уровней сложности ботов: // используется и для явного назначения бота на место, и для // случайного выбора (например, при замене выбывшего игрока). type botLevel struct { Suffix string // короткое имя уровня для отображения в UI, напр. "средний" New func() BotStrategy } var botLevels = []botLevel{ {Suffix: "простой", New: func() BotStrategy { return SimpleBot{} }}, {Suffix: "средний", New: func() BotStrategy { return MediumBot{} }}, {Suffix: "Тонк", New: func() BotStrategy { return TonkBot{} }}, } // randomBotStrategy возвращает бота случайного уровня сложности из // botLevels вместе с человекочитаемым названием уровня. Уровень // используется только во внутренней логике/тестах — в интерфейсе // игрока он не показывается (уровень бота держится в секрете). func randomBotStrategy() (BotStrategy, string) { lvl := botLevels[rand.Intn(len(botLevels))] return lvl.New(), lvl.Suffix } // botNamePool — имена, которые получают боты за столом. Уровень // сложности в имени никак не отражается — игрок не должен заранее // знать, с кем имеет дело. var botNamePool = []string{"Костоправ", "Гоблин", "Одноглазый", "Ворон", "Ильмо"} // botNamePoolEN — те же самые персонажи, но по-английски: смысловые // прозвища переведены (Костоправ -> Bonesetter и т.д.), а не просто // транслитерированы, чтобы сохранить характер имён. var botNamePoolEN = []string{"Bonesetter", "Goblin", "One-Eye", "Raven", "Ilmo"} func activeBotNamePool() []string { if CurrentLang() == LangEN { return botNamePoolEN } return botNamePool } // pickBotName выбирает случайное имя из активного (для текущего // языка) пула имён, по возможности не повторяющее уже занятые имена // (переданные в used). Если все имена из пула уже заняты, допускает // повтор — это лучше, чем оставить место вовсе без имени. func pickBotName(used map[string]bool) string { pool := activeBotNamePool() 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))] } // newBotRoster формирует n мест за столом: у каждого — случайное // (без повторов, если хватает имён в пуле) имя из botNamePool и // независимо выбранный случайный уровень сложности. Порядок уровней // не фиксирован (простой/средний/сложный может стоять в любом // месте, включая повторы) — это осознанное решение, чтобы нельзя // было угадать силу бота по его месту за столом. func newBotRoster(n int) (names []string, strategies []BotStrategy) { used := make(map[string]bool, n) names = make([]string, n) strategies = make([]BotStrategy, n) for i := 0; i < n; i++ { name := pickBotName(used) used[name] = true names[i] = name strat, _ := randomBotStrategy() strategies[i] = strat } return names, strategies } // --- Уровень 1: простой (жадный) бот ----------------------------- // // Берёт из сброса, только если карта сразу закрывает готовую // комбинацию, иначе берёт из колоды. Выкладывает всё, что может. // Сбрасывает самую тяжёлую карту, не участвующую ни в одной // потенциальной комбинации. type SimpleBot struct{} func (SimpleBot) PlayTurn(g *GameState) error { if dropped, err := maybeBotDrop(g); dropped { return err } if err := simpleDrawDecision(g); err != nil { return err } if g.Phase == PhaseGameOver { return nil } // по правилам Тонк может объявить любой игрок, не только // специализированный TonkBot — иначе бот способен бесконечно // держать на руках одну ненужную карту if CanDeclareTonk(g.current().Hand, g.TonkThreshold) { _, err := g.DeclareTonk() return err } layAllAvailableMelds(g) if g.Phase == PhaseGameOver { return nil } tryExtendTableMelds(g) if g.Phase == PhaseGameOver { return nil } discard := chooseDiscardSimple(g.current().Hand) return g.Discard(discard) } func simpleDrawDecision(g *GameState) error { if top, ok := g.PeekDiscard(); ok && wouldFormMeld(g.current().Hand, top) { _, err := g.DrawFromDiscard() return err } _, err := g.DrawFromDeck() return err } // chooseDiscardSimple выбирает карту с нулевым потенциалом // (не участвующую ни в одной намечающейся комбинации) и // максимальной очковой стоимостью среди таких. Если все карты // хоть немного полезны — сбрасывает просто самую дорогую. func chooseDiscardSimple(hand []Card) Card { var deadWeight []Card for _, c := range hand { if cardPotential(hand, c) == 0 { deadWeight = append(deadWeight, c) } } pool := deadWeight if len(pool) == 0 { pool = hand } best := pool[0] for _, c := range pool[1:] { if c.Value() > best.Value() { best = c } } return best } // --- Уровень 2: средний бот --------------------------------------- // // Оценивает руку целиком через cardPotential: держит карты, // близкие к комбинации, берёт из сброса не только при немедленном // закрытии сета/сиквенса, но и при заметном приросте потенциала. // Сбрасывает "мёртвый груз" — карту с наименьшим потенциалом. type MediumBot struct{} func (MediumBot) PlayTurn(g *GameState) error { if dropped, err := maybeBotDrop(g); dropped { return err } if err := mediumDrawDecision(g); err != nil { return err } if g.Phase == PhaseGameOver { return nil } if CanDeclareTonk(g.current().Hand, g.TonkThreshold) { _, err := g.DeclareTonk() return err } layAllAvailableMelds(g) if g.Phase == PhaseGameOver { return nil } tryExtendTableMelds(g) if g.Phase == PhaseGameOver { return nil } discard := deadestCard(g.current().Hand) return g.Discard(discard) } func mediumDrawDecision(g *GameState) error { top, ok := g.PeekDiscard() if ok { if wouldFormMeld(g.current().Hand, top) { _, err := g.DrawFromDiscard() return err } // Берём из сброса и без немедленного меlda, если карта // заметно усиливает руку (потенциал 2+ означает, что она // почти достраивает сет или сиквенс). trial := append(append([]Card{}, g.current().Hand...), top) if cardPotential(trial, top) >= 2 { _, err := g.DrawFromDiscard() return err } } _, err := g.DrawFromDeck() return err } // --- Уровень 3: бот со стратегией Тонка --------------------------- // // Действует как средний бот, но после каждого взятия карты // проверяет, можно ли объявить Тонк, и делает это немедленно, не // дожидаясь выкладывания комбинаций. Добавляет небольшую долю // случайности в выбор сбрасываемой карты, чтобы не быть полностью // предсказуемым. type TonkBot struct { Threshold int // порог объявления Тонка; 0 — использовать g.TonkThreshold } func (b TonkBot) PlayTurn(g *GameState) error { if dropped, err := maybeBotDrop(g); dropped { return err } if err := mediumDrawDecision(g); err != nil { return err } if g.Phase == PhaseGameOver { return nil } threshold := b.Threshold if threshold == 0 { threshold = g.TonkThreshold } if CanDeclareTonk(g.current().Hand, threshold) { _, err := g.DeclareTonk() return err } layAllAvailableMelds(g) if g.Phase == PhaseGameOver { return nil } tryExtendTableMelds(g) if g.Phase == PhaseGameOver { return nil } discard := chooseDiscardWithNoise(g.current().Hand) return g.Discard(discard) } // chooseDiscardWithNoise в большинстве случаев сбрасывает самую // "мёртвую" карту, но с вероятностью ~15% сбрасывает вторую по // бесполезности — чтобы поведение бота было чуть менее // предсказуемым для человека, играющего против него не первый раз. func chooseDiscardWithNoise(hand []Card) Card { if len(hand) == 1 { return hand[0] } type scored struct { card Card potential int } ranked := make([]scored, len(hand)) for i, c := range hand { ranked[i] = scored{card: c, potential: cardPotential(hand, c)} } // сортировка пузырьком — рука короткая (обычно до 10-12 карт), // оптимизация не нужна for i := 0; i < len(ranked); i++ { for j := i + 1; j < len(ranked); j++ { worse := ranked[j].potential < ranked[i].potential || (ranked[j].potential == ranked[i].potential && ranked[j].card.Value() > ranked[i].card.Value()) if worse { ranked[i], ranked[j] = ranked[j], ranked[i] } } } if len(ranked) >= 2 && rand.Intn(100) < 15 { return ranked[1].card } return ranked[0].card }