package main import "math/rand" // ПРИМЕЧАНИЕ О СИЛЕ ИГРЫ: сильный бот для Го в реальности требует // Monte-Carlo поиска по дереву или нейросетевой оценки позиции // (как в AlphaGo) — минимакс тут почти бесполезен из-за огромного // ветвления (до нескольких сотен ходов на 19x19). Все три уровня // ниже — эвристические, БЕЗ поиска вперёд по дереву партии, и // играют существенно слабее сильного человека даже на "Сильном" // уровне. Это осознанное ограничение, а не недоработка — честная // сильная игра в Го выходит далеко за рамки того, что можно // реализовать без полноценного MCTS/нейросети. // GoDifficulty — уровень сложности бота. type GoDifficulty int const ( GoDifficultyEasy GoDifficulty = iota GoDifficultyMedium GoDifficultyHard ) // GoBot — бот на основе эвристик без поиска вперёд. type GoBot struct { Difficulty GoDifficulty } // goPassScoreThreshold — если у бота нет ни одного хода лучше этого // эвристического порога, он пасует, а не портит позицию заведомо // плохим ходом (актуально в основном под конец партии). const goPassScoreThreshold = -5.0 // goEasyPassChance — простой бот не считает выгоду ходов, но всё же // изредка пасует сам, иначе полностью случайная игра почти никогда // не заканчивается естественным образом (лишние ходы почти всегда // находятся, если не пасовать вообще никогда). const goEasyPassChance = 0.04 // DecideMove выбирает ход бота. Возвращает (pos, true) для хода в // точку pos, либо (_, false), если бот решил пасовать. func (b GoBot) DecideMove(g *GoGameState, rnd *rand.Rand) (GoPos, bool) { legal := goLegalMoves(g) if len(legal) == 0 { return GoPos{}, false } switch b.Difficulty { case GoDifficultyEasy: if rnd.Float64() < goEasyPassChance { return GoPos{}, false } return legal[rnd.Intn(len(legal))], true default: best := legal[0] bestScore := goEvaluateMove(g, best, b.Difficulty) for _, pos := range legal[1:] { score := goEvaluateMove(g, pos, b.Difficulty) if score > bestScore { bestScore = score best = pos } } // при плотно заполненной доске бот больше не тратит ходы на // бессмысленное заполнение нейтральных точек ("дамэ"), если // лучший доступный ход не связан с реальным взятием (взятие // всегда даёт +10 к оценке за камень, так что порог 8 // надёжно отличает "взял что-то" от "просто занял свободную // точку") — иначе партии ботов друг с другом никогда не // заканчивались бы сами по себе if goBoardOccupancy(g) > 0.5 && bestScore < 8 { return GoPos{}, false } if bestScore < goPassScoreThreshold { return GoPos{}, false } return best, true } } // goBoardOccupancy возвращает долю занятых точек доски (0..1). func goBoardOccupancy(g *GoGameState) float64 { occupied := 0 for _, row := range g.Board { for _, c := range row { if c != GoEmpty { occupied++ } } } return float64(occupied) / float64(g.Size*g.Size) } // PlayFullTurn исполняет один ход бота (игру в точку либо пас). func (b GoBot) PlayFullTurn(g *GoGameState, rnd *rand.Rand) error { pos, ok := b.DecideMove(g, rnd) if !ok { return g.Pass() } return g.Play(pos) } // goLegalMoves перечисляет все легальные точки для текущего игрока. func goLegalMoves(g *GoGameState) []GoPos { var out []GoPos for r := 0; r < g.Size; r++ { for c := 0; c < g.Size; c++ { pos := GoPos{Row: r, Col: c} if g.IsLegal(pos) { out = append(out, pos) } } } return out } // goEvaluateMove — эвристическая оценка одного кандидатского хода // (без поиска вперёд): чем выше, тем привлекательнее ход для // текущего игрока. func goEvaluateMove(g *GoGameState, pos GoPos, level GoDifficulty) float64 { color := g.Turn sim := &GoGameState{ Size: g.Size, Board: cloneGoBoard(g.Board), Turn: g.Turn, prevBoardBeforeOpponentMove: g.prevBoardBeforeOpponentMove, } capturedBefore := boardStoneCount(sim.Board, color.Opponent()) if err := sim.Play(pos); err != nil { return -1000 } capturedAfter := boardStoneCount(sim.Board, color.Opponent()) captures := capturedBefore - capturedAfter score := float64(captures) * 10 _, liberties := sim.groupAndLiberties(pos) score += float64(liberties) if level == GoDifficultyHard { score += goDistanceToCenterBonus(pos, g.Size) score += goFriendlyNeighborBonus(g, pos) } return score } func boardStoneCount(board [][]GoColor, color GoColor) int { count := 0 for _, row := range board { for _, c := range row { if c == color { count++ } } } return count } // goDistanceToCenterBonus — небольшой позиционный бонус за игру // ближе к центру доски в дебюте (грубая, но стандартная для // простых Го-ботов эвристика). func goDistanceToCenterBonus(pos GoPos, size int) float64 { center := float64(size-1) / 2 dr := float64(pos.Row) - center dc := float64(pos.Col) - center dist := dr*dr + dc*dc maxDist := center * center * 2 if maxDist == 0 { return 0 } return 3 * (1 - dist/maxDist) } // goFriendlyNeighborBonus поощряет игру рядом со своими же камнями // (грубая замена полноценной оценке влияния/территории). func goFriendlyNeighborBonus(g *GoGameState, pos GoPos) float64 { color := g.Turn bonus := 0.0 for _, n := range goNeighbors(pos) { if !g.inBounds(n) { continue } if g.Board[n.Row][n.Col] == color { bonus += 1.5 } } return bonus }