package main

import "math/rand"

// ПРИМЕЧАНИЕ О СИЛЕ ИГРЫ: сильный бот для Го в реальности требует
// Monte-Carlo поиска по дереву или нейросетевой оценки позиции
// (как в AlphaGo) — минимакс тут почти бесполезен из-за огромного
// ветвления (до нескольких сотен ходов на 19x19). Все три уровня
// ниже — эвристические, БЕЗ поиска вперёд по дереву партии, и
// играют существенно слабее сильного человека даже на "Сильном"
// уровне. Это осознанное ограничение, а не недоработка — честная
// сильная игра в Го выходит далеко за рамки того, что можно
// реализовать без полноценного MCTS/нейросети.

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

const (
	GoDifficultyEasy GoDifficulty = iota
	GoDifficultyMedium
	GoDifficultyHard
)

// GoBot — бот на основе эвристик без поиска вперёд.
type GoBot struct {
	Difficulty GoDifficulty
}

// goPassScoreThreshold — если у бота нет ни одного хода лучше этого
// эвристического порога, он пасует, а не портит позицию заведомо
// плохим ходом (актуально в основном под конец партии).
const goPassScoreThreshold = -5.0

// goEasyPassChance — простой бот не считает выгоду ходов, но всё же
// изредка пасует сам, иначе полностью случайная игра почти никогда
// не заканчивается естественным образом (лишние ходы почти всегда
// находятся, если не пасовать вообще никогда).
const goEasyPassChance = 0.04

// DecideMove выбирает ход бота. Возвращает (pos, true) для хода в
// точку pos, либо (_, false), если бот решил пасовать.
func (b GoBot) DecideMove(g *GoGameState, rnd *rand.Rand) (GoPos, bool) {
	legal := goLegalMoves(g)
	if len(legal) == 0 {
		return GoPos{}, false
	}

	switch b.Difficulty {
	case GoDifficultyEasy:
		if rnd.Float64() < goEasyPassChance {
			return GoPos{}, false
		}
		return legal[rnd.Intn(len(legal))], true
	default:
		best := legal[0]
		bestScore := goEvaluateMove(g, best, b.Difficulty)
		for _, pos := range legal[1:] {
			score := goEvaluateMove(g, pos, b.Difficulty)
			if score > bestScore {
				bestScore = score
				best = pos
			}
		}
		// при плотно заполненной доске бот больше не тратит ходы на
		// бессмысленное заполнение нейтральных точек ("дамэ"), если
		// лучший доступный ход не связан с реальным взятием (взятие
		// всегда даёт +10 к оценке за камень, так что порог 8
		// надёжно отличает "взял что-то" от "просто занял свободную
		// точку") — иначе партии ботов друг с другом никогда не
		// заканчивались бы сами по себе
		if goBoardOccupancy(g) > 0.5 && bestScore < 8 {
			return GoPos{}, false
		}
		if bestScore < goPassScoreThreshold {
			return GoPos{}, false
		}
		return best, true
	}
}

// goBoardOccupancy возвращает долю занятых точек доски (0..1).
func goBoardOccupancy(g *GoGameState) float64 {
	occupied := 0
	for _, row := range g.Board {
		for _, c := range row {
			if c != GoEmpty {
				occupied++
			}
		}
	}
	return float64(occupied) / float64(g.Size*g.Size)
}

// PlayFullTurn исполняет один ход бота (игру в точку либо пас).
// Возвращает passed=true, если бот решил пасовать — вызывающий код
// (TUI) должен показать это явно, а не молчать, будто ничего не
// произошло.
func (b GoBot) PlayFullTurn(g *GoGameState, rnd *rand.Rand) (passed bool, err error) {
	pos, ok := b.DecideMove(g, rnd)
	if !ok {
		return true, g.Pass()
	}
	return false, g.Play(pos)
}

// goLegalMoves перечисляет все легальные точки для текущего игрока.
func goLegalMoves(g *GoGameState) []GoPos {
	var out []GoPos
	for r := 0; r < g.Size; r++ {
		for c := 0; c < g.Size; c++ {
			pos := GoPos{Row: r, Col: c}
			if g.IsLegal(pos) {
				out = append(out, pos)
			}
		}
	}
	return out
}

// goEvaluateMove — эвристическая оценка одного кандидатского хода
// (без поиска вперёд): чем выше, тем привлекательнее ход для
// текущего игрока.
func goEvaluateMove(g *GoGameState, pos GoPos, level GoDifficulty) float64 {
	color := g.Turn
	sim := &GoGameState{
		Size:                        g.Size,
		Board:                       cloneGoBoard(g.Board),
		Turn:                        g.Turn,
		prevBoardBeforeOpponentMove: g.prevBoardBeforeOpponentMove,
	}
	capturedBefore := boardStoneCount(sim.Board, color.Opponent())
	if err := sim.Play(pos); err != nil {
		return -1000
	}
	capturedAfter := boardStoneCount(sim.Board, color.Opponent())
	captures := capturedBefore - capturedAfter

	score := float64(captures) * 10

	_, liberties := sim.groupAndLiberties(pos)
	score += float64(liberties)

	if level == GoDifficultyHard {
		score += goDistanceToCenterBonus(pos, g.Size)
		score += goFriendlyNeighborBonus(g, pos)
	}

	return score
}

func boardStoneCount(board [][]GoColor, color GoColor) int {
	count := 0
	for _, row := range board {
		for _, c := range row {
			if c == color {
				count++
			}
		}
	}
	return count
}

// goDistanceToCenterBonus — небольшой позиционный бонус за игру
// ближе к центру доски в дебюте (грубая, но стандартная для
// простых Го-ботов эвристика).
func goDistanceToCenterBonus(pos GoPos, size int) float64 {
	center := float64(size-1) / 2
	dr := float64(pos.Row) - center
	dc := float64(pos.Col) - center
	dist := dr*dr + dc*dc
	maxDist := center * center * 2
	if maxDist == 0 {
		return 0
	}
	return 3 * (1 - dist/maxDist)
}

// goFriendlyNeighborBonus поощряет игру рядом со своими же камнями
// (грубая замена полноценной оценке влияния/территории).
func goFriendlyNeighborBonus(g *GoGameState, pos GoPos) float64 {
	color := g.Turn
	bonus := 0.0
	for _, n := range goNeighbors(pos) {
		if !g.inBounds(n) {
			continue
		}
		if g.Board[n.Row][n.Col] == color {
			bonus += 1.5
		}
	}
	return bonus
}
