- Home
- .NET tutorials
- How to create the Tic-Tac-Toe game in Blazor WebAssembly in one hour!
How to create the Tic-Tac-Toe game in Blazor WebAssembly in one hour!
Published: Saturday 8 May 2021
It's possible to create the Tic-Tac-Toe game in Blazor WebAssembly in one hour.
We did our first live coding challenge on YouTube, and our aim was to create Tic-Tac-Toe (also known as Noughts & Crosses) with the Blazor WebAssembly framework.
C# coding challenges
But, there was a catch. We only had one hour to do it!
As well as a playable game, we also wanted the ability to reset the game and keep a scoreboard.
We will talk through how we went about building the web application and share the code used.
In addition, watch our live stream where we successfully completed our challenge using C# and Razor components.
How does Tic-Tac-Toe work?
Tic-Tac-Toe is a two player game that is played on a 3x3 grid. One of the players will use the 'O' mark, and the other will use the 'X' mark.
The aim of the game is for a player to get three-in-a-row with their mark. They can either do it horizontally, vertically or diagonally.
If neither player is able to get three-in-a-row, the game is drawn.
In the example below, the player with the 'X' mark has won the game as they have managed to get three in a row diagonally.
X | ||
O | X | |
O | X |
The classes
We decided to create a number of classes and an enum.
The first was a MarkEnum
enum. This would represent each player mark on the grid, with the 'O' mark represented with an index of 0, and the 'X' mark represented with an index of 1.
// MarkEnum.cs
public enum MarkEnum
{
O = 0,
X = 1
}
Next, the different winning combinations needed to be recorded. Creating a WinningCombination
class enabled us to do that.
This class included three integer properties which would represent a particular square number on the grid.
// WinningCombination.cs
public class WinningCombination
{
public int Square1 { get; }
public int Square2 { get; }
public int Square3 { get; }
public WinningCombination(int square1, int square2, int square3)
{
Square1 = square1;
Square2 = square2;
Square3 = square3;
}
}
A model for each square
Next, we needed to create a Square
model. There would be nine instances of the Square
model, with each instance representing a square on the grid.
We needed to record the square number, as well as whether a player had left their mark on the square.
public class Square
{
public int Number { get; }
public MarkEnum? Mark { get; set; }
public Square(int number)
{
Number = number;
}
}
The game model
Finally, we went ahead and created a Game
model.
The Game
model has a number of properties included. The first is to list all the possible winning combinations of the game.
We numbered each square in the 3 x 3 grid like so:
1 | 2 | 3 |
4 | 5 | 6 |
7 | 8 | 9 |
There are 8 different ways of winning Tic-Tac-Toe. These are:
- Horizontally along the top (squares 1, 2 and 3)
- Vertically along the left (squares 1, 4 and 7)
- Horizontally along the middle (squares 4, 5 and 6)
- Vertically along the middle (squares 2, 5 and 8)
- Horizontally along the bottom (squares 7, 8 and 9)
- Vertically along the right (squares 3, 6 and 9)
- Diagonally from top left to bottom right (squares 1, 5 and 9)
- Diagonally from top right to bottom left (squares 3, 5 and 7)
In-addition, we need to create instances of each of the nine squares. We also needed to record the next turn of the player and whether there was a winner.
When a player takes a turn
Within the Game
model, there are two methods.
The Next
method has a great deal of functionality. This gets called when a player has taken their turn. It goes through each of the winning combinations to see if there is a winner.
If there is a winner, it declares the winning mark with a MarkEnum
value.
However, if the game is to continue, it will replace the NextTurn
property with the other player.
When the game has ended
The Reset
method is the other method in this class. This will reset the squares, the winner, and decide who has the next turn.
When a game is reset, the winner from the previous game will go first. However, if the game is drawn, then the player who went second in the last game will go first.
// Game.cs
public class Game
{
public List<WinningCombination> WinningCombinations = new List<WinningCombination>
{
new WinningCombination(1, 2, 3),
new WinningCombination(4, 5, 6),
new WinningCombination(7, 8, 9),
new WinningCombination(1, 4, 7),
new WinningCombination(2, 5, 8),
new WinningCombination(3, 6, 9),
new WinningCombination(1, 5, 9),
new WinningCombination(3, 5, 7)
};
public int OWinner { get; set; }
public int XWinner { get; set; }
public List<Square> Squares { get; protected set; }
public MarkEnum NextTurn { get; set; }
public MarkEnum? Winner { get; set; }
public Game()
{
ResetGame();
}
public void Next()
{
foreach (var winningCombination in WinningCombinations)
{
if (Squares[winningCombination.Square1 - 1].Mark == MarkEnum.O && Squares[winningCombination.Square2 - 1].Mark == MarkEnum.O && Squares[winningCombination.Square3 - 1].Mark == MarkEnum.O)
{
Winner = MarkEnum.O;
}
else if (Squares[winningCombination.Square1 - 1].Mark == MarkEnum.X && Squares[winningCombination.Square2 - 1].Mark == MarkEnum.X && Squares[winningCombination.Square3 - 1].Mark == MarkEnum.X)
{
Winner = MarkEnum.X;
}
}
if (Winner.HasValue)
{
if (Winner == MarkEnum.O)
{
OWinner += 1;
}
if (Winner == MarkEnum.X)
{
XWinner += 1;
}
NextTurn = Winner.Value;
}
else
{
if (NextTurn == MarkEnum.O)
{
NextTurn = MarkEnum.X;
}
else
{
NextTurn = MarkEnum.O;
}
}
}
public void ResetGame()
{
Squares = new List<Square>();
NextTurn = (Winner.HasValue ? Winner.Value : (NextTurn == MarkEnum.O ? MarkEnum.X : MarkEnum.O));
Winner = null;
for (var tt=1;tt<=9;tt++)
{
Squares.Add(new Square(tt));
}
}
}
Now onto creating the Blazor app
A number of Razor components were created in Blazor to make our game into a workable web application.
The square
We created a SquareComponent
razor component. For this component to function, a Square
instance has to be passed in as a parameter.
From here, we checked to see if the Square instance had a mark set for it. If it did, it will be displayed on-screen. However, if it didn't, it would be left empty.
In-addition, we created an onclick
method around the main SquareComponent
HTML tag. When this HTML tag is clicked, it would call a method that would invoke a delegate that we would pass into the SquareComponent
razor component as a parameter.
<!-- SquareComponent.razor -->
<div @onclick="@Click" class="square">
@if (Square != null)
{
if (Square.Mark.HasValue)
{
<span>@Square.Mark.Value</span>
}
}
</div>
@code {
[Parameter]
public Square Square { get; set; }
[Parameter]
public Action<MouseEventArgs> ClickParameter { get; set; }
public void Click (MouseEventArgs mouseEventArgs)
{
ClickParameter?.Invoke(mouseEventArgs);
}
}
The game
Finally, a GameComponent
Razor component was created. This would be the main root of the application.
For this to function, a Game
instance is created when it's initialised. Nine squares get created and this is shown in the Razor component.
Each square get passed through as a parameter to our SquareComponent
Razor component. In-addition, we had to pass through a method when the square is clicked.
What this does is it sets the mark that has been put down from the square. It gets that by looking at the NextTurn
property in the Game
model instance.
It also calls the Next
method in model which checks whether there is a winner and decides who's turn it is next.
Finally, we created a Reset button. This calls the ResetGame
method in the Game
model instance, which clears the squares and decides which player gets to go first in the next game.
<!-- GameComponent.razor -->
@page "/"
@using RoundTheCode.TicTacToe.Models
@if (Game != null)
{
<div class="game">
@for (var tt = 1; tt <= 3; tt++)
{
for (var vv = 1; vv <= 3; vv++)
{
var square = Game.Squares[((((tt - 1) * 3) + vv) - 1)];
<SquareComponent Square="@square" ClickParameter="@(e => SquareClick(e, square))"></SquareComponent>
}
}
</div>
<div class="footer">
<p><button @onclick="@Reset">Reset game</button></p>
@if (Game.Winner.HasValue)
{
<p class="score-board">@Game.Winner.Value has won the game!</p>
}
else if (Game.Squares.Count(x => x.Mark.HasValue) == 9)
{
<p class="score-board">Game has been drawn.</p>
}
<p class="score-board">Current Score: O @Game.OWinner-@Game.XWinner X</p>
</div>
}
@code {
public Game Game { get; set; }
protected override async Task OnInitializedAsync()
{
Game = new Game();
await base.OnInitializedAsync();
}
public void SquareClick(MouseEventArgs mouseEventArgs, Square square)
{
if (!Game.Winner.HasValue)
{
square.Mark = Game.NextTurn;
Game.Next();
StateHasChanged();
}
}
public void Reset(MouseEventArgs mouseEventArgs)
{
Game.ResetGame();
StateHasChanged();
}
}
The final result
As we said at the top of the article, we got a working ASP.NET Core Blazor Wasm application working, and here is how the final product looked.
We also had time to configure some CSS. When using Blazor with .NET 5, we can explicitly define CSS for a particular Razor component.
To see how the front-end functionality and CSS was built up, download the code example for this Blazor demo and try out our demo.