Hands-On Lab Building a Social Game with Windows Azure Lab version: 1.0.0 Last updated: 3/22/2016 Page | 1 CONTENTS OVERVIEW ................................................................................................................................................... 3 EXERCISE 0: TIC-TAC-TOE SOLO GAME................................................................................................. 5 Task 1 – Exploring Tic-Tac-Toe Solution ................................................................................................ 5 Verification – Running the Solution ...................................................................................................... 6 EXERCISE 1: ADDING SOCIAL PROVIDERS AND INVITING FRIENDS ................................................. 8 Task 1 – Configuring ACS ....................................................................................................................... 9 Task 2 – Adding Social Gaming APIs .................................................................................................... 11 Task 3 – Creating the Login View ........................................................................................................ 18 Task 4 – Creating a Friends List View .................................................................................................. 23 Verification .......................................................................................................................................... 27 EXERCISE 2: ENABLE MULTIPLAYER WITH STORAGE POLLING ..................................................... 29 Task 1 – Implementing Multiplayer .................................................................................................... 29 Task 2 – Adding Worker Role to Enable Game Invitations ................................................................. 34 Task 3 – Showing Game Invitation Messages from Friends................................................................ 35 Verification .......................................................................................................................................... 36 EXERCISE 3: ENABLE MULTIPLAYER WITH NODE.JS ........................................................................ 40 Task 1 – Adding Node.JS Worker Role to Enable Multiplayer Games ................................................ 40 Task 2 –Implementing Multiplayer with Node.JS ............................................................................... 43 Task 3 – Showing Game Invitation Messages from Friends................................................................ 47 Verification .......................................................................................................................................... 49 EXERCISE 4: CREATING A LEADERBOARD ......................................................................................... 52 Task 1 – Creating the Statistics Repository Logic ................................................................................ 52 Task 2 – Adding Statistics Entries ........................................................................................................ 56 Task 3 – Creating a Leaderboard......................................................................................................... 59 Verification .......................................................................................................................................... 63 Page | 2 Overview Building a game has multiple challenges, more if we are building social games that require multiplayers, contacting your friends, leaderboards and multiple ways to authenticate a user. In this lab, you will see how to take advantage of the Windows Azure services benefits when building a Social Game. Windows Azure offers Access Control service to authenticate your users using multiple Social Providers, such as Windows Live Id or Facebook. You can take advantage of Azure Table Storage to create a Leaderboard or to use it as a game storage, where clients access to update the game status in their browsers. Additionally, you will see how to use Node.JS to emit and broadcast the players game moves to other clients in real time. Objectives In this hands-on lab, you will learn how to: Add ACS Support and Invite friends Enable Multi-Player using Storage Polling Enable Multi-Player using Node.Js Create a Leaderboard using Azure Table Storage Prerequisites The following is required to complete this hands-on lab: Microsoft .NET Framework 4.0 Microsoft Visual Studio 2010 Windows Azure SDK and Windows Azure Tools for Microsoft Visual Studio 1.6 ASP.NET and ASP.NET MVC 3 Microsoft® Windows Identity Foundation SDK 4.0 Microsoft® Windows Identity Foundation Runtime Note: This hands-on lab has been designed to use the latest release of the Windows Azure Tools for Visual Studio 2010 (version 1.6) and the new Windows Azure Management Portal experience. Page | 3 Setup In order to execute the exercises in this hands-on lab you need to set up your environment. 1. Open a Windows Explorer window and browse to the Source folder. 2. Double-click the Setup.cmd file in this folder to launch the setup process that will configure your environment and install the Visual Studio code snippets for this lab. 3. If the User Account Control dialog is shown, confirm the action to proceed. Note: Make sure you have checked all the dependencies for this lab before running the setup. Using the Code Snippets Throughout the lab document, you will be instructed to insert code blocks. For your convenience, most of that code is provided as Visual Studio Code Snippets, which you can use from within Visual Studio 2010 to avoid having to add it manually. If you are not familiar with the Visual Studio Code Snippets, and want to learn how to use them, you can refer to the Assets\Setup.docx document, which contains a section describing how to use them. Exercises This hands-on lab includes the following exercises: 1. Adding Social Providers and Inviting Friends 2. Enable Multiplayer with Storage Polling 3. Enable Multiplayer with Node.js 4. Creating a Leaderboard Estimated time to complete this lab: 90 minutes. Note: When you first start Visual Studio, you must select one of the predefined settings collections. Every predefined collection is designed to match a particular development style and determines window layouts, editor behavior, IntelliSense code snippets, and dialog box options. The procedures in this lab describe the actions necessary to accomplish a given task in Visual Studio when using the Page | 4 General Development Settings collection. If you choose a different settings collection for your development environment, there may be differences in these procedures that you need to take into account. Exercise 0: Tic-Tac-Toe Solo Game In this exercise, you will explore the single player Tic-Tac-Toe solution’s structure and the logic used for the game flow. Throughout this lab, based on this example, you will update the Tic-Tac-Toe solution to support social gaming features for inviting friends, using ACS and Social Providers to authenticate the players and enabling multiplayer mode. Finally, you will generate a leaderboard by using Azure Table Storage. Task 1 – Exploring Tic-Tac-Toe Solution In this task, you will explore the Tic-Tac-Toe sample to identify the core libraries that uses and how do they work. 1. Start Microsoft Visual Studio 2010 as administrator. 2. Open the Begin.sln solution located at Source\Ex0-SoloGame\Begin. 3. In the Solution Explorer, expand Scripts\Game\TicTacToe folder within TicTacToe.Web node to display the core libraries used in the sample. Figure 1 Page | 5 Game core libraries a. Controllers.js: This file handles the interaction between the view and the game logic. It includes methods like start, onMove, updateGameStatus, etc. b. ViewModel.js: This file defines the view model structure that will be used to bind the view with the controller. Note: We are using the Knockout JavaScript library for binding our sample’s UI with the controller logic. Knockout is a library used to simplify dynamic JavaScript UIs by applying the Model-View-View Model (MVVM) pattern. You can find more information about Knockout library in this link: http://knockoutjs.com/. c. Board.js: This file contains the necessary logic to render the Tic-Tac-Toe board into an HTML5 canvas. d. Game.js: This class file contains the game logic, which includes player’s turns, movements, existence of a winner, etc. 4. Open the Index.cshtml view located in the Views\TictacToe folder. This view renders the Tic-Tac-Toe game and interacts with the controller. Notice that this file only contains the canvas element and the reference to the JavaScript libraries. Verification – Running the Solution In this task, you will run the solution and play a single-player Tic-Tac-Toe game. Note: To designate the start page, in Solution Explorer, right-click the TicTacToe.Web project and select Properties. In the Properties window, select the Web tab and in the Start Action, select Specific Page. Set the value of this field empty. 1. In Visual Studio, press F5 to build and run the solution. 2. In the Social Gaming Sample Home page, click Tic Tac Toe in the menu to start a new singleplayer game. Page | 6 Figure 2 Running the Tic-Tac-Toe sample 3. Play a new Tic-Tac-Toe game to verify the game’s flow. Then, close the browser and return to Visual Studio. Page | 7 Figure 3 Playing Tic-Tac-Toe, single player mode Exercise 1: Adding Social Providers and Inviting Friends In this exercise, you will use Windows Azure Access Control Service (ACS) to enable authentication in the Tic-Tac-Toe game. The Access Control Service takes care of engaging every identity provider with its own authentication protocol, normalizing the authentication results in a protocol supported by the .NET framework tooling. In order to simplify the flow of this exercise, you will add the necessary configuration to access the existing public namespace ‘local-watgames’ ,which is used in the Windows Azure Toolkit for Social Games. Next, you will create a view to send invitations to other users and see the list of friends you added. Page | 8 Note: In this exercise you will add social gaming features, but the multiplayer mode will be added in the following exercises. Task 1 – Configuring ACS 1. Open the Begin.sln solution located in the folder \Source\Ex1-ACSAndInvite\Begin. You can alternatively continue working with the solution from Exercise 0. 2. Follow these steps to install the NuGet package dependencies. a. Open the NuGet Package Manager Console. To do this, select Tools | Library Package Manager | Package Manager Console. b. In the Package Manager Console, type Install-Package NuGetPowerTools. c. After installing the package, type Enable-PackageRestore. d. Build the solution. The NuGet dependencies will be downloaded and installed automatically. Note: One of the advantages of using NuGet is that you don’t have to ship all the libraries in your project, reducing the project size. With NuGet Power Tools, by specifying the package versions in the Packages.config file, you will be able to download all the required libraries the first time you run the project. This is why you will have to run these steps after you open an existing solution from this lab. For more information, see this article: http://docs.nuget.org/docs/workflows/usingnuget-without-committing-packages. 3. Right-click the TicTacToe.Web project and select Add Windows Azure Deployment Project. This will create a Windows Azure deployment project that will host the TicTacToe.Web in the Cloud. 4. Open the Web.config file from the TicTacToe.Web project. You will next add the necessary entries to configure ACS to use the public namespace local-watgames. a. At the top of the file, below the configuration root node, add the following code to include the Windows Identity Foundation configuration section. XML <configSections> <section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSectio n, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> </configSections> Page | 9 b. In the appSettings element, add the following new entries with key AcsNamespace, ApiUrl, and BlobUrl. XML <add key="AcsNamespace" value="local-watgames.accesscontrol.windows.net" /> <add key="ApiUrl" value="http://127.0.0.1:81/" /> <add key="BlobUrl" value="http://127.0.0.1:10000/devstoreaccount1/" /> c. Locate the system.web closing node and add the following code above the closing tag. XML <httpRuntime requestValidationType="Microsoft.Samples.SocialGames.Web.Security.WSFederat ionRequestValidator" /> <httpModules> <add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> <add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> </httpModules> </system.web> d. Inside the system.webServer node, replace the modules node with the following code: XML <modules runAllManagedModulesForAllRequests="true"> <add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" /> <add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" /> </modules> e. At the end of the file, above the closing configuration node, add the Identity Model and the System.Service.Model sections copying the following code. Page | 10 XML <microsoft.identityModel> <service> <audienceUris> <add value="http://127.0.0.1:81/" /> </audienceUris> <federatedAuthentication> <wsFederation passiveRedirectEnabled="false" requireHttps="false" issuer="https://placeholder/" realm="https://placeholder/" /> <cookieHandler requireSsl="false" /> </federatedAuthentication> <certificateValidation certificateValidationMode="None" /> <issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistr y, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"> <trustedIssuers> <add thumbprint="C89ECEF72C2A79809509E1E294DC68485E5042CF" name="https://local-watgames.accesscontrol.windows.net/" /> </trustedIssuers> </issuerNameRegistry> </service> </microsoft.identityModel> <system.serviceModel> <serviceHostingEnvironment aspNetCompatibilityEnabled="true" /> </system.serviceModel> f. Press CTRL+S to save the file and close it. 5. Create a folder in the root of the TicTacToe.Web project and name it Security. 6. Right-click the Security folder and add an existing item. Browse to the Source\Assets\Security folder of the lab, select WSFederationRequestValidator.cs, and click Add. Task 2 – Adding Social Gaming APIs In this task, you will add the social gaming server APIs to enable a number of features on your game. This Server APIs (or Web APIs) are wrappers of a library named SocialGames.Core, which contains all the necessary logic to invite other users to your list of friends, as well as other features you will use throughout this lab. This library is based on the same library that is included in the Windows Azure Toolkit for Social Games. Page | 11 Note: The APIs provided in the Assets folder of this lab are using the same strategy and architecture than the one used in the Windows Azure Toolkit for Social Games projects. For more information, go to http://watgames.codeplex.com/. 1. In the TicTacToe.Web.Azure cloud project, open Properties for the TicTacToe.Web role and add a new setting with Name DataConnectionString, set Type to Connection String and for the Value choose Windows Azure Storage Emulator. Save and close the Properties page. Figure 4 Adding a new setting 2. Copy the SocialGames.Core folder located in \Source\Assets\ in your solution folder. Figure 5 Copying SocialGames.Core project in the solution folder 3. You will add the Social Games core library to the Solution. Right-click the solution node, select Add and then Add Existing Project. Browse to the SocialGames.Core folder you have Page | 12 just copied and select the SocialGames.Core.csproj file in order to add the library to your solution. Note: The SocialGames.Core is composed of four main folders: Common, Entities, Helpers and Repositories. The main logic resides in the Repositories classes. Figure 6 SocialGames.Core project 4. In TicTacToe.Web project, add a reference to the recently added SocialGames.Core project. 5. Add two new folders at the root of the Web project and name them Services and Extensions respectively. 6. Right-click the Services folder, select Add and then Add Exiting Item. Browse to the Source\Assets\Services folder of this lab and select all the files in the folder. 7. Right-click the Extensions folder, select Add and then Add Exiting Item. Browse to the Source\Assets\Extensions folder of this lab and select all the files in the folder. 8. The Web APIs and the Extensions you added in the previous steps requires some dependencies that can be installed using the NuGet Package Manager. Right-click the TicTacToe.Web and select Manage NuGet Packages. 9. Select Online from the left panel and then NuGet official package source. In the Search box, enter Autofac. Click the Install button on both Autofac and Autofac ASP.NET MVC3 Integration packages. Page | 13 Figure 7 Installing NuGet Packages 10. In the Search box, type WebApi.All and install the package. After installing, click Close. 11. Add the following references to the TicTacToe.Web project: Microsoft.IdentityModel, Microsoft.WindowsAzure.ServiceRuntime and Microsoft.WindowsAzure.StorageClient. 12. Open Global.asax file and replace the namespace declarations with the following: (Code Snippet – Building a Social Game - Ex1 Using Namespaces – CS) C# using using using using using using using using using using using System.Collections.Generic; System.Configuration; System.Web.Mvc; System.Web.Routing; Autofac; Autofac.Integration.Mvc; Microsoft.ApplicationServer.Http; Microsoft.IdentityModel.Tokens; Microsoft.IdentityModel.Web; Microsoft.IdentityModel.Web.Configuration; Microsoft.Samples.SocialGames; Page | 14 using using using using using using using Microsoft.Samples.SocialGames.Common.Storage; Microsoft.Samples.SocialGames.Entities; Microsoft.Samples.SocialGames.Extensions; Microsoft.Samples.SocialGames.Repositories; Microsoft.Samples.SocialGames.Web.Services; Microsoft.WindowsAzure; Microsoft.WindowsAzure.ServiceRuntime; 13. Now that you have the Web APIs in the project, we need to register the MVC routes to point to their address. Locate and replace the RegisterRoutes method with the following: (Code Snippet – Building a Social Game - Ex1 Registering Routes – CS) C# public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapServiceRoute<GameService>("game"); routes.MapServiceRoute<AuthService>("auth"); routes.MapServiceRoute<UserService>("user"); routes.MapServiceRoute<EventService>("event"); routes.MapRoute( "Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }); } 14. In the Application_Start method, you will setup the WCF Web Api configuration and register the core types in a Dependency Injection container using Autofac. To do this, replace the Application_Start method with the following code: Note: For more information about Autofac go to http://code.google.com/p/autofac/. (Code Snippet – Building a Social Game - Ex1 Application Start – CS) C# protected void Application_Start() { CloudStorageAccount.SetConfigurationSettingPublisher((configName, configSetter) => { string configuration = RoleEnvironment.IsAvailable ? Page | 15 RoleEnvironment.GetConfigurationSettingValue(configName) : ConfigurationManager.AppSettings[configName]; configSetter(configuration); }); // Setup AutoFac var builder = new ContainerBuilder(); this.DependencySetup(builder); var container = builder.Build(); DependencyResolver.SetResolver(new AutofacDependencyResolver(container)); // Setup WCF Web API Config var config = new WebApiConfiguration(); config.EnableTestClient = true; config.CreateInstance = (t, i, h) => DependencyResolver.Current.GetService(t); RouteTable.Routes.SetDefaultHttpConfiguration(config); AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); FederatedAuthentication.ServiceConfigurationCreated += this.OnServiceConfigurationCreated; // Call Initializers var initializers = DependencyResolver.Current.GetServices<IStorageInitializer>(); foreach (var initializer in initializers) { initializer.Initialize(); } } 15. Add the following methods at the end of the file. (Code Snippet – Building a Social Game - Ex1 DependencySetup – CS) C# protected void DependencySetup(ContainerBuilder builder) { // Cloud Storage Account builder.RegisterInstance<CloudStorageAccount>(CloudStorageAccount.FromConfiguratio nSetting("DataConnectionString")); Page | 16 // Queues builder.RegisterQueue<GameActionStatisticsMessage>(ConfigurationConstants.GameActi onStatisticsQueue) .AsImplementedInterfaces(); builder.RegisterQueue<GameActionNotificationMessage>(ConfigurationConstants.GameAc tionNotificationsQueue) .AsImplementedInterfaces(); builder.RegisterQueue<InviteMessage>(ConfigurationConstants.InvitesQueue) .AsImplementedInterfaces(); // Blobs builder.RegisterBlob<UserProfile>(ConfigurationConstants.UsersContainerName, true /* jsonpSupport */) .AsImplementedInterfaces(); builder.RegisterBlob<UserSession>(ConfigurationConstants.UserSessionsContainerName , true /* jsonpSupport */) .AsImplementedInterfaces(); builder.RegisterBlob<Friends>(ConfigurationConstants.FriendsContainerName, true /* jsonpSupport */) .AsImplementedInterfaces(); builder.RegisterBlob<NotificationStatus>(ConfigurationConstants.NotificationsConta inerName, true /* jsonpSupport */) .AsImplementedInterfaces(); builder.RegisterBlob<Game>(ConfigurationConstants.GamesContainerName, true /* jsonpSupport */) .AsImplementedInterfaces(); builder.RegisterBlob<GameQueue>(ConfigurationConstants.GamesQueuesContainerName, true /* jsonpSupport */) .AsImplementedInterfaces(); builder.RegisterBlob<UserProfile>(ConfigurationConstants.GamesContainerName, true /* jsonpSupport */) .AsImplementedInterfaces(); // Tables builder.RegisterTable<UserStats>(ConfigurationConstants.UserStatsTableName, true /* jsonpSupport */) .AsImplementedInterfaces(); // Repositories builder.RegisterType<GameActionNotificationQueue>().AsImplementedInterfaces(); builder.RegisterType<GameActionStatisticsQueue>().AsImplementedInterfaces(); builder.RegisterType<GameRepository>().AsImplementedInterfaces(); builder.RegisterType<IdentityProviderRepository>().AsImplementedInterfaces(); builder.RegisterType<NotificationRepository>().AsImplementedInterfaces(); Page | 17 builder.RegisterType<UserRepository>().AsImplementedInterfaces(); builder.RegisterType<StatisticsRepository>().AsImplementedInterfaces(); // Controllers builder.RegisterControllers(typeof(MvcApplication).Assembly); // Services builder.RegisterType<AuthService>().AsImplementedInterfaces().AsSelf(); builder.RegisterType<EventService>().AsImplementedInterfaces().AsSelf(); builder.RegisterType<GameService>().AsImplementedInterfaces().AsSelf(); builder.RegisterType<HttpContextUserProvider>().AsImplementedInterfaces().AsSelf() ; builder.RegisterType<UserService>().AsImplementedInterfaces().AsSelf(); } private void OnServiceConfigurationCreated(object sender, ServiceConfigurationCreatedEventArgs e) { var sessionTransforms = new List<CookieTransform>(new CookieTransform[] { new DeflateCookieTransform() }); var sessionHandler = new SessionSecurityTokenHandler(sessionTransforms.AsReadOnly()); e.ServiceConfiguration.SecurityTokenHandlers.AddOrReplace(sessionHandler); } Task 3 – Creating the Login View In this task, you will create the Login View that lists the Social Providers from Access Control. 1. Open TicTacToeController and add the Authorize decorator to the Index method. If an unauthorized user tries to access the Tic Tac Toe view, the application will redirect the user to the LogOn view. C# [Authorize] public ActionResult Index() { return View(); } 2. In the Controllers folder, add a new controller with the name BaseController. Page | 18 3. The BaseController reads the values of the addresses for the Blob and API URLs and exposes to the View using the ViewBag. To do this, replace the class code with the following: (Code Snippet – Building a Social Game - Ex1 BaseController – CS) C# using System.Web.Mvc; namespace TicTacToe.Web.Controllers { public class BaseController : Controller { protected override void OnActionExecuting(ActionExecutingContext filterContext) { this.ViewBag.BlobUrl = System.Configuration.ConfigurationManager.AppSettings["BlobUrl"]; this.ViewBag.ApiUrl = System.Configuration.ConfigurationManager.AppSettings["ApiUrl"]; base.OnActionExecuting(filterContext); } } } 4. Create a new controller and name it AccountController. 5. Inherit AccountController from the BaseController you created in previous steps. C# public class AccountController : BaseController { ... { 6. Add the following using statements in the AccountController. (Code Snippet – Building a Social Game - Ex1 BaseController Using– CS) C# using using using using using using using System.Threading; Microsoft.IdentityModel.Claims; Microsoft.IdentityModel.Protocols.WSFederation; Microsoft.Samples.SocialGames.Repositories; Microsoft.Samples.SocialGames.Web.Services; Microsoft.Samples.SocialGames.Entities; Microsoft.Samples.SocialGames; Page | 19 7. The AccountController is responsible of authenticating and reading Claims from the Identity Provider. The user Id is then saved (or updated if already exists) into a Table Storage to be used in the Game and in the Friends list. Implement the following methods to authenticate a user. (Code Snippet – Building a Social Game - Ex1 Implementing AccountController – CS) C# private readonly IUserRepository userRepository; private IUserProvider userProvider; public AccountController(IUserRepository userRepository, IUserProvider userProvider) { this.userRepository = userRepository; this.userProvider = userProvider; } public ActionResult LogOn(string returnUrl) { return View(); } [HttpPost] public ActionResult LogOn() { if (this.Request.IsAuthenticated) { // Ensure user profile var userId = this.GetClaimValue(ClaimTypes.NameIdentifier); var userProfile = this.userRepository.GetUser(userId); if (userProfile == null) { var loginType = this.GetClaimValue(ConfigurationConstants.IdentityProviderClaimType); userProfile = new UserProfile { Id = userId, DisplayName = Thread.CurrentPrincipal.Identity.Name ?? string.Empty, LoginType = loginType, AssociatedUserAccount = loginType.StartsWith("Facebook", StringComparison.OrdinalIgnoreCase) ? this.GetClaimValue(ConfigurationConstants.FacebookAccessTokenClaimType) : string.Empty Page | 20 }; this.userRepository.AddOrUpdateUser(userProfile); } var effectiveReturnUrl = this.GetContextFromRequest(); if (!string.IsNullOrWhiteSpace(effectiveReturnUrl)) { return Redirect(effectiveReturnUrl); } } return Redirect("~/"); } private string GetContextFromRequest() { Uri requestBaseUrl = WSFederationMessage.GetBaseUrl(this.Request.Url); var message = WSFederationMessage.CreateFromNameValueCollection(requestBaseUrl, this.Request.Form); return message != null ? message.Context : string.Empty; } private string GetClaimValue(string claimType) { if (!Thread.CurrentPrincipal.Identity.IsAuthenticated) { throw new InvalidOperationException("User is not authenticated"); } var claimsIdentity = (IClaimsIdentity)Thread.CurrentPrincipal.Identity; if (!claimsIdentity.Claims.Any(c => c.ClaimType.Equals(claimType, StringComparison.OrdinalIgnoreCase))) { throw new InvalidOperationException("Claim not found: " + claimType); } return claimsIdentity.Claims.FirstOrDefault(c => c.ClaimType.Equals(claimType, StringComparison.OrdinalIgnoreCase)).Value; } 8. In the AccountController, right-click LogOn action method and select Add View. Name it LogOn and leave the default values for the remaining fields. Page | 21 9. Replace the entire code of LogOn view with the following code. CSHTML @{ ViewBag.Title = "Login"; } <h2>Login</h2> <p> Login using any of the following identity providers: </p> <div class="status"> </div> <table id="loginsTable"> </table> <script type="text/javascript"> function getQueryVariable(variable) { var query = window.location.search.substring(1); var vars = query.split("&"); for (var i = 0; i < vars.length; i++) { var pair = vars[i].split("="); if (pair[0] == variable) { return pair[1]; } } } jQuery(document).ready(function () { var apiURL = "@this.ViewBag.ApiUrl"; $(".status").text("Loading..."); var returnUrl = getQueryVariable("ReturnUrl"); if (returnUrl == null || returnUrl == undefined) returnUrl = ""; // Get login info $.ajax({ type: "GET", url: apiURL + "auth/loginselector?returnUrl=" + returnUrl, dataType: "json", success: function (result) { createLoginButtons(result); }, Page | 22 error: function (req, status, error) { alert(error); } }); }); // Create the login button for each identity provider function createLoginButtons(loginInfoList) { var loginsTable = $('#loginsTable'); loginsTable.empty(); $(".status").text(""); $.each(loginInfoList, function (i, loginInfo) { var loginButton = $('<input type="button">'); loginButton.attr("value", loginInfo.Name); loginButton.click(function () { window.location.href = loginInfo.LoginUrl; }); var loginsTableTd = $('<td>'); loginsTableTd.append(loginButton); var loginsTableTr = $('<tr>'); loginsTableTr.append(loginsTableTd); loginsTable.append(loginsTableTr); }); } </script> The View uses JQuery to consume the Web API AuthService to retrieve the list of available providers in the ACS namespace. Once the view has the list, one button is generated for each provider to allow users select any of the available Identity providers. Task 4 – Creating a Friends List View In this task, you will create a Friends list consuming the server APIs to invite and retrieve the user’s friends. 1. In order to interact with the Web APIs on client-side, you need to send a JSON request to the Web API Url. To do this, you will use two JavaScript files that encapsulate the calls to the APIs. Open the Game folder inside Scripts and add UserService.js and ServerInterface.js files from Assets\Scripts folder of this lab. Page | 23 2. In the TicTacToe.Web project, add a new View named Friends under the Account folder. 3. Replace the entire code with the following: CSHTML @{ ViewBag.Title = "Your Friends"; Layout = "~/Views/Shared/_Layout.cshtml"; } <script src="@Url.Content("~/Scripts/jQuery.tmpl.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/knockout-1.2.1.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/Game/ServerInterface.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/Game/UserService.js")" type="text/javascript"></script> <h2>Your Friends</h2> <p>Use this URL to invite other users: <span data-bind="text: inviteURL()" /> </p> <h3>Friends</h3> <fieldset> <div data-bind='template: { name: "FriendList", foreach: friends() }' /> </fieldset> <script id="FriendList" type="text/html"> ${$data.DisplayName} <br> </script> 4. Add the following code at the bottom of the View. This code interacts with the UserService.js to perform JSON requests to the Web APIs. In this case, you will request for the Friends list by calling the UserService Web API. CSHTML <script var var var var type="text/javascript"> userId = "@this.ViewBag.CurrentUserId"; BlobUrl = "@this.ViewBag.BlobUrl"; si = new ServerInterface(); user = new UserService(userId, BlobUrl, si); function getFriends() { user.getFriendsInfo(function (friends) { window.viewModel.refreshFriends(friends); Page | 24 }, function (req, status, error) { var errorMessage; if (req.responseText == undefined) { errorMessage = "POST Error:\nreq:" + req + "\nstatus:" + status + "\nerror:" + error; } else { errorMessage = "POST Error:\nreq:" + req.responseText + "\nstatus:" + status + "\nerror:" + error; } alert(errorMessage); }); } </script> <script type="text/javascript"> function ViewModel() { this.user = ko.observable(userId); this.friends = ko.observableArray(); this.notifications = ko.observableArray(); this.inviteURL = ko.observable(document.location.href + "/?id=" + encodeURIComponent(userId)); } ViewModel.prototype.refreshFriends = function (friends) { for (n in friends) this.friends.push(friends[n]); } jQuery(document).ready(function () { window.viewModel = new ViewModel(); ko.applyBindings(window.viewModel); user.getFriendsInfo( function (friends) { window.viewModel.refreshFriends(friends); }, function () { alert('Error Get Friends'); }); }); </script> 5. Open GameService.cs located in the Services folder. Replace the contents of the method Invite with the following code: (Code Snippet – Building a Social Game - Ex1 Invite Method – CS) C# [Authorize] public HttpResponseMessage Invite(Guid gameQueueId, HttpRequestMessage request) { Page | 25 dynamic formContent = request.Content.ReadAsAsync<JsonValue>().Result; var users = formContent.users != null ? ((JsonArray)formContent.users).ToObjectArray().Select(o => o.ToString()).ToList() : null; string message = formContent.message != null ? formContent.message.Value : null; string url = formContent.url != null ? formContent.url.Value : null; this.gameRepository.Invite(this.CurrentUserId, gameQueueId, message, url, users); return SuccessResponse; } 6. Open the _Layout.cshtml View located in the Shared folder under Views. 7. Add a new menu item that points to the recently created Friends view. CSHTML <nav> <ul id="menu"> <li>@Html.ActionLink("Home", "Index", "Home", new { area = "" }, null)</li> <li>@Html.ActionLink("Tic Tac Toe", "Index", "TicTacToe", new { area = "" }, null)</li> <li>@Html.ActionLink("Friends", "Friends", "Account", new { area = "" }, null)</li> </ul> </nav> 8. Now you will add a method in the AccountController to retrieve the friends to the view. The Friends View provides you with an invitation URL, which is no more than the same Url with a user Id as a parameter. If the Id is provided, the Friend relationship is saved in an Azure blob container and it will appear in your friends list. (Code Snippet – Building a Social Game - Ex1 AccountController Friends – CS) C# [Authorize] public ActionResult Friends(string id) { if (!string.IsNullOrEmpty(id)) { string currentUserId = this.userProvider.UserId; Page | 26 string inviteUserId = id; this.userRepository.AddFriend(currentUserId, inviteUserId); this.userRepository.AddFriend(inviteUserId, currentUserId); return RedirectToAction("Index", "Home"); } this.ViewBag.CurrentUserId = this.userProvider.UserId; return View(); } 9. Press CTRL+SHIFT+B to build the solution. Verification Note: Before you execute the solution, make sure that the start-up project and the start-up page are set. To set the startup project, in Solution Explorer, right-click the TicTacToe.Web.Azure project and select Set as StartUp Project. To designate the start page, in Solution Explorer, right-click the TicTacToe.Web project and select Properties. In the Properties window, select the Web tab and in the Start Action, select Specific Page. Set the value of this field empty. 1. Press to F5 to run the solution. Figure 8 Tic-Tac-Toe Home Page Page | 27 2. In the Home page menu, click Tic Tac Toe. The browser will redirect you to the LogOn view. 3. Select Windows Live Id as the identity provider. Figure 9 Selecting an identity provider 4. You will be redirected to the Windows Live Id login page. Enter your credentials and log on. The page will redirect you back to the Tic-Tac-Toe game. 5. Now that you are authenticated, go to the Friends page by clicking Friends link in the menu. 6. As this is the first time you run the application with the Web APIs, the Friends list will be empty. Copy the Invitation URL. Figure 10 Copying the Invitation URL Page | 28 7. Open a new session in Internet Explorer or an In-Private session by pressing CTRL+SHIFT+P. 8. Paste the invitation URL in the browse and press enter. In the LogOn view, log on with a different account. 9. Go to the Friends page. In the Friends list, you will see the user id of your first account. Figure 11 Showing the Friend’s user id 10. Go back to the first browser session, and refresh the Friends page. You will see the user id of the second account. Exercise 2: Enable Multiplayer with Storage Polling In this exercise, you will enable multiplayer for the Tic-Tac-Toe using the Storage Polling approach. This consists in querying a Blob container for game updates and rendering the result in the board. Additionally, you will learn how to invite another user to play a Tic-Tac-Toe game. Task 1 – Implementing Multiplayer In this task, you will add multiplayer features to the Tic Tac Toe view. To do this, you will update the client-side scripts and modify the view to interact with the game service. 1. Open the Begin.sln solution located in the folder \Source\Ex2-AMultiplayerPolling\Begin. You can alternatively continue working with the solution obtained by completing Exercise 1. Page | 29 2. If you continued working with the solution obtained by completing Exercise 1, skip this step. Otherwise, follow these steps to install the NuGet package dependencies. a. Open the NuGet Package Manager Console. To do this, select Tools | Library Package Manager | Package Manager Console. b. In the Package Manager Console, type Install-Package NuGetPowerTools. c. After installing the package, type Enable-PackageRestore. d. Build the solution. The NuGet dependencies will be downloaded and installed automatically. Note: One of the advantages of using NuGet is that you don’t have to ship all the libraries in your project, reducing the project size. With NuGet Power Tools, by specifying the package versions in the Packages.config file, you will be able to download all the required libraries the first time you run the project. This is why you will have to run these steps after you open an existing solution from this lab. For more information see this article: http://docs.nuget.org/docs/workflows/usingnuget-without-committing-packages. 3. Open the TicTacToeController class located in the Controllers folder and inherit it from BaseController. C# public class TicTacToeController : BaseController { ... } 4. In the TicTacToe.Web project, add the GameService.js file located in the Assets\Scripts folder of this lab, to the Scripts\Game folder. 5. Expand the TicTacToe folder under Scripts\Game, and add the files Controller.js and ViewModel.js located in the Assets\Scripts folder of this lab. When asked, confirm to replace the old files. 6. Open the Index.cshtml view in the Views\TicTacToe folder of the Web project. 7. Add the following references to the core client-side JavaScript files. CSHTML <script src="@Url.Content("~/Scripts/jQuery.tmpl.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/knockout-1.2.1.js")" type="text/javascript"></script> Page | 30 <script src="@Url.Content("~/Scripts/Game/ServerInterface.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/Game/GameService.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/Game/UserService.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/Game/TicTacToe/Board.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/Game/TicTacToe/Game.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/Game/TicTacToe/ViewModel.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/Game/TicTacToe/Controller.js")" type="text/javascript"></script> 8. Add the following code at the beginning of the div element with Id game. This code renders the Invitation URL and the list of players that are in the queue waiting to play. CSHTML <div style="display: none" data-bind="visible: gameQueueId() != null"> <fieldset> <legend>Player</legend> <div> Welcome <b><span data-bind='text: playerName()'></span></b> </div> <div data-bind="visible: isOwner() && gameQueueId() != null && gameId() == null"> <span>Use this URL to invite other players: <span data-bind="text: inviteURL()" /> </span> <br /> <div data-bind="visible: friends() != null && friends().length > 0"> Your Friends <select id="friends" data-bind="options: friends, optionsValue: 'Id', optionsText: 'DisplayName'"> </select> <a href="#" data-bind="click: inviteFriend">Invite Friend</a> </div> </div> <br /> <div data-bind="visible: noPlayers() == 1 && isOwner()"> <span>Waiting for your opponent...</span> </div> </fieldset> </div> <div style="display: none" data-bind="visible: gameQueueId() != null"> Page | 31 <fieldset> <legend>Players</legend> <div data-bind='template: { name: "queueStatus", foreach: players()}' /> <script id="queueStatus" type="text/html"> <li><b>${$data.UserName}</b></li> </script> </fieldset> </div> 9. Locate the div which contains the legend Game, and replace it with the following: CSHTML <div style="display: none" data-bind="visible: gameId() != null"> <fieldset> <legend>Game</legend> <div> You are <span data-bind="visible: playerColor() == TTTColor.Cross">Xs</span> <span data-bind="visible: playerColor() == TTTColor.Circle">Os</span> </div> <div data-bind="visible: playerColor() == currentColor()"> Your turn!</div> <div data-bind="visible: playerColor() != currentColor()"> Opponent turn!</div> <div data-bind="visible: isTie()"> Tie!!</div> <div data-bind="visible: playerColor() == winnerColor()"> You won!!</div> <div data-bind="visible: playerColor() != winnerColor() && winnerColor() != TTTColor.Empty"> You lose!!</div> <canvas id="board" width="300" height="300"></canvas> </fieldset> </div> 10. Locate the script block and insert the following code at the beginning. javascript var viewModel; var gameQueueId = getQueryVariable("id"); var nullGameId = "00000000-0000-0000-0000-000000000000"; function getQueryVariable(variable) { var query = window.location.search.substring(1); Page | 32 var vars = query.split("&"); for (var i = 0; i < vars.length; i++) { var pair = vars[i].split("="); if (pair[0] == variable) { return pair[1]; } } } var apiURL = "@this.ViewBag.ApiUrl"; var blobURL = "@this.ViewBag.BlobUrl"; var si = new ServerInterface(); var gs = new GameService(apiURL, blobURL, si); var user = new UserService(apiURL, blobURL, si); 11. Next to the previously inserted code, add the following: javascript function start(userId) { // check for canvas, show an "Upgrade your browser" screen if they don't have it. var canvas = document.getElementById('board'); if (canvas.getContext == null || canvas.getContext('2d') == null) { $("#game").toggle(); $("#notSupported").toggle(); return; } var board = new TicTacToeBoard(canvas); var game = new TicTacToeGame(); var controller = new TicTacToeController(viewModel, gs, board, game); controller.setGameQueueId(gameQueueId); controller.start(); user.getFriendsInfo(function (friends) { viewModel.friends(friends); }); user.getUser(userId, function () { }); window.onbeforeunload = function () { controller.finish(); }; } 12. Replace the body of the $(function ()) method with the following. Page | 33 javascript $(function () { viewModel = new TicTacToeViewModel(); ko.applyBindings(viewModel); user.verify( function (userId) { start(userId); }, function () { var newlocation = '@Url.Action("LogOn", "Account", new { Area = "", ReturnUrl = "replace-in-js" })'; newlocation = newlocation.replace("replace-in-js", encodeURIComponent(window.location.href)); window.location.assign(newlocation); } ); }); 13. Add the following two methods to the script block. javascript function inviteFriend() { var userId = $("#friends").val(); var userName = $("#friends :selected").text(); gs.inviteUser(viewModel.gameQueueId(), userId, "Invitation for Tic Tac Toe", viewModel.inviteURL(), function () { alert(userName + " was invited") }); } function sgusersCallback(user) { if (user.DisplayName != null && user.DisplayName != "") viewModel.playerName(user.DisplayName); else viewModel.playerName(user.Id); } Task 2 – Adding Worker Role to Enable Game Invitations In this task, you will add an existing project that will be in charge of processing invitation requests. 1. Copy the folder \Source\Assets\SocialGames.Worker to the root of your project folder. Page | 34 Figure 12 Copying the worker to the project folder 2. Add the project SocialGames.Worker.csproj located in the folder you copied (\SocialGames.Worker\) to the solution. 3. In the TicTacToe.Web.Azure cloud project, associate the recently added project as a Worker Role in the Roles folder. 4. Open the role Properties for the SocialGames.Worker and add a new setting with Name DataConnectionString, set the Type to Connection String and for Value choose Windows Azure Storage Emulator. Save and close the Properties page. Task 3 – Showing Game Invitation Messages from Friends In this task, you will update the Friends view to show the invitation messages from other users. 1. Open the Friends view located in Views\Account folder of the Web project. 2. Add the Invitations table below the script block with id FriendList. javascript <h3>Invitations</h3> <table> <thead> <tr> <th>Message</th> <th>From</th> <th>Action</th> </tr> </thead> <tbody data-bind='template: { name: "NotificationList", foreach: notifications() }' /> </table> Page | 35 <script id="NotificationList" type="text/html"> <tr> <td>${$data.Message} </td> <td>${$data.SenderName} </td> <td><a href="${$data.Url}">Go</a> </td> </tr> </script> 3. Add a call to the user.getNotifications method at the end of jQuery ready function within the script block in order to retrieve the game invitation messages. javascript jQuery(document).ready(function () { window.viewModel = new ViewModel(); ko.applyBindings(window.viewModel); user.getFriendsInfo( function (friends) { window.viewModel.refreshFriends(friends); }, function () { alert('Error Get Friends'); }); user.getNotifications(userId, function () { }); }); 4. Add a callback method at the end of the block script. This method is used by the UserService to return the invitation messages. javascript function sgnotificationsCallback(data) { for (var n in data.Notifications) { var notification = data.Notifications[n]; if (notification.SenderName == null || notification.SenderName == "") notification.SenderName = notification.SenderId; if (notification.Type == "Invite") viewModel.notifications.push(notification); } } Verification Page | 36 Note: Before you execute the solution, make sure that the start-up project and the start-up page are set. To set the startup project, in Solution Explorer, right-click the TicTacToe.Web.Azure project and select Set as StartUp Project. To designate the start page, in Solution Explorer, right-click the TicTacToe.Web project and select Properties. In the Properties window, select the Web tab and in the Start Action, select Specific Page. Set the value of this field empty. 1. Press to F5 to run the solution. 2. Click the Tic Tac Toe menu link and log on with an identity provider. You will see the current list of Players that are in the same game queue. Additionally, an invitation URL will be generated. Copy this URL. Figure 13 Listing players and the auto generated invitation URL 3. Open a new session in Internet Explorer or an In-Private session by pressing CTRL+SHIFT+P without closing the current one. 4. Paste the invitation URL. When prompted, enter other credentials to log on. Now, the list of players will show both users. Check the first browser to verify that both users are listed as well. Page | 37 Figure 14 Playing a multiplayer Tic-Tac-Toe 5. Start playing the game. You will see how the browsers are updated after each move. 6. Restart the game by clicking the Tic Tac Toe menu link in the first browser. You will see a new drop-down list that shows your friends. Page | 38 Figure 15 Listing friends 7. Click the Invite Friend link. A confirmation message will appear. Click OK. Figure 16 Invite a friend confirmation 8. In the second browser, click the Friends link. The Invitations table will show a new message from the other user. The message has a link that takes you to the Tic Tac Toe page with the game queue Id. Click the Go link. Page | 39 Figure 17 Invitation Message 9. The link takes you to the Tic Tac Toe page and a new game will be started. 10. Close the browsers. Exercise 3: Enable Multiplayer with Node.Js In this exercise, you will enable multiplayer for the Tic-Tac-Toe game using a different approach than the one from Exercise 2. Instead of querying the storage for updates, you will start a Node.JS Worker Role node that enables communication between Web clients by broadcasting incoming messages between connected clients. To do this, Node.JS uses a module named Socket.IO that opens Web Sockets between the client browser and the Node server. This way, the client browser emits a message to the Server, which is then broadcasted to other clients (in this case, the other player’s browser). To enable multiplayer, Node.JS and Socket.IO allows you to send the game’s actions to the other player in real-time, without needing to poll the game status. Task 1 – Adding Node.JS Worker Role to Enable Multiplayer Games Page | 40 In this task, you will add an existing project that includes a Node.JS Worker Role. 1. Open the Begin.sln solution located in the folder \Source\Ex3-UsingNodeJs\Begin. You can alternatively continue working with the solution obtained by completing Exercise 1. 2. If you opened the Begin.sln solution, follow these steps to install the NuGet package dependencies. a. Open the NuGet Package Manager Console. To do this, select Tools | Library Package Manager | Package Manager Console. b. In the Package Manager Console, type Install-Package NuGetPowerTools. c. After installing the package, type Enable-PackageRestore. d. Compile the solution. The NuGet dependencies will be downloaded and installed automatically. Note: One of the advantages of using NuGet is that you don’t have to ship all the libraries in your project, reducing the project size. With NuGet Power Tools, by specifying the package versions in the Packages.config file, you will be able to download all the required libraries the first time you run the project. This is why you will have to run these steps after you open an existing solution from this lab. For more information see this article: http://docs.nuget.org/docs/workflows/usingnuget-without-committing-packages. 3. Copy the folder Source\Assets\SocialGames.WorkerNodeJs to the root of your project. Figure 18 Copying the WorkerNodeJs project Page | 41 4. Add the project SocialGames.WorkerNodeJs.csproj located in the folder you have recently added to the Solution. 5. In the TicTacToe.Web.Azure cloud project, associate the recently added project as a Worker Role in the Roles folder. 6. Open the role Properties for the SocialGames.Worker and add a new setting with Name DataConnectionString, set the Type Connection String and for Value choose Windows Azure Storage Emulator. Save and close the Properties page. 7. Open the ServiceDefinition.csdef file and replace the WorkerRole block with the following: XML <WorkerRole name="SocialGames.Worker" vmsize="Small"> <Imports> <Import moduleName="Diagnostics" /> </Imports> <Startup> <Task commandLine="installSocketIO.cmd" executionContext="elevated" taskType="simple" /> </Startup> <Endpoints> <InputEndpoint name="HttpIn" protocol="tcp" port="8080" /> </Endpoints> <Runtime> <Environment> <Variable name="PORT"> <RoleInstanceValue xpath="/RoleEnvironment/CurrentInstance/Endpoints/Endpoint[@name='HttpIn']/@port" /> </Variable> <Variable name="EMULATED"> <RoleInstanceValue xpath="/RoleEnvironment/Deployment/@emulated" /> </Variable> </Environment> </Runtime> <ConfigurationSettings> <Setting name="DataConnectionString" /> </ConfigurationSettings> </WorkerRole> Note: The Node.JS Worker Role requires a Startup task in order to install the required module Socket.IO. The script automatically download and install the NPM in the application root of Node.JS. Page | 42 8. In the TicTacToe.Web project, add the GameService.js file located in the Assets\NodeJS folder of this lab, to the Scripts\Game folder. Additionally, add the socket.io.js to the root of the Scripts folder of the same project. 9. Expand the TicTacToe folder under Scripts\Game, and add the files Controller.js and ViewModel.js located in the Assets\NodeJS folder of this lab. When asked, confirm to replace the old files. 10. Open Web.config located at the root of the project and add a new setting under the appSettings node. Set its name to NodeJsUrl and its value to http://127.0.0.1:8080/. xml <add key="ApiUrl" value="http://127.0.0.1:81/" /> <add key="BlobUrl" value="http://127.0.0.1:10000/devstoreaccount1/" /> <add key="NodeJsUrl" value="http://127.0.0.1:8080/"/> Task 2 –Implementing Multiplayer with Node.JS In this task, you will update the client-side scripts to be able to play a game with two client browsers. 1. First, replace the TicTacToeController class located in the Controllers folder, to inherit from the BaseController and send the API URLs using the ViewBag. C# public class TicTacToeController : BaseController { [Authorize] public ActionResult Index() { this.SetConfigurationData(); return View(); } private void SetConfigurationData() { this.ViewBag.BlobUrl = System.Configuration.ConfigurationManager.AppSettings["BlobUrl"]; this.ViewBag.ApiUrl = System.Configuration.ConfigurationManager.AppSettings["ApiUrl"]; this.ViewBag.NodeJsUrl = System.Configuration.ConfigurationManager.AppSettings["NodeJsUrl"]; } } 2. Open the Index.cshtml view in the Views\TicTacToe folder of the Web project. Page | 43 3. Add the following references to the core client-side javascript files. CSHTML <script src="@Url.Content("~/Scripts/jQuery.tmpl.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/knockout-1.2.1.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/socket.io.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/Game/ServerInterface.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/Game/GameService.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/Game/UserService.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/Game/TicTacToe/Board.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/Game/TicTacToe/Game.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/Game/TicTacToe/ViewModel.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/Game/TicTacToe/Controller.js")" type="text/javascript"></script> 4. Add the following code at the beginning of the div element with Id game. This code renders the Invitation URL and the list of players that are in the queue waiting to play. CSHTML <div style="display: none" data-bind="visible: gameQueueId() != null"> <fieldset> <legend>Player</legend> <div> Welcome <b><span data-bind='text: playerName()'></span></b> </div> <div data-bind="visible: isOwner() && gameQueueId() != null && gameId() == null"> <span>Use this URL to invite other players: <span data-bind="text: inviteURL()" /> </span> <br /> <div data-bind="visible: friends() != null && friends().length > 0"> Your Friends <select id="friends" data-bind="options: friends, optionsValue: 'Id', optionsText: 'DisplayName'"> </select> <a href="#" data-bind="click: inviteFriend">Invite Friend</a> Page | 44 </div> </div> <br /> <div data-bind="visible: noPlayers() == 1 && isOwner()"> <span>Waiting for your opponent...</span> </div> </fieldset> </div> <div style="display: none" data-bind="visible: gameQueueId() != null"> <fieldset> <legend>Players</legend> <div data-bind='template: { name: "queueStatus", foreach: players()}' /> <script id="queueStatus" type="text/html"> <li><b>${$data.UserName}</b></li> </script> </fieldset> </div> 5. Locate the div that contains the legend Game, and add the following attributes. CSHTML <div style="display: none" data-bind="visible: gameId() != null"> <fieldset> <legend>Game</legend> ... </fieldset> </div> 6. Replace the script block with the following code. javascript <script var var var type="text/javascript"> viewModel; gameQueueId = getQueryVariable("id"); nullGameId = "00000000-0000-0000-0000-000000000000"; function getQueryVariable(variable) { var query = window.location.search.substring(1); var vars = query.split("&"); for (var i = 0; i < vars.length; i++) { var pair = vars[i].split("="); if (pair[0] == variable) { return pair[1]; } Page | 45 } } var apiURL = "@this.ViewBag.ApiUrl"; var blobURL = "@this.ViewBag.BlobUrl"; var nodeJsURL = "@this.ViewBag.NodeJsUrl"; var socket = io.connect(nodeJsURL); var si = new ServerInterface(); var gs = new GameService(apiURL, blobURL, si, socket); var user = new UserService(apiURL, blobURL, si); </script> The previous code instantiates a new Socket pointing to the Node.JS server URL. Additionally, the code instantiates the Game and User services, which interacts with the Web APIs and Socket.IO. 7. Add the following code at the end of the script block. javascript function start(userId) { // check for canvas, show an "Upgrade your browser" screen if they don't have it. var canvas = document.getElementById('board'); if (canvas.getContext == null || canvas.getContext('2d') == null) { $("#game").toggle(); $("#notSupported").toggle(); return; } var board = new TicTacToeBoard(canvas); var game = new TicTacToeGame(); var controller = new TicTacToeController(viewModel, gs, board, game); controller.setGameQueueId(gameQueueId); controller.start(); user.getFriendsInfo(function (friends) { viewModel.friends(friends); }); user.getUser(userId, function () { }); window.onbeforeunload = function () { controller.finish(); }; } The start function initializes the Tic-Tac-Toe board and sets a game queue Id. This Id identifies a game and can be used to invite a Friend to play. 8. Add the following code at the end of the script block. Page | 46 javascript $(function () { viewModel = new TicTacToeViewModel(); ko.applyBindings(viewModel); user.verify( function (userId) { start(userId); }, function () { var newlocation = '@Url.Action("LogOn", "Account", new { Area = "", ReturnUrl = "replace-in-js" })'; newlocation = newlocation.replace("replace-in-js", encodeURIComponent(window.location.href)); window.location.assign(newlocation); } ); }); 9. Add the following code at the end of the script block. This enables to invite other users to play the current game from the view. javascript function inviteFriend() { var userId = $("#friends").val(); var userName = $("#friends :selected").text(); gs.inviteUser(viewModel.gameQueueId(), userId, "Invitation for Tic Tac Toe", viewModel.inviteURL(), function () { alert(userName + " was invited") }); } function sgusersCallback(user) { if (user.DisplayName != null && user.DisplayName != "") viewModel.playerName(user.DisplayName); else viewModel.playerName(user.Id); } Task 3 – Showing Game Invitation Messages from Friends In this task, you will update the Friends view to show the invitation messages from other users. 1. Open the Friends view located in Views\Account folder of the Web project. 2. Add the Invitations table below the script block with id FriendList. javascript <h3>Invitations</h3> Page | 47 <table> <thead> <tr> <th>Message</th> <th>From</th> <th>Action</th> </tr> </thead> <tbody data-bind='template: { name: "NotificationList", foreach: notifications() }' /> </table> <script id="NotificationList" type="text/html"> <tr> <td>${$data.Message} </td> <td>${$data.SenderName} </td> <td><a href="${$data.Url}">Go</a> </td> </tr> </script> 3. Add a call to the user.getNotifications method in order to retrieve the game invitation messages. javascript jQuery(document).ready(function () { window.viewModel = new ViewModel(); ko.applyBindings(window.viewModel); user.getFriendsInfo( function (friends) { window.viewModel.refreshFriends(friends); }, function () { alert('Error Get Friends'); }); user.getNotifications(userId, function () { }); }); 4. Add a callback method at the end of the block script. This method is used by the UserService class to return the invitation messages. javascript function sgnotificationsCallback(data) { for (var n in data.Notifications) { var notification = data.Notifications[n]; if (notification.SenderName == null || notification.SenderName == "") notification.SenderName = notification.SenderId; Page | 48 if (notification.Type == "Invite") viewModel.notifications.push(notification); } } Verification Note: Before you execute the solution, make sure that the start-up project and the start-up page are set. To set the startup project, in Solution Explorer, right-click the TicTacToe.Web.Azure project and select Set as StartUp Project. To designate the start page, in Solution Explorer, right-click the TicTacToe.Web project and select Properties. In the Properties window, select the Web tab and in the Start Action, select Specific Page. Set the value of this field empty. 1. Press to F5 to run the solution. 2. Click the Tic Tac Toe menu link and log on with an identity provider. You will see the current list of Players that are in the same game queue. Additionally, an invitation URL will be generated. Copy this URL. Figure 19 Listing players and the auto generated invitation URL Page | 49 3. Open a new Internet Explorer session or an In-Private by pressing CTRL+SHIFT+P without closing the current one. 4. Paste the invitation URL. When prompted, enter other credentials to log on. Now, the list of players will show both users. Check the first browser to verify that both users are listed as well. Figure 20 Playing a multiplayer Tic-Tac-Toe 5. Start playing the game. You will see how the browsers are updated after each move. 6. Restart the game by clicking the Tic Tac Toe menu link in the first browser. You will see a new drop-down list that shows you friends. Page | 50 Figure 21 Listing friends 7. Click the Invite Friend link. A confirmation message will appear. Click OK. Figure 22 Invite a friend confirmation 8. In the second browser, click the Friends link. The Invitations table will show a new message from the other user. The message has a link that takes you to the Tic Tac Toe page with the game queue Id. Click the Go link. Page | 51 Figure 23 Invitation Message 9. The link takes you to the Tic Tac Toe page and a new game will be started. 10. Close the browsers. Exercise 4: Creating a Leaderboard In this exercise, you will add the necessary logic to generate a Leaderboard using the User’s scores and Azure Table Storage. Then you will be able to obtain the top users scores ordered by victories and the amount of defeats and played games of each top user. Task 1 – Creating the Statistics Repository Logic In this task, you will update the Statistics Repository adding the necessary code to save and retrieve statistics. 1. If you completed Exercise 2 or Exercise 3 you might continue with the obtained solution, otherwise start Visual Studio 2010 as Administrator and open Begin.sln solution located at Source\Ex4-CreatingLeaderboard\Begin\[NodeJs|StoragePolling]. You will see two begin solutions: one is using the Storage Polling approach from Exercise 2 and the other is using Node.Js from Exercise 3, choose the one you prefer for this exercise. 2. If you opened the Begin.sln solution located in the Exercise 4 folder, follow these steps to install the NuGet package dependencies. Page | 52 a. Open the NuGet Package Manager Console. To do this, select Tools | Library Package Manager | Package Manager Console. b. In the Package Manager Console, type Install-Package NuGetPowerTools. c. After installing the package, type Enable-PackageRestore. d. Build the solution. The NuGet dependencies will be downloaded and installed automatically. Note: One of the advantages of using NuGet is that you don’t have to ship all the libraries in your project, reducing the project size. With NuGet Power Tools, by specifying the package versions in the Packages.config file, you will be able to download all the required libraries the first time you run the project. This is why you will have to run these steps after you open an existing solution from this lab. For more information see this article: http://docs.nuget.org/docs/workflows/usingnuget-without-committing-packages. 3. Open UserStats class within Entities folder in the SocialGames.Core project to check the entity structure used to save the user statistics in the Azure Table Storage. C# public class UserStats : TableServiceEntity { public string UserId { get; set; } public int GameCount { get; set; } public int Victories { get; set; } public int Defeats { get; set; } } 4. Update the StatisticsRepository class to implement the necessary code to save and retrieve user statistics. In the Repositories folder within SocialGames.Core project, open StatisticsRepository.cs file. 5. Add an IAzureTable read only property named statsTable. The AzureTable class, which implements the IAzureTable interface, has the necessary methods to interact with the Azure Table Storage (in this case, the UserStats table). (Code Snippet – Building a Social Game - Ex4 Adding StatisticsRepository Property – CS) C# private readonly IAzureTable<UserStats> statsTable; Page | 53 6. Update the StatisticsRepository constructor to set the recently added property when the StatisticsRepository class is initialized. (Code Snippet – Building a Social Game - Ex4 StatisticsRepository Constructor – CS) C# public StatisticsRepository(IAzureTable<UserStats> statsTable) { if (statsTable == null) { throw new ArgumentNullException("statsTable"); } this.statsTable = statsTable; } 7. Locate Initialize method and update it to call the CreateIfNotExist method. This method will create the UserStats table in case if it does not already exists. (Code Snippet – Building a Social Game - Ex4 StatisticsRepository Initialize – CS) C# public void Initialize() { this.statsTable.CreateIfNotExist(); } 8. Update the Save method by replacing its content with the following code. (Code Snippet – Building a Social Game - Ex4 StatisticsRepository Save – CS) C# public void Save(UserStats stats) { stats.RowKey = EncodeKey(stats.UserId); UserStats currentStat = this.statsTable.Query.Where(item => item.RowKey == stats.RowKey).FirstOrDefault(); if (currentStat != null) { this.statsTable.DeleteEntity(currentStat); } this.statsTable.AddEntity(stats); } Page | 54 9. Locate the Retrieve method and add the necessary code to retrieve the scores for a specific user. (Code Snippet – Building a Social Game - Ex4 StatisticsRepository Retrieve – CS) C# public UserStats Retrieve(string userId) { return this.statsTable.Query.Where(item => item.RowKey.Equals(EncodeKey(userId))).FirstOrDefault(); } 10. Finally, update GenerateLeaderboard method. This method returns a Board object that contains the board name and an array of Users’ Scores. (Code Snippet – Building a Social Game - Ex4 StatisticsRepository GenerateLeaderboard – CS) C# public Board GenerateLeaderboard(int focusCount) { int id = 0; var board = new Board() { Id = ++id, Name = "Victories", Scores = null }; UserStats[] data = this.statsTable.Query.Take(focusCount).ToArray(); board.Scores = new Score[data.Count()]; int a = 0; foreach (UserStats stats in data) { board.Scores[a] = new Score() { Id = ++a, UserId = stats.UserId, Victories = stats.Victories, Defeats = stats.Defeats, GameCount = stats.GameCount }; } return board; } 11. Save changes in StatisticsRepository.cs. Page | 55 Note: The StatisticsRepository class has two static methods: EncodeKey and DecodeKey. You will use these methods to encode/decode the UserId when assigning it as RowKey and for querying the Azure Table filtering by RowKey. This is because RowKey and PartitionKey values do not support some characters that might be included in the claims that the ACS providers return. For more information, go to http://msdn.microsoft.com/en-us/library/windowsazure/dd179338.aspx. Task 2 – Adding Statistics Entries In this task, you will update the Worker Role to save each game’s statistics in Azure Table Storage. 1. In the Solution Explorer, locate the SocialGames.Worker project and expand the Commands folder. 2. Add GameActionCommand and GameActionStatisticsCommand classes from Assets\Commands folder. 3. Open the WorkerRole.cs class within SocialGames.Worker project. 4. Update Run method adding the following code immediately below the task builder callback for logging errors declaration. (Code Snippet – Building a Social Game - Ex4 WorkerRole Run – CS) C# public override void Run() { … // TaskBuilder callback for logging errors Action<ICommand, IDictionary<string, object>, Exception> logException = (cmd, context, ex) => { Trace.TraceError(ex.ToString()); }; // Game Action for Statistics messages Task.TriggeredBy(Message.OfType<GameActionStatisticsMessage>(account, ConfigurationConstants.GameActionStatisticsQueue)) .SetupContext((message, context) => { context.Add("gameAction", message.GameAction); }) .Do(container.Resolve<GameActionStatisticsCommand>()) .OnError(logException) .Start(); Page | 56 … } In this code, you are registering the command in the job engine, which will execute these instructions asynchronously. 5. Register the new types in a Dependency Injection container using Autofac. To do this, add the following code at the end of DependencySetup method. (Code Snippet – Building a Social Game - Ex4 WorkerRole DependencySetup – CS) C# builder.RegisterType<StatisticsRepository>().AsImplementedInterfaces(); builder.RegisterType<GameActionCommand>(); builder.RegisterType<GameActionStatisticsCommand>(); 6. Open the GameActionStatisticsCommand class. Locate the Do method and implement the following code to generate and save the statistics. The application will call this method each time a Tic-Tac-Toe game finishes. (Code Snippet – Building a Social Game - Ex4 GameActionStatisticsCommand Do – CS) C# public override void Do(GameAction gameAction) { if (string.IsNullOrWhiteSpace(gameAction.UserId)) { return; } // Retrieve existent statistics and update them with the new results UserStats currentStatistics = this.statisticsRepository.Retrieve(gameAction.UserId); currentStatistics = UpdateCurrentStats(gameAction, currentStatistics); // Generate a new UserStats with the updated RowKey to add in the TableStorage UserStats statistics = null; statistics = CreateUpdatedStats(currentStatistics); statistics.PartitionKey = (int.MaxValue statistics.Victories).ToString().PadLeft(int.MaxValue.ToString().Length); statistics.RowKey = statistics.UserId; this.statisticsRepository.Save(statistics); } Page | 57 Note: This solution takes advantage of how Table Storage sorts entities to ensure the top players appear first on the table; that way, retrieving the top(N) entities from the table will return the first N players in the leaderboard sorted correctly (the one with the most victories first.) In Table Storage the Partition and Row Keys are strings, and records are sorted ascendant using first the Partition Key and then the Row Key. Taking this into account, if all the keys have the same amount of characters, and the top player has the smallest PartitionKey (using string comparison) it will appear first in the table. To achieve this, the solution generates the partition key using the following formula: PartitionKey = int.maxValue - UserVictories, and ensures all the keys have the same length by prepending “0” to the resulting number. This guarantees that the first records correspond to those players with the most victories. The downside of using this approach is that, in order to update the player's score, the record needs to be deleted and added again using the new Partition Key; otherwise the table will not be sorted correctly. 7. Add the UpdateCurrentStats method that updates the current User’s statistics with the latest game’s scores. (Code Snippet – Building a Social Game - Ex4 GameActionStatisticsCommand UpdateCurrentStats – CS) C# private static UserStats UpdateCurrentStats(GameAction gameAction, UserStats originalStatistics) { if (originalStatistics == null) { originalStatistics = new UserStats(); originalStatistics.UserId = gameAction.UserId; } originalStatistics.Victories += GetValue(gameAction.CommandData, "Victories"); originalStatistics.Defeats += GetValue(gameAction.CommandData, "Defeats"); originalStatistics.GameCount += GetValue(gameAction.CommandData, "GameCount"); return originalStatistics; } 8. Add the CreateUpdatedStats method. This method generates a new UserStats object with the updated user’s scores. Page | 58 (Code Snippet – Building a Social Game - Ex4 GameActionStatisticsCommand CreateUpdatedStats – CS) C# private static UserStats CreateUpdatedStats(UserStats originalStatistics) { var statistics = new UserStats { UserId = originalStatistics.UserId, Victories = originalStatistics.Victories, Defeats = originalStatistics.Defeats, GameCount = originalStatistics.GameCount }; return statistics; } Task 3 – Creating a Leaderboard In this task, you will update the Web project to implement all the functionality added in the previous tasks to generate the leaderboard. 1. In the Solution Explorer, locate the TicTacToe.Web project and expand the Views\Account folder. Add a new view named Leaderboard.cshtml. 2. Replace the Leaderboard.cshtml content with the following code. CSHTML @{ ViewBag.Title = "TicTacToe Leaderboard"; } <h2>Leaderboard</h2> <div> <p> The leaderboard shows the scores of the Tic Tac Toe game sample.</p> <p class="status"> Loading... </p> <div data-bind="visible: isScoresEmpty()"> There are no scores yet. Start playing to gather the leaderboard records.</div> <div data-bind="visible: !isScoresEmpty(), template: { name: 'board', foreach: topScores}"> </div> <script id="board" type="text/html"> <table class="style1 board" data-bind='attr: { id: "board" + Id }'> <thead> Page | 59 <tr> <th class="style1"> Position </th> <th class="style1"> Player </th> <th class="style1" data-bind='css: { highlight: Name == "Victories" }'> Victories </th> <th class="style1" data-bind='css: { highlight: Name == "Defeats" }'> Defeats </th> <th class="style1" data-bind='css: { highlight: Name == "GameCount" }'> Game Count </th> </tr> </thead> <tbody data-bind='template: { name: "score", foreach: Scores }'> </tbody> </table> </script> <script id="score" type="text/html"> <tr class="style1" data-bind='css: { d1: Id % 2 == 0, d1: Id % 2 == 1 }'> <td class="style1" data-bind='css: { highlight: window.viewModel.currentUserId == UserId }, text: Id'></td> <td class="left-aligned" data-bind='css: { highlight: window.viewModel.currentUserId == UserId }, text: UserName'></td> <td class="style1" data-bind='css: { highlight: window.viewModel.currentUserId == UserId }, text: Victories'></td> <td class="style1" data-bind='css: { highlight: window.viewModel.currentUserId == UserId }, text: Defeats'></td> <td class="style1" data-bind='css: { highlight: window.viewModel.currentUserId == UserId }, text: GameCount'></td> </tr> </script> </div> 3. Add the following script references at the top of the view. The UserService.js library is used to communicate with the Server API UserService and retrieve the top scores for the leaderboard. javascript @{ Page | 60 ViewBag.Title = "TicTacToe Leaderboard"; } <script src="@Url.Content("~/Scripts/jquery.tmpl.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/knockout-1.2.1.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/Game/ServerInterface.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/Game/UserService.js")" type="text/javascript"></script> <h2>Leaderboard</h2> 4. Finally, add the following JavaScript code block to bind the view with the View Model. javascript <script type="text/javascript"> var apiURL = "@this.ViewBag.ApiUrl"; var blobURL = "@this.ViewBag.BlobUrl"; var si = new ServerInterface(); var user = new UserService(apiURL, blobURL, si); function ViewModel() { this.isScoresEmpty = ko.observable(false); this.currentUserId = "@this.ViewBag.CurrentUserId"; this.topScores = ko.observableArray(); } window.topScoresCallback = function (items) { window.viewModel.topScores(items); window.viewModel.isScoresEmpty(window.viewModel.topScores().Scores.length == 0); $(".status").hide(); }; $(function () { window.viewModel = new ViewModel(); user.getLeaderboard(10, topScoresCallback, errorCallback); ko.applyBindings(viewModel); }); function errorCallback(er) { alert("The leaderboard is not available right now, please try later."); Page | 61 } </script> 5. Open AccountController.cs file from Controllers folder and add the Leaderboard action method. (Code Snippet – Building a Social Game - Ex4 AccountController Leaderboard – CS) C# [Authorize] public ActionResult Leaderboard() { this.ViewBag.CurrentUserId = this.userProvider.UserId; return View(); } 6. Open the UserService.cs class from Services folder and update the Leaderboard method to return the board’s JSON code to the view. (Code Snippet – Building a Social Game - Ex4 UserService Leaderboard – CS) C# public HttpResponseMessage Leaderboard(int count) { try { var board = this.statsRepository.GenerateLeaderboard(count); this.UpdateUserName(ref board); var response = HttpResponse<Board>(board, contentType: "application/json"); response.Headers.CacheControl = new CacheControlHeaderValue(); response.Headers.CacheControl.Public = false; response.Headers.CacheControl.NoStore = true; response.Headers.CacheControl.NoCache = true; return response; } catch (Exception ex) { return BadRequest("Could not retrieve statistics from the database. " + ex.Message); } } Page | 62 7. Finally, add a new menu item in the _Layout.cshtml view within Views\Shared folder. CSHTML <nav> <ul id="menu"> <li>@Html.ActionLink("Home", "Index", "Home", new { area = "" }, null)</li> <li>@Html.ActionLink("Tic Tac Toe", "Index", "TicTacToe", new { area = "" }, null)</li> <li>@Html.ActionLink("Friends", "Friends", "Account", new { area = "" }, null)</li> <li>@Html.ActionLink("Leaderboard", "Leaderboard", "Account", new { area = "" }, null)</li> </ul> </nav> 8. Save all the changes. Verification Note: Before you execute the solution, make sure that the start-up project and the start-up page are set. To set the startup project, in Solution Explorer, right-click the TicTacToe.Web.Azure project and select Set as StartUp Project. To designate the start page, in Solution Explorer, right-click the TicTacToe.Web project and select Properties. In the Properties window, select the Web tab and in the Start Action, select Specific Page. Set the value of this field empty. 1. Press to F5 to run the solution. 2. Click Leaderboard in the menu and log in with your Windows Live Id or Facebook account. If this is the first time you run this verification, the leaderboard page should show a There are no scores yet message. You will populate this table by playing some Tic-Tac-Toe games in the following steps. Page | 63 Figure 24 Empty Leaderboard 3. To start generating Scores, click Tic Tac Toe in the menu and play a Tic-Tac-Toe game. Figure 25 Generating Statistics playing Tic Tac Toe Page | 64 Note: You will need to open a second browser and join the Tic-Tac-Toe game using another user account and the invite URL provided in order to generate the game’s statistics. Once you finish the game, it will save the scores in the UserStats Azure Table Storage. 4. Go back to the Leaderboard section and verify that the Leaderboard table shows two entries one for each user and the scores they have. Figure 26 Top Scores Leaderboard 5. Repeat the Step 3 using different accounts and verify the updated Leaderboard table. Page | 65