using System.Collections.Concurrent; using Connect4; namespace MinMaxAB; /// /// Реализация MiniMax-алгоритма для игры в 4 в ряд /// public class Connect4Minimax { /// /// Посчитать счет доски из количества последовательностей игрока и ИИ /// /// Количество последовательностей игрока /// Количество последовательностей ИИ /// protected static int EvaluateScoreDiff(int player, int computer) { if ((player == 0 && computer == 0) || (computer > 0 && player > 0)) return 0; if (computer > 0) { // Последовательности растут экспоненциально. Для более агрессивной игры - увеличить значения return computer switch { 1 => 1, 2 => 8, 3 => 64, _ => 0 }; } if (player > 0) { // Последовательности игрока проигрышны для ИИ. Для более пассивной игры ("в защиту") - увеличить значения return player switch { 1 => -1, 2 => -8, 3 => -64, _ => 0 }; ; } return 0; } /// /// Посчитать итоговый счет по всем направлениям для компьютера /// /// Игровая доска /// Итоговый счет по всем направлениям для компьютера public static int CountSequences(GameBoard board) { int score = 0; // Считаем по горизонтали for (int col = 0; col < board.dimensions.Columns; col++) { for (int row = 0; row < board.dimensions.Rows - 3; row++) { int c = 0; int p = 0; for (int k = 0; k < 4; k++) { if (board.cells[col][row + k] == Player.Computer) c++; else if (board.cells[col][row + k] != Player.None) p++; } score += EvaluateScoreDiff(p, c); } } // По вертикали for (int col = 0; col < board.dimensions.Columns - 3; col++) { for (int row = 0; row < board.dimensions.Rows; row++) { int c = 0; int p = 0; for (int k = 0; k < 4; k++) { if (board.cells[col + k][row] == Player.Computer) c++; else if (board.cells[col + k][row] != Player.None) p++; } score += EvaluateScoreDiff(p, c); } } // По диагонали for (int col = 0; col < board.dimensions.Columns - 3; col++) { for (int row = 0; row < board.dimensions.Rows - 3; row++) { int c = 0; int p = 0; for (int k = 0; k < 4; k++) { if (board.cells[col + k][row + k] == Player.Computer) c++; else if (board.cells[col + k][row + k] != Player.None) p++; } score += EvaluateScoreDiff(p, c); } } // По второй диагонали for (int col = 3; col < board.dimensions.Columns; col++) { for (int row = 0; row < board.dimensions.Rows - 3; row++) { int c = 0; int p = 0; for (int k = 0; k < 4; k++) { if (board.cells[col - k][row + k] == Player.Computer) c++; else if (board.cells[col - k][row + k] != Player.None) p++; } score += EvaluateScoreDiff(p, c); } } return score; } /// /// Итерация minimax-алгоритма /// /// Копия игровой доски для текущей итерации /// Оставшаяся глубина просчета /// Максимизировать игрока (иначе - минимизировать) /// Минимальный порог счета /// Максимальный порог счета /// Счет ИИ для текущего хода private static int EvaluateMinMax(GameBoard board, int depth, bool maxPlayer = false, int alpha = int.MinValue, int beta = int.MaxValue) { var currentWinner = board.GetWinner(); // Выходим, если выиграли или проиграли if (currentWinner == Player.Computer) return (depth + 1) * 100_000; else if (currentWinner == Player.Human) return -(depth + 1) * 100_000; // Выходим, если больше некуда ходить if (!board.HasEmptyColumns()) return 0; // Последняя итерация if (depth == 0) return CountSequences(board); int best = maxPlayer ? int.MinValue : int.MaxValue; // Считаем все возможные ходы for (int i = 0; i < board.dimensions.Columns; i++) { // Если невозможно сделать ход if (!board.PlaceCoin(maxPlayer ? Player.Computer : Player.Human, i)) continue; // Итерация оппонента int x = EvaluateMinMax(board, depth - 1, !maxPlayer, alpha, beta); // Выбираем лучший (или худший) исход best = maxPlayer ? int.Max(best, x) : int.Min(best, x); // Отменяем сделанный ход board.RemoveFromTop(i); // Обновляем альфа-бета значения if (maxPlayer) alpha = int.Max(alpha, best); else beta = int.Min(beta, best); // Если альфа и бета разошлись в противоположные стороны - выходим if (beta <= alpha) break; } return best; } /// /// Найти самый лучший следующий ход /// /// Текущее состояние доски /// Глубина просчета /// Номер столбца для лучшего хода public static int GetBestMove(GameBoard board, int depth) { ConcurrentBag<(int Column, int Score)> moves = []; // Считаем все ходы параллельно Parallel.For(0, board.dimensions.Columns, i => { // Копируем доску для этой вселенной (нужно для параллелизации) var newBoard = board.Clone(); // Если поместить монетку в этот столбец уже нельзя - пропускаем if (!newBoard.PlaceCoin(Player.Computer, i)) return; // Сохраняем счет для этого хода moves.Add((i, EvaluateMinMax(newBoard, depth))); }); // Находим счет с максимальным счетом, с приоритетом на центральные колонки return moves.OrderByDescending(m => m.Score).ThenBy(m => int.Abs(m.Column - (board.dimensions.Columns / 2))).First().Column; } }