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