AI plays TicTacToe interactive

Author: Mahbub Alam

Introduction

This notebook demonstrates how a simple AI agent can play TicTacToe against a human using the minimax algorithm combined with a clever magic square representation to make optimal moves.

The 3x3 magic square representation helps to quickly check for winning combinations.

🎮 Play online (no setup required): Binder Run all cells, then find the game at the bottom. Click to make your moves as ⭕, while the AI responds as ❌.

from itertools import combinations
from IPython.display import display, clear_output
import ipywidgets as widgets
from time import sleep

Magic Square Representation

The classic 3Ă—3 magic square:
8 3 4
1 5 9
6 7 2

ensures that any winning line in TicTacToe (row, column, diagonal) sums to 15.

This allows us to check wins using simple arithmetic instead of explicit board patterns. Each cell maps to its magic square value, enabling efficient win detection. See my blog post for more explanation.

Let’s start by defining the core constants.

magic = [8, 3, 4, 1, 5, 9, 6, 7, 2]
magic_ = {
    8 : 0,
    3 : 1,
    4 : 2,
    1 : 3,
    5 : 4,
    9 : 5,
    6 : 6,
    7 : 7,
    2 : 8
}

Board Display Functions

These functions create and update the interactive game board using ipywidgets. The board uses border styling to create the classic # grid pattern.

def make_board_display(board, click_handler):
    """Create a 3Ă—3 grid of buttons for an interactive TicTacToe board."""
    buttons = []
    for i in range(9):
        row, col = i // 3, i % 3

        border_top = "3px solid #333" if row > 0 else "0px"
        border_left = "3px solid #333" if col > 0 else "0px"
        margin_top = "-3px" if row > 0 else "0px"
        margin_left = "-3px" if col > 0 else "0px"

        b = widgets.Button(
            description=board[i] if board[i] != "_" else " ",
            layout=widgets.Layout(
                width="72px",
                height="72px",
                border_top=border_top,
                border_left=border_left,
                margin=f"{margin_top} 0px 0px {margin_left}",
                padding="0px"
            ),
            style=widgets.ButtonStyle(button_color="#ffffff"),
            disabled=False,
            tooltip=""
        )
        b.index = i
        b.on_click(click_handler)
        buttons.append(b)
    grid = widgets.GridBox(
        buttons,
        layout=widgets.Layout(
            grid_template_columns="repeat(3, 72px)",
            grid_gap="0px",
            justify_content="center"
        )
    )
    return grid, buttons

def update_(buttons, board):
    """Update button colors and text to reflect current board state."""
    for i, cell in enumerate(board):
        if cell == "X":
            buttons[i].description = "❌"
            buttons[i].style.button_color = "#ffe6e6"
        elif cell == "O":
            buttons[i].description = "â­•"
            buttons[i].style.button_color = "#e6f0ff"
        else:
            buttons[i].description = " "
            buttons[i].style.button_color = "#ffffff"

def disable_all(buttons):
    """Disable all buttons on the board."""
    for b in buttons:
        b.disabled = True

Game Logic Functions

Core game mechanics including win detection and the minimax AI algorithm.

A helper dictionary and a function first.

# AI will always play "X" and human "O"
score_dict = {
    "X" : 1,
    "O" : -1
}

def avail_moves(board):
    """ This function simply returns all empty positions (marked with '_') on the board.  """

    return [i for i in range(9) if board[i] == "_"]

Check for Winning Moves

This function uses the magic square logic. For each pair of the player’s existing moves, check if the magic square values of those positions and of an available position add up to 15 (winning line).

If a winning move exists, it returns:

  • the index of the winning square, and
  • the score of the board position.

Returns None if there’s no winning moves.

Definition:

Score of a board position is defined to be +1 if AI has a winning strategy and -1 if human has a winning strategy.

def check_player_win(board, player):
    player_moves_ = [i for i in range(9) if board[i] == player]
    avail_magic = [magic[i] for i in avail_moves(board)]

    for tuple_ in combinations(player_moves_, 2):
        surp = 15 - (magic[tuple_[0]] + magic[tuple_[1]])
        if surp in avail_magic:
            return magic_[surp], score_dict[player]

    return None

The Minimax Algorithm

Minimax algorithm for optimal AI move selection.

Algorithm: Recursively explores all possible game states assuming optimal play.

  • X (AI) maximizes the score (+1 for win)
  • O (human) minimizes the score (-1 for win)
  • Returns (move, score): score of a board position and a move that achieves that score
  • Returns (None, 0) for draw
def minimax(board, player, opp):
    avail_moves_ = avail_moves(board)
    if not avail_moves_:
        return None, 0

    res = check_player_win(board, player)
    if res is not None:
        return res

    scores = []
    for i in avail_moves_:
        new_board = board.copy()
        new_board[i] = player
        _, score_ = minimax(new_board, opp, player)
        scores.append((i, score_))

    scores.sort(key=lambda x: x[1])
    return scores[-1] if player == "X" else scores[0]

Interactive TicTacToe

Now play the game! Open this notebook in your Jupyter Notebook (or Binder) and run all code blocks.

Click cells to make your move as ⭕ while the AI plays as ❌. The AI uses the minimax algorithm, so it plays optimally - try to get a draw! 🎮

def play_interactive():
    """Jupyter version of main() from AI_plays_TicTacToe.py"""
    board = ["_"] * 9

    print("")
    print(68 * "=")
    print(f"==={'='*14}[[ Welcome to AI plays TicTacToe ]]{'='*14}==\n")
    print("AI will play 'X', you play 'O'.\n")

    first = input("Do you want to play first? [Y/n] ")
    player, opp = ("O", "X") if first.lower() != 'n' else ("X", "O")
    print(f"")

    out = widgets.Output()

    def ai_move():
        nonlocal player, opp
        print("\nThis is AI's turn.\n")
        sleep(0.3)
        res_x = check_player_win(board, "X")
        res_o = check_player_win(board, "O")

        if res_x is not None:
            move_index = res_x[0]
            board[move_index] = "X"
            update_(buttons, board)
            with out:
                clear_output(wait=True)
                print("\nAI wins!")
            disable_all(buttons)
            return True
        elif res_o is not None:
            move_index = res_o[0]
        else:
            move_index, _ = minimax(board, "X", "O")

        board[move_index] = "X"
        update_(buttons, board)

        player, opp = opp, player

        # If your turn is the last turn, already decides if there will be no result
        if len(avail_moves(board)) == 1:
            sleep(0.2)
            board[avail_moves(board)[0]] = player
            update_(buttons, board)
            res = check_player_win(board, player)
            if res is not None:
                p_name = "You"
                print(f"{p_name} win!!")
            else:
                with out:
                    clear_output(wait=True)
                    print(f"\nIt will be a draw!")
            disable_all(buttons)
            return True

        # Ends game when AI takes last empty slot
        if not avail_moves(board):
            with out:
                clear_output(wait=True)
                print("\nIt's a draw!")
            disable_all(buttons)
            return True

        return False

    def handle_click(btn):
        nonlocal player, opp
        if player != "O" or board[btn.index] != "_":
            return

        print("\nThis is your turn.\n")
        board[btn.index] = "O"
        update_(buttons, board)

        player, opp = opp, player

        if not avail_moves(board):
            with out:
                clear_output(wait=True)
                print("\nIt's a draw!")
            disable_all(buttons)
            return

        done = ai_move()
        if done:
            return

    # Build interactive board
    grid, buttons = make_board_display(board, handle_click)

    update_(buttons, board)
    display(grid, out)

    # If AI starts
    if player == "X":
        ai_move()

play_interactive()
Back to top