package main

import "math/rand"

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

const (
	// NardyDifficultyEasy — случайный выбор среди легальных ходов.
	NardyDifficultyEasy NardyDifficulty = iota
	// NardyDifficultyMedium — жадная эвристика: на каждом отдельном
	// подшаге хода оценивает результирующую позицию и берёт лучший
	// вариант, не заглядывая дальше одного подшага вперёд.
	NardyDifficultyMedium
	// NardyDifficultyHard — перебирает ВСЕ полные последовательности
	// ходов на весь текущий ход (с учётом обязательного использования
	// максимума костей) и оценивает итоговую позицию целиком,
	// выбирая лучшую последовательность. Не идеальная игра (в
	// отличие от крестиков-ноликов нарды не решены полностью из-за
	// случайности костей), но заметно сильнее эвристики среднего
	// уровня за счёт взгляда на весь ход целиком, а не по подшагам.
	NardyDifficultyHard
)

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

// nardyPipCount — суммарное "расстояние" (в очках хода), которое
// игроку ещё нужно преодолеть, чтобы вывести все шашки с доски.
// Шашка на относительной точке rel требует ровно rel+1 очков, чтобы
// точно выйти; шашка на баре считается как самая дальняя (25).
func nardyPipCount(g *NardyGameState, player NardyPlayer) int {
	total := g.Bar[player] * 25
	for rel := 0; rel <= 23; rel++ {
		abs := nardyPointToAbsolute(g.Variant, player, rel)
		pt := g.Board[abs]
		if pt.Owner == player {
			total += pt.Count * (rel + 1)
		}
	}
	return total
}

// nardyEvaluate оценивает позицию с точки зрения player — чем выше
// значение, тем позиция выгоднее для player. Учитывает: разницу в
// снятых шашках (важнее всего), разницу в суммарном пути (pip
// count), собственные блоты (только короткие нарды — уязвимость
// быть сбитым), собственные закрытые пункты (блокируют соперника) и
// шашки на баре с обеих сторон.
func nardyEvaluate(g *NardyGameState, player NardyPlayer) float64 {
	opp := nardyOpponent(player)
	score := 0.0

	score += float64(g.BorneOff[player]-g.BorneOff[opp]) * 50
	score += float64(nardyPipCount(g, opp) - nardyPipCount(g, player))

	for rel := 0; rel <= 23; rel++ {
		abs := nardyPointToAbsolute(g.Variant, player, rel)
		pt := g.Board[abs]
		if pt.Owner != player {
			continue
		}
		if g.Variant == NardyShort && pt.Count == 1 {
			score -= 10 // блот — риск быть сбитым (актуально только в коротких нардах)
		}
		if pt.Count >= 2 {
			score += 2 // закрытый пункт — мешает продвижению соперника
		}
	}

	score -= float64(g.Bar[player]) * 15
	score += float64(g.Bar[opp]) * 15

	return score
}

// enumerateMaximalSequences перечисляет ВСЕ последовательности
// ходов, использующие максимально возможное число оставшихся костей
// текущего хода (см. NardyGameState.LegalMovesNow).
func enumerateMaximalSequences(g *NardyGameState) [][]NardyMove {
	start := g.cloneForSearch()
	needed := start.nardyMaxDiceUsableFrom(start.DiceLeft)

	var out [][]NardyMove
	var walk func(state *NardyGameState, path []NardyMove)
	walk = func(state *NardyGameState, path []NardyMove) {
		if len(path) == needed {
			out = append(out, append([]NardyMove{}, path...))
			return
		}
		for _, mv := range state.LegalMovesNow() {
			clone := state.cloneForSearch()
			clone.applyMoveRaw(mv)
			clone.removeDieOnce(mv.Die)
			walk(clone, append(path, mv))
		}
	}
	walk(start, nil)
	if len(out) == 0 {
		out = [][]NardyMove{{}}
	}
	return out
}

// DecideSequence решает весь текущий ход целиком (список одиночных
// перемещений, готовых к последовательному применению через
// ApplyMove) согласно уровню сложности бота.
func (b NardyBot) DecideSequence(g *NardyGameState, rnd *rand.Rand) []NardyMove {
	switch b.Difficulty {
	case NardyDifficultyEasy:
		return b.decideEasySequence(g, rnd)
	case NardyDifficultyMedium:
		return b.decideMediumSequence(g, rnd)
	default:
		return b.decideHardSequence(g, rnd)
	}
}

func (b NardyBot) decideEasySequence(g *NardyGameState, rnd *rand.Rand) []NardyMove {
	var seq []NardyMove
	state := g.cloneForSearch()
	for {
		moves := state.LegalMovesNow()
		if len(moves) == 0 {
			break
		}
		mv := moves[rnd.Intn(len(moves))]
		state.applyMoveRaw(mv)
		state.removeDieOnce(mv.Die)
		seq = append(seq, mv)
	}
	return seq
}

func (b NardyBot) decideMediumSequence(g *NardyGameState, rnd *rand.Rand) []NardyMove {
	var seq []NardyMove
	state := g.cloneForSearch()
	player := g.CurrentPlayer
	for {
		moves := state.LegalMovesNow()
		if len(moves) == 0 {
			break
		}
		bestScore := 0.0
		var best []NardyMove
		for i, mv := range moves {
			clone := state.cloneForSearch()
			clone.applyMoveRaw(mv)
			score := nardyEvaluate(clone, player)
			if i == 0 || score > bestScore {
				bestScore = score
				best = []NardyMove{mv}
			} else if score == bestScore {
				best = append(best, mv)
			}
		}
		chosen := best[rnd.Intn(len(best))]
		state.applyMoveRaw(chosen)
		state.removeDieOnce(chosen.Die)
		seq = append(seq, chosen)
	}
	return seq
}

func (b NardyBot) decideHardSequence(g *NardyGameState, rnd *rand.Rand) []NardyMove {
	sequences := enumerateMaximalSequences(g)
	player := g.CurrentPlayer
	bestScore := 0.0
	var best [][]NardyMove
	for i, seq := range sequences {
		clone := g.cloneForSearch()
		for _, mv := range seq {
			clone.applyMoveRaw(mv)
		}
		score := nardyEvaluate(clone, player)
		if i == 0 || score > bestScore {
			bestScore = score
			best = [][]NardyMove{seq}
		} else if score == bestScore {
			best = append(best, seq)
		}
	}
	return best[rnd.Intn(len(best))]
}

// PlayFullTurn бросает кости (если нужно) и доигрывает весь ход
// бота целиком согласно его уровню сложности.
func (b NardyBot) PlayFullTurn(g *NardyGameState, rnd *rand.Rand) error {
	if g.Phase == NardyPhaseRoll {
		g.Roll(rnd.Intn(6)+1, rnd.Intn(6)+1)
	}
	if g.Phase != NardyPhaseMove {
		return nil
	}
	for _, mv := range b.DecideSequence(g, rnd) {
		if err := g.ApplyMove(mv); err != nil {
			return err
		}
	}
	return nil
}
