Creating a memory game using Blazor WebAssembly App C# and .Net6
Let’s creating a simple memory card game without using any database. The idea is just exploring a little bit more about Blazor because I don’t have a previous experience, so let’s do that together guys.
In this sample we are trying to show a simple way to create a memory card game exploring the framework Blazor. In my next article I am planning to show you how we can do the same game using the framework Angular.
But, keep in mind that for this specific project we are assume that it’s only a kind of draft, then you can increase it by putting your thoughts and insights to turn the game more interesting than this one.
To start the project let’s open the Visual Studio, in my case I am using the version 2022. Creating a new project select the template Blazor WebAssembly App and then define a name for the project.
After has been created a new project the structure should came with some templates for pages and css files. So you already have a minimum structure already created and you can use it if you want. Let’s do that in this sample.
Also, if you running the default project with the templates pre-created automatically, so you can see something like that picture bellow:
This sample project also coming with the .css file defined to be a responsive project. Then, if you take this modules pre-defined you can keep this structure and start a new project using the best practices to be able run the project using any device (mobile or desktop).
Let’s talk about the description and goals of our project:
The goal is to create a Memory Card/Color game. The board should consist of a 4 by 4 square grid, 16 squares in total with 8 colors. But, if you want you can decrease or increase this amount, feel free to be creative.
So, for each position there is a card face-down and the objective is flipping any two cards (one at a time) trying to find pairs of the same color.
Whenever two flipped cards have the same color, the player gets a point and the cards are removed from the board after two seconds (during this time it is not possible to flip any new cards).
If the cards have different colors the player will lose one point and the cards are flipped back again after two seconds (also, during this time the game cannot to allow you to flip a new one). Then, this continues until the player has found all pairs. As a suggestion you also can put a countdown timer to finish the game instead of to wait until all pairs found.
Starting the code:
As in my mind I was planning to work a kind of single page. So, we should create a MainPage where we will have the most of our business logic for this game. Just like we have the componentization for Angular, we also can use this approach working with Blazor.
At MainPage we have the main Html code for our project:
<div style="text-align:center">YOUR SCORE: </div>
<div style="text-align:center;font-size:xx-large;">@score pts</div>
<div style="display:@showWin">
<div style="text-align:center; font-weight:bold">@messageScore</div>
<div style="text-align:center;font-size:xx-large;font-weight:bolder"><a href="" @onclick="@RestartGame">PLAY AGAIN!</a></div>
</div>
<div class="game-board" style="margin: auto;">
@foreach (var card in _cards)
{
<div class=@(card.IsFlipped ? "flipped" : "card") style="border-color: transparent;" @onclick="@(() => FlipCard(card))">
@if (card.IsFlipped)
{
@if (!card.IsMatched)
{
<div class=@(card.IsFlipped ? "front-flipped" : "front") style="background-color:@card.Color; border:hidden">
</div>
}
}
else
{
<div class="front" style="background-color:black;">
<img src="img/question-mark.png" style="width:100px;height:148px;" asp-append-version="true">
</div>
}
<div class="back" @if(card.IsFlipped) { style="display:none;"}></div>
</div>
}
</div>
At the first part of the code we just have the “topo” where we are showing the score and we are also showing a friendly message after finished the game. We are using the flag variable “showWin” to display or not. If you take a look at the “div game-board” so you can see the logic implemented to flip the cards and to show the front or back side. Beside have this logic is necessary to implement to magic into the .css file. In our case we are using the defaut app.css
.game-board {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-top: 40px;
width:700px;
}
.card {
width: 120px;
height: 160px;
margin: 20px;
position: relative;
transform-style: preserve-3d;
transition: transform 1s;
}
.flipped {
width: 120px;
height: 160px;
margin: 20px;
position: relative;
transform-style: preserve-3d;
transition: transform 1s;
transform: rotateY(-180deg);
}
.front, .back {
width: 100%;
height: 100%;
position: absolute;
backface-visibility: hidden;
transform-style: preserve-3d;
}
.front {
display: flex;
justify-content: center;
align-items: center;
font-size: 48px;
color: white;
}
.front-flipped {
width: 120px;
height: 160px;
position: absolute;
transform-style: preserve-3d;
transition: transform 1s;
transform: rotateY(180deg)
}
.back {
background-color: lightgray;
transform: rotateY(180deg);
}
@media (max-width: 700px) {
.game-board {
width: 400px;
}
}
Looking to this css file, we can find a specific class for each case of our project. You can find classes for the “card, front, back, front-flipped and etc”. Besides that you can see the code that makes the responsiveness.
@media (max-width: 700px) {
.game-board {
width: 400px;
}
}
In the middle of the code we also have the for each responsible to load the list of the objects cards to build the board 4 x 4. The definition for this card are stored in the appsettings.json file. Then, we need to add this file in our project, as it’s not coming by default. Let’s add into the folder wwwroot.
{
"MatchesPerColor": 2,
"ColorsData": {
"Colors": [
"Red",
"Green",
"Blue",
"Yellow",
"Orange",
"Purple",
"Pink",
"Brown"
]
},
"showWinBlock": "block",
"showWinNone": "none"
}
In addition to storing the array of colors, we are storing the keys to define the number of cards that must be combined and the key to show or not the final friendly message. By default following the rules to this project let’s keep 2 for the matches, but if can change it if you like.
Now, let’s see how we are filling the for each:
private string showWin = string.Empty;
private List<CardDto> _cards = new();
private int _matchesPerColor;
private int score = 0;
private int cardsMatched = 0;
private string messageScore = string.Empty;
private List<CardDto> _selectedCards = new List<CardDto>();
private ColorsData colorsData = new();
protected override void OnInitialized()
{
_matchesPerColor = int.Parse(Config["MatchesPerColor"]);
showWin = Config["showWinNone"];
colorsData.Colors = Config.GetSection("ColorsData:Colors").Get<string[]>();
_cards = SetRandonCards();
}
private List<CardDto> SetRandonCards()
{
var cards = colorsData.Colors
.SelectMany(color => Enumerable.Range(1, _matchesPerColor)
.Select(id => new CardDto { Id = id, Color = color }))
.ToList();
Random r = new Random();
var randomized = cards.OrderBy(x => r.Next()).ToList();
return randomized;
}
With this line code: colorsData.Colors = Config.GetSection(“ColorsData:Colors”).Get<string[]>(); We are taking the values from appsettings file. To do that also we need to invoke the IConfiguration interface. So, let’s inject it in our code like this:
@inject IConfiguration Config
To receive these values let’s create a model class called ColorsData with a property “Colors” having the type a string[]:
namespace MemoryGame.Model
{
public class ColorsData
{
public string[] Colors { get; set; }
}
}
As we are pretending to load the data always when we are staring a new game, so let’s using the default method called “OnInitialized” to instance our default values. This method should work as constructor for our project and will start the values over the beginning.
protected override void OnInitialized()
{
_matchesPerColor = int.Parse(Config["MatchesPerColor"]);
showWin = Config["showWinNone"];
colorsData.Colors = Config.GetSection("ColorsData:Colors").Get<string[]>();
_cards = SetRandonCards();
}
Note that we are setting the values to turn our project dynamic as we need. So, we are taking the definition from appsettings to the matches per color, status to display or not the friendly message, array of colors and after that, we are executing the SetRandomCards() method, responsible to random the cards for each new game that has just been started. As you can see in this part of the code we are using filers provided by LinqSql, it’s a good to have a best performance and also became our code a little bit more fancy. But, not suggesting but if you want you also can work here exposing your Colors data and instead of take it from the appsetting you can simply instance you object setting the value manually one by one for each card. I really don’t recommend this. Besides not being a good practice, your code will look not so professional, looking very beginner.
private List<CardDto> SetRandonCards()
{
var cards = colorsData.Colors
.SelectMany(color => Enumerable.Range(1, _matchesPerColor)
.Select(id => new CardDto { Id = id, Color = color }))
.ToList();
Random r = new Random();
var randomized = cards.OrderBy(x => r.Next()).ToList();
return randomized;
}
To store a list of cards with the definition that we need let’s create a Data Transfer Object file called CardDto.
namespace MemoryGame.Dto
{
public class CardDto
{
public int Id { get; set; }
public string Color { get; set; }
public bool IsFlipped { get; set; } = false;
public bool IsMatched { get; set; } = false;
}
}
The Id and Color will be filled in combination with the values coming from appsetting and the rule made by “.SelectMany” and the number of “_matchesPerColor”, so for each color the code will add a new one according to the matches per color.
Now almost everything is defined so just left to create the FlipCard() method. If we back a bit to the html code we can find this event: @onclick=”@(() => FlipCard(card))”
It means that when we are clicking over each card this function should be called.
private async Task FlipCard(CardDto card)
{
if (_selectedCards.Count >= _matchesPerColor || card.IsFlipped)
{
return;
}
card.IsFlipped = true;
_selectedCards.Add(card);
if (_selectedCards.Count == _matchesPerColor)
{
await Task.Delay(2000);
var groups = from obj in _selectedCards
group obj by obj.Color into colorGroup
where colorGroup.Count() == _matchesPerColor
select colorGroup;
if (groups.Count() > 0)
{
_selectedCards.ForEach(card => card.IsMatched = true);
score += 1;
cardsMatched += 1;
}
else
{
if (score > 0)
score -= 1;
_selectedCards.ForEach(card => card.IsFlipped = false);
}
_selectedCards.Clear();
if (cardsMatched == colorsData.Colors.Count())
{
showWin = Config["showWinBlock"];
messageScore = MessageScore();
}
}
}
One of the first rule for this code is: at the beginning validate if the number of cards flipped are respecting the matches per color. For example: The matches per color defined equal 2, so if you click over a third card nothing should happen as this why we are “return;” and skipping the function.
Otherwise we should keep going through the code making the settings and adding the card selected to our list “_selectedCards”. So, if the number of selected cards equal matches per color, we are making a delay timer of 2 seconds as we defined being one of the rules (await Task.Delay(2000);). Also, we are making a filter to identify if the cards selected by counting are equals.
var groups = from obj in _selectedCards
group obj by obj.Color into colorGroup
where colorGroup.Count() == _matchesPerColor
select colorGroup;
Then, if the count result > 0 we know that the cards selected have the same color. So, at this moment we need to set the property IsMatched = true and increase one point to the score and store the matched cards to finalize the game when this number equal the value defined to the _matchesPerColor variable. If the count == 0 it means that the cards are different and we need to decrease one point from the score and set the property IsFlipped = false to each selected card to continue the game.
if (groups.Count() > 0)
{
_selectedCards.ForEach(card => card.IsMatched = true);
score += 1;
cardsMatched += 1;
}
else
{
if (score > 0)
score -= 1;
_selectedCards.ForEach(card => card.IsFlipped = false);
}
After the validation to the last round we need to clear the “_selectedCards” and check is the game is over or not.
_selectedCards.Clear();
if (cardsMatched == colorsData.Colors.Count())
{
showWin = Config["showWinBlock"];
messageScore = MessageScore();
}
Note that we have a function called MessageScore() that is responsible for showing a friendly message at the end of the game.
public string MessageScore()
{
string message = string.Empty;
switch (score)
{
case int n when (n >= 1 && n <= 3):
message = $"{ScoreMessage.Okay.ToEnumGetDescription()}";
break;
case int n when (n > 3 && n <= 5):
message = $"{ScoreMessage.WellDone.ToEnumGetDescription()}";
break;
case int n when (n > 5 && n <= 7):
message = $"{ScoreMessage.Great.ToEnumGetDescription()}";
break;
default:
message = $"{ScoreMessage.Excellent.ToEnumGetDescription()}";
break;
}
return message;
}
For this message we are using Enum file to store the messages as description for each enum type. So, if we need to change these messages we just need to change those in one unique place and you can extend this functionality for others files if you need.
using System.ComponentModel;
using System.Runtime.Serialization;
namespace MemoryGame.Enums
{
public enum ScoreMessage
{
[EnumMember(Value = "Okay"), Description("Okay, it was good! But you can improve!")]
Okay,
[EnumMember(Value = "WellDone"), Description("Well done! Keep focus next time!")]
WellDone,
[EnumMember(Value = "Great"), Description("Great game! You were almost perfect!")]
Great,
[EnumMember(Value = "Excellent"), Description("Excellent job! YOU WIN and matched all pairs!")]
Excellent
}
}
As so far as I know we don’t have a way to get the description and value from enum without an extended function. So to solve it lets create a helper class file to make it for us.
using System.ComponentModel;
namespace MemoryGame.Utils
{
public static class EnumHelper
{
public static string? ToEnumGetDescription(this Enum @enum)
{
var attr =
@enum.GetType().GetMember(@enum.ToString()).FirstOrDefault()?.
GetCustomAttributes(false).OfType<DescriptionAttribute>().
FirstOrDefault();
if (attr == null)
return @enum.ToString();
return attr.Description;
}
}
}
We also can do the same to get the values, just need to change the type “DescriptionAttribute” to “EnumMemberAttribute”. Besides that we need change the return attr.Values intead of attr.Description.
To finalize the code we need to create a way to restart the game:
void RestartGame()
{
_navigationManager.NavigateTo("", true);
}
Finally let’s see how should work our game now!
Thank you guys!
Next time let’s do the same using Angular. See you!