Build Tic Tac Toe game with Blazor WebAssembly
lghou
—July 12, 2020
Blazor WebAssembly has officially made it to its first production release in the Microsoft Build 2020. In this post, we are going to learn some features of this amazing technology step by step building a Tic Tac Toe game.
This is the final version of our game: Tic-Tac-Toe, let's build it step by step.
What is Blazor WebAssembly?
Blazor is an open source and cross-platform web UI framework for building single-page apps using .NET and C# instead of JavaScript. Blazor is based on a powerful and flexible component model (like components in modern javascript frameworks) for building rich interactive web UI. You implement Blazor UI components using a combination of .NET code and Razor syntax: an elegant melding of HTML and C#. Blazor components can seamlessly handle UI events, bind to user input, and efficiently render UI updates.
Blazor components can then be hosted in different ways to create your web app. The first supported way is called Blazor Server. In a Blazor Server app, the components run on the server using .NET Core. All UI interactions and updates are handled using a real-time WebSocket connection with the browser.
Blazor WebAssembly is now the second supported way to host your Blazor components: client-side in the browser using a WebAssembly-based .NET runtime. Blazor WebAssembly includes a proper .NET runtime implemented in WebAssembly, a standardized bytecode for the web. This .NET runtime is downloaded with your Blazor WebAssembly app and enables running normal .NET code directly in the browser. No plugins or code transpilation are required. Blazor WebAssembly works with all modern web browsers, both desktop and mobile. Similar to JavaScript, Blazor WebAssembly apps run securely on the user’s device from within the browser’s security sandbox. These apps can be deployed as completely standalone static sites without any .NET server component at all, or they can be paired with ASP.NET Core to enable full stack web development with .NET, where code can be effortlessly shared with the client and server.
Get started
Getting started with Blazor WebAssembly is easy: simply go to https://blazor.net and install the latest .NET Core SDK, which includes everything you need to build and run Blazor WebAssembly apps.
You can then create and run your first Blazor WebAssembly app by running:
1dotnet new blazorwasm -o TicTacToe2cd TicTacToe3dotnet run
Browse to https://localhost:portNumber
, and congratulations! You’ve just built and run your first Blazor WebAssembly app! note that you can find your port number in the TicTacToe/Properties/lanchSetting.json
file, it's often 5000/5001 unless you are using IIS.
To maximize your Blazor productivity, be sure to install a supported version of Visual Studio for your platform of choice, or use our dear friend Visual Studio Code with C# extension.
📝 There is a guide explaining the starter project if you are interested.
Clean up the starter project
To start our coding journey we will clean the starter project first:
- Add a gitignore file. If you find trouble creating a .gitignore file for the starter template you can execute this command:
1dotnet new gitignore
in the root folder of your project.
- Remove unused components;
TicTacToe/Pages/Counter.razor
,TicTacToe/Pages/FetchData.razor
,TicTacToe/Shared/SurveyPrompt.razor
and their usage. - Version your project with git:
1git init2git add .3git commit -m "initial commit"
Create components
From blazor documentation, Blazor apps are built using components. A component is a self-contained chunk of user interface (UI), such as a page, dialog, or form. A component includes HTML markup and the processing logic required to inject data or respond to UI events. Components are flexible and lightweight. They can be nested, reused, and shared among projects.
All we need for now is to create a board with 9 squares. So, we create a Square
and a Board
components.
The Square component will look like this
1// Components/Square.razor23<div class="square">O</div>45<style scoped>6 .square {7 background-color: rgba(255, 255, 255, 0.8);8 border: 1px solid rgba(0, 0, 0, 0.8);9 width: 60px;10 height: 60px;11 font-size: 30px;12 text-align: center;13 vertical-align: middle;14 line-height: 60px;15 border-radius: 10%;16 cursor: pointer;17 }18 .square:hover {19 background-color: rgba(106, 202, 9, 0.8);20 }21</style>
And the Board component:
1// Components/Board.razor23<div class="board">4 <Square />5 <Square />6 <Square />7 <Square />8 <Square />9 <Square />10 <Square />11 <Square />12 <Square />13</div>1415<style scoped>16 .board {17 display: grid;18 grid-template-columns: auto auto auto;19 background-color: #0a8efa;20 padding: 10px;21 width: 200px;22 height: 200px;23 border-radius: 10%;24 }25</style>
Finally, let's modify our index page to hold the board:
1// Pages/Index.razor23@page "/"4@using TicTacToe.Components56<h2>Tic Tac Toe game</h2>7<hr />8<Board />
Our application looks good for now!
The javascript way: State, Props and events handling
Let's add a custom value
to our squares and a click handler method, all in C# code.
1// Components/Square.razor23<div class="square" @onclick="ChangeValue">@value</div>4@code {5 private char value = ' ';6 private void ChangeValue()7 {8 value = value == 'O' ? 'X' : 'O';9 }10}11<style scoped>12 .square {13 background-color: rgba(255, 255, 255, 0.8);14 border: 1px solid rgba(0, 0, 0, 0.8);15 width: 60px;16 height: 60px;17 font-size: 30px;18 text-align: center;19 vertical-align: middle;20 line-height: 60px;21 border-radius: 10%;22 cursor: pointer;23 }24 .square:hover {25 background-color: rgba(106, 202, 9, 0.8);26 }27</style>
These values compose the State of the squares, we define the state in the code section of a component to hold component internal data that can change over time. The change can happen as a response to user action or system event. It is the heart of the component which determines its behavior and how it will render. It can only be accessed or modified inside the component or by the component directly.
And here is the result of these changes we have made:
Now, since we have to determine how the game ends and what is its state in a given time, it's better to let the Board component control the game. So we will manage our state (the squares values) in the Board component.
We will pass the values and the click handler from the Board (the parent component) to the Squares (the child components).
The Square component define some parameters that we can set their values whenever we use them, these are the Props in blazor. We can't change their value inside the component receiving them.
1// Components/Square.razor23<div class="square" @onclick="@ClickHandler">@value</div>45@functions {6 [Parameter]7 public char value { get; set; }8 [Parameter]9 public EventCallback ClickHandler { get; set; }10}1112<style scoped>13.square {14 background-color: rgba(255, 255, 255, 0.8);15 border: 1px solid rgba(0, 0, 0, 0.8);16 width: 60px;17 height: 60px;18 font-size: 30px;19 text-align: center;20 vertical-align: middle;21 line-height: 60px;22 border-radius: 10%;23 cursor: pointer;24}2526.square:hover {27 background-color: rgba(106, 202, 9, 0.8);28}29</style>
The value of the ClickHandler
and value
will be set from the Board component where the Squares are used
1// Components/Board.razor23<div class="board">4 @for (int i = 0; i < 9; i++)5 {6 int squareNumber = i;7 <Square @key=squareNumber8 value=values[squareNumber]9 ClickHandler="@(() => HandleClick(squareNumber))"/>10 }11</div>1213@code {14 private char[] values = new char[9];15 protected override Task OnInitialized()16 {17 values = new char[9]18 {19 ' ', ' ', ' ',20 ' ', ' ', ' ',21 ' ', ' ', ' '22 };23 }24 private void HandleClick(int i)25 {26 values[i] = values[i] == 'O' ? 'X' : 'O';27 }28}2930<style scoped>31 .board {32 display: grid;33 grid-template-columns: auto auto auto;34 background-color: #0a8efa;35 padding: 10px;36 width: 200px;37 height: 200px;38 border-radius: 10%;39 }40</style>
Let's see how this works:
- The
@key=squareNumber
is used to uniquely identify a component from other components. value=values[squareNumber]
assign the Props value of the Square component.ClickHandler="@(() => HandleClick(squareNumber))"/>
define the click handler to be executed when a Square is clicked.
📝 It's important to use the local variable squareNumber
, since the HandleClick is not called until the square is clicked, at this time the value of i is already equal to 9.
Add players turns:
Until now we are toggling the value of squares without respecting real turns.
To do so, we just add a new boolean xIsNext
to our state that will toggle its value on every square click.
1// Components/Board.razor23<h3>Next player: "@(xIsNext ? 'X' : 'O')"</h3>4<div class="board">5 @for (int i = 0; i < 9; i++)6 {7 int squareNumber = i;8 <Square @key=squareNumber9 value=values[squareNumber]10 ClickHandler="@(() => HandleClick(squareNumber))" />11 }12</div>1314@code {15 private bool xIsNext;16 private char[] values = new char[9];17 protected override Task OnInitialized()18 {19 values = new char[9]20 {21 ' ', ' ', ' ',22 ' ', ' ', ' ',23 ' ', ' ', ' '24 };25 xIsNext = true;26 }27 private void HandleClick(int i)28 {29 bool xToPlay = xIsNext;30 values[i] = xToPlay ? 'X' : 'O';31 xIsNext = !xToPlay;32 }33}3435<style scoped>36 .board {37 display: grid;38 grid-template-columns: auto auto auto;39 background-color: #0a8efa;40 padding: 10px;41 width: 200px;42 height: 200px;43 border-radius: 10%;44 }45</style>
And now turns are functional:
Declare a winner:
Since the players can take turns, the next step is to be able to determine the game status at every moment so we can declare a winner or a draw when the game is over.
First, let's add a Helpers
folder where we will create the Helper.cs
class that hold a static method to calculate the game status depending on the state of the squares when it's called.
1// Helpers/Helper.cs23namespace TicTacToe.Helpers4{5 public static class Helper6 {7 public static GameStatus CalculateGameStatus(char[] squares)8 {9 var winningCombos = new int [8,3]10 {11 { 0, 1, 2 },12 { 3, 4, 5 },13 { 6, 7, 8 },14 { 0, 3, 6 },15 { 1, 4, 7 },16 { 2, 5, 8 },17 { 0, 4, 8 },18 { 2, 4, 6 },19 };20 for (int i = 0; i < 8; i++)21 {22 if (squares[winningCombos[i, 0]] != ' '23 && squares[winningCombos[i, 0]] == squares[winningCombos[i, 1]]24 && squares[winningCombos[i, 0]] == squares[winningCombos[i, 2]])25 {26 return squares[winningCombos[i, 0]] == 'X' ? GameStatus.X_wins : GameStatus.O_wins;27 }28 }29 bool isBoardFull = true;30 for(int i = 0; i < squares.Length; i++)31 {32 if(squares[i] == ' ')33 {34 isBoardFull = false;35 break;36 }37 }38 return isBoardFull ? GameStatus.Draw : GameStatus.NotYetFiniched;39 }40 }41 public enum GameStatus42 {43 X_wins,44 O_wins,45 Draw,46 NotYetFiniched47 }48}
📝 Note that we have to add @using TicTacToe.Helpers
to the _Imports.razor
file so we can use it in our Board component.
Now we will update the game status to show a win or a draw if it's the case, otherwise, keep switching turns normally.
Replace this line of code:
1<h3>Next player: "@(xIsNext ? 'X' : 'O')"</h3>
With the following:
1// Components/Board.razor23@{4 var gameStatus = Helper.CalculateGameStatus(values);5 string status;6 if (gameStatus == GameStatus.X_wins)7 {8 status = "Winner: X";9 }10 else if (gameStatus == GameStatus.O_wins)11 {12 status = "Winner: O";13 }14 else if (gameStatus == GameStatus.Draw)15 {16 status = "Draw !";17 }18 else19 {20 char nextPlayer = xIsNext ? 'X' : 'O';21 status = $"Next player: {nextPlayer}";22 }23 <h3>@status</h3>24}
This sounds good, except that we can modify an already filled square, or continue playing even if the game is over, let's fix this by adding some checks in the HandleClick
method:
1// Components/Board.razor23<h3>Next player: "@(xIsNext ? 'X' : 'O')"</h3>4<div class="board">5 @for (int i = 0; i < 9; i++)6 {7 int squareNumber = i;8 <Square @key=squareNumber9 value=values[squareNumber]10 ClickHandler="@(() => HandleClick(squareNumber))" />11 }12</div>1314@code {15 private bool xIsNext;16 private char[] values = new char[9];17 protected override Task OnInitialized()18 {19 values = new char[9]20 {21 ' ', ' ', ' ',22 ' ', ' ', ' ',23 ' ', ' ', ' '24 };25 xIsNext = true;26 }27 private void HandleClick(int i)28 {29 if (values[i] != ' ')30 {31 return;32 }33 bool isGameFiniched = Helper.CalculateGameStatus(values) != GameStatus.NotYetFiniched;34 if (isGameFiniched)35 {36 return;37 }38 bool xToPlay = xIsNext;39 values[i] = xToPlay ? 'X' : 'O';40 xIsNext = !xToPlay;41 }42}4344<style scoped>45 .board {46 display: grid;47 grid-template-columns: auto auto auto;48 background-color: #0a8efa;49 padding: 10px;50 width: 200px;51 height: 200px;52 border-radius: 10%;53 }54</style>
This is the state of our game after these changes, yes we can play a complete game now 😀
Wanna play again ?
We are almsot there, but once a game is over we can't play anymore (unless we refresh the page), let's play again 💪
First we add a button to start a new game:
1<!-- Components/Board.razor -->23<button class="btn btn-primary" @onclick="PlayAgainHandler">4 New game5</button>
With some styles:
1/* Components/Board.razor */23button {4 border-radius: 10%;5 margin: 10px;6}
In the PlayAgainHandler
we will just re-initiliaze the state of the board so we can start another game, and since we are already doing so in OnInitialized()
, we can perform a small refactoring by extracting a new method InitState()
and call it in both PlayAgainHandler()
and OnInitialized()
:
1// Components/Board.razor23protected override void OnInitialized()4{5 InitState();6}7private void PlayAgainHandler()8{9 InitState();10}11private void InitState()12{13 values = new char[9]14 {15 ' ', ' ', ' ',16 ' ', ' ', ' ',17 ' ', ' ', ' '18 };19 xIsNext = true;20}
Finally, this is our complete game!
Final words
Great work! We just finished our blazor webassembly game. It is not mush of a thing, but we discovered some of blazor features building a UI application using C# instead of javascript.
Of course, we can add more features to the game but, let's keep it short for an introductive guide.
Here is a live demo of our game along with the source code in github:
I hope you liked this guide. Please, feel free to share your feedback about it!