Connect4Minimax.cs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. using System.Collections.Concurrent;
  2. using Connect4;
  3. namespace MinMaxAB;
  4. /// <summary>
  5. /// Реализация MiniMax-алгоритма для игры в 4 в ряд
  6. /// </summary>
  7. public class Connect4Minimax
  8. {
  9. /// <summary>
  10. /// Посчитать счет доски из количества последовательностей игрока и ИИ
  11. /// </summary>
  12. /// <param name="player">Количество последовательностей игрока</param>
  13. /// <param name="computer">Количество последовательностей ИИ</param>
  14. /// <returns></returns>
  15. protected static int EvaluateScoreDiff(int player, int computer)
  16. {
  17. if ((player == 0 && computer == 0) || (computer > 0 && player > 0)) return 0;
  18. if (computer > 0)
  19. {
  20. // Последовательности растут экспоненциально. Для более агрессивной игры - увеличить значения
  21. return computer switch
  22. {
  23. 1 => 1,
  24. 2 => 8,
  25. 3 => 64,
  26. _ => 0
  27. };
  28. }
  29. if (player > 0)
  30. {
  31. // Последовательности игрока проигрышны для ИИ. Для более пассивной игры ("в защиту") - увеличить значения
  32. return player switch
  33. {
  34. 1 => -1,
  35. 2 => -8,
  36. 3 => -64,
  37. _ => 0
  38. }; ;
  39. }
  40. return 0;
  41. }
  42. /// <summary>
  43. /// Посчитать итоговый счет по всем направлениям для компьютера
  44. /// </summary>
  45. /// <param name="board">Игровая доска</param>
  46. /// <returns>Итоговый счет по всем направлениям для компьютера</returns>
  47. public static int CountSequences(GameBoard board)
  48. {
  49. int score = 0;
  50. // Считаем по горизонтали
  51. for (int col = 0; col < board.dimensions.Columns; col++)
  52. {
  53. for (int row = 0; row < board.dimensions.Rows - 3; row++)
  54. {
  55. int c = 0;
  56. int p = 0;
  57. for (int k = 0; k < 4; k++)
  58. {
  59. if (board.cells[col][row + k] == Player.Computer) c++;
  60. else if (board.cells[col][row + k] != Player.None) p++;
  61. }
  62. score += EvaluateScoreDiff(p, c);
  63. }
  64. }
  65. // По вертикали
  66. for (int col = 0; col < board.dimensions.Columns - 3; col++)
  67. {
  68. for (int row = 0; row < board.dimensions.Rows; row++)
  69. {
  70. int c = 0;
  71. int p = 0;
  72. for (int k = 0; k < 4; k++)
  73. {
  74. if (board.cells[col + k][row] == Player.Computer) c++;
  75. else if (board.cells[col + k][row] != Player.None) p++;
  76. }
  77. score += EvaluateScoreDiff(p, c);
  78. }
  79. }
  80. // По диагонали
  81. for (int col = 0; col < board.dimensions.Columns - 3; col++)
  82. {
  83. for (int row = 0; row < board.dimensions.Rows - 3; row++)
  84. {
  85. int c = 0;
  86. int p = 0;
  87. for (int k = 0; k < 4; k++)
  88. {
  89. if (board.cells[col + k][row + k] == Player.Computer) c++;
  90. else if (board.cells[col + k][row + k] != Player.None) p++;
  91. }
  92. score += EvaluateScoreDiff(p, c);
  93. }
  94. }
  95. // По второй диагонали
  96. for (int col = 3; col < board.dimensions.Columns; col++)
  97. {
  98. for (int row = 0; row < board.dimensions.Rows - 3; row++)
  99. {
  100. int c = 0;
  101. int p = 0;
  102. for (int k = 0; k < 4; k++)
  103. {
  104. if (board.cells[col - k][row + k] == Player.Computer) c++;
  105. else if (board.cells[col - k][row + k] != Player.None) p++;
  106. }
  107. score += EvaluateScoreDiff(p, c);
  108. }
  109. }
  110. return score;
  111. }
  112. /// <summary>
  113. /// Итерация minimax-алгоритма
  114. /// </summary>
  115. /// <param name="board">Копия игровой доски для текущей итерации</param>
  116. /// <param name="depth">Оставшаяся глубина просчета</param>
  117. /// <param name="maxPlayer">Максимизировать игрока (иначе - минимизировать)</param>
  118. /// <param name="alpha">Минимальный порог счета</param>
  119. /// <param name="beta">Максимальный порог счета</param>
  120. /// <returns>Счет ИИ для текущего хода</returns>
  121. private static int EvaluateMinMax(GameBoard board, int depth, bool maxPlayer = false, int alpha = int.MinValue, int beta = int.MaxValue)
  122. {
  123. var currentWinner = board.GetWinner();
  124. // Выходим, если выиграли или проиграли
  125. if (currentWinner == Player.Computer) return (depth + 1) * 100_000;
  126. else if (currentWinner == Player.Human) return -(depth + 1) * 100_000;
  127. // Выходим, если больше некуда ходить
  128. if (!board.HasEmptyColumns()) return 0;
  129. // Последняя итерация
  130. if (depth == 0) return CountSequences(board);
  131. int best = maxPlayer ? int.MinValue : int.MaxValue;
  132. // Считаем все возможные ходы
  133. for (int i = 0; i < board.dimensions.Columns; i++)
  134. {
  135. // Если невозможно сделать ход
  136. if (!board.PlaceCoin(maxPlayer ? Player.Computer : Player.Human, i)) continue;
  137. // Итерация оппонента
  138. int x = EvaluateMinMax(board, depth - 1, !maxPlayer, alpha, beta);
  139. // Выбираем лучший (или худший) исход
  140. best = maxPlayer ? int.Max(best, x) : int.Min(best, x);
  141. // Отменяем сделанный ход
  142. board.RemoveFromTop(i);
  143. // Обновляем альфа-бета значения
  144. if (maxPlayer) alpha = int.Max(alpha, best);
  145. else beta = int.Min(beta, best);
  146. // Если альфа и бета разошлись в противоположные стороны - выходим
  147. if (beta <= alpha) break;
  148. }
  149. return best;
  150. }
  151. /// <summary>
  152. /// Найти самый лучший следующий ход
  153. /// </summary>
  154. /// <param name="board">Текущее состояние доски</param>
  155. /// <param name="depth">Глубина просчета</param>
  156. /// <returns>Номер столбца для лучшего хода</returns>
  157. public static int GetBestMove(GameBoard board, int depth)
  158. {
  159. ConcurrentBag<(int Column, int Score)> moves = [];
  160. // Считаем все ходы параллельно
  161. Parallel.For(0, board.dimensions.Columns, i =>
  162. {
  163. // Копируем доску для этой вселенной (нужно для параллелизации)
  164. var newBoard = board.Clone();
  165. // Если поместить монетку в этот столбец уже нельзя - пропускаем
  166. if (!newBoard.PlaceCoin(Player.Computer, i)) return;
  167. // Сохраняем счет для этого хода
  168. moves.Add((i, EvaluateMinMax(newBoard, depth)));
  169. });
  170. // Находим счет с максимальным счетом, с приоритетом на центральные колонки
  171. return moves.OrderByDescending(m => m.Score).ThenBy(m => int.Abs(m.Column - (board.dimensions.Columns / 2))).First().Column;
  172. }
  173. }