diff --git a/src/main/java/com/thealgorithms/others/MiniMaxAlgorithm.java b/src/main/java/com/thealgorithms/others/MiniMaxAlgorithm.java index e0b96570a4fe..28dc980034f3 100644 --- a/src/main/java/com/thealgorithms/others/MiniMaxAlgorithm.java +++ b/src/main/java/com/thealgorithms/others/MiniMaxAlgorithm.java @@ -4,54 +4,99 @@ import java.util.Random; /** - * MiniMax is an algorithm used int artificial intelligence and game theory for - * minimizing the possible loss for the worst case scenario. + * MiniMax is an algorithm used in artificial intelligence and game theory for + * minimizing the possible loss for the worst case scenario. It is commonly used + * in two-player turn-based games such as Tic-Tac-Toe, Chess, and Checkers. * - * See more (https://en.wikipedia.org/wiki/Minimax, - * https://www.geeksforgeeks.org/minimax-algorithm-in-game-theory-set-1-introduction/). + *

+ * The algorithm simulates all possible moves in a game tree and chooses the + * move that minimizes the maximum possible loss. The algorithm assumes both + * players play optimally. + * + *

+ * Time Complexity: O(b^d) where b is the branching factor and d is the depth + *

+ * Space Complexity: O(d) for the recursive call stack + * + *

+ * See more: + *

* * @author aitofi (https://github.com/aitorfi) */ -public class MiniMaxAlgorithm { +public final class MiniMaxAlgorithm { + + private static final Random RANDOM = new Random(); /** * Game tree represented as an int array containing scores. Each array - * element is a leaf node. + * element is a leaf node. The array length must be a power of 2. */ private int[] scores; + + /** + * The height of the game tree, calculated as log2(scores.length). + */ private int height; /** - * Initializes the scores with 8 random leaf nodes + * Initializes the MiniMaxAlgorithm with 8 random leaf nodes (2^3 = 8). + * Each score is a random integer between 1 and 99 inclusive. */ public MiniMaxAlgorithm() { - scores = getRandomScores(3, 99); - height = log2(scores.length); + this(getRandomScores(3, 99)); + } + + /** + * Initializes the MiniMaxAlgorithm with the provided scores. + * + * @param scores An array of scores representing leaf nodes. The length must be + * a power of 2. + * @throws IllegalArgumentException if the scores array length is not a power of + * 2 + */ + public MiniMaxAlgorithm(int[] scores) { + if (!isPowerOfTwo(scores.length)) { + throw new IllegalArgumentException("The number of scores must be a power of 2."); + } + this.scores = Arrays.copyOf(scores, scores.length); + this.height = log2(scores.length); } + /** + * Demonstrates the MiniMax algorithm with a random game tree. + * + * @param args Command line arguments (not used) + */ public static void main(String[] args) { - MiniMaxAlgorithm miniMaxAlgorith = new MiniMaxAlgorithm(); + MiniMaxAlgorithm miniMaxAlgorithm = new MiniMaxAlgorithm(); boolean isMaximizer = true; // Specifies the player that goes first. - boolean verbose = true; // True to show each players choices. int bestScore; - bestScore = miniMaxAlgorith.miniMax(0, isMaximizer, 0, verbose); + bestScore = miniMaxAlgorithm.miniMax(0, isMaximizer, 0, true); - if (verbose) { - System.out.println(); - } - - System.out.println(Arrays.toString(miniMaxAlgorith.getScores())); + System.out.println(); + System.out.println(Arrays.toString(miniMaxAlgorithm.getScores())); System.out.println("The best score for " + (isMaximizer ? "Maximizer" : "Minimizer") + " is " + bestScore); } /** * Returns the optimal score assuming that both players play their best. * - * @param depth Indicates how deep we are into the game tree. - * @param isMaximizer True if it is maximizers turn; otherwise false. - * @param index Index of the leaf node that is being evaluated. - * @param verbose True to show each players choices. + *

+ * This method recursively evaluates the game tree using the minimax algorithm. + * At each level, the maximizer tries to maximize the score while the minimizer + * tries to minimize it. + * + * @param depth The current depth in the game tree (0 at root). + * @param isMaximizer True if it is the maximizer's turn; false for minimizer. + * @param index Index of the current node in the game tree. + * @param verbose True to print each player's choice during evaluation. * @return The optimal score for the player that made the first move. */ public int miniMax(int depth, boolean isMaximizer, int index, boolean verbose) { @@ -75,7 +120,7 @@ public int miniMax(int depth, boolean isMaximizer, int index, boolean verbose) { } // Leaf nodes can be sequentially inspected by - // recurssively multiplying (0 * 2) and ((0 * 2) + 1): + // recursively multiplying (0 * 2) and ((0 * 2) + 1): // (0 x 2) = 0; ((0 x 2) + 1) = 1 // (1 x 2) = 2; ((1 x 2) + 1) = 3 // (2 x 2) = 4; ((2 x 2) + 1) = 5 ... @@ -87,46 +132,73 @@ public int miniMax(int depth, boolean isMaximizer, int index, boolean verbose) { } /** - * Returns an array of random numbers which lenght is a power of 2. + * Returns an array of random numbers whose length is a power of 2. * - * @param size The power of 2 that will determine the lenght of the array. - * @param maxScore The maximum possible score. - * @return An array of random numbers. + * @param size The power of 2 that will determine the length of the array + * (array length = 2^size). + * @param maxScore The maximum possible score (scores will be between 1 and + * maxScore inclusive). + * @return An array of random numbers with length 2^size. */ public static int[] getRandomScores(int size, int maxScore) { int[] randomScores = new int[(int) Math.pow(2, size)]; - Random rand = new Random(); for (int i = 0; i < randomScores.length; i++) { - randomScores[i] = rand.nextInt(maxScore) + 1; + randomScores[i] = RANDOM.nextInt(maxScore) + 1; } return randomScores; } - // A utility function to find Log n in base 2 + /** + * Calculates the logarithm base 2 of a number. + * + * @param n The number to calculate log2 for (must be a power of 2). + * @return The log2 of n. + */ private int log2(int n) { return (n == 1) ? 0 : log2(n / 2) + 1; } - // A utility function to check if a number is a power of 2 + /** + * Checks if a number is a power of 2. + * + * @param n The number to check. + * @return True if n is a power of 2, false otherwise. + */ private boolean isPowerOfTwo(int n) { return n > 0 && (n & (n - 1)) == 0; } + /** + * Sets the scores array for the game tree. + * + * @param scores The array of scores. Length must be a power of 2. + * @throws IllegalArgumentException if the scores array length is not a power of + * 2 + */ public void setScores(int[] scores) { if (!isPowerOfTwo(scores.length)) { - System.out.println("The number of scores must be a power of 2."); - return; + throw new IllegalArgumentException("The number of scores must be a power of 2."); } - this.scores = scores; + this.scores = Arrays.copyOf(scores, scores.length); height = log2(this.scores.length); } + /** + * Returns a copy of the scores array. + * + * @return A copy of the scores array. + */ public int[] getScores() { - return scores; + return Arrays.copyOf(scores, scores.length); } + /** + * Returns the height of the game tree. + * + * @return The height of the game tree (log2 of the number of leaf nodes). + */ public int getHeight() { return height; } diff --git a/src/test/java/com/thealgorithms/others/MiniMaxAlgorithmTest.java b/src/test/java/com/thealgorithms/others/MiniMaxAlgorithmTest.java index 821eb3f16029..4e81c8b7e34f 100644 --- a/src/test/java/com/thealgorithms/others/MiniMaxAlgorithmTest.java +++ b/src/test/java/com/thealgorithms/others/MiniMaxAlgorithmTest.java @@ -1,12 +1,9 @@ package com.thealgorithms.others; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.io.ByteArrayOutputStream; import java.io.PrintStream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,56 +23,82 @@ void setUp() { System.setOut(new PrintStream(outputStream)); } + @AfterEach + void tearDown() { + System.setOut(originalOut); + } + @Test void testConstructorCreatesValidScores() { // The default constructor should create scores array of length 8 (2^3) - assertEquals(8, miniMax.getScores().length); - assertEquals(3, miniMax.getHeight()); + Assertions.assertEquals(8, miniMax.getScores().length); + Assertions.assertEquals(3, miniMax.getHeight()); // All scores should be positive (between 1 and 99) for (int score : miniMax.getScores()) { - assertTrue(score >= 1 && score <= 99); + Assertions.assertTrue(score >= 1 && score <= 99); } } + @Test + void testConstructorWithValidScores() { + int[] validScores = {10, 20, 30, 40}; + MiniMaxAlgorithm customMiniMax = new MiniMaxAlgorithm(validScores); + + Assertions.assertArrayEquals(validScores, customMiniMax.getScores()); + Assertions.assertEquals(2, customMiniMax.getHeight()); // log2(4) = 2 + } + + @Test + void testConstructorWithInvalidScoresThrowsException() { + int[] invalidScores = {10, 20, 30}; // Length 3 is not a power of 2 + Assertions.assertThrows(IllegalArgumentException.class, () -> new MiniMaxAlgorithm(invalidScores)); + } + + @Test + void testConstructorDoesNotModifyOriginalArray() { + int[] originalScores = {10, 20, 30, 40}; + int[] copyOfOriginal = {10, 20, 30, 40}; + MiniMaxAlgorithm customMiniMax = new MiniMaxAlgorithm(originalScores); + + // Modify the original array + originalScores[0] = 999; + + // Constructor should have made a copy, so internal state should be unchanged + Assertions.assertArrayEquals(copyOfOriginal, customMiniMax.getScores()); + } + @Test void testSetScoresWithValidPowerOfTwo() { int[] validScores = {10, 20, 30, 40}; miniMax.setScores(validScores); - assertArrayEquals(validScores, miniMax.getScores()); - assertEquals(2, miniMax.getHeight()); // log2(4) = 2 + Assertions.assertArrayEquals(validScores, miniMax.getScores()); + Assertions.assertEquals(2, miniMax.getHeight()); // log2(4) = 2 } @Test void testSetScoresWithInvalidLength() { int[] invalidScores = {10, 20, 30}; // Length 3 is not a power of 2 - miniMax.setScores(invalidScores); - - // Should print error message and not change the scores - String output = outputStream.toString(); - assertTrue(output.contains("The number of scores must be a power of 2.")); + Assertions.assertThrows(IllegalArgumentException.class, () -> miniMax.setScores(invalidScores)); // Scores should remain unchanged (original length 8) - assertEquals(8, miniMax.getScores().length); + Assertions.assertEquals(8, miniMax.getScores().length); } @Test void testSetScoresWithZeroLength() { int[] emptyScores = {}; // Length 0 is not a power of 2 - miniMax.setScores(emptyScores); - - // Should print error message and not change the scores - String output = outputStream.toString(); - assertTrue(output.contains("The number of scores must be a power of 2.")); + Assertions.assertThrows(IllegalArgumentException.class, () -> miniMax.setScores(emptyScores)); // Scores should remain unchanged (original length 8) - assertEquals(8, miniMax.getScores().length); + Assertions.assertEquals(8, miniMax.getScores().length); } @Test void testSetScoresWithVariousInvalidLengths() { - // Test multiple invalid lengths to ensure isPowerOfTwo function is fully covered + // Test multiple invalid lengths to ensure isPowerOfTwo function is fully + // covered int[][] invalidScoreArrays = { {1, 2, 3, 4, 5}, // Length 5 {1, 2, 3, 4, 5, 6}, // Length 6 @@ -86,17 +109,11 @@ void testSetScoresWithVariousInvalidLengths() { }; for (int[] invalidScores : invalidScoreArrays) { - // Clear the output stream for each test - outputStream.reset(); - miniMax.setScores(invalidScores); - - // Should print error message for each invalid length - String output = outputStream.toString(); - assertTrue(output.contains("The number of scores must be a power of 2."), "Failed for array length: " + invalidScores.length); + Assertions.assertThrows(IllegalArgumentException.class, () -> miniMax.setScores(invalidScores), "Failed for array length: " + invalidScores.length); } // Scores should remain unchanged (original length 8) - assertEquals(8, miniMax.getScores().length); + Assertions.assertEquals(8, miniMax.getScores().length); } @Test @@ -104,8 +121,8 @@ void testSetScoresWithSingleElement() { int[] singleScore = {42}; miniMax.setScores(singleScore); - assertArrayEquals(singleScore, miniMax.getScores()); - assertEquals(0, miniMax.getHeight()); // log2(1) = 0 + Assertions.assertArrayEquals(singleScore, miniMax.getScores()); + Assertions.assertEquals(0, miniMax.getHeight()); // log2(1) = 0 } @Test @@ -116,7 +133,7 @@ void testMiniMaxWithKnownScores() { // Maximizer starts: should choose max(min(3,12), min(8,2)) = max(3, 2) = 3 int result = miniMax.miniMax(0, true, 0, false); - assertEquals(3, result); + Assertions.assertEquals(3, result); } @Test @@ -127,7 +144,7 @@ void testMiniMaxWithMinimizerFirst() { // Minimizer starts: should choose min(max(3,12), max(8,2)) = min(12, 8) = 8 int result = miniMax.miniMax(0, false, 0, false); - assertEquals(8, result); + Assertions.assertEquals(8, result); } @Test @@ -139,8 +156,8 @@ void testMiniMaxWithLargerTree() { // Maximizer starts int result = miniMax.miniMax(0, true, 0, false); // Expected: max(min(max(5,6), max(7,4)), min(max(5,3), max(6,2))) - // = max(min(6, 7), min(5, 6)) = max(6, 5) = 6 - assertEquals(6, result); + // = max(min(6, 7), min(5, 6)) = max(6, 5) = 6 + Assertions.assertEquals(6, result); } @Test @@ -151,41 +168,41 @@ void testMiniMaxVerboseOutput() { miniMax.miniMax(0, true, 0, true); String output = outputStream.toString(); - assertTrue(output.contains("Maximizer")); - assertTrue(output.contains("Minimizer")); - assertTrue(output.contains("chooses")); + Assertions.assertTrue(output.contains("Maximizer")); + Assertions.assertTrue(output.contains("Minimizer")); + Assertions.assertTrue(output.contains("chooses")); } @Test void testGetRandomScoresLength() { int[] randomScores = MiniMaxAlgorithm.getRandomScores(4, 50); - assertEquals(16, randomScores.length); // 2^4 = 16 + Assertions.assertEquals(16, randomScores.length); // 2^4 = 16 // All scores should be between 1 and 50 for (int score : randomScores) { - assertTrue(score >= 1 && score <= 50); + Assertions.assertTrue(score >= 1 && score <= 50); } } @Test void testGetRandomScoresWithDifferentParameters() { int[] randomScores = MiniMaxAlgorithm.getRandomScores(2, 10); - assertEquals(4, randomScores.length); // 2^2 = 4 + Assertions.assertEquals(4, randomScores.length); // 2^2 = 4 // All scores should be between 1 and 10 for (int score : randomScores) { - assertTrue(score >= 1 && score <= 10); + Assertions.assertTrue(score >= 1 && score <= 10); } } @Test void testMainMethod() { // Test that main method runs without errors - assertDoesNotThrow(() -> MiniMaxAlgorithm.main(new String[] {})); + Assertions.assertDoesNotThrow(() -> MiniMaxAlgorithm.main(new String[] {})); String output = outputStream.toString(); - assertTrue(output.contains("The best score for")); - assertTrue(output.contains("Maximizer")); + Assertions.assertTrue(output.contains("The best score for")); + Assertions.assertTrue(output.contains("Maximizer")); } @Test @@ -193,11 +210,11 @@ void testHeightCalculation() { // Test height calculation for different array sizes int[] scores2 = {1, 2}; miniMax.setScores(scores2); - assertEquals(1, miniMax.getHeight()); // log2(2) = 1 + Assertions.assertEquals(1, miniMax.getHeight()); // log2(2) = 1 int[] scores16 = new int[16]; miniMax.setScores(scores16); - assertEquals(4, miniMax.getHeight()); // log2(16) = 4 + Assertions.assertEquals(4, miniMax.getHeight()); // log2(16) = 4 } @Test @@ -206,7 +223,7 @@ void testEdgeCaseWithZeroScores() { miniMax.setScores(zeroScores); int result = miniMax.miniMax(0, true, 0, false); - assertEquals(0, result); + Assertions.assertEquals(0, result); } @Test @@ -218,11 +235,7 @@ void testEdgeCaseWithNegativeScores() { // Level 1 (minimizer): min(-5,-2) = -5, min(-8,-1) = -8 // Level 0 (maximizer): max(-5, -8) = -5 int result = miniMax.miniMax(0, true, 0, false); - assertEquals(-5, result); - } - - void tearDown() { - System.setOut(originalOut); + Assertions.assertEquals(-5, result); } @Test @@ -233,12 +246,9 @@ void testSetScoresWithNegativeLength() { // Test with array length 0 (edge case for n > 0 condition) int[] emptyArray = new int[0]; - outputStream.reset(); - miniMax.setScores(emptyArray); + Assertions.assertThrows(IllegalArgumentException.class, () -> miniMax.setScores(emptyArray)); - String output = outputStream.toString(); - assertTrue(output.contains("The number of scores must be a power of 2.")); - assertEquals(8, miniMax.getScores().length); // Should remain unchanged + Assertions.assertEquals(8, miniMax.getScores().length); // Should remain unchanged } @Test @@ -250,8 +260,8 @@ void testSetScoresWithLargePowerOfTwo() { } miniMax.setScores(largeValidScores); - assertArrayEquals(largeValidScores, miniMax.getScores()); - assertEquals(5, miniMax.getHeight()); // log2(32) = 5 + Assertions.assertArrayEquals(largeValidScores, miniMax.getScores()); + Assertions.assertEquals(5, miniMax.getHeight()); // log2(32) = 5 } @Test @@ -270,8 +280,65 @@ void testSetScoresValidEdgeCases() { for (int i = 0; i < validPowersOf2.length; i++) { miniMax.setScores(validPowersOf2[i]); - assertEquals(validPowersOf2[i].length, miniMax.getScores().length, "Failed for array length: " + validPowersOf2[i].length); - assertEquals(expectedHeights[i], miniMax.getHeight(), "Height calculation failed for array length: " + validPowersOf2[i].length); + Assertions.assertEquals(validPowersOf2[i].length, miniMax.getScores().length, "Failed for array length: " + validPowersOf2[i].length); + Assertions.assertEquals(expectedHeights[i], miniMax.getHeight(), "Height calculation failed for array length: " + validPowersOf2[i].length); } } + + @Test + void testGetScoresReturnsDefensiveCopy() { + int[] originalScores = {10, 20, 30, 40}; + miniMax.setScores(originalScores); + + // Get the scores and modify them + int[] retrievedScores = miniMax.getScores(); + retrievedScores[0] = 999; + + // Internal state should remain unchanged + Assertions.assertEquals(10, miniMax.getScores()[0]); + } + + @Test + void testSetScoresCreatesDefensiveCopy() { + int[] originalScores = {10, 20, 30, 40}; + miniMax.setScores(originalScores); + + // Modify the original array after setting + originalScores[0] = 999; + + // Internal state should remain unchanged + Assertions.assertEquals(10, miniMax.getScores()[0]); + } + + @Test + void testMiniMaxWithAllSameScores() { + int[] sameScores = {5, 5, 5, 5}; + miniMax.setScores(sameScores); + + // When all scores are the same, result should be that score + int result = miniMax.miniMax(0, true, 0, false); + Assertions.assertEquals(5, result); + } + + @Test + void testMiniMaxAtDifferentDepths() { + int[] testScores = {3, 12, 8, 2, 14, 5, 2, 9}; + miniMax.setScores(testScores); + + // Test maximizer first + int result = miniMax.miniMax(0, true, 0, false); + // Expected: max(min(max(3,12), max(8,2)), min(max(14,5), max(2,9))) + // = max(min(12, 8), min(14, 9)) = max(8, 9) = 9 + Assertions.assertEquals(9, result); + } + + @Test + void testMiniMaxWithMinIntAndMaxInt() { + int[] extremeScores = {Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 1}; + miniMax.setScores(extremeScores); + + int result = miniMax.miniMax(0, true, 0, false); + // Expected: max(min(MIN, MAX), min(0, 1)) = max(MIN, 0) = 0 + Assertions.assertEquals(0, result); + } }