Browse Source

Added comments

Vsevolod Levitan 3 months ago
parent
commit
316a54904b
5 changed files with 111 additions and 15 deletions
  1. 52 11
      Connect4/GameBoard.cs
  2. 5 0
      Connect4/GameDimensions.cs
  3. 6 3
      Connect4/Player.cs
  4. 36 1
      Connect4Minimax.cs
  5. 12 0
      Extensions.cs

+ 52 - 11
Connect4/GameBoard.cs

@@ -3,6 +3,9 @@ using MinMaxAB;
 
 namespace Connect4;
 
+/// <summary>
+/// Доска для игры в 4 в ряд
+/// </summary>
 public class GameBoard
 {
     public Player[][] cells;
@@ -23,6 +26,12 @@ public class GameBoard
     private bool cacheInvalidated = false;
     private Player winnerCache = Player.None;
 
+    /// <summary>
+    /// Закинуть монетку сверху
+    /// </summary>
+    /// <param name="player">Игрок, закинувший монетку</param>
+    /// <param name="column">Столбец, в который закинуть монетку</param>
+    /// <returns>Получилось ли поместить монетку</returns>
     public bool PlaceCoin(Player player, int column)
     {
         int row = 0;
@@ -38,6 +47,11 @@ public class GameBoard
         return true;
     }
 
+    /// <summary>
+    /// Убрать верхнюю монетку
+    /// </summary>
+    /// <param name="column">Столбец, из которого убрать монетку</param>
+    /// <returns>Удалось ли убрать монетку</returns>
     public bool RemoveFromTop(int column)
     {
         int row = 0;
@@ -53,6 +67,10 @@ public class GameBoard
         return true;
     }
 
+    /// <summary>
+    /// Найти победители
+    /// </summary>
+    /// <returns>Игрок-победитель</returns>
     public Player? GetWinner()
     {
         if (!cacheInvalidated) return winnerCache;
@@ -66,26 +84,32 @@ public class GameBoard
                 if (cells[i][j] == Player.None)
                     continue;
 
-                bool h = i + 3 < dimensions.Columns;
-                bool v = j + 3 < dimensions.Rows;
+                // Горизонталь
+                bool horizontal = i + 3 < dimensions.Columns;
 
-                if (!h && !v)
+                // Вертикаль
+                bool vertical = j + 3 < dimensions.Rows;
+
+                if (!horizontal && !vertical)
                     continue;
 
-                bool d = h && v;
-                bool bd = v && i - 3 >= 0;
+                // Диагональ
+                bool diagonal = horizontal && vertical;
+                // Вторая диагональ
+                bool reverseDiagonal = vertical && i - 3 >= 0;
 
+                // Проходим по всем возможным длинам последовательности
                 for (int k = 1; k < 4; k++)
                 {
-                    h = h && cells[i][j] == cells[i + k][j];
-                    v = v && cells[i][j] == cells[i][j + k];
-                    d = d && cells[i][j] == cells[i + k][j + k];
-                    bd = bd && cells[i][j] == cells[i - k][j + k];
-                    if (!h && !v && !d && !bd)
+                    horizontal = horizontal && cells[i][j] == cells[i + k][j];
+                    vertical = vertical && cells[i][j] == cells[i][j + k];
+                    diagonal = diagonal && cells[i][j] == cells[i + k][j + k];
+                    reverseDiagonal = reverseDiagonal && cells[i][j] == cells[i - k][j + k];
+                    if (!horizontal && !vertical && !diagonal && !reverseDiagonal)
                         break;
                 }
 
-                if (h || v || d || bd)
+                if (horizontal || vertical || diagonal || reverseDiagonal)
                 {
                     winnerCache = cells[i][j];
                     return cells[i][j];
@@ -97,6 +121,10 @@ public class GameBoard
         return Player.None;
     }
 
+    /// <summary>
+    /// Проверить, есть ли пустые столбцы
+    /// </summary>
+    /// <returns>true если есть пустые столбцы</returns>
     public bool HasEmptyColumns()
     {
         for (int x = 0; x < dimensions.Columns; x++)
@@ -107,6 +135,11 @@ public class GameBoard
         return false;
     }
 
+    /// <summary>
+    /// Найти символ, обозначающий игрока
+    /// </summary>
+    /// <param name="player">Игрок</param>
+    /// <returns>Символ, обозначающий данного игрока</returns>
     protected char PlayerChar(Player player) => player switch
     {
         Player.Human => 'O',
@@ -114,6 +147,10 @@ public class GameBoard
         _ => ' ',
     };
 
+    /// <summary>
+    /// Сериализовать доску
+    /// </summary>
+    /// <returns>Строка, содержащая отформатированное представление доски</returns>
     public override string ToString()
     {
         string sep = $"\n{new('-', dimensions.Columns * 4)}";
@@ -139,6 +176,10 @@ public class GameBoard
         return builder.ToString();
     }
 
+    /// <summary>
+    /// Скопировать доску вместе с ее содержимым
+    /// </summary>
+    /// <returns>Копия доски с таким же содержимым</returns>
     public GameBoard Clone()
     {
         var newBoard = new GameBoard(dimensions, cells.DeepCopy());

+ 5 - 0
Connect4/GameDimensions.cs

@@ -1,3 +1,8 @@
 namespace Connect4;
 
+/// <summary>
+/// Размер доски для игры в 4 в ряд
+/// </summary>
+/// <param name="Columns">Количество столбцов</param>
+/// <param name="Rows">Количество строк</param>
 public record struct GameDimensions(int Columns, int Rows);

+ 6 - 3
Connect4/Player.cs

@@ -1,8 +1,11 @@
 namespace Connect4;
 
+/// <summary>
+/// Игрок в 4 в ряд
+/// </summary>
 public enum Player
 {
-    None,
-    Human,
-    Computer
+    None,     // Для пустых клеток и отсутствия победителя
+    Human,    // Человек
+    Computer  // ИИ
 }

+ 36 - 1
Connect4Minimax.cs

@@ -3,14 +3,24 @@ using Connect4;
 
 namespace MinMaxAB;
 
+/// <summary>
+/// Реализация MiniMax-алгоритма для игры в 4 в ряд
+/// </summary>
 public class Connect4Minimax
 {
+    /// <summary>
+    /// Посчитать счет доски из количества последовательностей игрока и ИИ
+    /// </summary>
+    /// <param name="player">Количество последовательностей игрока</param>
+    /// <param name="computer">Количество последовательностей ИИ</param>
+    /// <returns></returns>
     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,
@@ -22,6 +32,7 @@ public class Connect4Minimax
 
         if (player > 0)
         {
+            // Последовательности игрока проигрышны для ИИ. Для более пассивной игры ("в защиту") - увеличить значения
             return player switch
             {
                 1 => -1,
@@ -34,6 +45,11 @@ public class Connect4Minimax
         return 0;
     }
 
+    /// <summary>
+    /// Посчитать итоговый счет по всем направлениям для компьютера
+    /// </summary>
+    /// <param name="board">Игровая доска</param>
+    /// <returns>Итоговый счет по всем направлениям для компьютера</returns>
     public static int CountSequences(GameBoard board)
     {
         int score = 0;
@@ -113,6 +129,15 @@ public class Connect4Minimax
         return score;
     }
 
+    /// <summary>
+    /// Итерация minimax-алгоритма
+    /// </summary>
+    /// <param name="board">Копия игровой доски для текущей итерации</param>
+    /// <param name="depth">Оставшаяся глубина просчета</param>
+    /// <param name="maxPlayer">Максимизировать игрока (иначе - минимизировать)</param>
+    /// <param name="alpha">Минимальный порог счета</param>
+    /// <param name="beta">Максимальный порог счета</param>
+    /// <returns>Счет ИИ для текущего хода</returns>
     private static int EvaluateMinMax(GameBoard board, int depth, bool maxPlayer = false, int alpha = int.MinValue, int beta = int.MaxValue)
     {
         var currentWinner = board.GetWinner();
@@ -146,20 +171,30 @@ public class Connect4Minimax
             if (maxPlayer) alpha = int.Max(alpha, best);
             else beta = int.Min(beta, best);
 
+            // Если альфа и бета разошлись в противоположные стороны - выходим
             if (beta <= alpha) break;
         }
 
         return best;
     }
 
+    /// <summary>
+    /// Найти самый лучший следующий ход
+    /// </summary>
+    /// <param name="board">Текущее состояние доски</param>
+    /// <param name="depth">Глубина просчета</param>
+    /// <returns>Номер столбца для лучшего хода</returns>
     public static int GetBestMove(GameBoard board, int depth)
     {
         ConcurrentBag<(int Column, int Score)> moves = [];
-        // Parallel.For(0, board.dimensions.Columns, new() { MaxDegreeOfParallelism = 1 }, i =>
+        // Считаем все ходы параллельно
         Parallel.For(0, board.dimensions.Columns, i =>
         {
+            // Копируем доску для этой вселенной (нужно для параллелизации)
             var newBoard = board.Clone();
+            // Если поместить монетку в этот столбец уже нельзя - пропускаем
             if (!newBoard.PlaceCoin(Player.Computer, i)) return;
+            // Сохраняем счет для этого хода
             moves.Add((i, EvaluateMinMax(newBoard, depth)));
         });
 

+ 12 - 0
Extensions.cs

@@ -4,6 +4,13 @@ namespace MinMaxAB;
 
 public static class Extensions
 {
+    /// <summary>
+    /// Заполнить двумерный массив заданным значением
+    /// </summary>
+    /// <param name="cols">Количество столбцов в массиве</param>
+    /// <param name="rows">Количество строк в массиве</param>
+    /// <param name="value">Значение, которое установить во все ячейки</param>
+    /// <returns>Новый двумерный массив, заполненный заданным значением</returns>
     public static Player[][] Fill(int cols, int rows, Player value)
     {
         Player[][] array = new Player[cols][];
@@ -22,6 +29,11 @@ public static class Extensions
         return array;
     }
 
+    /// <summary>
+    /// Скопировать двоичный массив
+    /// </summary>
+    /// <param name="source">Исходный массив</param>
+    /// <returns>Новый двумерный массив с такими же значениями</returns>
     public static Player[][] DeepCopy(this Player[][] source)
     {
         var len = source.Length;