Step-by-Step Guide To Develop Tic-Tac-Toe AI With Blazor

Introduction

Tic Tac Toe is a popular game that has been enjoyed for generations, on the other hand, our generation enjoys AI. It's about time we clash them together. In this article, we will be designing Tic Tac Toe in Blazor and developing AI-driven game logic with the Min Max algorithm, an artificial intelligence algorithm used for game decision-making.

Published game => Play here

Step-by-Step Guide to develop Tic-Tac-Toe AI with Blazor

What is the Min Max Algorithm?

The Min Max algorithm is used to design digital board games like chess and checkers to help determine the best possible move for a player. The algorithm works by evaluating all possible moves a player can make and predicting the best outcome of each move.

To begin with, let me lay down the steps:

  1. Create a new Blazor application.
  2. Create a new component called "TicTacToe".
  3. Iterate div tags under 2 for loops to create a 3x3 matrix
  4. Create a char array of 3x3 to support matrix data, and create a list of winning combinations
  5. Add a method that will be called when the user clicks on a cell in the grid
  6. Design a MinMax algorithm to suggest the next possible best move
  7. Use the MinMax algorithm with the Blazor app to predict the next best move for AI
    1. Check if the cell is already occupied. If it is, return
    2. Call the MinMax algorithm to get the next best move
    3. Update the cell with the current player's mark (X or O).
    4. Check if the game has been won or if it is a tie.
    5. If the game is finished, show animation with JavaScript and reset the grid.
    6. Repeat the above steps until the game is won or a tie is reached.

Let's take a closer look at each of these steps.

Step 1. Create a new Blazor application

To create a new Blazor application, open Visual Studio and select "Create a new project". In the "Create a new project" dialog, select "Blazor WebAssembly App" or "Blazor Server App" and click "Next".

Step-by-Step Guide to develop Tic-Tac-Toe AI with Blazor

Give your project a name and click "Create".

Step-by-Step Guide to develop Tic-Tac-Toe AI with Blazor

Step 2. Add a new razor component called "TicTacToe"

To add a new component, right-click on the project, select "Add" then select "New item", which will bring the dialog below, then select "Razor component", give it a name, and click on the "Add" button.

Step-by-Step Guide to develop Tic-Tac-Toe AI with Blazor

Step 3. Iterate div tags under 2 for loops to create a 3x3 matrix

In the "TicTacToe" component, add the following HTML to create a 3x3 grid:

@page "/"
@inject IJSRuntime JS

<div>
    <div class="board">
        @for(int i=0; i < 3; i++)
        {
            int row = i;
            for (int j = 0; j < 3; j++)
            {
                int col = j;
                <div class="square" @onclick="()=> SquareCliked(row, col)">
                    <h5 class="char">@Board[row, col]</h5>
                </div>
            }
        }
    </div>
</div>

Listing 1: TicTacToe.razor (HTML)

Step 4. Create a char array of 3x3 to support matrix data and create a list of winning combinations

Create an empty char array, "Board", and create a char to represent a "Player", There are 8 possible winning combos, create a list of "WinningCombos"

@code {

    char[,] Board = { { ' ', ' ', ' ' }, { ' ', ' ', ' ' }, { ' ', ' ', ' ' } };
    char Player = 'o';
    List<List<int[]>> WinningCombos = new()
    {
        //First row
        new List<int[]>() {new int[] { 0,0 }, new int[] { 0, 1 }, new int[] { 0, 2} },
        //Second row
        new List<int[]>() {new int[] { 1,0 }, new int[] { 1, 1 }, new int[] { 1, 2} },
        //Third row
        new List<int[]>() {new int[] { 2,0 }, new int[] { 2, 1 }, new int[] { 2, 2} },

        //First column
        new List<int[]>() {new int[] { 0,0 }, new int[] { 1, 0 }, new int[] { 2, 0} },
        //Second column
        new List<int[]>() {new int[] { 0,1 }, new int[] { 1, 1 }, new int[] { 2, 1} },
        //Third column
        new List<int[]>() {new int[] { 0,2 }, new int[] { 1, 2 }, new int[] { 2, 2} },

        //Backward diagonal
        new List<int[]>() {new int[] { 0,0 }, new int[] { 1, 1 }, new int[] { 2, 2} },
        //Forward diagonal
        new List<int[]>() {new int[] { 0,2 }, new int[] { 1, 1 }, new int[] { 2, 0} },
    };
}

Listing 2: TicTacToe.razor (C#)

 Step 5. Add a method that will be called when the user clicks on a cell in the grid

private async Task SquareCliked(int row, int col)
{
    
}

Listing 3: TicTacToe.razor (C#)

Step 6. Design a MinMax algorithm to suggest the next possible best move

Let's write the algorithm in a separate file. Right-click on project > add a folder, name "AI," then add a C# file inside the folder, name it "MinMaxAlgorithm.cs"

namespace BlazingTicTacToe.AI
{
    public class MinMaxAlgorithm
    {
        public class Turn
        {
            public int row, col;
        };

        private static readonly char Player = 'x';
        private static readonly char Opponent = 'o';
        private static readonly char EmptyCell = ' ';

        //Returns true if there are moves left
        static bool AreMoveLeft(char[,] board)
        {
            for (int i = 0; i < 3; i++)
            {
                for (int j = 0; j < 3; j++)
                {
                    if (board[i, j] == EmptyCell)
                    {
                        return true;
                    }
                }
            }
            return false;
        }

        static int GetCurrentScore(char[,] board)
        {
            // Validate for Rows.
            for (int i = 0; i < 3; i++)
            {
                if (board[i, 0] == board[i, 1] && board[i, 1] == board[i, 2])
                {
                    if (board[i, 0] == Player)
                    {
                        return +10;
                    }
                    else if (board[i, 0] == Opponent)
                    {
                        return -10;
                    }
                }
            }

            // Validate for Columns.
            for (int j = 0; j < 3; j++)
            {
                if (board[0, j] == board[1, j] && board[1, j] == board[2, j])
                {
                    if (board[0, j] == Player)
                    {
                        return +10;
                    }

                    else if (board[0, j] == Opponent)
                    {
                        return -10;
                    }
                }
            }

            // Validate for Backward diagonal.
            if (board[0, 0] == board[1, 1] && board[1, 1] == board[2, 2])
            {
                if (board[0, 0] == Player)
                {
                    return +10;
                }
                else if (board[0, 0] == Opponent)
                {
                    return -10;
                }
            }
            // Validate for Forward diagonal.
            if (board[0, 2] == board[1, 1] && board[1, 1] == board[2, 0])
            {
                if (board[0, 2] == Player)
                {
                    return +10;
                }
                else if (board[0, 2] == Opponent)
                {
                    return -10;
                }
            }

            return 0;
        }


        static int ComputeMinMax(char[,] board, int depth, bool isMax)
        {
            int score = GetCurrentScore(board);

            // If Max has won the game
            if (score == 10) return score;

            // If Mini has won the game
            if (score == -10) return score;

            // If it is a tie
            if (AreMoveLeft(board) == false) return 0;

            // Max move
            if (isMax)
            {
                int bestValue = -1000;

                for (int i = 0; i < 3; i++)
                {
                    for (int j = 0; j < 3; j++)
                    {
                        if (board[i, j] == EmptyCell)
                        {
                            // Make the move
                            board[i, j] = Player;

                            // Call ComputeMinMax recursively to get max
                            bestValue = Math.Max(bestValue, ComputeMinMax(board, depth + 1, !isMax));

                            // Undo the move
                            board[i, j] = EmptyCell;
                        }
                    }
                }
                return best;
            }
            else
            {
                int bestValue = 1000;

                for (int i = 0; i < 3; i++)
                {
                    for (int j = 0; j < 3; j++)
                    {
                        if (board[i, j] == EmptyCell)
                        {
                            // Make the move
                            board[i, j] = Opponent;

                            // Call ComputeMinMax recursively to get min
                            bestValue = Math.Min(bestValue, ComputeMinMax(board, depth + 1, !isMax));

                            // Undo the move
                            board[i, j] = EmptyCell;
                        }
                    }
                }
                return bestValue ;
            }
        }

        // AI will select best possible move
        public static Turn GetNextBestMove(char[,] board)
        {
            int bestValue = -1000;
            Turn bestTurn = new()
            {
                row = -1,
                col = -1
            };

            // GetCurrentScore ComputeMinMax function And return the cell with best value.
            for (int i = 0; i < 3; i++)
            {
                for (int j = 0; j < 3; j++)
                {
                    if (board[i, j] == EmptyCell)
                    {
                        board[i, j] = Player;
                        int currentTurnValue = ComputeMinMax(board, 0, false);

                        // Undo the move
                        board[i, j] = EmptyCell;

                        if (currentTurnValue > bestValue)
                        {
                            bestTurn.row = i;
                            bestTurn.col = j;
                            bestValue = currentTurnValue;
                        }
                    }
                }
            }
            return bestTurn;
        }
    }
}

Listing 4: MinMaxAlgorithm.cs

 Step 7. Use the MinMax algorithm to predict the next best move for AI and Update the cell with the current player's mark (X or O)

private async Task SquareCliked(int row, int col)
{
    if (Board[row, col] != ' ') return; 
    Board[row, col] = Player;

    Turn currentTurn = MinMaxAlgorithm.GetNextBestMove(Board);
    if(!(currentTurn.row == -1 && currentTurn.col == -1))
    Board[currentTurn.row, currentTurn.col] = 'x';          

    foreach (var combo in WinningCombos)
    {
       int[] first = combo[0];
       int[] second = combo[1];
       int[] third = combo[2];
       if (Board[first[0], first[1]] == ' ' || Board[second[0], second[1]] == ' ' || Board[third[0], third[1]] == ' ') continue;
       if (Board[first[0], first[1]] == Board[second[0], second[1]] && Board[second[0], second[1]] == Board[third[0], third[1]] && Board[first[0], first[1]] == Board[third[0], third[1]])
        {  
               string winner = Player == 'o' ? "AI" : "Player ONE";
               await JS.InvokeVoidAsync("ShowSwal", winner);
               await Task.Delay(1000);
               ResetGame();
         }
      }

      if (IsGameReset())
      {
          await JS.InvokeVoidAsync("ShowTie");
          ResetGame();
      }
}

Listing 5: TicTacToe.razor, SquareClick method (C#)

Add a few helper methods to reset the game.

private bool IsGameReset()
{
    bool isReset = true;
    for (int i = 0; i < 3; i++)
    {
       for (int j = 0; j < 3; j++)
       {
           if(Board[i, j] == ' ')
           {
              isReset = false;
           }
       }
    }
    return isReset;
}

private void ResetGame()
{
   for (int i = 0; i < 3; i++)
   {
      for (int j = 0; j < 3; j++)
      {
          Board[i, j] = ' ';
      }
    }
}

Listing 5: TicTacToe.razor, helper methods (C#)

The JavaScript

You must wonder what those statements are in listing 5 > line numbers 19 and 27. Well, that's how we call javascript methods using JSRuntime. 

There are 2 scenarios when we are calling JS, 

  • When either Player 1 or 2 wins.
  • If the game is tied.

First and foremost, go to wwwroot, and create a new folder named "js" inside the folder, add a new javascript file, name it common.js

There are 2 methods,

  • ShowSwal means to show a sweet alert. In code snippet 2 at line 34, we mention this method name as a parameter, so JsRuntime looks for the same method we specify as a parameter.
  • ShowTie, representing the tie, in code snippet 2 at line number 41, we are specifying this method name as a parameter.
window.ShowSwal = (player) => {
    Swal.fire({
        title: player + ' won!!',
        width: 350,
        padding: '3em',
        color: '#716add',
        backdrop: `
                        rgba(0,0,123,0.4)
                        url("/images/nyan-cat-nyan.gif")
                        left top
                        no-repeat
                      `
    })
}
window.ShowTie = () => {
    Swal.fire({
        title: 'Go home, nobody won!',
        width: 350,
        padding: '3em',
        color: '#716add',
        backdrop: `
                        rgba(0,0,123,0.4)
                        url("/images/crying-tear.gif")
                        left top
                        no-repeat
                      `
    })
}

Listing 6: Common.js

Now let's integrate JavaScript with blazor app.

Open Index.html under wwwroot folder. And inside a head tag, add these 3 script tags.

<script src="js/common.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="sweetalert2.min.js"></script>

Listing 7: Index.html

The CSS

we are almost done, but it's not done unless we have some CSS, right? If you remember, for code snippet 1, we have added a bunch of classes to our divs. Let's code those classes in a separate CSS file. 

Here is the trick to create a razor specific CSS file. Click on the folder named "pages" and say "Add new item" then select Style Sheet. Here you have to give the same name as your razor file. For our example, we will name TicTacToe.razor.css, refer image below.

Now you will see how newly added CSS is automatically assigned below the razor component. 

Here is the CSS with basic properties with flex and hover.

* {
    padding: 0;
    margin: 0;
}

h1 {
    text-align: center;
    margin-bottom: 10px;
}

p {
    margin-top: 10px;
    text-align: center;
    font-family: cursive;
}

.board {
    width: 22rem;
    height: 22rem;
    margin: auto;
    border: 1px solid white;
    display: flex;
    flex-wrap: wrap;
}

.square {
    width: 7rem;
    height: 7rem;
    border: 1px solid white;
    margin-right: 4px;
    border-radius: 30px;
    background: #78bec5;
    opacity: 80;
}

    .square:hover {
        background: #ecaf4f;
        cursor: pointer;
    }

.char {
    font-size: 3.5rem;
    text-align: center;
    font-weight: 800;
    margin-top: 15%;
    color: #dc685a;
    font-family: cursive;
}

Listing 8: TicTacToe.razor.css

Conclusion

I believe, Implementing the Tic Tac Toe game using the Min Max algorithm in Blazor was a great learning experience. This algorithm is widely used in game development and can help you create more intelligent and challenging games.

Blazor provides an excellent platform to implement this game as it uses C# language to write the code, making it easier for most developers. It also allows you to create interactive UI and responsive web applications using HTML, CSS, and JavaScript.


Similar Articles