package main

import "math/rand"

// SenetDifficulty — уровень сложности бота.
type SenetDifficulty int

const (
	SenetDifficultyEasy SenetDifficulty = iota
	SenetDifficultyMedium
	SenetDifficultyHard
)

// SenetBot — бот-соперник для Сенета.
type SenetBot struct {
	Difficulty SenetDifficulty
}

// senetStickResultByMarked — перевод числа "отмеченных вверх" граней
// четырёх метательных палочек в игровое значение: 0 -> 5 очков,
// 1 -> 1, 2 -> 2, 3 -> 3, 4 -> 4 (историческое соглашение).
var senetStickResultByMarked = [5]int{5, 1, 2, 3, 4}

// senetStickWeights — вероятностные веса каждого числа отмеченных
// граней (на 16, те же биномиальные "четыре монеты", что и в Уре).
var senetStickWeights = [5]int{1, 4, 6, 4, 1}

// senetRollSticks бросает четыре монеты и переводит число
// "отмеченных вверх" в игровое значение.
func senetRollSticks(rnd *rand.Rand) int {
	marked := 0
	for i := 0; i < 4; i++ {
		if rnd.Intn(2) == 1 {
			marked++
		}
	}
	return senetStickResultByMarked[marked]
}

// senetEvaluate оценивает позицию с точки зрения player.
func senetEvaluate(g *SenetGameState, player SenetPlayer) float64 {
	opp := senetOpponent(player)
	score := float64(g.BorneOff[player]) * 40
	score -= float64(g.BorneOff[opp]) * 40
	for sq := 1; sq <= SenetBoardSize; sq++ {
		owner, ok := senetCellOwner(g.Board[sq])
		if !ok {
			continue
		}
		if owner == player {
			score += float64(sq) * 0.5
			if senetSafeSquares[sq] {
				score += 5
			}
			if g.senetRunLength(sq) == 1 && !senetSafeSquares[sq] {
				score -= 4 // одинокая шашка вне безопасной клетки — уязвима к обмену
			}
		} else {
			score -= float64(sq) * 0.45
		}
	}
	return score
}

// DecideMove выбирает ход бота (клетку-источник) согласно уровню
// сложности. Предполагается, что кости уже брошены и есть хотя бы
// один легальный ход.
func (b SenetBot) DecideMove(g *SenetGameState, rnd *rand.Rand) int {
	moves := g.LegalMoves()
	if len(moves) == 0 {
		return -1
	}
	if b.Difficulty == SenetDifficultyEasy {
		return moves[rnd.Intn(len(moves))].From
	}

	player := g.CurrentPlayer
	bestScore := 0.0
	var best []int
	for i, mv := range moves {
		clone := *g
		clone.Move(mv.From)
		score := senetEvaluate(&clone, player)
		if b.Difficulty == SenetDifficultyHard {
			score -= b.opponentThreatPenalty(&clone, player)
		}
		if i == 0 || score > bestScore {
			bestScore = score
			best = []int{mv.From}
		} else if score == bestScore {
			best = append(best, mv.From)
		}
	}
	return best[rnd.Intn(len(best))]
}

// opponentThreatPenalty оценивает (усреднённо по возможным броскам
// соперника) риск того, что после нашего хода соперник сможет
// сразу же взять одну из наших незащищённых шашек.
func (b SenetBot) opponentThreatPenalty(g *SenetGameState, player SenetPlayer) float64 {
	opp := senetOpponent(player)
	penalty := 0.0
	for marked := 0; marked <= 4; marked++ {
		weight := float64(senetStickWeights[marked]) / 16
		roll := senetStickResultByMarked[marked]
		clone := *g
		clone.CurrentPlayer = opp
		clone.Rolled = true
		clone.DiceValue = roll
		for _, mv := range clone.legalMovesForDirection(1) {
			dest := mv.From + roll
			if dest >= 1 && dest <= SenetBoardSize {
				if owner, ok := senetCellOwner(clone.Board[dest]); ok && owner == player {
					penalty += weight * 4
				}
			}
		}
	}
	return penalty
}

// PlayFullTurn бросает кости и доигрывает весь ход бота — включая
// повторные броски, которые дают значения 1, 4 и 5.
func (b SenetBot) PlayFullTurn(g *SenetGameState, rnd *rand.Rand) error {
	me := g.CurrentPlayer
	for g.Result == nil && g.CurrentPlayer == me {
		if !g.Rolled {
			if err := g.Roll(senetRollSticks(rnd)); err != nil {
				return err
			}
			continue
		}
		from := b.DecideMove(g, rnd)
		if from < 0 {
			return nil
		}
		if err := g.Move(from); err != nil {
			return err
		}
	}
	return nil
}
