May 26

Written by: Michael Washington
5/26/2015 5:29 PM  RssIcon

image

NOTE: You can try the application at this link: http://lsfrackinggame.lightswitchhelpwebsite.com/HTMLClient/ (use your username and password from LightSwitchHelpWebsite.com)

To demonstrate how powerful the Visual Studio LightSwitch HTML Client framework is, we created the beginnings of a turn based multi-user game. Right now the game doesn’t do much. It simply displays a game board and allows you to move. However, it does show the moves of other players in real-time and handles all security and game play collisions.

What would we do this?

The reason Thomas Capps (he collaborated on a previous endeavor here) and I decided to use LightSwitch for the proof of concept for the game we are planning, is time. We don’t have a lot of it. In fact we only have 30-45 minutes a week, and we frequently skip weeks. Even with the amount of functionality we implemented, the project has a surprisingly small amount of custom user code. In addition, all of the back-end server and OData code will be used, as is, in the final version of the game. We plan to use Unity as the client for the final game.

The Game

image

After logging in, players are presented with a multicolored game board with five controls at the bottom of the screen.

The player is represented by a red “P”. All other players are represented by black “X”’s.

image

Clicking the Left, Right, Up and Down buttons will move the entire game board.

image

Clicking the Roll Dice button will display possible moves.

image

The player can then click on one of the highlighted spaces to move to it.

image

Players who are currently logged in will see the moves of other players in real-time.

About The Game

This article will cover the important programming aspects of the game including:

  • Start-Up
    • ASHX File Handler
  • Command Table Pattern
  • WCF RIA Services
  • JavaScript Client
    • Display Game Board
    • Taking A Turn
      • Rolling Dice
      • Moving Player
  • Real-Time Multiplayer (SignalR)

Start-Up

When the player logs into the game, the game board displays, and the following JavaScript code runs:

myapp.BrowseGetGameBoardSpacesForUser.GetGameBoardSpacesForUser_render = function (element, contentItem) {
    // This method is called so that player profile (and extra game board spaces) can be created if needed
    msls.promiseOperation(CallGetUserName).then(function PromiseSuccess(GetUserNameResult) {
        strCurrentUserName = GetUserNameResult;
    });

This calls an .ashx file handler that uses ServerApplicationContext to call the CreateSpaces method using the Command Table Pattern (covered in the following section).

        public void ProcessRequest(HttpContext context)
        {
            using (var serverContext = ServerApplicationContext.CreateContext())
            {
                // Get the current user
                string strCurrentUserName = serverContext.Application.User.Name.ToLower();
                // Try to get the Profile
                var objPlayer = (from player in serverContext.DataWorkspace.ApplicationData.Players
                                 where player.UserName.ToLower() == strCurrentUserName
                                 select player).Execute().FirstOrDefault();
                if (objPlayer == null) // No Profile found
                {
                    // Create a Default Profile
                    Player objNewPlayer;
                    objNewPlayer = serverContext.DataWorkspace.ApplicationData.Players.AddNew();
                    objNewPlayer.UserName = strCurrentUserName;
                    objNewPlayer.NetWorth = 0;
                    objNewPlayer.LastNetWorthCalculatedTime = DateTimeOffset.Now;
                    objNewPlayer.RemainingRolls = 5;
                    objNewPlayer.CounterResetTime = DateTimeOffset.Now.AddHours(24);
                    objNewPlayer.Credits = 100;
                    serverContext.DataWorkspace.ApplicationData.SaveChanges();
                    GameSessionCommand objGameSessionCommand = 
                        serverContext.DataWorkspace.ApplicationData.GameSessionCommands.AddNew();
                    // Create game board spaces and player if necessary
                    objGameSessionCommand.CommandAction = "CreateSpaces";
                    objGameSessionCommand.CommandParameter = " ";
                    serverContext.DataWorkspace.ApplicationData.SaveChanges();
                }
                context.Response.ContentType = "text/plain";
                context.Response.Write(serverContext.Application.User.Name.ToLower());
            }
        }

 

The Command Table Pattern

As we discovered in the previous endeavor, Manipulyte: A Proof Of Concept Visual Studio LightSwitch Turn Based HTML5 SPA Game, there was the need for, and the implementation of, the Command Table Pattern. Essentially it means that:

You need a method to allow a user to perform an action that you cannot simply give them the ability to do directly.

Even if you are not creating a video game you will find you may have this need. It is frequently required in business applications. For example, you may not want to allow a user to cancel an order directly.

image

You can see a detailed explanation at the following link: OData Security Using The Command Table Pattern And Permission Elevation.

For our current game, we implemented the Command Table Pattern by creating a table called GameSessionCommands.

image

We then blocked off some, and in some cases, all access to all the other tables (to everyone but administrators) using code such as this:

        partial void Players_CanDelete(ref bool result)
        {
            result = (this.Application.User.HasPermission(Permissions.SecurityAdministration) || 
                this.Application.User.HasPermission(Permissions.CanWriteToTables));
        }
        partial void Players_CanUpdate(ref bool result)
        {
            result = (this.Application.User.HasPermission(Permissions.SecurityAdministration) || 
                this.Application.User.HasPermission(Permissions.CanWriteToTables));
        }
The only way a player can make a move is by inserting a record into the GameSessionCommands table.

image

These methods contain code to enforce game rules and prevent cheating.

For example, the following code is used for CreateSpaces:

            if (entity.CommandAction == "CreateSpaces")
            {
                // Elevate the user's permission to allow the Entity to be updated
                this.Application.User.AddPermissions(Permissions.CanWriteToTables);
                // Get count of GameBoardSpaces
                int CountOfGameBoardSpaces = GameBoardSpaces.Count();
                // Get count of Occupied GameBoardSpaces
                int CountOfOccupiedGameBoardSpaces =
                    GameBoardSpaces.Where(x => x.SpaceOwner != null).Count();
                // If there are not 100 spaces for every 10 wells drilled make 100 spaces
                if (CountOfOccupiedGameBoardSpaces * 10 
                    < CountOfGameBoardSpaces || CountOfGameBoardSpaces == 0)
                {
                    CreateNewGameSpaces();
                }
                // See if current user already has a space assigned
                // Get the current user
                string strCurrentUserName = this.Application.User.Name.ToLower();
                var objCurrentSpace = (from gameSpace in this.GameBoardSpaces
                                       where gameSpace.SpaceOccupier.UserName.ToLower() 
                                       == strCurrentUserName
                                       select gameSpace).FirstOrDefault();
                if (objCurrentSpace == null)
                {
                    // The user is not on a current space, so find him one and place him there
                    var objCurrentPlayer = (from player in this.Players
                                            where player.UserName.ToLower() 
                                            == strCurrentUserName
                                            select player).FirstOrDefault();
                    var openSpace = (from gameSpace in this.GameBoardSpaces
                                     where gameSpace.SpaceOccupier == null
                                     select gameSpace).OrderBy(x => x.Latitude)
                                     .ThenBy(x => x.Longitude).FirstOrDefault();
                    if (openSpace != null)
                    {
                        openSpace.SpaceOccupier = objCurrentPlayer;
                    }
                }
            }

 

WCF RIA Service

image

Because most of the tables have their access blocked, when data needs to be shown, such as the game board, a WCF RIA Service allows granular access.

The following method is used to return a section of the game board when passed a players current latitude and longitude:

        public IQueryable<WCFGameBoardSpace> GetGameBoardSpacesForUser(int? Latitude, int? Longitude)
        {
            var colWCFGameBoardSpace = (from objGameBoardSpace in this.Context.GameBoardSpaces
                                        where objGameBoardSpace.Latitude >= Latitude
                                        where objGameBoardSpace.Latitude < (Latitude + 10)
                                        where objGameBoardSpace.Longitude >= Longitude
                                        where objGameBoardSpace.Longitude < (Longitude + 10)
                                        orderby objGameBoardSpace.Latitude, objGameBoardSpace.Longitude
                                        select new WCFGameBoardSpace
                                        {
                                            Id = objGameBoardSpace.Id,
                                            Latitude = objGameBoardSpace.Latitude,
                                            Longitude = objGameBoardSpace.Longitude,
                                            Playable = objGameBoardSpace.Playable,
                                            SpaceType = objGameBoardSpace.SpaceType,
                                            SpaceOwner = objGameBoardSpace.SpaceOwner.UserName.ToLower(),
                                            SpaceOccupier = objGameBoardSpace.SpaceOccupier.UserName.ToLower()
                                        }).ToList();
            return colWCFGameBoardSpace.AsQueryable();
        }

The HTML Client (JavaScript)

image

The application contains a single screen that contains a Custom Control that displays the game board and the players.

Display Game Board

image

If we click the Edit Render Code link for the control, we are taken to the JavaScript that calls the GetGameStatus.ashx file handler that returns the data that indicates the status of the player and the other players on the board. It then calls the showItems method that calls the WCF RIA Service that provides the game board data:

    // Call CallGetGameStatus to get latest game session info
    msls.promiseOperation(CallGetGameStatus).then(function PromiseSuccess(GetGameStatusResult) {
        // Parse the JSON returned
        var objGameStatus = jQuery.parseJSON(GetGameStatusResult);
        // Set lblMessage
        objScreen.lblMessage = objGameStatus.PlayerLastStatusMessage;
        objScreen.PlayerLatitude = objGameStatus.PlayerLatitude;
        objScreen.PlayerLongitude = objGameStatus.PlayerLongitude;
        var boolRecenterPlayer = false;
        // If player is off the board, reset the upper-left-hand corner to correct area
        if ((objScreen.PlayerLatitude >= (objScreen.Latitude + 10)) ||
            (objScreen.PlayerLatitude < objScreen.Latitude)) {
            objScreen.Latitude = objScreen.PlayerLatitude;
            boolRecenterPlayer = true;
        }
        if ((objScreen.PlayerLongitude >= (objScreen.Longitude + 10)) ||
            (objScreen.PlayerLongitude < objScreen.Longitude)) {
            objScreen.Longitude = objScreen.PlayerLongitude;
            boolRecenterPlayer = true;
        }
        if (boolRecenterPlayer === true) {
            showItems([]);
        }
    });

 

The ShowItems method actually displays the squares on the game board. It allows an array of possible moves to be highlighted when called as part of a player taking a turn (coved in the next section).

 

function showItems(paramColValidMoves) {
    var i = 0;
    var tablecontentrowTemplate = [$("<tr></tr>")];
    var lastLatitude = 0;
    NumberOfSpacesReturned = objScreen.GetGameBoardSpacesForUser.count;
    // Loop through all GameBoardSpaces
    $.each(objScreen.GetGameBoardSpacesForUser.data, function (i, GameBoardSpace) {
        // Insert row break if latitude changes
        if (lastLatitude != 0 && lastLatitude != GameBoardSpace.Latitude) {
            // Create tableRow
            tablecontentrowTemplate[0].appendTo($(objTable));
            // Reset the row
            tablecontentrowTemplate = [$("<tr></tr>")];
        }
        lastLatitude = GameBoardSpace.Latitude;
        // Set IsPossibleMove
        var IsPossibleMove = false;
        for (var i = 0, len = paramColValidMoves.length; i < len; i++) {
            if (paramColValidMoves[i].Latitude == GameBoardSpace.Latitude
                && paramColValidMoves[i].Longitude == GameBoardSpace.Longitude) {
                IsPossibleMove = true;
                break;
            }
        }
        // Create cell
        if (IsPossibleMove) {
            $("<td style='padding: 0px; vertical-align: center'></td>")
                .html(GetGameSpaceImage(GameBoardSpace, IsPossibleMove))
                .appendTo($(tablecontentrowTemplate))
                .click(function () {
                    MoveGamePiece(GameBoardSpace.Latitude, GameBoardSpace.Longitude);
                    return false;
                });
        }
        else {
            $("<td style='padding: 0px; vertical-align: center'></td>")
                .html(GetGameSpaceImage(GameBoardSpace, IsPossibleMove))
                .appendTo($(tablecontentrowTemplate));
        }
    });
    // Create tableRow
    tablecontentrowTemplate[0].appendTo($(objTable));
};

More information on the method used to dynamically create a table in LightSwitch can be found here: LightSwitch HTML Client For The Desktop Web Browser.

 

Rolling Dice

image

When the Roll Dice button is pressed, the following code runs that calls the RollDie code in the GameSessionCommands table. This updates the database. Then the GetGameStatus.ashx file handler is called, this time returning an array of possible moves. showItems is called to display the game board and the players, this time with the possible moves highlighted:

myapp.BrowseGetGameBoardSpacesForUser.RollDice_execute = function (screen) {
    // Create a new Game Session Command
    var GameSessionCommand = new myapp.GameSessionCommand();
    GameSessionCommand.CommandAction = "RollDie";
    GameSessionCommand.CommandParameter = " ";
    var filter = "(UserName eq " + msls._toODataString(strCurrentUserName, ":String") + ") ";
    myapp.activeDataWorkspace.ApplicationData.Players.filter(filter).execute().then(function (result) {
        // Get the results of the query
        var objPlayer = result.results[0];
        // Set the player
        GameSessionCommand.setPlayer(objPlayer);
        // Save the GameSessionCommand to execute a die roll
        return myapp.activeDataWorkspace.ApplicationData.saveChanges().then(function () {
            // Call CallGetGameStatus to get latest game session info
            msls.showProgress(msls.promiseOperation(CallGetGameStatus)
                .then(function PromiseSuccess(GetGameStatusResult) {
                // Parse the JSON returned
                var objGameStatus = jQuery.parseJSON(GetGameStatusResult);
                // Set lblMessage
                screen.lblMessage = objGameStatus.PlayerLastStatusMessage;
                screen.PlayerLatitude = objGameStatus.PlayerLatitude;
                screen.PlayerLongitude = objGameStatus.PlayerLongitude;
                // Refresh game board
                screen.GetGameBoardSpacesForUser.load().then(function (results) {
                    // clear the table  
                    $("#gameTable tr").remove();
                    showItems(objGameStatus.colValidMoves);
                });
            }));
        }, function fail(e) {
            myapp.cancelChanges();
            throw e;
        });
    }, function (error) {
        alert(error);
    });
};

 

Moving Player

image

When possible moves are highlighted, a player can click on one of the squares to move to it.

The code to move a game piece is simple. However it still calls the GameSessionCommand table and then gets a response by refreshing the game board to ensure that only a valid move can be made.

function MoveGamePiece(Latitude, Longitude) {
    // Create a new Game Session Command
    var GameSessionCommand = new myapp.GameSessionCommand();
    GameSessionCommand.CommandAction = "MovePlayer";
    GameSessionCommand.CommandParameter = Latitude + ',' + Longitude;
    var filter = "(UserName eq " + msls._toODataString(strCurrentUserName, ":String") + ") ";
    myapp.activeDataWorkspace.ApplicationData.Players.filter(filter).execute().then(function (result) {
        // Get the results of the query
        var objPlayer = result.results[0];
        // Set the player
        GameSessionCommand.setPlayer(objPlayer);
        // Save the GameSessionCommand to move the player
        return myapp.activeDataWorkspace.ApplicationData.saveChanges().then(function () {
            // Set lblMessage
            objScreen.lblMessage = "";
            objScreen.PlayerLatitude = Latitude;
            objScreen.PlayerLongitude = Longitude;
            // Refresh game board
            objScreen.GetGameBoardSpacesForUser.load().then(function (results) {
                // clear the table  
                $("#gameTable tr").remove();
                showItems([]);
            });
        }, function fail(e) {
            myapp.cancelChanges();
            throw e;
        });
    }, function (error) {
        alert(error);
    });
}

 

Real-Time Multiplayer (SignalR)

When a player makes a move, all other players have their screens updated instantly. This is done using SignalR.

When a player makes a move, the following C# code in the GameSessionCommands_Inserting method runs to create a message, describing the move, and who is making it, and broadcasting it:

                string message = strCurrentUserName + "," 
                    + CurrentPlayerSpace.Latitude + "," 
                    + CurrentPlayerSpace.Longitude + "," 
                    + LatitudeMove + "," + LongitudeMove;
                var context = GlobalHost.ConnectionManager.GetHubContext<ContactHub>();
                context.Clients.All.broadcastMessage(message);

The following JavaScript listens for the broadcasts and updates the game board:

myapp.BrowseGetGameBoardSpacesForUser.created = function (screen) {
    // Write code here.
    screen.Latitude = 1;
    screen.Longitude = 1;
    objScreen = screen;
    // This sets up SignalR to work on the page
    $(function () {
        contact = $.connection.contactHub;
        contact.client.broadcastMessage = function (message) {
            // only show updates for players on visible 10x10 window
            var arrayPlayerMove = message.split(',');
            var PlayerName = arrayPlayerMove[0];
            var PlayerLatitudeOld = arrayPlayerMove[1];
            var PlayerLongitudeOld = arrayPlayerMove[2];
            var PlayerLatitudeNew = arrayPlayerMove[3];
            var PlayerLongitudeNew = arrayPlayerMove[4];
            // This filters out any players not on visible board
            if ((PlayerLatitudeNew >= (objScreen.Latitude - 10))
                && (PlayerLatitudeNew <= (objScreen.Latitude + 20))) {
                if ((PlayerLongitudeNew >= (objScreen.Longitude - 10))
                    && (PlayerLongitudeNew <= (objScreen.Longitude + 20))) {
                    screen.updates = message;
                    // Do not show messages of your own moves
                    if (strCurrentUserName.toLowerCase() !== PlayerName.toLowerCase()) {
                        // Refresh game board
                        objScreen.GetGameBoardSpacesForUser.load().then(function (results) {
                            // clear the table  
                            $("#gameTable tr").remove();
                            showItems([]);
                        });
                    }
                }
            }
        };
        $.connection.hub.start()
        .done(function () {
        })
        .fail(function () {
            alert("Could not Connect! - ensure EnableCrossDomain = true");
        });
    });
};

Links

Manipulyte: A Proof Of Concept Visual Studio LightSwitch Turn Based HTML5 SPA Game

OData Security Using The Command Table Pattern And Permission Elevation

LightSwitch HTML Client For The Desktop Web Browser

Download Code

The LightSwitch project is available at http://lightswitchhelpwebsite.com/Downloads.aspx

(you must have Visual Studio 2013 (or higher) installed to run the code)


Your name:
Gravatar Preview
Your email:
(Optional) Email used only to show Gravatar.
Your website:
Title:
Comment:
Security Code
CAPTCHA image
Enter the code shown above in the box below
Add Comment   Cancel 
Microsoft Visual Studio is a registered trademark of Microsoft Corporation / LightSwitch is a registered trademark of Microsoft Corporation