27 min read

Learning is more fun if we do it while making games. With this thought, let’s continue our quest to learn .NET Core 2.0 by writing a Tic-tac-toe game in .NET Core 2.0. We will develop the game in the ASP.NET Core 2.0 web app, using SignalR Core. We will follow a step-by-step approach and use Visual Studio 2017 as the primary IDE, but will list the steps needed while using the Visual Studio Code editor as well.

Let’s do the project setup first and then we will dive into the coding.

This tutorial has been extracted from the book .NET Core 2.0 By Example, by Rishabh Verma and Neha Shrivastava.

Installing SignalR Core NuGet package

Create a new ASP.NET Core 2.0 MVC app named TicTacToeGame.

With this, we will have a basic working ASP.NET Core 2.0 MVC app in place. However, to leverage SignalR Core in our app, we need to install SignalR Core NuGet and the client packages.

To install the SignalR Core NuGet package, we can perform one of the following two approaches in the Visual Studio IDE:

  • In the context menu of the TicTacToeGame project, click on Manage NuGet Packages. It will open the NuGet Package Manager for the project. In the Browse section, search for the Microsoft.AspNetCore.SignalR package and click Install. This will install SignalR Core in the app. Please note that currently the package is in the preview stage and hence the pre-release checkbox has to be ticked:

Microsoft ASP.Net Core SignalR

  • Edit the TicTacToeGame.csproj file, add the following code snippet in the ItemGroup code containing package references, and click Save. As soon as the file is saved, the tooling will take care of restoring the packages and in a while, the SignalR package will be installed. This approach can be used with Visual Studio Code as well. Although Visual Studio Code detects the unresolved dependencies and may prompt you to restore the package, it is recommended that immediately after editing and saving the file, you run the dotnet restore command in the terminal window at the location of the project:
<ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.All" 
     Version="2.0.0" />
     <PackageReference Include="Microsoft.AspNetCore.SignalR" 
     Version="1.0.0-alpha1-final" />
 </ItemGroup>

Now we have server-side packages installed. We still need to install the client-side package of SignalR, which is available through npm. To do so, we need to first ascertain whether we have npm installed on the machine or not. If not, we need to install it. npm is distributed with Node.js, so we need to download and install Node.js from https://nodejs.org/en/. The installation is quite straightforward.

Once this installation is done, open a Command Prompt at the project location and run the following command:

npm install @aspnet/signalr-client

This will install the SignalR client package. Just go to the package location (npm creates a node_modules folder in the project directory). The relative path from the project directory would be \node_modules\@aspnet\signalr-client\dist\browser.

From this location, copy the signalr-client-1.0.0-alpha1-final.js file into the wwwroot\js folder. In the current version, the name is signalr-client-1.0.0-alpha1-final.js.

With this, we are done with the project setup and we are ready to use SignalR goodness as well. So let’s dive into the coding.

Coding the game

In this section, we will implement our gaming solution. The end output will be the working two-player Tic-Tac-Toe game. We will do the coding in steps for ease of understanding:

  1.  In the Startup class, we modify the ConfigureServices method to add SignalR to the container, by writing the following code:
//// Adds SignalR to the services container.
 services.AddSignalR();
  1. In the Configure method of the same class, we configure the pipeline to use SignalR and intercept and wire up the request containing gameHub to our SignalR hub that we will be creating with the following code:
//// Use - SignalR & let it know to intercept and map any request having gameHub.
 app.UseSignalR(routes => 
 {
        routes.MapHub<GameHub>("gameHub");
 });

The following is the code for both methods, for the sake of clarity and completion. Other methods and properties are removed for brevity:

 // This method gets called by the run-time. Use this method to add 
    services to the container.
 public void ConfigureServices(IServiceCollection services)
 {
     services.AddMvc();
     //// Adds SignalR to the services container.
     services.AddSignalR();
 }
// This method gets called by the runtime. Use this method to 
configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment 
env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
//// Use - SignalR & let it know to intercept and map any request 
having gameHub.
app.UseSignalR(routes => 
{
routes.MapHub<GameHub>("gameHub");
});
  1. The previous two steps set up SignalR for us. Now, let’s start with the coding of the player registration form. We want the player to be registered with a name and display the picture. Later, the server will also need to know whether the player is playing, waiting for a move, searching for an opponent, and so on. Let’s create the Player model in the Models folder in the app. The code comments are self-explanatory:
/// <summary>
/// The player class. Each player of Tic-Tac-Toe game would 
    be an instance of this class.
/// </summary>
internal class Player
{
     /// <summary>
     /// Gets or sets the name of the player. This would be 
         set at the time user registers.
     /// </summary>
     public string Name { get; set; }
/// <summary>
/// Gets or sets the opponent player. The player 
against whom the player would be playing.
/// This is determined/ set when the players click Find 
Opponent Button in the UI.
/// </summary>
public Player Opponent { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the player 
is playing.
/// This is set when the player starts a game.
/// </summary>
public bool IsPlaying { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the player 
is waiting for opponent to make a move.
/// </summary>
public bool WaitingForMove { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the player 
is searching for opponent.
/// </summary>
public bool IsSearchingOpponent { get; set; }
/// <summary>
/// Gets or sets the time when the player registered.
/// </summary>
public DateTime RegisterTime { get; set; }
/// <summary>
/// Gets or sets the image of the player.
/// This would be set at the time of registration, if 
the user selects the image.
/// </summary>
public string Image { get; set; }
/// <summary>
/// Gets or sets the connection id of the player 
connection with the gameHub.
/// </summary>
public string ConnectionId { get; set; }
}
  1. Now, we need to have a UI in place so that the player can fill in the form and register. We also need to show the image preview to the player when he/she browses the image. To do so, we will use the Index.cshtml view of the HomeController class that comes with the default MVC template. We will refer to the following two .js files in the _Layout.cshtml partial view so that they are available to all the views. Alternatively, you could add these in the Index.cshtml view as well, but its highly recommended that common scripts should be added in _Layout.cshtml. The version of the script file may be different in your case. These are the currently available latest versions. Although jQuery is not required to be the library of choice for us, we will use jQuery to keep the code clean, simple, and compact. With these references, we have jQuery and SignalR available to us on the client side:
<script src="~/lib/jquery/dist/jquery.js"></script> <!-- jQuery-->
<script src="~/js/signalr-client-1.0.0-alpha1-final.js"></script> <!-- SignalR-->

After adding these references, create the simple HTML UI for the image preview and registration, as follows:

<div id="divPreviewImage"> <!-- To display the browsed image-->
        <fieldset>
            <div class="form-group"> 
                <div class="col-lg-2">
                    <image src="" id="previewImage" 
                    style="height:100px;width:100px;border:solid 
                    2px dotted; float:left" />
                </div>
                <div class="col-lg-10" id="divOpponentPlayer"> <!-- 
                To display image of opponent player-->
                    <image src="" id="opponentImage" 
                    style="height:100px;width:100px;border:solid 
                    2px dotted; float:right;" />
                </div>
            </div>
        </fieldset>
    </div>
<div id="divRegister"> <!-- Our Registration form-->
<fieldset>
<legend>Register</legend>
<div class="form-group">
<label for="name" class="col-lg-2 control-
label">Name</label>
<div class="col-lg-10">
<input type="text" class="form-control" id="name" 
placeholder="Name">
</div>
</div>
<div class="form-group">
<label for="image" class="col-lg-2 control-
label">Avatar</label>
<div class="col-lg-10">
<input type="file" class="form-control" id="image" 
/>
</div>
</div>
<div class="form-group">
<div class="col-lg-10 col-lg-offset-2">
<button type="button" class="btn btn-primary" 
id="btnRegister">Register</button>
</div>
</div>
</fieldset>
</div>
  1. When the player registers by clicking the Register button, the player’s details need to be sent to the server. To do this, we will write the JavaScript to send details to our gameHub:
 let hubUrl = '/gameHub';
 let httpConnection = new signalR.HttpConnection(hubUrl);
 let hubConnection = new signalR.HubConnection(httpConnection);
 var playerName = "";
 var playerImage = "";
 var hash = "#";
 hubConnection.start();
$("#btnRegister").click(function () { //// Fires on button click
playerName = $('#name').val(); //// Sets the player name 
with the input name.
playerImage = $('#previewImage').attr('src'); //// Sets the 
player image variable with specified image
var data = playerName.concat(hash, playerImage); //// The 
registration data to be sent to server.
hubConnection.invoke('RegisterPlayer', data); //// Invoke 
the "RegisterPlayer" method on gameHub.
});
$("#image").change(function () { //// Fires when image is changed.
readURL(this); //// HTML 5 way to read the image as data 
url.
});
function readURL(input) {
if (input.files && input.files[0]) { //// Go in only if 
image is specified.
var reader = new FileReader();
reader.onload = imageIsLoaded;
reader.readAsDataURL(input.files[0]);
}
}
function imageIsLoaded(e) {
if (e.target.result) {
$('#previewImage').attr('src', e.target.result); //// 
Sets the image source for preview.
$("#divPreviewImage").show();
}
};
  1. The player now has a UI to input the name and image, see the preview image, and click Register. On clicking the Register button, we are sending the concatenated name and image to the gameHub on the server through hubConnection.invoke('RegisterPlayer', data);  So, it’s quite simple for the client to make a call to the server. Initialize the hubConnection by specifying hub name as we did in the first three lines of the preceding code snippet. Start the connection by hubConnection.start();, and then invoke the server hub method by calling the invoke method, specifying the hub method name and the parameter it expects. We have not yet created the hub, so let’s create the GameHub class on the server:
/// <summary>
/// The Game Hub class derived from Hub
/// </summary>
public class GameHub : Hub
{
     /// <summary>
     /// To keep the list of all the connected players 
         registered with the game hub. We could have      
     /// used normal list but used concurrent bag as its thread 
         safe.
     /// </summary>
     private static readonly ConcurrentBag<Player> players = 
     new ConcurrentBag<Player>();
/// <summary>
/// Registers the player with name and image.
/// </summary>
/// <param name="nameAndImageData">The name and image data 
sent by the player.</param>
public void RegisterPlayer(string nameAndImageData)
{
var splitData = nameAndImageData?.Split(new char[] { 
'#' }, StringSplitOptions.None);
string name = splitData[0];
string image = splitData[1];
var player = players?.FirstOrDefault(x => 
x.ConnectionId == Context.ConnectionId);
if (player == null)
{
player = new Player { ConnectionId = 
Context.ConnectionId, Name = name, IsPlaying = 
false, IsSearchingOpponent = false, RegisterTime = 
DateTime.UtcNow, Image = image };
if (!players.Any(j => j.Name == name))
{
players.Add(player);
}
}

this.OnRegisterationComplete(Context.ConnectionId);
}

/// <summary>
/// Fires on completion of registration.
/// </summary>
/// <param name="connectionId">The connectionId of the 
player which registered</param>
public void OnRegisterationComplete(string connectionId)
{
//// Notify this connection id that the registration 
is complete.
this.Clients.Client(connectionId).
InvokeAsync(Constants.RegistrationComplete);
}
}

The code comments make it self-explanatory. The class should derive from the SignalR Hub class for it to be recognized as Hub.

There are two methods of interest which can be overridden. Notice that both the methods follow the async pattern and hence return Task:

  • Task OnConnectedAsync(): This method fires when a client/player connects to the hub.
  • Task OnDisconnectedAsync(Exception exception): This method fires when a client/player disconnects or looses the connection. We will override this method to handle the scenario where the player disconnects.

There are also a few properties that the hub class exposes:

    • Context: This property is of type HubCallerContext and gives us access to the following properties:
      • Connection: Gives access to the current connection
      • User: Gives access to the ClaimsPrincipal of the user who is currently connected
      • ConnectionId: Gives the current connection ID string
    • Clients: This property is of type IHubClients and gives us the way to communicate to all the clients via the client proxy
    • Groups: This property is of type IGroupManager and provides a way to add and remove connections to the group asynchronously

To keep the things simple, we are not using a database to keep track of our registered players. Rather we will use an in-memory collection to keep the registered players. We could have used a normal list of players, such as List<Player>, but then we would need all the thread safety and use one of the thread safety primitives, such as lock, monitor, and so on, so we are going with ConcurrentBag<Player>, which is thread safe and reasonable for our game development.

That explains the declaration of the players collection in the class. We will need to do some housekeeping to add players to this collection when they resister and remove them when they disconnect.

We saw in previous step that the client invoked the RegisterPlayer method of the hub on the server, passing in the name and image data. So we defined a public method in our hub, named RegisterPlayer, accepting the name and image data string concatenated through #.

This is just one of the simple ways of accepting the client data for demonstration purposes, we can also use strongly typed parameters. In this method, we split the string on # and extract the name as the first part and the image as the second part. We then check if the player with the current connection ID already exists in our players collection.

If it doesn’t, we create a Player object with default values and add them to our players collection. We are distinguishing the player based on the name for demonstration purposes, but we can add an Id property in the Player class and make different players have the same name also. After the registration is complete, the server needs to update the player, that the registration is complete and the player can then look for the opponent.

To do so, we make a call to the OnRegistrationComplete method which invokes a method called  registrationComplete on the client with the current connection ID. Let’s understand the code to invoke the method on the client:

this.Clients.Client(connectionId).InvokeAsync(Constants.RegistrationComplete);

On the Clients property, we can choose a client having a specific connection ID (in this case, the current connection ID from the Context) and then call InvokeAsync to invoke a method on the client specifying the method name and parameters as required. In the preceding case method, the name is registrationComplete with no parameters.

Now we know how to invoke a server method from the client and also how to invoke the client method from the server. We also know how to select a specific client and invoke a method there. We can invoke the client method from the server, for all the clients, a group of clients, or a specific client, so rest of the coding stuff would be just a repetition of these two concepts.

  1. Next, we need to implement the registrationComplete method on the client. On registration completion, the registration form should be hidden and the player should be able to find an opponent to play against. To do so, we would write JavaScript code to hide the registration form and show the UI for finding the opponent. On clicking the Find Opponent button, we need the server to pair us against an opponent, so we need to invoke a hub method on server to find opponent.
  2. The server can respond us with two outcomes:
    • It finds an opponent player to play against. In this case, the game can start so we need to simulate the coin toss, determine the player who can make the first move, and start the game. This would be a game board in the client-user interface.
    • It doesn’t find an opponent and asks the player to wait for another player to register and search for an opponent. This would be a no opponent found screen in the client.

In both the cases, the server would do some processing and invoke a method on the client. Since we need a lot of different user interfaces for different scenarios, let’s code the HTML markup inside div to make it easier to show and hide sections based on the server response. We will add the following code snippet in the body. The comments specify the purpose of each of the div elements and markup inside them:

<div id="divFindOpponentPlayer"> <!-- Section to display Find Opponent -->
     <fieldset>
         <legend>Find a player to play against!</legend>
         <div class="form-group">
             <input type="button" class="btn btn-primary" 
             id="btnFindOpponentPlayer" value="Find Opponent                 
             Player" />
         </div>
     </fieldset>
 </div>
     <div id="divFindingOpponentPlayer"> <!-- Section to display 
     opponent not found, wait -->
     <fieldset>
         <legend>Its lonely here!</legend>
         <div class="form-group">
             Looking for an opponent player. Waiting for someone to 
             join!
         </div>
     </fieldset>
 </div>
 <div id="divGameInformation" class="form-group"> <!-- Section to 
 display game information-->
     <div class="form-group" id="divGameInfo"></div>
     <div class="form-group" id="divInfo"></div>
 </div>
 <div id="divGame" style="clear:both"> <!-- Section where the game 
 board would be displayed -->
     <fieldset>
         <legend>Game On</legend>
         <div id="divGameBoard" style="width:380px"></div>
     </fieldset>
 </div>

The following client-side code would take care of Steps 7 and 8. Though the comments are self-explanatory, we will quickly see what all stuff is that is going on here. We handle the registartionComplete method and display the Find Opponent Player section. This section has a button to find an opponent player called btnFindOpponentPlayer.

We define the event handler of the button to invoke the FindOpponent method on the hub. We will see the hub method implementation later, but we know that the hub method would either find an opponent or would not find an opponent, so we have defined the methods opponentFound and opponentNotFound, respectively, to handle these scenarios. In the opponentNotFound method, we just display a section in which we say, we do not have an opponent player.

In the opponentFound method, we display the game section, game information section, opponent display picture section, and draw the Tic-Tac-Toe game board as a 3×3 grid using CSS styling. All the other sections are hidden:

 $("#btnFindOpponentPlayer").click(function () {
        hubConnection.invoke('FindOpponent');
    });
hubConnection.on('registrationComplete', data => { //// Fires on registration complete. Invoked by server hub
$("#divRegister").hide(); // hide the registration div
$("#divFindOpponentPlayer").show(); // display find opponent 
player div.
});
hubConnection.on('opponentNotFound', data => { //// Fires when no opponent is found.
$('#divFindOpponentPlayer').hide(); //// hide the find 
opponent player section.
$('#divFindingOpponentPlayer').show(); //// display the 
finding opponent player div.
});
hubConnection.on('opponentFound', (data, image) => { //// Fires 
when opponent player is found.
$('#divFindOpponentPlayer').hide(); 
$('#divFindingOpponentPlayer').hide();
$('#divGame').show(); //// Show game board section.
$('#divGameInformation').show(); //// Show game information
$('#divOpponentPlayer').show(); //// Show opponent player 
image.
opponentImage = image; //// sets the opponent player image
for display
$('#opponentImage').attr('src', opponentImage); //// Binds 
the opponent player image
$('#divGameInfo').html("<br/><span><strong> Hey " + 
playerName + "! You are playing against <i>" + data + "</i>
</strong></span>"); //// displays the information of 
opponent that the player is playing against.
//// Draw the tic-tac-toe game board, A 3x3 grid :) by 
proper styling.
for (var i = 0; i < 9; i++) {
$("#divGameBoard").append("<span class='marker' id=" + i 
+ " style='display:block;border:2px solid 
black;height:100px;width:100px;float:left;margin:10px;'>" 
+ i + "</span>");
}
});

First we need to have a Game object to track a game, players involved, moves left, and check if there is a winner. We will have a Game class defined as per the following code. The comments detail the purpose of the methods and the properties defined:

internal class Game
{
     /// <summary>
     /// Gets or sets the value indicating whether the 
         game is over.
     /// </summary>
     public bool IsOver { get; private set; }
/// <summary>
/// Gets or sets the value indicating whether the 
game is draw.
/// </summary>
public bool IsDraw { get; private set; }
/// <summary>
/// Gets or sets Player 1 of the game
/// </summary>
public Player Player1 { get; set; }
/// <summary>
/// Gets or sets Player 2 of the game
/// </summary>
public Player Player2 { get; set; }
/// <summary>
/// For internal housekeeping, To keep track of value in each 
of the box in the grid.
/// </summary>
private readonly int[] field = new int[9];
/// <summary>
/// The number of moves left. We start the game with 9 moves 
remaining in a 3x3 grid.
/// </summary>
private int movesLeft = 9;
/// <summary>
/// Initializes a new instance of the 
<see cref="Game"/> class.
/// </summary>
public Game()
{
//// Initialize the game
for (var i = 0; i < field.Length; i++)
{
field[i] = -1;
}
}
/// <summary>
/// Place the player number at a given position for a player
/// </summary>
/// <param name="player">The player number would be 0 or 
1</param>
/// <param name="position">The position where player number 
would be placed, should be between 0 and 
///8, both inclusive</param>
/// <returns>Boolean true if game is over and 
we have a winner.</returns>
public bool Play(int player, int position)
{
if (this.IsOver)
{
return false;
}
//// Place the player number at the given position
this.PlacePlayerNumber(player, position);
//// Check if we have a winner. If this returns true, 
//// game would be over and would have a winner, else game 
would continue.
return this.CheckWinner();
}
}

Now we have the entire game mystery solved with the Game class. We know when the game is over, we have the method to place the player marker, and check the winner. The following server side-code on the GameHub will handle Steps 7 and 8:

/// <summary>
/// The list of games going on.
/// </summary>
private static readonly ConcurrentBag<Game> games = new ConcurrentBag<Game>();
/// <summary>
/// To simulate the coin toss. Like heads and tails, 0 belongs to 
one player and 1 to opponent.
/// </summary>
private static readonly Random toss = new Random();
/// <summary>
/// Finds the opponent for the player and sets the Seraching for 
Opponent property of player to true. 
/// We will use the connection id from context to identify the 
current player.
/// Once we have 2 players looking to play, we can pair them and 
simulate coin toss to start the game.
/// </summary>
public void FindOpponent()
{
//// First fetch the player from our players collection having 
current connection id
var player = players.FirstOrDefault(x => x.ConnectionId == 
Context.ConnectionId);
if (player == null)
{
//// Since player would be registered before making this 
call,
//// we should not reach here. If we are here, something 
somewhere in the flow above is broken.
return;
}
//// Set that player is seraching for opponent.
player.IsSearchingOpponent = true;
//// We will follow a queue, so find a player who registered 
earlier as opponent. 
//// This would only be the case if more than 2 players are 
looking for opponent.
var opponent = players.Where(x => x.ConnectionId != 
Context.ConnectionId && x.IsSearchingOpponent && 
!x.IsPlaying).OrderBy(x =>x.RegisterTime).FirstOrDefault();
if (opponent == null)
{
//// Could not find any opponent, invoke opponentNotFound 
method in the client.
Clients.Client(Context.ConnectionId)
.InvokeAsync(Constants.OpponentNotFound);
return;
}
//// Set both players as playing.
player.IsPlaying = true;
player.IsSearchingOpponent = false; //// Make him unsearchable 
for opponent search

opponent.IsPlaying = true;
opponent.IsSearchingOpponent = false;

//// Set each other as opponents.
player.Opponent = opponent;
opponent.Opponent = player;
//// Notify both players that they can play by invoking 
opponentFound method for both the players.
//// Also pass the opponent name and opoonet image, so that 
they can visualize it.
//// Here we are directly using connection id, but group is a 
good candidate and use here.
Clients.Client(Context.ConnectionId)
.InvokeAsync(Constants.OpponentFound, opponent.Name, 
opponent.Image);
Clients.Client(opponent.ConnectionId)
.InvokeAsync(Constants.OpponentFound, player.Name, 
player.Image);
//// Create a new game with these 2 player and add it to 
games collection.
games.Add(new Game { Player1 = player, Player2 = opponent });
}

Here, we have created a games collection to keep track of ongoing games and a Random field named toss to simulate the coin toss. How FindOpponent works is documented in the comments and is intuitive to understand.

  1. Once the game starts, each player has to make a move and then wait for the opponent to make a move, until the game ends. The move is made by clicking on the available grid cells. Here, we need to ensure that cell position that is already marked by one of the players is not changed or marked. So, as soon as a valid cell is marked, we set its CSS class to notAvailable so we know that the cell is taken. While clicking on a cell, we will check whether the cell has notAvailablestyle. If yes, it cannot be marked. If not, the cell can be marked and we then send the marked position to the server hub. We also see the waitingForMove, moveMade, gameOver, and opponentDisconnected events invoked by the server based on the game state. The code is commented and is pretty straightforward. The moveMade method in the following code makes use of the MoveInformation class, which we will define at the server for sharing move information with both players:
//// Triggers on clicking the grid cell.
 $(document).on('click', '.marker', function () {
     if ($(this).hasClass("notAvailable")) { //// Cell is already 
     taken.
     return;
     }
hubConnection.invoke('MakeAMove', $(this)[0].id); //// Cell is 
valid, send details to hub.
});

//// Fires when player has to make a move.
hubConnection.on('waitingForMove', data => {
$('#divInfo').html("<br/><span><strong> Your turn <i>" + 
playerName + "</i>! Make a winning move! </strong></span>");
});
//// Fires when move is made by either player. hubConnection.on('moveMade', data => { if (data.Image == playerImage) { //// Move made by player. $("#" + data.ImagePosition).addClass("notAvailable"); $("#" + data.ImagePosition).css('background-image', 'url(' + data.Image + ')'); $('#divInfo').html("<br/><strong>Waiting for <i>" + data.OpponentName + "</i> to make a move. </strong>"); } else
{ $("#" + data.ImagePosition).addClass("notAvailable"); $("#" + data.ImagePosition).css('background-image', 'url(' + data.Image + ')'); $('#divInfo').html("<br/><strong>Waiting for <i>" + data.OpponentName + "</i> to make a move. </strong>"); } });
//// Fires when the game ends.
hubConnection.on('gameOver', data => {
$('#divGame').hide();
$('#divInfo').html("<br/><span><strong>Hey " + playerName + 
"! " + data + " </strong></span>");
$('#divGameBoard').html(" ");
$('#divGameInfo').html(" ");
$('#divOpponentPlayer').hide();
});
//// Fires when the opponent disconnects.
hubConnection.on('opponentDisconnected', data => {
$("#divRegister").hide();
$('#divGame').hide();
$('#divGameInfo').html(" ");
$('#divInfo').html("<br/><span><strong>Hey " + playerName + 
"! Your opponent disconnected or left the battle! You 
are the winner ! Hip Hip Hurray!!!</strong></span>");
});

After every move, both players need to be updated by the server about the move made, so that both players’ game boards are in sync. So, on the server side we will need an additional model called MoveInformation, which will contain information on the latest move made by the player and the server will send this model to both the clients to keep them in sync:

 /// <summary>
 /// While playing the game, players would make moves. This class 
     contains the information of those moves.
 /// </summary>
 internal class MoveInformation
 {
     /// <summary>
     /// Gets or sets the opponent name.
     /// </summary>
     public string OpponentName { get; set; }

/// <summary>
/// Gets or sets the player who made the move.
/// </summary>
public string MoveMadeBy { get; set; }

/// <summary>
/// Gets or sets the image position. The position in the game 
board (0-8) where the player placed his 
/// image.
/// </summary>
public int ImagePosition { get; set; }
/// <summary>
/// Gets or sets the image. The image of the player that he 
placed in the board (0-8)
/// </summary>
public string Image { get; set; }
}

Finally, we will wire up the remaining methods in the GameHub class to complete the game coding. The MakeAMove method is called every time a player makes a move. Also, we have overidden the OnDisconnectedAsync method to inform a player when their opponent disconnects. In this method, we also keep our players and games list current. The comments in the code explain the workings of the methods:

 /// <summary>
 /// Invoked by the player to make a move on the board.
 /// </summary>
 /// <param name="position">The position to place 
     the player</param>
 public void MakeAMove(int position)
 {
        //// Lets find a game from our list of games where one of  
             the player has the same connection Id as the current 
             connection has.
            var game = games?.FirstOrDefault(x => 
            x.Player1.ConnectionId == Context.ConnectionId || 
            x.Player2.ConnectionId == Context.ConnectionId);
if (game == null || game.IsOver)
{
//// No such game exist!
return;
}

//// Designate 0 for player 1
int symbol = 0;
if (game.Player2.ConnectionId == Context.ConnectionId)
{
//// Designate 1 for player 2.
symbol = 1;
}

var player = symbol == 0 ? game.Player1 : game.Player2;

if (player.WaitingForMove)
{
return;
}
//// Update both the players that move is made.
Clients.Client(game.Player1.ConnectionId)
.InvokeAsync(Constants.MoveMade, new MoveInformation { 
OpponentName = player.Name, ImagePosition = position, 
Image = player.Image });
Clients.Client(game.Player2.ConnectionId)
.InvokeAsync(Constants.MoveMade, new MoveInformation { 
OpponentName = player.Name, ImagePosition = position, 
Image = player.Image });

//// Place the symbol and look for a winner after every 
move.
if (game.Play(symbol, position))
{
Remove<Game>(games, game);
Clients.Client(game.Player1.ConnectionId)
.InvokeAsync(Constants.GameOver, $"The winner is 
{player.Name}");
Clients.Client(game.Player2.ConnectionId)
.InvokeAsync(Constants.GameOver, $"The winner is 
{player.Name}");
player.IsPlaying = false;
player.Opponent.IsPlaying = false;
this.Clients.Client(player.ConnectionId)
.InvokeAsync(Constants.RegistrationComplete);
this.Clients.Client(player.Opponent.ConnectionId)
.InvokeAsync(Constants.RegistrationComplete);
}
//// If no one won and its a tame draw, update the 
players that the game is over and let them 
look for new game to play.
if (game.IsOver && game.IsDraw)
{
Remove<Game>(games, game);
Clients.Client(game.Player1.ConnectionId)
.InvokeAsync(Constants.GameOver, "Its a tame 
draw!!!");
Clients.Client(game.Player2.ConnectionId)
.InvokeAsync(Constants.GameOver, "Its a tame 
draw!!!");
player.IsPlaying = false;
player.Opponent.IsPlaying = false;
this.Clients.Client(player.ConnectionId)
.InvokeAsync(Constants.RegistrationComplete);
this.Clients.Client(player.Opponent.ConnectionId)
.InvokeAsync(Constants.RegistrationComplete);
}
if (!game.IsOver)
{
player.WaitingForMove = !player.WaitingForMove;
player.Opponent.WaitingForMove = 
!player.Opponent.WaitingForMove;
Clients.Client(player.Opponent.ConnectionId)
.InvokeAsync(Constants.WaitingForOpponent, 
player.Opponent.Name);
Clients.Client(player.ConnectionId)
.InvokeAsync(Constants.WaitingForOpponent, 
player.Opponent.Name);
}
}

With this, we are done with the coding of the game and are ready to run the game app. So there you have it! You’ve just built your first game in .NET Core! The detailed source code can be downloaded from Github.

If you’re interested in learning more, head on over to get the book, .NET Core 2.0 By Example, by Rishabh Verma and Neha Shrivastava.

Read next:

Applying Single Responsibility principle from SOLID in .NET Core

Unit Testing in .NET Core with Visual Studio 2017 for better code quality

Get to know ASP.NET Core Web API [Tutorial]

I'm a technology enthusiast who designs and creates learning content for IT professionals, in my role as a Category Manager at Packt. I also blog about what's trending in technology and IT. I'm a foodie, an adventure freak, a beard grower and a doggie lover.

1 COMMENT

LEAVE A REPLY

Please enter your comment!
Please enter your name here