package main import "math/rand" // UrDifficulty — уровень сложности бота. type UrDifficulty int const ( // UrDifficultyEasy — случайный выбор среди легальных ходов. UrDifficultyEasy UrDifficulty = iota // UrDifficultyMedium — жадная эвристика: оценивает результат // каждого легального хода и берёт лучший, не заглядывая вперёд. UrDifficultyMedium // UrDifficultyHard — вдобавок к жадной оценке своей позиции, // штрафует ходы, после которых соперник получает большой шанс // сразу же взять шашку в следующем броске — простой одношаговый // взгляд вперёд, а не полный перебор (в Уре, в отличие от нард, // за ход двигается только одна шашка, так что перебирать нечего — // но реакцию соперника всё же можно оценить). UrDifficultyHard ) // UrBot — бот-соперник для Игры Ур. type UrBot struct { Difficulty UrDifficulty } // urRollDistribution — вероятностное распределение суммы четырёх // бинарных костей (0..4), как в оригинальных тетраэдрических // костях: P(0)=1/16, P(1)=4/16, P(2)=6/16, P(3)=4/16, P(4)=1/16. var urRollDistribution = []int{1, 4, 6, 4, 1} // веса на 16 // urRollDice бросает четыре монеты и возвращает сумму выпавших "1" — // именно так исторически имитировались четыре тетраэдрические кости // с двумя отмеченными гранями из четырёх. func urRollDice(rnd *rand.Rand) int { sum := 0 for i := 0; i < 4; i++ { if rnd.Intn(2) == 1 { sum++ } } return sum } // urEvaluate оценивает позицию с точки зрения player: чем выше // значение, тем позиция выгоднее для него. func urEvaluate(g *UrGameState, player UrPlayer) float64 { opp := urOpponent(player) score := 0.0 for _, pos := range g.Path[player] { score += float64(pos) // чем дальше по пути (и особенно снята с доски = 15) — тем лучше if urRosettePathIndices[pos] && pos != UrPathLength+1 { score += 3 // стоять на розетке — безопасно и стратегически ценно } } for _, pos := range g.Path[opp] { score -= float64(pos) * 0.9 // прогресс соперника — плохо для нас (чуть менее весом, чтобы не быть зеркалом) } return score } // DecideMove выбирает ход бота (индекс шашки) согласно уровню // сложности. Предполагается, что кости уже брошены и есть хотя бы // один легальный ход. func (b UrBot) DecideMove(g *UrGameState, rnd *rand.Rand) int { moves := g.LegalMoves() if len(moves) == 0 { return -1 } switch b.Difficulty { case UrDifficultyEasy: return moves[rnd.Intn(len(moves))].PieceIdx default: return b.decideHeuristic(g, moves, rnd) } } func (b UrBot) decideHeuristic(g *UrGameState, moves []UrMove, rnd *rand.Rand) int { player := g.CurrentPlayer bestScore := 0.0 var best []int for i, mv := range moves { clone := *g clone.Move(mv.PieceIdx) score := urEvaluate(&clone, player) if b.Difficulty == UrDifficultyHard { score -= b.opponentThreatPenalty(&clone, player) } if i == 0 || score > bestScore { bestScore = score best = []int{mv.PieceIdx} } else if score == bestScore { best = append(best, mv.PieceIdx) } } return best[rnd.Intn(len(best))] } // opponentThreatPenalty оценивает (усреднённо по всем возможным // броскам соперника) риск того, что после нашего хода соперник // сможет сразу же взять одну из наших шашек на общем ряду — // упрощённый взгляд на один ход вперёд, без полного перебора. func (b UrBot) opponentThreatPenalty(g *UrGameState, player UrPlayer) float64 { opp := urOpponent(player) penalty := 0.0 for roll := 0; roll <= 4; roll++ { weight := float64(urRollDistribution[roll]) / 16 clone := *g clone.CurrentPlayer = opp clone.Rolled = true clone.DiceValue = roll for _, mv := range clone.LegalMoves() { pos := clone.Path[opp][mv.PieceIdx] dest := pos + roll if dest >= 5 && dest <= 12 { if occ, _, occupied := clone.occupantOnShared(urSharedIndex(dest)); occupied && occ == player { penalty += weight * 5 // соперник может взять нашу шашку этим броском } } } } return penalty } // PlayFullTurn бросает кости и доигрывает весь ход бота — включая // повторные броски, которые даёт розетка. Цикл продолжается, пока // партия не окончена и очередь остаётся за тем же игроком, который // был текущим на момент вызова (переход хода к сопернику или победа // естественным образом завершают цикл). func (b UrBot) PlayFullTurn(g *UrGameState, rnd *rand.Rand) error { me := g.CurrentPlayer for g.Result == nil && g.CurrentPlayer == me { if !g.Rolled { if err := g.Roll(urRollDice(rnd)); err != nil { return err } continue } pieceIdx := b.DecideMove(g, rnd) if pieceIdx < 0 { return nil } if err := g.Move(pieceIdx); err != nil { return err } } return nil }