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
}
