Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 93

Minimax for Game AI

Recommended resources

Hello! Welcome to the fifth lesson in the "Artificial Intelligence: Search and Reasoning" module.

In our previous lesson, we explored how to solve puzzles like Sudoku and N-Queens using the backtracking algorithm. You learned to formulate these as Constraint Satisfaction Problems (CSPs) and apply a systematic search that prunes impossible paths. This involved exploring a state space to find a goal state that satisfied all constraints.

Today, we're moving from collaborative puzzle-solving to competitive game-playing. We'll tackle problems where an opponent is actively trying to work against you. Our goal is to implement the Minimax algorithm for adversarial, two-player games. This is a foundational algorithm in AI game theory, used to determine the optimal move for a player, assuming the opponent also plays optimally.

1. From Search to Adversarial Search

In backtracking, you searched for a path to a goal. In adversarial games, the "goal" is to win, but the environment is no longer static. An opponent's moves change the state of the game, trying to lead you towards a losing state.

Minimax is designed for two-player, zero-sum, perfect-information games. This means:

  • Two-player: Just you and one opponent (e.g., you are 'X', they are 'O').
  • Zero-sum: A win for one player is a loss for the other.
  • Perfect information: Both players know the complete state of the game at all times (no hidden cards).

Tic-Tac-Toe is the classic example.

The core idea of Minimax is to explore the game tree of all possible future moves. You act as the Maximizer, trying to achieve the highest possible score, while your opponent is the Minimizer, trying to force you into the lowest possible score.

This image connects the game tree structure with the logic of the algorithm.

Minimax Algorithm: Game Tree and Pseudocode
A visual representation of a game tree. The `MAX` nodes represent our turn, where we choose the move leading to the maximum score. The `MIN` nodes represent the opponent's turn, where they will choose the move leading to the minimum score.

2. The Mechanics of Minimax

The algorithm works in three main steps, which are best understood by starting from the end of the game and working backward.

  1. Generate the Game Tree: From the current state, map out all possible sequences of moves until the game ends. The end-states are called terminal nodes.
  2. Evaluate Terminal Nodes: Assign a utility score to each terminal node. For Tic-Tac-Toe, a simple utility function could be:
    • +1: If the Maximizer (you) wins.
    • -1: If the Minimizer (opponent) wins.
    • 0: If it's a draw.
  3. Propagate Scores Upward: This is the "mini-max" part.
    • At each MIN node (opponent's turn), the score passed up is the minimum of its children's scores.
    • At each MAX node (your turn), the score passed up is the maximum of its children's scores.

This process continues until the scores reach the top of the tree. The move you should make is the one that leads to the child node with the highest score.

Let's watch a video that demonstrates this process clearly with Tic-Tac-Toe.

Coding an UNBEATABLE Tic Tac Toe AI (Game Theory Minimax Algorithm EXPLAINED)

The video 'Coding an UNBEATABLE Tic Tac Toe AI' by Kylie Ying provides a fantastic visual explanation of how scores are assigned and propagated up the game tree.

Watch from 01:24 to 05:34. Pay close attention to how the utility values (+1, 0, -1) are calculated for the final board states and how these values are recursively passed up the tree to determine the best move at the current state.


3. Implementing the Minimax Algorithm

The upward propagation of scores naturally lends itself to a recursive implementation. Since you are a computer science student, it's valuable to see how to write this algorithm not just for one specific game, but in a general, reusable way.

The article "Minimax in Python" from Real Python does an excellent job of this. It starts with a simple game called Nim and then refactors the code to create a general Minimax solver.

Minimax in Python: Learn How to Lose the Game of Nim

This article, 'Minimax in Python: Learn How to Lose the Game of Nim', will guide us through building a flexible Minimax implementation. We will focus on the refactoring process, which is a key software engineering skill.

Read the section titled 'Refactor to a General Minimax Algorithm'. Focus on how the game-specific logic is separated into two helper functions, possible_new_states() and evaluate(), while the minimax() function contains the core, reusable algorithm.

Let's break down the beautiful, refactored Python code from that article.

A general Minimax implementation has three key parts:

  1. evaluate(state, is_maximizing): This is a game-specific function. It checks if the current state is a terminal state (game over). If it is, it returns the utility score. Otherwise, it returns None.
  2. possible_new_states(state): This is also game-specific. It takes a game state and returns a list of all possible next states that can be reached in one move.
  3. minimax(state, is_maximizing): This is the core, general algorithm. It doesn't know the rules of the game, it only knows how to search the tree.

Here's the structure of the minimax function:

def minimax(state, is_maximizing):
    # Base Case: If the game is over, return its score.
    score = evaluate(state, is_maximizing)
    if score is not None:
        return score

    # Recursive Step:
    if is_maximizing:
        # Find the max score among all possible next moves.
        best_score = -infinity
        for new_state in possible_new_states(state):
            score = minimax(new_state, False) # It's now the minimizer's turn
            best_score = max(score, best_score)
        return best_score
    else: # is_minimizing
        # Find the min score among all possible next moves.
        best_score = +infinity
        for new_state in possible_new_states(state):
            score = minimax(new_state, True) # It's now the maximizer's turn
            best_score = min(score, best_score)
        return best_score

This structure is powerful. To make your AI play Tic-Tac-Toe, you just need to implement evaluate() and possible_new_states() for Tic-Tac-Toe. To make it play Checkers, you'd swap in different versions of those two functions, but the minimax function would remain unchanged!

Test your understanding!

Consider the following simple game tree. MAX is trying to get the highest score. What is the value of the root node 'A'?

      A (MAX)
     / \
    /   \
 B (MIN)   C (MIN)
 / \     / \
D   E   F   G
|   |   |   |
3   5   2   9 
Show answer

The value of the root node 'A' is 3.

Here's the reasoning:

  1. Node B (MIN): This is a minimizer's turn. It will choose the move that leads to the minimum score between its children, D (3) and E (5). So, the value of node B is min(3, 5) = 3.
  2. Node C (MIN): This is also a minimizer's turn. It will choose the move that leads to the minimum score between its children, F (2) and G (9). So, the value of node C is min(2, 9) = 2.
  3. Node A (MAX): This is the maximizer's turn. It will choose the move that leads to the maximum score between its children, B (3) and C (2). So, the value of node A is max(3, 2) = 3.

Therefore, the optimal move for MAX is to go left, which guarantees a score of at least 3.


4. A Live Coding Example

To see these concepts translated into a full, working program, it's helpful to watch someone build it from scratch. The following video provides a live-coding walkthrough for creating an unbeatable Tic-Tac-Toe AI.

Coding Challenge 154: Minimax Algorithm for Tic Tac Toe

The video 'Coding Challenge 154: Minimax Algorithm for Tic Tac Toe' from The Coding Train shows the entire implementation process, including common pitfalls and debugging, which is very instructive for a new programmer.

Watch from 07:31 to 17:15. This section covers the core implementation of the recursive minimax function. Observe how the function handles: The base case (terminal state check). The is_maximizing flag to switch between max and min logic. The recursive calls for exploring the game tree.

This hands-on approach reinforces the structure we discussed and shows how the pieces fit together in a complete project.


Conclusion

You have now learned one of the most important algorithms in game-playing AI. While modern game AIs for complex games like Chess or Go use more advanced techniques, they are often built upon the fundamental principles of Minimax that you learned today.

Key Takeaways:

  • Minimax is a recursive, depth-first algorithm used to find the optimal move in two-player, zero-sum, perfect-information games.
  • It models the game as a game tree, where nodes are game states and edges are moves.
  • It assumes an optimal opponent: the Maximizer (us) tries to maximize the score, while the Minimizer (opponent) tries to minimize it.
  • The algorithm is implemented using a recursive function that determines a node's value based on the values of its children (max() for MAX's turn, min() for MIN's turn).
  • By separating the game rules (evaluate, possible_new_states) from the algorithm (minimax), we can write clean, reusable code.

Preview of the Next Lesson:

Minimax works perfectly when we can see the entire game tree. But what about more complex scenarios where things are uncertain, or the number of states is too vast to search? In our next lesson, "Model sequential decision-making problems using Markov Decision Processes (MDPs)," we will learn a framework for making optimal decisions in environments that involve probability and long-term rewards, moving us closer to the core of modern reinforcement learning.