go_games_collection/ur_bot.go

149 lines
5.9 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"
// 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
}