Skip to content

minimax algorithm added #11678

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from
30 changes: 30 additions & 0 deletions maths/Game Theory/minimax/README.md
Original file line number Diff line number Diff line change
@@ -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)

183 changes: 183 additions & 0 deletions maths/Game Theory/minimax/minimax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# ==================== 0. Evaluation & Utilities ==================

Check failure on line 1 in maths/Game Theory/minimax/minimax.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (INP001)

maths/Game Theory/minimax/minimax.py:1:1: INP001 File `maths/Game Theory/minimax/minimax.py` is part of an implicit namespace package. Add an `__init__.py`.

# If the amount of cubes on the table is 0, the last player to pick up cubes off the table is the winner.

Check failure on line 3 in maths/Game Theory/minimax/minimax.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

maths/Game Theory/minimax/minimax.py:3:89: E501 Line too long (105 > 88)
# 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.

Check failure on line 4 in maths/Game Theory/minimax/minimax.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

maths/Game Theory/minimax/minimax.py:4:89: E501 Line too long (134 > 88)
# In states with a negative amount of cubes availiable on the table, the last person played is the loser.

Check failure on line 5 in maths/Game Theory/minimax/minimax.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

maths/Game Theory/minimax/minimax.py:5:89: E501 Line too long (105 > 88)
# If the current state is not final, we don't care on the current evaluation so we simply initialise it to 0.

Check failure on line 6 in maths/Game Theory/minimax/minimax.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

maths/Game Theory/minimax/minimax.py:6:89: E501 Line too long (109 > 88)


def evaluate(state, player):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file maths/Game Theory/minimax/minimax.py, please provide doctest for the function evaluate

Please provide return type hint for the function: evaluate. If the function does not return a value, please provide the type hint as: def function() -> None:

Please provide type hint for the parameter: state

Please provide type hint for the parameter: player

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file maths/Game Theory/minimax/minimax.py, please provide doctest for the function evaluate

Please provide return type hint for the function: evaluate. If the function does not return a value, please provide the type hint as: def function() -> None:

Please provide type hint for the parameter: state

Please provide type hint for the parameter: 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):

Check failure on line 24 in maths/Game Theory/minimax/minimax.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (N802)

maths/Game Theory/minimax/minimax.py:24:5: N802 Function name `gameOver` should be lowercase

Check failure on line 24 in maths/Game Theory/minimax/minimax.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (N803)

maths/Game Theory/minimax/minimax.py:24:14: N803 Argument name `remainingCubes` should be lowercase

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file maths/Game Theory/minimax/minimax.py, please provide doctest for the function gameOver

Variable and function names should follow the snake_case naming convention. Please update the following name accordingly: gameOver

Please provide return type hint for the function: gameOver. If the function does not return a value, please provide the type hint as: def function() -> None:

Variable and function names should follow the snake_case naming convention. Please update the following name accordingly: remainingCubes

Please provide type hint for the parameter: remainingCubes

Please provide type hint for the parameter: player

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file maths/Game Theory/minimax/minimax.py, please provide doctest for the function gameOver

Variable and function names should follow the snake_case naming convention. Please update the following name accordingly: gameOver

Please provide return type hint for the function: gameOver. If the function does not return a value, please provide the type hint as: def function() -> None:

Variable and function names should follow the snake_case naming convention. Please update the following name accordingly: remainingCubes

Please provide type hint for the parameter: remainingCubes

Please provide type hint for the parameter: 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):

Check failure on line 38 in maths/Game Theory/minimax/minimax.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (N802)

maths/Game Theory/minimax/minimax.py:38:5: N802 Function name `validateM` should be lowercase

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file maths/Game Theory/minimax/minimax.py, please provide doctest for the function validateM

Variable and function names should follow the snake_case naming convention. Please update the following name accordingly: validateM

Please provide return type hint for the function: validateM. If the function does not return a value, please provide the type hint as: def function() -> None:

Please provide type hint for the parameter: message

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file maths/Game Theory/minimax/minimax.py, please provide doctest for the function validateM

Variable and function names should follow the snake_case naming convention. Please update the following name accordingly: validateM

Please provide return type hint for the function: validateM. If the function does not return a value, please provide the type hint as: def function() -> None:

Please provide type hint for the parameter: message

while True:
try:
inp = input(message)
if inp == "q" or inp == "Q":

Check failure on line 42 in maths/Game Theory/minimax/minimax.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (PLR1714)

maths/Game Theory/minimax/minimax.py:42:16: PLR1714 Consider merging multiple comparisons: `inp in ("q", "Q")`. Use a `set` if the elements are hashable.
quit() # Exit tha game

Check failure on line 43 in maths/Game Theory/minimax/minimax.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (PLR1722)

maths/Game Theory/minimax/minimax.py:43:17: PLR1722 Use `sys.exit()` instead of `quit`
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):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file maths/Game Theory/minimax/minimax.py, please provide doctest for the function validateK

Variable and function names should follow the snake_case naming convention. Please update the following name accordingly: validateK

Please provide return type hint for the function: validateK. If the function does not return a value, please provide the type hint as: def function() -> None:

Please provide type hint for the parameter: message

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file maths/Game Theory/minimax/minimax.py, please provide doctest for the function validateK

Variable and function names should follow the snake_case naming convention. Please update the following name accordingly: validateK

Please provide return type hint for the function: validateK. If the function does not return a value, please provide the type hint as: def function() -> None:

Please provide type hint for the parameter: 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):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file maths/Game Theory/minimax/minimax.py, please provide doctest for the function validateInput

Variable and function names should follow the snake_case naming convention. Please update the following name accordingly: validateInput

Please provide return type hint for the function: validateInput. If the function does not return a value, please provide the type hint as: def function() -> None:

Please provide type hint for the parameter: message

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file maths/Game Theory/minimax/minimax.py, please provide doctest for the function validateInput

Variable and function names should follow the snake_case naming convention. Please update the following name accordingly: validateInput

Please provide return type hint for the function: validateInput. If the function does not return a value, please provide the type hint as: def function() -> None:

Please provide type hint for the parameter: 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):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file maths/Game Theory/minimax/minimax.py, please provide doctest for the function plural

Please provide return type hint for the function: plural. If the function does not return a value, please provide the type hint as: def function() -> None:

Please provide type hint for the parameter: choice

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file maths/Game Theory/minimax/minimax.py, please provide doctest for the function plural

Please provide return type hint for the function: plural. If the function does not return a value, please provide the type hint as: def function() -> None:

Please provide type hint for the parameter: 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):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file maths/Game Theory/minimax/minimax.py, please provide doctest for the function MiniMax

Variable and function names should follow the snake_case naming convention. Please update the following name accordingly: MiniMax

Please provide return type hint for the function: MiniMax. If the function does not return a value, please provide the type hint as: def function() -> None:

Please provide type hint for the parameter: state

Please provide type hint for the parameter: player

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file maths/Game Theory/minimax/minimax.py, please provide doctest for the function MiniMax

Variable and function names should follow the snake_case naming convention. Please update the following name accordingly: MiniMax

Please provide return type hint for the function: MiniMax. If the function does not return a value, please provide the type hint as: def function() -> None:

Please provide type hint for the parameter: state

Please provide type hint for the parameter: 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 = []

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable and function names should follow the snake_case naming convention. Please update the following name accordingly: availiableChoices

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable and function names should follow the snake_case naming convention. Please update the following name accordingly: 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(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable and function names should follow the snake_case naming convention. Please update the following name accordingly: userChoice

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable and function names should follow the snake_case naming convention. Please update the following name accordingly: userChoice

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.
Loading