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) }