- Home
- .NET tutorials
- Create a Roulette wheel. Game development using Blazor WebAssembly
Create a Roulette wheel. Game development using Blazor WebAssembly
Published: Saturday 19 June 2021
Another live coding challenge happened on YouTube. This time, we had an hour to build a Roulette wheel in Blazor WebAssembly.
This C# game development tutorial gives the option for the user to select red or black. The ball will then spin round the roulette wheel and will randomly stop on a number.
C# coding challenges
Each number on the wheel is either red or black (apart from 0). If the user has guessed the colour correctly, they win the game.
We will talk you through how to code it. And, if you wish to try it yourself, you can download our code example.
In-addition, you can watch back our YouTube live stream where we manage to successfully create the Roulette wheel in an hour.
The classes
Before we can get on with building the Roulette wheel in Blazor, we need to go ahead and create some classes.
The wheel number
There are 37 numbers on a European Roulette wheel. The numbers are either red or black. That is apart from 0, where green is the colour.
We created a WheelNumberColourEnum
enum which would represent the three colours.
// WheelNumberColourEnum.cs
public enum WheelNumberColourEnum
{
Red,
Black,
Green
}
From there, a WheelNumber
class was created. The number and colour are featured properties on this class. In addition, a boolean selected property was included. This is so we know where the ball has landed on the roulette wheel.
// WheelNumber.cs
public class WheelNumber
{
public int Number { get; }
public WheelNumberColourEnum Colour { get; }
public bool Selected { get; set; }
public WheelNumber(int number, WheelNumberColourEnum colour)
{
Number = number;
Colour = colour;
}
}
The wheel
Our final class is where a lot of the business logic will occur. The Wheel
class has a number of properties, events and methods to make the game work.
There is a readonly WheelNumber
array which stores all 37 numbers of the roulette wheel. In addition, we have a number of other properties, such as where the ball is sitting on the roulette wheel, if there is a winning number and a winning colour.
Then, we go onto the events. We set up when the ball has started spinning on the roulette wheel, when the ball changes number and when the ball has finshed spinning on the roulette wheel.
The whole point of these events is so we can refresh the Razor component every time these events are invoked, something we will come onto later on.
Finally, a roll the ball method was setup. This method controls what happens when the ball starts spinning on the roulette wheel to when it ends.
We randomly set between 5-15 seconds as to the duration of the ball spinning. In addition, we set three different speed intervals of the ball spinning. The ball will spin fast for the first speed interval, and will gradually slow down until it finally stops on a number.
// Wheel.cs
public class Wheel
{
public readonly WheelNumber[] WheelNumbers =
{
new WheelNumber(0, WheelNumberColourEnum.Green),
new WheelNumber(32, WheelNumberColourEnum.Red),
new WheelNumber(15, WheelNumberColourEnum.Black),
new WheelNumber(19, WheelNumberColourEnum.Red),
new WheelNumber(4, WheelNumberColourEnum.Black),
new WheelNumber(21, WheelNumberColourEnum.Red),
new WheelNumber(2, WheelNumberColourEnum.Black),
new WheelNumber(25, WheelNumberColourEnum.Red),
new WheelNumber(17, WheelNumberColourEnum.Black),
new WheelNumber(34, WheelNumberColourEnum.Red),
new WheelNumber(6, WheelNumberColourEnum.Black),
new WheelNumber(27, WheelNumberColourEnum.Red),
new WheelNumber(13, WheelNumberColourEnum.Black),
new WheelNumber(36, WheelNumberColourEnum.Red),
new WheelNumber(11, WheelNumberColourEnum.Black),
new WheelNumber(30, WheelNumberColourEnum.Red),
new WheelNumber(8, WheelNumberColourEnum.Black),
new WheelNumber(23, WheelNumberColourEnum.Red),
new WheelNumber(10, WheelNumberColourEnum.Black),
new WheelNumber(5, WheelNumberColourEnum.Red),
new WheelNumber(24, WheelNumberColourEnum.Black),
new WheelNumber(16, WheelNumberColourEnum.Red),
new WheelNumber(33, WheelNumberColourEnum.Black),
new WheelNumber(1, WheelNumberColourEnum.Red),
new WheelNumber(20, WheelNumberColourEnum.Black),
new WheelNumber(14, WheelNumberColourEnum.Red),
new WheelNumber(31, WheelNumberColourEnum.Black),
new WheelNumber(9, WheelNumberColourEnum.Red),
new WheelNumber(22, WheelNumberColourEnum.Black),
new WheelNumber(18, WheelNumberColourEnum.Red),
new WheelNumber(29, WheelNumberColourEnum.Black),
new WheelNumber(7, WheelNumberColourEnum.Red),
new WheelNumber(28, WheelNumberColourEnum.Black),
new WheelNumber(12, WheelNumberColourEnum.Red),
new WheelNumber(35, WheelNumberColourEnum.Black),
new WheelNumber(3, WheelNumberColourEnum.Red),
new WheelNumber(26, WheelNumberColourEnum.Black)
};
public int CurrentNumberIndex { get; protected set; }
public WheelNumber WinningNumber { get; protected set; }
public WheelNumberColourEnum? Colour { get; set; }
public bool Running { get; protected set; }
public event Func<Task> OnStartAsync;
public event Func<Task> OnFinishAsync;
public event Func<Task> OnNumberChangedAsync;
public async Task RollTheBallAsync()
{
WinningNumber = null;
Running = true;
if (OnStartAsync != null)
{
await OnStartAsync.Invoke();
}
var running = true;
var random = new Random();
var stopwatch = new Stopwatch();
stopwatch.Start();
var lengthOfSpin = new TimeSpan(0, 0, random.Next(5, 15));
random = new Random();
var speed = new TimeSpan(random.Next(30000, 40000));
while (running)
{
CurrentNumberIndex += 1;
if (CurrentNumberIndex > WheelNumbers.GetUpperBound(0))
{
CurrentNumberIndex = 0;
}
if (OnNumberChangedAsync != null)
{
await OnNumberChangedAsync.Invoke();
}
await Task.Delay(speed);
if (stopwatch.Elapsed.TotalSeconds > lengthOfSpin.TotalSeconds - 5)
{
random = new Random();
speed = new TimeSpan(random.Next(100000, 200000));
}
if (stopwatch.Elapsed.TotalSeconds > lengthOfSpin.TotalSeconds - 2)
{
random = new Random();
speed = new TimeSpan(random.Next(500000, 700000));
}
if (stopwatch.Elapsed.TotalSeconds > lengthOfSpin.TotalSeconds)
{
running = false;
}
}
WinningNumber = WheelNumbers[CurrentNumberIndex];
Running = false;
if (OnFinishAsync != null)
{
await OnFinishAsync.Invoke();
}
}
}
Creating Razor components in Blazor
Time to create the Razor components in Blazor.
The wheel number component
A WheelNumberRazorComponent
was created, and this represents the number on the wheel.
Each number is wrapped within a <li>
tag. If the ball has landed on that number, there are separate HTML elements to represent the ball.
We have passed in a WheelNumber
instance as a parameter. This is so we know the number, colour and whether the ball has landed on that number.
Once again, we used CSS isolation, a featured introduced within Blazor when .NET 5 was launched. This allows us to create CSS for a specific razor component.
<!-- WheelNumberComponent.razor -->
@using RoundTheCode.Roulette.Models
@if (WheelNumber != null)
{
<li class="@WheelNumber.Colour.ToString().ToLower()"><span class="number">@WheelNumber.Number</span>
@if (WheelNumber.Selected)
{
<span class="ball-container"><span class="ball"></span></span>
}
</li>
}
@code {
[Parameter]
public WheelNumber WheelNumber { get; set; }
}
/* WheelNumberComponent.razor.css */
li {
position: absolute;
height: 700px;
width: 59.435536px;
left: 320px;
border-top: 350px #000 solid;
border-left: 30.3px transparent solid;
border-right: 30.3px transparent solid;
box-sizing: border-box;
margin-top: -16px;
}
li.red {
border-top-color: #ff0000;
}
li.green {
border-top-color: #009933;
}
li span.number {
z-index: 5;
width: 59.435536px;
height: 40px;
display: inline-block;
position: absolute;
color: #fff;
top: -350px;
margin-left: -29.717768px;
text-align: center;
font-size: 30px;
font-weight: bold;
}
li span.ball-container {
z-index: 6;
width: 59.435536px;
height: 20px;
top: -250px;
display: block;
position: absolute;
margin-left: -29.717768px;
text-align: center;
}
li span.ball-container span.ball {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #fff;
display: inline-block;
}
li:nth-child(0) {
transform: rotateZ(0deg);
}
li:nth-child(1) {
transform: rotateZ(9.72972973deg);
}
li:nth-child(2) {
transform: rotateZ(19.45945946deg);
}
li:nth-child(3) {
transform: rotateZ(29.18918919deg);
}
li:nth-child(4) {
transform: rotateZ(38.91891892deg);
}
li:nth-child(5) {
transform: rotateZ(48.64864865deg);
}
li:nth-child(6) {
transform: rotateZ(58.37837838deg);
}
li:nth-child(7) {
transform: rotateZ(68.10810811deg);
}
li:nth-child(8) {
transform: rotateZ(77.83783784deg);
}
li:nth-child(9) {
transform: rotateZ(87.56756757deg);
}
li:nth-child(10) {
transform: rotateZ(97.2972973deg);
}
li:nth-child(11) {
transform: rotateZ(107.02702703deg);
}
li:nth-child(12) {
transform: rotateZ(116.75675676deg);
}
li:nth-child(13) {
transform: rotateZ(126.48648649deg);
}
li:nth-child(14) {
transform: rotateZ(136.21621622deg);
}
li:nth-child(15) {
transform: rotateZ(145.94594595deg);
}
li:nth-child(16) {
transform: rotateZ(155.67567568deg);
}
li:nth-child(17) {
transform: rotateZ(165.40540541deg);
}
li:nth-child(18) {
transform: rotateZ(175.13513514deg);
}
li:nth-child(19) {
transform: rotateZ(184.86486486deg);
}
li:nth-child(20) {
transform: rotateZ(194.59459459deg);
}
li:nth-child(21) {
transform: rotateZ(204.32432432deg);
}
li:nth-child(22) {
transform: rotateZ(214.05405405deg);
}
li:nth-child(23) {
transform: rotateZ(223.78378378deg);
}
li:nth-child(24) {
transform: rotateZ(233.51351351deg);
}
li:nth-child(25) {
transform: rotateZ(243.24324324deg);
}
li:nth-child(26) {
transform: rotateZ(252.97297297deg);
}
li:nth-child(27) {
transform: rotateZ(262.7027027deg);
}
li:nth-child(28) {
transform: rotateZ(272.43243243deg);
}
li:nth-child(29) {
transform: rotateZ(282.16216216deg);
}
li:nth-child(30) {
transform: rotateZ(291.89189189deg);
}
li:nth-child(31) {
transform: rotateZ(301.62162162deg);
}
li:nth-child(32) {
transform: rotateZ(311.35135135deg);
}
li:nth-child(33) {
transform: rotateZ(321.08108108deg);
}
li:nth-child(34) {
transform: rotateZ(330.81081081deg);
}
li:nth-child(35) {
transform: rotateZ(340.54054054deg);
}
li:nth-child(36) {
transform: rotateZ(350.27027027deg);
}
The wheel component
The WheelComponent
is were the magic happens. We create a Wheel instance, and use our WheelNumberRazorComponent
to display each of the 37 numbers.
In addition, the user can select whether the ball will land on red or black. And, it will display what number they selected, where the ball actually landed, and whether the user actually won.
When the user selects the colour, the ball will start spinning on the roulette wheel. This calls the RollTheBallAsync
method from our Wheel
instance. This instance calls the OnStartAsync
, OnNumberChangedAsync
, and OnFinishAsync
events from our Wheel instance.
Our WheelRazorComponent
hooks into these events, refreshes the razor component and changes where the ball is currently residing.
<!-- WheelComponent.razor -->
@using RoundTheCode.Roulette.Models
@page "/"
@if (Wheel != null)
{
<div class="container">
<div class="roulette">
<div class="numbers">
<ul>
@foreach (var wheelNumber in Wheel.WheelNumbers)
{
<WheelNumberComponent WheelNumber="@wheelNumber"></WheelNumberComponent>
}
</ul>
</div>
<div class="ball-circle">
</div>
<div class="inner-circle">
</div>
</div>
@if (Wheel.Colour != null)
{
<p>You have picked @Wheel.Colour.Value</p>
}
@if (Wheel.WinningNumber != null)
{
<p>The winning number is @Wheel.WinningNumber.Number @Wheel.WinningNumber.Colour. @(Wheel.WinningNumber.Colour == Wheel.Colour ? "You win" : "You lose")</p>
}
@if (!Wheel.Running)
{
<p>
<button type="submit" @onclick="@(async(e) => await OnRollAsync(e, WheelNumberColourEnum.Red))">Red</button>
<button type="submit" @onclick="@(async(e) => await OnRollAsync(e, WheelNumberColourEnum.Black))">Black</button>
</p>
}
</div>
}
@code {
public Wheel Wheel { get; set; }
protected override async Task OnInitializedAsync()
{
Wheel = new Wheel();
Wheel.OnStartAsync += async () =>
{
StateHasChanged();
await Task.CompletedTask;
};
Wheel.OnFinishAsync += async () =>
{
StateHasChanged();
await Task.CompletedTask;
};
Wheel.OnNumberChangedAsync += async () =>
{
for (var index = 0; index <= Wheel.WheelNumbers.GetUpperBound(0); index ++)
{
if (index == Wheel.CurrentNumberIndex)
{
Wheel.WheelNumbers[index].Selected = true;
}
else
{
Wheel.WheelNumbers[index].Selected = false;
}
}
StateHasChanged();
await Task.CompletedTask;
};
await base.OnInitializedAsync();
}
public async Task OnRollAsync(MouseEventArgs mouseEventArgs, WheelNumberColourEnum colour)
{
Wheel.Colour = colour;
await Wheel.RollTheBallAsync();
}
}
/* WheelComponent.razor.css */
@keyframes rotate {
0% {
transform: rotateZ(360deg);
}
100% {
transform: rotateZ(0deg);
}
}
.container {
width: 700px;
margin-left: auto;
margin-right: auto;
}
.roulette {
border: 10px #D4AF37 solid;
border-radius: 50%;
width: 700px;
height: 700px;
animation: rotate 15s infinite linear
}
.numbers {
z-index: 1;
}
.ball-circle {
z-index: 2;
width: 600px;
height: 600px;
border-radius: 50%;
border: 10px #D4AF37 solid;
position: absolute;
margin: 40px;
}
.inner-circle {
z-index: 3;
width: 400px;
height: 400px;
border-radius: 50%;
border: 10px #D4AF37 solid;
position: absolute;
background-color: #aaa;
margin: 140px;
}
.numbers ul {
list-style: none;
padding-left: 0;
position: absolute;
}
p {
font-size: 26px;
}
The final result
The final result looks like a Roulette wheel. The user can select whether the ball will land on red or black.
From there, the ball will then spin around the Roulette wheel and stop on a random number.
Finally, the winning number and colour will be displayed.