Read about how to develop an online game on Azure. Or just play the game for fun.
Table of Contents
Read about how to develop an online game on Azure.
Or just play the game at https://backgammon.azurewebsites.net/.
During the last month or so, I've spent most of my free time building an online backgammon game. The main goal was to try to improve my full stack developer skills and perhaps discover a new trick or two. In this article, I share the technologies I use and small things I think can be useful if you plan to start a similar project. It's by no means a complete guide of my code, but you can find the source open to read on GitHub (2).
Some of the game features are listed below:
- Play a random opponent or an AI
- Invite a friend
- Elo rating and toplist
- Mobile responsive design
The application is hosted on Azure(10) as an App Service and the data is stored in SQL Server. You authenticate via Facebook or Google Oauth 2.0. The backend is written in C# .NET. (latest .NET Core). For SQL Server database integration, I use Entity Framework Core with code first migrations.
The communication between frontend and backend is done with websockets during game play and a REST API for everything else.
For frontend, I use Angular 15 and the game board is drawn on an HTML canvas
element.
The rules(3) of Backgammon might look quite simple at first: Roll the dice and move checkers the number you get on the dice towards your home. If an opponent has two or more checkers on a point, that point is blocked. If the opponent has only one checker on a point, you can hit it and that checker is moved to the bar, forced to start from point zero. But there are a few complex situations also, for example that you always have to use both dice if you can. If the move of a checker prevents using the other dice, you can't move that checker.
Read more about Backgammon rules here. For these reasons, I decided to develop the rules of the game using Test Driven Development (TDD) and keep them in a separate DLL. TDD is my choice of method when calculations get complicated and you don't want to spend hours and days tracking down ugly bugs.
I also realized early that the game state had to be kept on the server and that the client should have as little game rules as possible. The game rules are developed in C# and is a part of the backend. One main priority was to keep network traffic low, in case many users start playing at the same time. Larger servers cost more money on Azure.
Since the roll of dice are random, the Rules.Game
class also has a FakeRoll
function for testing, which of course isn't accessible from the client. Below are a few examples of test cases of the Rules.Game
class.
[TestMethod]
public void TestMoveGeneration()
{
game.FakeRoll(1, 2);
var moves = game.GenerateMoves();
Assert.AreEqual(7, moves.Count);
}
[TestMethod]
public void TestCheckerOnTheBarBlocked()
{
game.AddCheckers(2, Player.Color.Black, 0);
game.FakeRoll(6, 6);
var moves = game.GenerateMoves();
Assert.AreEqual(0, moves.Count);
}
One Less Webserver
The client connects to the server through a web API. Since the API is served with an App Service, which is basically a web server, I wanted to use the same App Service to serve the Angular client in production. Then you will have one less Webserver to worry about. The alternative could have been to use for example Nginx on a Ubuntu machine.
This is the configuration needed to make it work.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env,
ILogger<GameManager> logger, IHostApplicationLifetime applicationLifetime)
{
app.UseHttpsRedirection();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseWebSockets();
app.UseDefaultFiles();
app.Use(async (context, next) =>
{
if (context.Request.Path == "/ws/game")
{
if (context.WebSockets.IsWebSocketRequest)
{
logger.LogInformation($"New web socket request.");
}
else
{
context.Response.StatusCode = 400;
}
}
else
{
await next();
if (SinglePageAppRequestCheck(context))
{
context.Request.Path = "/index.html";
await next();
}
}
});
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseStaticFiles();
}
Web sockets is a technology I hadn't worked with before, but I was very curious about it. I am familiar with regular sockets on Windows so I understood that the communication is actually simpler than the regular request response pattern. The difference with web sockets and a http request is that sockets are always open. Either the client or the server can send data at any time.
There are good libraries for both Angular and .NET Core, and they are fully compatible. The only thing that might feel strange is that when the function on the .NET server side accepts the socket returns, the connection is closed. So you have to read the socket in a loop until you decide to close communications. The socket is also wrapped in an http request, which isn't returned until socket closure.
Below is my Startup
configure function, little bit simplified.
if (context.Request.Path == "/ws/game")
{
if (context.WebSockets.IsWebSocketRequest)
{
var socket = await context.WebSockets.AcceptWebSocketAsync();
try
{
while (socket.State != WebSocketState.Closed &&
socket.State != WebSocketState.Aborted &&
socket.State != WebSocketState.CloseReceived)
{
var buffer = new byte[512];
var sb = new StringBuilder();
WebSocketReceiveResult result = null;
while (result == null ||
(!result.EndOfMessage && !result.CloseStatus.HasValue))
{
result = await socket.ReceiveAsync
(new ArraySegment<byte>(buffer), CancellationToken.None);
var text = Encoding.UTF8.GetString(buffer.Take(result.Count).ToArray());
sb.Append(text);
}
}
logger.LogInformation("Socket is closed");
}
catch (Exception exc)
{
logger.LogError(exc.ToString());
}
}
else
{
context.Response.StatusCode = 400;
}
}
It is not important for the backend to know who the players are. What matters is to identify if the user has been here before to keep a score when different players compete. For these requirements, I think the perfect choice is to use an external social provider for authentication. I've enabled Facebook and Google provider.
I see no reason for a user to log in every time he (or she) starts the app, so the login UserDto
is stored in the browser's local storage. These are steps occurring during login.
- A user clicks the Google or Facebook login button.
signIn
as called on Angular package angularx-social-login(6). - The signin modal is opened.
- A
SocialUser
object is returned including an OpenId jwt
. - The
jwt
is sent securely to the backend where it is validated. - If valid, a user is created, if not already created.
- The user's unique user Id is sent back to the client and stored in local storage. The user is now logged in and can play other users.
Drawing is the most fun part of the application I think. I find canvas drawing(7) to be quite easy if you are used to think in x-y-coordinates. The main benefit is that you can make the board 100% responsive, so it will fit nicely on any screen size. That is, if you calculate all coordinates in relation to height and width of the screen. This is how you get the drawing context and draw a filled circle on it.
@ViewChild('canvas') public canvas: ElementRef | undefined;
ngAfterViewInit(): void {
const canvasEl: HTMLCanvasElement = this.canvas.nativeElement;
const cx = canvasEl.getContext('2d');
cx.beginPath();
cx.ellipse(x, y, width, width, 0, 0, 2 * Math.PI);
cx.closePath();
cx.fill();
}
One thing I learned is to use the built in function: requestAnimationFrame
. It is called every time something changes on the board and then it's up to the browser if it feels it has time to draw a frame. I find that the CPU impact is quite low using this method.
requestAnimationFrame(this.draw.bind(this));
I'm so happy to see how Entity Framework(9) has evolved during the last years. Nowadays, it is very easy to write some C# classes, make sure every class has a primary key and then let Entity Framework generate everything for you. I was surprised that EF core is so good to realize many to many relations for example. I also like that properties named "Id
" with datatype int
automatically get defined as an auto incrementing identity.
Error messages were always clear and descriptive to me so this part of the development was what I spent the least time with.
And everytime I need to make a database update, I just add some data class, property or whatever and then call:
Add-Migration a-name-I-choose
(Inspect the changes in the generated migration files)
Update-Database
(done)
In all client server applications, it is important to give extra time and thought on the parts that make the integration possible. The client and server are essentially two different programs, which very often have different pace of development. To minimize the risk of changing things on one side that will break the integration, you use data transfer objects, (dto
). Their purpose is to define the data transferred between the client and server. Since they are written in different languages I use a package called MTT by Cody Schrank(8). It is set it up in the .csproj file like this:
<Target Name="Convert" BeforeTargets="PrepareForBuild">
<ConvertMain WorkingDirectory="Dto/" ConvertDirectory="tsdto/" />
</Target>
MTT takes the C# dto
s and converts them at compile time to typescript interfaces which are then used as definition for either sent or received data in both client and server. Unfortunately, MTT can only save its files below the project directory, so I also have to copy them to the client file source tree.
Example of conversion:
namespace Backend.Dto
{
public class CheckerDto
{
public PlayerColor color { get; set; }
}
}
gets converted to typescript:
import { PlayerColor } from "./playerColor";
export interface CheckerDto {
color: PlayerColor;
}
There is also an AI that you can play if you can't find a human opponent. I've written a separate article here if you want to know the details about that.
If you find this article interesting, but have an opinion on what technologies you would have chosen, or any other comments, let me know in the comments below.
If you find a bug in the game, please also let me know. The code is open for anyone to read, so if you want to make pull request and help out with improvements, you are very welcome.
And please also don´t forget to vote. ;)
I also want to thank Shane, Patrik and Linn for helping me with testing.
- 24th March, 2021: Version 1.0
- 4th April, 2021: Version 1.1
- 9th April, 2021: Version 1.1.1
- 30th April, 2021: Version 1.2
- App updated with translations, sound and many small features and bugfixes.
- 6th June, 2021: Version 3.0
- 9th February, 2022: Version 3.4.1
- Rules tutorial
- Fixed a bug for some touch-devices
- 25th April, 2022: Version 3.6
- AI Bug fix (Thanks to Hans-Jürgen who found the bug)
- Small improvements in AI logic for better bearing off
- If you play a practice game, you can request a hint on a good move.
- 19th November, 2022: Version 4.0