package main

import (
	"math/rand"
	"sync"
)

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

const (
	// TicTacToeDifficultyEasy — совершенно случайный ход. Легко
	// обыграть.
	TicTacToeDifficultyEasy TicTacToeDifficulty = iota
	// TicTacToeDifficultyMedium — простая эвристика: выиграть, если
	// можно прямо сейчас; иначе заблокировать выигрыш соперника;
	// иначе предпочесть центр, потом углы, потом стороны. Без
	// просчёта на несколько ходов вперёд — обыграть можно, но не
	// тривиально.
	TicTacToeDifficultyMedium
	// TicTacToeDifficultyHard — полный перебор (минимакс) на всю
	// оставшуюся партию. Крестики-нолики — полностью решённая игра:
	// при точной игре с обеих сторон всегда ничья, так что на этом
	// уровне бот в принципе не проигрывает — выиграть у него можно
	// только его собственной ошибкой, которых он не делает.
	TicTacToeDifficultyHard
)

// TicTacToeBot — бот-соперник для крестиков-ноликов.
type TicTacToeBot struct {
	Difficulty TicTacToeDifficulty
}

// ticTacToeWouldWin проверяет: если знак mark поставить в клетку
// pos, появится ли выигрышная линия.
func ticTacToeWouldWin(board [9]TicTacToeMark, mark TicTacToeMark, pos int) bool {
	trial := board
	trial[pos] = mark
	winner, _, ok := ticTacToeCheckWin(trial)
	return ok && winner == mark
}

// ticTacToeCellPriority — порядок предпочтения клеток при отсутствии
// более важных соображений: центр сильнее всего (входит в 4
// линии), затем углы (по 3 линии каждый), затем стороны (по 2).
var ticTacToeCellPriority = []int{4, 0, 2, 6, 8, 1, 3, 5, 7}

// ticTacToeHeuristicMove — эвристика среднего уровня сложности: не
// упускает свою победу и не позволяет сопернику выиграть следующим
// ходом, но не считает партию на несколько ходов вперёд.
func ticTacToeHeuristicMove(board [9]TicTacToeMark, mark TicTacToeMark, empty []int, rnd *rand.Rand) int {
	for _, pos := range empty {
		if ticTacToeWouldWin(board, mark, pos) {
			return pos
		}
	}
	opponent := ticTacToeOpponent(mark)
	for _, pos := range empty {
		if ticTacToeWouldWin(board, opponent, pos) {
			return pos
		}
	}
	for _, pos := range ticTacToeCellPriority {
		if board[pos] == TicTacToeEmpty {
			return pos
		}
	}
	return empty[rnd.Intn(len(empty))]
}

// ticTacToeMemo кэширует уже посчитанные оценки позиций — в
// крестиках-ноликах конечное и небольшое число различных позиций
// (не больше 3^9 на каждого из двух "toMove"), так что кэш на уровне
// пакета быстро прогревается и резко ускоряет повторные вычисления
// (одни и те же позиции встречаются заново при каждой новой партии).
var (
	ticTacToeMemoMu sync.Mutex
	ticTacToeMemo   = map[ticTacToeMemoKey]int{}
)

type ticTacToeMemoKey struct {
	board  [9]TicTacToeMark
	toMove TicTacToeMark
}

// ticTacToeNegamax оценивает позицию board с точки зрения игрока
// toMove, при условии что сейчас его ход и никто ещё не выиграл до
// этого хода: +1 — toMove выигрывает при точной игре обеих сторон,
// -1 — проигрывает, 0 — ничья.
func ticTacToeNegamax(board [9]TicTacToeMark, toMove TicTacToeMark) int {
	key := ticTacToeMemoKey{board: board, toMove: toMove}

	ticTacToeMemoMu.Lock()
	v, ok := ticTacToeMemo[key]
	ticTacToeMemoMu.Unlock()
	if ok {
		return v
	}

	result := ticTacToeNegamaxUncached(board, toMove)

	ticTacToeMemoMu.Lock()
	ticTacToeMemo[key] = result
	ticTacToeMemoMu.Unlock()
	return result
}

func ticTacToeNegamaxUncached(board [9]TicTacToeMark, toMove TicTacToeMark) int {
	if winner, _, ok := ticTacToeCheckWin(board); ok {
		if winner == toMove {
			return 1
		}
		return -1
	}
	empty := ticTacToeEmptyCells(board)
	if len(empty) == 0 {
		return 0
	}
	best := -2
	for _, pos := range empty {
		trial := board
		trial[pos] = toMove
		score := -ticTacToeNegamax(trial, ticTacToeOpponent(toMove))
		if score > best {
			best = score
		}
	}
	return best
}

// ticTacToeMinimaxMove находит один из объективно наилучших ходов
// для mark (полным перебором всей оставшейся партии).
func ticTacToeMinimaxMove(board [9]TicTacToeMark, mark TicTacToeMark) int {
	bestScore := -2
	bestPos := -1
	for _, pos := range ticTacToeEmptyCells(board) {
		trial := board
		trial[pos] = mark
		score := -ticTacToeNegamax(trial, ticTacToeOpponent(mark))
		if score > bestScore {
			bestScore = score
			bestPos = pos
		}
	}
	return bestPos
}

// DecideMove выбирает ход бота согласно его уровню сложности.
// Возвращает -1, если свободных клеток нет (партия уже должна была
// завершиться раньше).
func (b TicTacToeBot) DecideMove(g *TicTacToeGameState, rnd *rand.Rand) int {
	empty := ticTacToeEmptyCells(g.Board)
	if len(empty) == 0 {
		return -1
	}
	switch b.Difficulty {
	case TicTacToeDifficultyEasy:
		return empty[rnd.Intn(len(empty))]
	case TicTacToeDifficultyMedium:
		return ticTacToeHeuristicMove(g.Board, g.CurrentTurn, empty, rnd)
	default:
		return ticTacToeMinimaxMove(g.Board, g.CurrentTurn)
	}
}

// PlayFullTurn исполняет один ход бота, если сейчас действительно
// его очередь.
func (b TicTacToeBot) PlayFullTurn(g *TicTacToeGameState, rnd *rand.Rand) error {
	if g.Phase != TicTacToePhasePlay || g.CurrentTurn == g.HumanMark {
		return nil
	}
	pos := b.DecideMove(g, rnd)
	if pos < 0 {
		return nil
	}
	return g.PlaceMark(pos)
}
