Create a Roulette wheel. Game development using Blazor WebAssembly

19th 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.

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.

A roulette wheel written in C# for game development in Blazor WebAssembly
A roulette wheel written in C# for game development in Blazor WebAssembly

Have any comments about this article?

If you have any questions or comments about this article, any business opportunities, or any feedback about the site in general, we would love to hear from you! Contact us

David Grace

David Grace

I am a .NET developer, building web applications in .NET Framework and .NET Core with a SQL Server database.

Some of the .NET packages I have used include Entity Framework and MVC.

I've also used many JavaScript frameworks such as React and jQuery, and have experience building CSS in SASS.

Twitter Feed