Create a Simple Counting Game using Blazor Web Assembly

Getting started with Blazor Web Assembly in .NET 8

Juldhais Hengkyawan
7 min readApr 2, 2024

I created a simple monkey counting game for my two years old daughter:

It was developed using Blazor Web Assembly and hosted in the Azure Static App. It is part of my journey learning Blazor through hands-on experience.

This article will be a detailed guide to developing the monkey counting game from scratch.

Let's begin.

Create a New Blazor Web Assembly Project

Open your Visual Studio 2022, then select the Blazor Web Assembly Standalone App project template.

Set the project name to BlazorCountingGame or any other name of your choice.

Set the framework to .NET 8.0, the authentication type to None, and select Configure for HTTPS.

Click [Create] to initialize a new Blazor Web Assembly project. The project structure will be like this:

Add Bootstrap CSS from CDN

We will be using Bootstrap for styling. Open the index.html file inside the wwwroot folder, then add the link to the bootstrap CSS file below to the <head> tag:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" 
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">

I also modified the content of index.html to make it simpler. This is the complete code of the index.html file:

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Monkey Count - juldhais.net</title>
<base href="/" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<link rel="stylesheet" href="css/app.css" />
</head>

<body>
<div id="app">
Loading...
</div>

<script src="_framework/blazor.webassembly.js"></script>
</body>

</html>

Generate a Random Number of Monkeys

Now go to the Home.razor file inside the Pages folder. This is the place where we write our code for the game.

@page "/"

<PageTitle>Monkey Count</PageTitle>

<div class="mx-4 my-4">
<div class="display-3 text-center">How many monkeys are there?</div>

<div>
<div class="row row-cols-3">
@for (int i = 0; i < count; i++)
{
<div class="col">🙉</div>
}
</div>
</div>
</div>

@code {
int count;

protected override void OnInitialized()
{
NextQuestion();
}

void NextQuestion()
{
// generate random number of monkeys
count = Random.Shared.Next(1, 10);
}
}

@page Directive

@page "/"
This directive makes the component a page accessible via the root URL ("/"). It defines a routing endpoint for the application.

PageTitle Component

<PageTitle>Monkey Count</PageTitle>
Sets the title of the page, which appears on the browser tab.

HTML Content

The <div class="mx-4 my-4"> acts as the main container with margin properties set by Bootstrap classes (mx-4 for horizontal margins and my-4 for vertical margins).

The div structures are used to create a grid (using Bootstrap's grid system with row row-cols-3 classes) that will display the monkey emojis. The number of emojis displayed is dynamically generated based on the value of the count variable.

@code Block

The @code block contains the C# code that handles the interactivity of the page:

  • count variable holds the number of monkey emojis to be displayed.
  • OnInitialized is a lifecycle method overridden to call the NextQuestion method when the component is initialized.
  • NextQuestion method generates a random number between 1 and 9 and assigns it to the count variable. This determines the number of monkey emojis (🙉) that will be displayed in the grid.

Create and Shuffle the Choices

The player must select the correct number of monkeys. Three choices are given, and only one of them is correct.

We have to modify the code section like below:

@code {
int count;
List<int> choices = [];

protected override void OnInitialized()
{
NextQuestion();
}

void NextQuestion()
{
// generate random number
count = Random.Shared.Next(1, 10);

choices.Clear();

// add count to choices
choices.Add(count);

// add 2 more unique random number to choices
while (choices.Count < 3)
{
int choice = Random.Shared.Next(1, 9);

if (!choices.Contains(choice))
{
choices.Add(choice);
}
}

// shuffle the choices using Fisher-Yates algorithm
var n = choices.Count;
while (n > 1)
{
n--;
int k = Random.Shared.Next(n + 1);
int value = choices[k];
choices[k] = choices[n];
choices[n] = value;
}
}
}

The choices variable is a list of integers. It is intended to store the correct answer (count) and two additional unique random numbers as possible choices for the answer.

Changes in the NextQuestion method:

  • The choices.Clear() removes any choices from previous questions.
  • The choices.Add(count) adds the count (the correct answer) to the choices list.
  • The while loop continues to add random numbers between 1 and 9 to the choices list until there are three choices in total. It ensures that these numbers are unique.
  • Lastly, it implements the Fisher-Yates shuffle algorithm to randomize the order of the elements in the choices list.

Next, we have to display the content of choices variable:

<div class="mx-4 my-4">

...

<div class="row">
@foreach (var choice in choices)
{
<div class="col-4 text-center border py-3">
@choice
</div>
}
</div>
</div>

Add Choice Selection Event

We have to add the CheckAnswer method to handle the event when users make a choice. If the answer is correct, proceed to the next question.

@code {

...

void CheckAnswer(int choice)
{
if (choice == count)
{
NextQuestion();
}
}
}

We need to set the @onclick event callback to call the CheckAnswer method when clicked:

<div class="row">
@foreach (var choice in choices)
{
<div class="col-4 text-center border py-3" @onclick="(() => CheckAnswer(choice))">
@choice
</div>
}
</div>

Add Custom CSS and Animation

We will add custom CSS to make our monkeys and choices look bigger and some animations when the player selects the correct or wrong answer.

Custom CSS

Open the app.css file in the wwwroot/css folder, then replace its content with the code below:

.monkey {
font-size: 60px;
text-align: center;
}

.choice {
font-size: 60px;
cursor: pointer;
}

@keyframes shake {

0%, 100% {
transform: translateX(0);
}

10%, 30%, 50%, 70%, 90% {
transform: translateX(-10px);
}

20%, 40%, 60%, 80% {
transform: translateX(10px);
}
}

.shake {
animation: shake 0.82s cubic-bezier(.36, .07, .19, .97) both;
color: darkred;
}

@keyframes jump {

0%, 100% {
transform: translateY(0);
}

10%, 30%, 50%, 70%, 90% {
transform: translateY(-10px);
}

20%, 40%, 60%, 80% {
transform: translateY(0);
}
}

.jump {
animation: jump 1s ease;
color: green;
}

ChatGPT helped me create the shake and jump animation 😄

Now go back to the Home.razor file and add the .monkey and .choice CSS classes into the corresponding element:

<div style="height:330px">
<div class="row row-cols-3">
@for (int i = 0; i < count; i++)
{
<div class="col monkey">🙉</div>
}
</div>
</div>

<div class="row">
@foreach (var choice in choices)
{
<div class="col-4 text-center border py-3 choice" @onclick="(() => CheckAnswer(choice))">
@choice
</div>
}
</div>

Animation

The jump animation will be performed when the player chooses the correct answer. If the answer is incorrect, it will instead perform the shake animation.

To achieve that, we have to dynamically add the .shake or .jump classes depending on the answer. In JavaScript, we can easily accomplish that by using the combination of getElementById and classList.add method.

However, in Blazor, directly manipulating the DOM elements by ID is not the standard approach. Blazor encourages a component-based architecture, where we control the UI elements through component state and properties rather than direct DOM manipulation.

To enable the animation, we have to add two more variables, animationClass and selectedChoice:

@code {
...
string animationClass = "";
int? selectedChoice = null;

...

}

We also have to add TriggerAnimation and GetAnimationClass method:

@code {

...

async Task TriggerAnimation(int choice)
{
// prevent overlapping animation
if (selectedChoice != null) return;

selectedChoice = choice;

animationClass = selectedChoice == count ? "jump" : "shake";

await Task.Delay(1500);

animationClass = "";

selectedChoice = null;
}

string GetAnimationClass(int choice)
{
return selectedChoice == choice ? animationClass : "";
}
}

We need to modify the CheckAnswer to call the TriggerAnimation method and call the StateHasChanged to notify the component that its state has changed:

async void CheckAnswer(int choice)
{
await TriggerAnimation(choice);

if (choice == count)
{
NextQuestion();
}

StateHasChanged();
}

To apply the animation class only to the selected choice, we use the GetAnimationClass(choice) method within the @foreach loop that renders the choices:

<div class="row">
@foreach (var choice in choices)
{
<div class="col-4 text-center border py-3 choice" @onclick="(() => CheckAnswer(choice))">
<div class="@GetAnimationClass(choice)">
@choice
</div>
</div>
}
</div>

We can also perform the animation for monkeys by adding the @animationClass into the monkey emoji:

<div style="height:330px">
<div class="row row-cols-3">
@for (int i = 0; i < count; i++)
{
<div class="col monkey @animationClass">🙉</div>
}
</div>
</div>

--

--