/// <summary> /// Start a play sequence i.e. human player plays and the AI player plays if human didn't win. /// </summary> /// <param name="p_ColumnIndex"> The column index the human player played in. </param> public void Connect4GameLoop(int p_ColumnIndex) { m_Player1.Play(m_GameGrid, p_ColumnIndex); OnHumanPlayerPlayed(); m_GameGrid.CalculateGridScore(m_Player1.TokenColor); if (!CheckIfWinner(m_Player1)) { string[] CalculationStats = m_Player2.Play(m_GameGrid); m_GameGrid.CalculateGridScore(m_Player2.TokenColor); OnScoreCalculated(m_GameGrid.Score, CalculationStats[0], CalculationStats[1]); CheckIfWinner(m_Player2); } }
/// <summary> /// Run the minimax algorithm. /// </summary> /// <param name="p_Node"> The node used to run the algorithm. </param> /// <param name="p_MaxDepth"> The maximum depth for the algorithm. </param> /// <param name="p_Alpha"> The alpha parameter for the prunning. </param> /// <param name="p_Beta"> The beta parameter for the prunning. </param> /// <param name="p_MaximizingPlayer"> The maximizing player. </param> /// <param name="p_MinimizingPlayer"> The minimizing player. </param> /// <returns> The node with the column to play in. </returns> /* * DISCLAIMER * The algorithm could probalby be written in a more summarized version but we chose * to clearly decompose it at the maximum to make it more understandable. */ private static Node Minimax(Node p_Node, int p_MaxDepth, int p_Alpha, int p_Beta, Connect4Player p_MaximizingPlayer, Connect4Player p_MinimizingPlayer) { // We check if we have 4 tokens aligned. // It's interesting to note that since calculating the heuristic and checking // if we have 4 tokens align are almost the same (scan grid in all directions), // it takes as less time to calculate the score everytime than to scan it for // a winning combination and then calculate the score only if we have a // winning combination (which would mean scanning the grid entirely twice !) p_Node.Grid.CalculateGridScore(p_MaximizingPlayer.TokenColor); // If we reached the maximum depth defined for the algorith execution, we return the result node. if (p_Node.Grid.FourTokenAligned || p_Node.Depth == p_MaxDepth) { m_IterationNumber++; return(p_Node); } // Otherwise we run the recurrence. else { // If it's the maximizing player's (i.e. AI player) turn... if (p_Node.WhoseTurnItIs == p_MaximizingPlayer) { // Variable used to store the node with the maximum score. Node nodeWithScoreMax = null; // ...we play in each column that's not already full : foreach (int column in p_Node.Grid.ColumnNotFull) { // We clone the grid. GameGrid newGameGrid = p_Node.Grid.CloneGameGrid(p_Node.Grid); // We add a tocken in the given column in the cloned cell. newGameGrid.AddTokenInColumn(column, p_MaximizingPlayer.TokenColor); // We create a new child node with these data. Node newNode = new Node(p_MinimizingPlayer, newGameGrid, column, p_Node.Depth + 1); // Accelerate the execution in case of an obvious win at a depth of 0 (i.e. at the first // comuted turn of the AI). if (p_Node.Depth == 0) { newGameGrid.CalculateGridScore(p_MaximizingPlayer.TokenColor); if (newGameGrid.FourTokenAligned) { return(newNode); } } // If we have no obvious win... if (!newGameGrid.FourTokenAligned) { // ...we run the minimax algorithm recursively. Node currentNode = Minimax(newNode, p_MaxDepth, p_Alpha, p_Beta, p_MaximizingPlayer, p_MinimizingPlayer); // We store the first node found to initiate the comparison. if (currentNode == null) { nodeWithScoreMax = newNode; } // After that, we compare the score of the node found in the recurrence with the one stored. else { // We store the score computed by the recurrence. newNode.Grid.Score = currentNode.Grid.Score; // Case handling the 6 last turn when we cannot reach the reccurrence max depth, therefore we // return the last node computed before get a null result. if (nodeWithScoreMax == null) { nodeWithScoreMax = newNode; } // if the recurrence result isn't null... else { // ...we compare the score of the node found in the recurrence with the one stored in nodeWithScoreMax. // if it's greater that the previous one, we update the value. nodeWithScoreMax = currentNode.Grid.Score > nodeWithScoreMax.Grid.Score ? newNode : nodeWithScoreMax; // Here we use the beta prunning to exclude the next nodes if the node score is greater // that the minimum one computed before (i.e. all next nodes will be discarded anyway). if (nodeWithScoreMax.Grid.Score > p_Beta) { return(nodeWithScoreMax); } // We update the value for the alpha prunning. p_Alpha = Math.Max(p_Alpha, nodeWithScoreMax.Grid.Score); } } } } return(nodeWithScoreMax); } // If it's the minimizing player's (i.e. the human player) turn, the code structure is pretty much the same. else { // Variable used to store the node with the minimum score. Node nodeWithScoreMin = null; foreach (int column in p_Node.Grid.ColumnNotFull) { GameGrid newGameGrid = p_Node.Grid.CloneGameGrid(p_Node.Grid); newGameGrid.AddTokenInColumn(column, p_MinimizingPlayer.TokenColor); Node newNode = new Node(p_MaximizingPlayer, newGameGrid, column, p_Node.Depth + 1); Node currentNode = Minimax(newNode, p_MaxDepth, p_Alpha, p_Beta, p_MaximizingPlayer, p_MinimizingPlayer); if (currentNode == null) { nodeWithScoreMin = newNode; } else { newNode.Grid.Score = currentNode.Grid.Score; if (nodeWithScoreMin == null) { nodeWithScoreMin = currentNode; } else { // We compare the score of the node found in the recurrence with the one stored in nodeWithScoreMin. // if it's smaller that the previous one, we update the value. nodeWithScoreMin = currentNode.Grid.Score < nodeWithScoreMin.Grid.Score ? newNode : nodeWithScoreMin; // Here we use the alpha prunning to exclude the next nodes if the node score is smaller // that the maximum one computed before (i.e. all next nodes will be discarded anyway). if (p_Alpha > nodeWithScoreMin.Grid.Score) { return(nodeWithScoreMin); } // We update the value for the beta prunning. p_Beta = Math.Min(p_Beta, nodeWithScoreMin.Grid.Score); } } } return(nodeWithScoreMin); } } }