diff --git a/maths/Game Theory/minimax/README.md b/maths/Game Theory/minimax/README.md new file mode 100644 index 000000000000..b6f73f301e49 --- /dev/null +++ b/maths/Game Theory/minimax/README.md @@ -0,0 +1,30 @@ + +# Minimax Algorithm + +A decision-making algorithm for two-player games to minimize the maximum possible loss (This is a simple, recursive, implementation of the MiniMax algorithm in Python) + +MiniMax is used in decision, game theory, statistics and philosophy. It can be implemented on a two player's game given full information of the states like the one offered by games like Tic Tac Toe. That means MM cannot be used in games that feature randomness like dices. The reason is that it has to be fully aware of all possible moves/states during gameplay before it makes its mind on the best move to play. + +The following implementation is made for the cube/sticks game: User sets an initial number of cubes available on a table. Both players (the user & the PC implementing MM) can pick up a number of cubes off the table in groups of 1, 2 or K. K is also set by the user. The player who picks up the last remaining cubes from the table on a single take wins the game. + +The MiniMax algorithm is being implemented for the PC player and it always assume that the opponent (user) is also playing optimum. MM is fully aware of the remaining cubes and its valid moves at all states. So technically it will recursively expand the whole game tree and given the fact that the amount of possible moves are three (1,2,K), all tree nodes will end up with 3 leaves, one for each option. + +Game over is the case where there are no available cubes on the table or in the case of a negative amount of cubes. The reason for the negative scenario is due to the fact that MM will expand the whole tree without checking if all three options are allowed during a state. In a better implementation we could take care of that scenario as we also did on the user side. No matter what, if MM’s move lead to negative cubes he will lose the game. + +Evaluation starts on the leaves of the tree. Both players alternate during game play so each layer of the tree marks the current player (MAX or MIN). That way the evaluation function can set a higher/positive value if player MAX wins and a lower/negative value if he loses (remember evaluation happens from the MiniMax’s perspective so he will be the MAX player). When all leaves get their evaluation and thanks to the recursive implementation of the algorithm, their values climb up on each layer till the root of the tree also gets evaluated. That way MAX player will try to lead the root to get the highest possible value, assuming that MIN player (user) will try its best to lead to the lowest value possible. When the root gets its value, MAX player (who will be the first one to play) knows what move would lead to victory or at least lead to a less painful loss. + +So the goal of MiniMax is to minimize the possible loss for a worst case scenario, from the algorithm's perspective. + + + +/// There is an example code implemented with deatailed explanation in the minimax.py file /// + + + + +## Acknowledgements + + - [Original Author](https://github.com/savvasio) + - [Wiki](https://en.wikipedia.org/wiki/Minimax) + - [Video Explanation](https://www.youtube.com/watch?v=l-hh51ncgDI) + diff --git a/maths/Game Theory/minimax/minimax.py b/maths/Game Theory/minimax/minimax.py new file mode 100644 index 000000000000..13cf0cdfabb6 --- /dev/null +++ b/maths/Game Theory/minimax/minimax.py @@ -0,0 +1,183 @@ +# ==================== 0. Evaluation & Utilities ================== + +# If the amount of cubes on the table is 0, the last player to pick up cubes off the table is the winner. +# State evaluation is set on the MAX player's perspective (PC), so if he wins he gets eval +100. If he loses, his eval is set to -100. +# In states with a negative amount of cubes availiable on the table, the last person played is the loser. +# If the current state is not final, we don't care on the current evaluation so we simply initialise it to 0. + + +def evaluate(state, player): + if state == 0: + if -player == MAX: + return +100 + else: + return -100 + elif state < 0: + if -player == MAX: + return -100 + else: + return +100 + else: + return 0 + + +def gameOver(remainingCubes, player): + if remainingCubes == 0: + if player == MAX: # If MAX's turn led to 0 cubes on the table + print("=" * 20) + print("Im sorry, you lost!") + print("=" * 20) + else: + print("=" * 69) + print("Hey congrats! You won MiniMax. Didnt see that coming!") + print("=" * 69) + return True + + +# M input validation +def validateM(message): + while True: + try: + inp = input(message) + if inp == "q" or inp == "Q": + quit() # Exit tha game + M = int(inp) + except ValueError: + print("Try again with an integer!") + continue + else: + if M >= 4: # We can not accept less than 4 + return M + else: + print("Please try again with an integer bigger than 3.") + continue + + +# K input validation +def validateK(message): + while True: + try: + inp = input(message) + if inp == "q" or inp == "Q": + quit() + K = int(inp) + except ValueError: + print("Try again with an integer!") + continue + if (K > 2) and (K < M): # acceptable K limits are 2+1 & M-1 respectively. + return K + else: + print(f"You need to insert an integer in the range of 3 to {M-1}!") + + +# Game play input validation +# Input is considered valid only if its one of the 3 availiable options and does not cause a negative amount of cubes on the table. +def validateInput(message): + while True: + try: + inp = input(message) + if inp == "q" or inp == "Q": + quit() + inp = int(inp) # in the cause of not integer input it causes an error + except ValueError: + print(f"Try again with an integer!") + continue + if inp in choices: + if M - inp >= 0: + return inp # Accepted input + else: + print(f"There are no {inp} availiable cubes. Try to pick up less..") + else: + print(f"Wrong choice, try again. Availiable options are: 1 or 2 or {K}: ") + + +def plural(choice): + if choice == 1: + return "cube" + else: + return "cubes" + + +# ==================== 1. MiniMax for the optimal choice from MAX ================== +# It recursively expands the whole tree and returns the list [score, move], +# meaning the pair of best score tighten to the actual move that caused it. +def MiniMax(state, player): + if state <= 0: # Base case that will end recursion + return [ + evaluate(state, player), + 0, + ] # We really do not care on the move at this point + + availiableChoices = [] + for i in range( + len(choices) + ): # for every availiable choice/branch of the tree 1, 2 ή K + score, move = MiniMax( + state - choices[i], -player + ) # Again we dont care on the move here + availiableChoices.append(score) + + if player == MAX: + score = max(availiableChoices) + move = [i for i, value in enumerate(availiableChoices) if value == score] + # move list consists of all indexes where min or max shows up but we will + # use only the 1st one. + return [score, move[0]] + else: + score = min(availiableChoices) + move = [i for i, value in enumerate(availiableChoices) if value == score] + return [score, move[0]] + + +# ====================== 2. MAIN EXECUTION ====================== +print("+" * 126) +print( + "INSTUCTIONS: There are M availiable cubes on the table. Both players are allowed to remove 1, 2 or K cubes at the same time." +) +print( + "You will set the M & K variables. Since tree prunning has not been implemented, its Minimax after all, we suggest you set M < 20 for the execution to be smooth." +) +print("Press q to exit the game.") +print( + "The player who removes the last cube off the table will be the winner. The first player is the PC. Good luck!" +) +print("+" * 126) + +MAX = +1 +MIN = -1 +M = validateM( + "Please insert an initial number of cubes (M) availiable on the table: " +) # M = state/depth/remainingCubes +K = validateK( + "Please insert an integer K, 2 < K < M, that will act as the 3rd option for the ammount of cubes both players can get off the table: " +) +choices = [1, 2, K] + +print( + f"\nThe game begins with {M} cubes availiable on the table and each player can pick 1, 2 ή {K}:" +) +while M > 0: + # ===== PC's turn ===== + print("Please wait for the PC to make its mind..") + score, move = MiniMax(M, MAX) + M = M - choices[move] + + print( + f"\nPc chose to remove {choices[move]} {plural(choices[move])} off the table. Remaining cubes are {M}." + ) + if gameOver(M, MAX): + break # Game over check + + # ===== Παίζει ο χρήστης ===== + else: + userChoice = validateInput( + f"\nHow many cubes would you like to pick up (1, 2 ή {K}): " + ) + # In valid the game goes on. In any other case it gets stacked on the validation function till a proper input is given. + + M = M - int(userChoice) + print( + f"\nYou chose to remove {userChoice} {plural(int(userChoice))} from the table. Remaining cubes are {M}." + ) + if gameOver(M, MIN): + break # Game over check.