Hypermedia Systems on ASP.NET MVC 5. Part Two

Rapid development of hypermedia-oriented web application with HTMX 2.0.

Continuation of the first part

In the previous part, you got acquainted with hypermedia systems, started creating a new application – the online board game “Spy” and added the first hypermedia element to the player waiting page. In this part, we will continue working on this application.

Refactoring

Before adding the following hypermedia elements, we should do some refactoring. After refactoring, it becomes easier to add new functionality.

According to the MVC convention, controllers should only process incoming requests, operate models, and select a view to display to the user. And our example violates this convention. The server-side code of hypermedia systems should generally be moved to regular hypermedia models, and we will pass web models filled with ready-made data to visual representations, not primitive data types. And the controllers themselves should not interact directly with repository models or domain models.

Hypermedia model of game participants' expectations

Create a new folder called Hypermedia in the Web folder. This folder will contain hypermedia models to support the operation of hypermedia elements on visual representations. In other words, the server code of hypermedia systems. Create a new class called WaitHypermedia in this folder and fill it with the contents from the listing below.

Listing: WaitHypermedia Hypermedia Model

using System.Linq;
using System.Web;
using SpyOnlineGame.Data;
using SpyOnlineGame.Models;
using SpyOnlineGame.Web.Models;

namespace SpyOnlineGame.Web.Hypermedia
{
    public class WaitHypermedia
    {
        private readonly HttpRequestBase _request;
        private readonly int _id;
        private readonly Player _current;
        
        public bool IsHtmx => _request.Headers.AllKeys.Contains("hx-request");

        public bool IsNotFound => _current is null;

        public WaitHypermedia(HttpRequestBase request, int id)
        {
            _request = request;
            _id = id;

            _current = PlayersRepository.GetById(_id);
        }

        public WaitWebModel Model()
        {
            return new WaitWebModel
            {
                Id = _id,
                Current = _current ?? new Player(),
                All = PlayersRepository.All,
            };
        }
    }
}

Simplifying the controller's action method

Now we can simplify the Index method of the Wait controller. All the logic of the work is taken out of the action method into the hypermedia model, and only the logic specific to the controller remains in the action method.

Listing: Wait controller with simplified method

using System.Web.Mvc;
using SpyOnlineGame.Web.Hypermedia; // Добавить

namespace SpyOnlineGame.Controllers
{
    public class WaitController : Controller
    {
        public ActionResult Index(int id)
        {
            var hypermedia = new WaitHypermedia(Request, id); // Добавить
            if (hypermedia.IsNotFound) 
              return new HttpNotFoundResult(); // Добавить

            if (hypermedia.IsHtmx) // Добавить
            {
                return PartialView("Partial/WaitPartial", 
                  hypermedia.Model()); // Добавить
            }
            return View(hypermedia.Model()); // Добавить
        }
    }
}

At the beginning of this method execution we create a hypermedia model. Then the boundary operator checks whether the hypermedia model was able to find the current user. And finally, depending on the nature of the request, it returns either a partial view or a regular one.

By this refactoring we do not violate the MVC conventions in any way. On the contrary, we emphasize them. The controller action method works with the model and selects which visual representation to return in response to the request. The hypermedia model will concentrate the hypermedia logic. The web model will concentrate the logic related to the visual representation.

It is very important to understand that the hypermedia model of this example, the controller's method of action, the hypermedia element of the visual representation and the communication between them are one hypermedia system.

After this refactoring, we can begin adding the remaining hypermedia systems to the player registration waiting page. We will not move the business logic into the domain model from the hypermedia model for the sake of simplicity of the example, since in this example our goal is primarily to get acquainted with hypermedia systems.

Player Registration Waiting Page Interactivity

Now we will work on adding the rest of the interactive hypermedia elements of the waiting page. I consider it very important to refactor those parts of the application where changes are supposed to be made before adding any new functionality to the application. This rule prevents code rot and gives us a performance boost for some time.

Update elements on the page as needed

Currently, the list of registered users on the browser page of each registered participant is updated every second. It is more logical to update only when someone else has registered as a player. We will add an update as needed.

First, let's add a new property to the user model to indicate whether the player needs to be updated. Add the new property highlighted in bold in the listing below to the class.

Listing: Augmented Player Model

namespace SpyOnlineGame.Models
{
    public class Player
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool IsNeedUpdate { get; set; } // Добавить
    }
}

Adjust the PlayersRepository code so that when a new member is added or any member is removed, the update flag is raised. Make the changes shown in the following listing.

Listing: Corrected PlayersRepository

…
        public static int Add(Player player)
        {
            player.Id = _lastId++;
            _players.Add(player);
            IsNeedAllUpdate(); // Добавить
            return player.Id;
        }

        public static void Remove(int id)
        {
            var deleted = GetById(id);
            if (deleted is null) return;
            _players.Remove(deleted);
            IsNeedAllUpdate() // Добавить
        }

        public static void IsNeedAllUpdate() // Добавить метод
        {
            foreach (var each in All) each.IsNeedUpdate = true; // Добавить
        }
    }
}

After that, add a new method to the hypermedia participant wait model that will return a sign that the model already contains old, modified data. You also need to add a new property that will encapsulate the logic of the boundary operator condition from the controller action method. Add the method and property shown in the following listing.

Listing: New method in the WaitHypermedia hypermedia model

…
        public bool IsNotFound => _current is null;

        public bool IsNoContent => IsHtmx && HasOldData(); // Добавить

        public WaitHypermedia(HttpRequestBase request, int id)
        {
…
        }

        public bool HasOldData() // Добавить метод
        {
            if (_current?.IsNeedUpdate != true) return true; // Добавить
            _current.IsNeedUpdate = false; // Добавить
            return false; // Добавить
        }

        public WaitWebModel Model()
        {
…
        }
…

This method should only allow updating the list of users on each participant's page once, and then prohibit it again. But allow it only after, for example, registering another participant in the game.

All that remains is to add a new boundary operator to the Index method of the Wait controller, as in the listing below.

Listing: New boundary operator in the Index method of the WaitController class

…
        public ActionResult Index(int id)
        {
            var hypermedia = new WaitHypermedia(Request, id);
            if (hypermedia.IsNotFound) return new HttpNotFoundResult();
            if (hypermedia.IsNoContent) // Добавить блок
              return new HttpStatusCodeResult(HttpStatusCode.NoContent); 

            if (hypermedia.IsHtmx)
            {
                return PartialView("Partial/WaitPartial", hypermedia.Model());
            }
            return View(hypermedia.Model());
        }
…

Using the HttpStatusCodeResult object with the NoContent parameter, we return code 204 as a result. When receiving such a code, the hypermedia element does not perform any actions and the table of game participants is not updated.

Adding the players' readiness status for the game.

Add a player's readiness to the game flag to the Player model. This flag signals that the game participant is ready to start the game. It is necessary to add a display of readiness of all players and a button to change the readiness status.

Listing: Player participant model with new ready flag

namespace SpyOnlineGame.Models
{
    public class Player
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool IsReady { get; set; } // Добавить
        public bool IsNeedUpdate { get; set; }
    }
}

Change the partial visual representation of the registered players display. Add the display of the player's readiness for the game in a new table column. Additionally, we will style the readiness cell based on the readiness status value.

Listing: Partial visual representation of Wait/WaitPartial.cshtml with the ready table

@model WaitWebModel

<p>Все зарегистрированные игроки:</p>
<table class="table table-striped table-bordered">
    <thead>
        <tr><th>Id</th><th>Имя</th><th>Готовность</th></tr> // Изменить
    </thead>
    <tbody>
    @foreach (var each in Model.All)
    {
        <tr>
            <th scope="row" class="align-middle">@each.Id</th>
            <td class="align-middle">@each.Name</td>
            <td class="align-middle text-white 
              @(each.IsReady ? "bg-success" : "bg-danger")"> // Добавить блок
                @(each.IsReady ? "Готов" : "Не готов") 
            </td>
        </tr>
    }
    </tbody>
</table>

Now we need to add a method to the hypermedia model to change the player's status. Let's add a new method to the hypermedia model according to the following listing.

Listing: WaitHypermedia hypermedia model with new method

…
        public bool HasOldData()
        {
            if (_current?.IsNeedUpdate != true) return true;
            _current.IsNeedUpdate = false;
            return false;
        }

        public void SwitchReady() // Добавить новый метод
        {
            if (_current is null) return;
            _current.IsReady = !_current.IsReady;
            PlayersRepository.IsNeedAllUpdate();
        }
…

This method reverses the player's ready flag and updates the pages for all participants. Before adding the button to the visual representation, you need to add a new action method to the Wait controller.

Listing: New method for changing readiness in Wait controller

…
        public ActionResult Index(int id)
        {
…
        }

        public ActionResult SwitchReady(int id) // Добавить новый метод
        {
            var hypermedia = new WaitHypermedia(Request, id);
            hypermedia.SwitchReady();
            return Index(id);
        }
…

This action method, as you can see, calls the readiness change method of the hypermedia model of waiting for participants. And as a result, it returns a visual representation of the Index method. We did this so that after changing the readiness status of a game participant, he immediately sees the change in his status. All that remains is to add a stylized button for changing the player's readiness to the partial visual representation.

Listing: Partial visual representation of Wait/WaitPartial.cshtml with ready button

@model WaitWebModel

<div class="my-1"> // Добавить весь новый блок
    <p class="form-label">Ваша готовность:</p>
    <button class="btn @(Model.Current.IsReady ? "btn-success" : "btn-danger")"
            hx-get="@Url.Action("SwitchReady", new { Model.Id })"
            hx-target="#wait">
        @(Model.Current.IsReady ? "Готов" : "Не готов")
    </button>
</div>

<p>Все зарегистрированные игроки:</p>
…

Now after running the application in several browsers you can see that after changing the readiness status of any participant in the game, all players will immediately see the change.

Change of participant name

We will add an interactive option for a registered game participant to change their name. We will add this option as a text field directly to the game participants' waiting page. The user changes the text in the field – their name changes for all game participants. With the help of hypermedia systems, such as many other options are very easy to implement.

First of all, let's add a new method for changing the name to the hypermedia model of waiting for game participants.

Listing: WaitHypermedia hypermedia model with new name change method

…
        public void SwitchReady()
        {
…
        }

        public void SetName(string name) // Добавить новый метод
        {
            if (_current is null || _current.Name == name) return;
            _current.Name = name;
            PlayersRepository.IsNeedAllUpdate();
        }
…

Then we will add a new method for changing the name of the registered game participant to the Wait controller.

Listing: New method to change the name of a registered game participant in the Wait controller

…
        public ActionResult SwitchReady(int id)
        {
…
        }

        public void SetName(int id, string name) // Добавить новый метод
        {
            var hypermedia = new WaitHypermedia(Request, id);
            hypermedia.SetName(name);
        }
…

All that remains is to add a new hypermedia element to the Index visual representation.

Listing: Visual representation of Wait/Index.cshtml with name change text field

…
<h4>@ViewBag.Title</h4>
<p>Ожидание окончания регистрации всех участников игры.</p>

<div class="my-1"> // Добавить весь новый блок
    <label class="form-label">Ваше имя:</label>
    <input type="text" name="name" class="form-control"
           hx-get="@Url.Action("SetName", new { Model.Id })"
           hx-trigger="keyup changed dalay:500ms"
           value="@Model.Current.Name" />
</div>
…

We set the execution trigger for this new element to keyup changed – from releasing the key after entering a new character with a delay of delay:500ms of 500 milliseconds. When this trigger is triggered, a request will be made to the specified address. The special feature is that we specified the id attribute of the request, and the name of the second attribute – name – will be automatically substituted based on the method name, and the value will be taken from the text field.

Run the application and check if you can change the name.

Exit game button

It is necessary to add a method of exiting the game to the hypermedia model of the game participant. By calling this method, the participant removes himself from the list of registered players.

Listing: WaitHypermedia Player Waiting Model with New Method

…
        public void SetName(string name)
        {
…
        }

        public void Logout() // Добавить новый метод
        {
            PlayersRepository.Remove(_id);
        }
…

After that, you need to add a button to the visual representation and add an action method to the Wait controller. Add a new action method to the Wait controller as in the listing below.

Listing: New exit game action method in Wait controller

…
        public void SetName(int id, string name)
        {
…
        }
 
        public ActionResult Logout(int id) // Добавить новый метод
        {
            var hypermedia = new WaitHypermedia(Request, id);
            hypermedia.Logout();

            return RedirectToAction("Index", "Home");
        }
…

And finally, we will add a logout button to the top of the player registration waiting page.

Listing: Visual representation with exit button Wait/Index.cshtml

@model WaitWebModel
@{
    ViewBag.Title = "Ожидание";
}

@Html.ActionLink("Выход", "Logout", new { Model.Id }, 
    new { @class="btn btn-warning my-1" }) // Добавить

<h4>@ViewBag.Title</h4>
<p>Ожидание окончания регистрации всех участников игры.</p>
…

Now you can run the application and check the functionality of the button. When you click this button, you are redirected to the main page of the game and all participants see changes in the list of participants.

Buttons for excluding players from the list

Let's add the ability to exclude already registered extra players. In the user repository, we already have a method for deleting players. This method will be used by the hypermedia model. Let's modify it. Add a new method to this model, shown in the listing below. Place this method next to the Logout method – these are two methods that perform similar functions.

Listing: WaitHypermedia Player Waiting Model with New Method

…
        public void Kick(int playerId) // Добавить новый метод
        {
            PlayersRepository.Remove(playerId);
        }

        public void Logout()
        {
            PlayersRepository.Remove(_id);
        }
…

Now let's add a new method to the Wait controller. This method will call the hypermedia model method. We also need to modify the Index method so that it now redirects excluded players from the participant list to the application's start page.

Listing: New player exception action method in Wait controller

…
        public ActionResult Index(int id)
        {
            var hypermedia = new WaitHypermedia(Request, id);
            if (hypermedia.IsNotFound) // Добавить весь блок
            {
                if (!hypermedia.IsHtmx) return RedirectToAction("Index", "Home");
                Response.Headers.Add("hx-redirect", Url.Action("Index", "Home"));
            }
            if (hypermedia.IsNoContent) 
                return new HttpStatusCodeResult(HttpStatusCode.NoContent);

            if (hypermedia.IsHtmx)
            {
                return PartialView("Partial/WaitPartial", hypermedia.Model());
            }
            return View(hypermedia.Model());
        }
…
        public ActionResult Kick(int id, int playerId) // Добавить новый метод
        {
            var hypermedia = new WaitHypermedia(Request, id);
            hypermedia.Kick(playerId);
            return Index(id);
        }

        public ActionResult Logout(int id)
        {
…
        }
…

As a result of execution, the Kick action method will call the Index method to return the current partial view with the modified list of users. And in the Index method, if the participant with the specified ID is missing, we will now redirect to the start page. In this example, redirection to another page now works using the hypermedia system. Since the page of the excluded participant is updated every second, then calling this method will redirect immediately after he is excluded.

All that remains is to modify the table in the visual partial representation. Add one new column to this table and a button for the ability to exclude players from the list.

Listing: Wait/WaitPartial.cshtml partial view with player kick buttons

@model WaitWebModel

<button class="btn @(Model.Current.IsReady ? "btn-success" : "btn-danger")"
        hx-get="@Url.Action("SwitchReady", new { Model.Id })"
        hx-target="#wait">
    @(Model.Current.IsReady ? "Готов" : "Не готов")
</button>

<p>Все зарегистрированные игроки:</p>
<table class="table table-striped table-bordered">
    <thead>
        <tr><th>Id</th><th>Имя</th><th>Готовность</th>
          <th class="col-1"></th></tr> // Изменить
    </thead>
    <tbody>
    @foreach (var each in Model.All)
    {
        <tr>
            <th scope="row" class="align-middle">@each.Id</th>
            <td class="align-middle">@each.Name</td>
            <td class="align-middle text-white 
              @(each.IsReady ? "bg-success" : "bg-danger")">
                @(each.IsReady ? "Готов" : "Не готов")
            </td>
            <td> // Добавить весь новый блок
                @if (each.Id != Model.Id)
                {
                    <button class="btn btn-warning"
                            hx-get="@Url.Action("Kick", 
                              new { Model.Id, playerId = each.Id })"
                            hx-target="#wait">
                        Выгнать
                    </button>
                }
            </td>
        </tr>
    }
    </tbody>
</table>

If you run the application, you can check the possibility of excluding players from the list of registered players.

The next large screenshot shows the functionality of excluding a player from the list of players. The two upper windows are the windows of the two players initially. And below, under the red stripe, are the windows of the participants after clicking the “Kick Out” button in the window of the first player. As a result, the main page of the application with the registration form began to be displayed in the browser window of the second player. And in the left window of the first player, only one player is now shown in the list of registered users instead of two.

Hypermedia systems make it very easy to add such complex interactive behaviors to web application pages.

Start of the game

The last interactive functionality left to add to our waiting page for players to register is the game start button. It should only be available when there are more than three registered players and they are all ready to play. But everything is complicated by the fact that after pressing the game start button, all registered and ready to play players should be redirected to the game page at the same time. And all other players should be prohibited from registering. In this application, we do not implement game sessions specifically for the sake of simplifying the example. There will be only one session, and it is also the last one. That is, to start a new game, you need to restart the application, and after the end of the game, the application will be useless. This is a special convention only for the sake of maximum simplification.

Add a new property to the participant data model – an indication of his participation in the game.

Listing: Player participant model with a sign of participation in the game

namespace SpyOnlineGame.Models
{
    public class Player
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool IsReady { get; set; }
        public bool IsPlay { get; set; } // Добавить
        public bool IsNeedUpdate { get; set; }
    }
}

Modify the web model for waiting for players to register so that it can now pass to the visual representation not a list of all users, but a list of waiting and a list of already playing players. Also add a sign that the game can be started. And the game can only be started if the number of participants is greater than or equal to three and all of them are ready to play.

Listing: Modified WaitWebModel web model

using System;
using System.Collections.Generic;
using SpyOnlineGame.Models;

namespace SpyOnlineGame.Web.Models
{
    public class WaitWebModel
    {
        public int Id { get; set; }
        public Player Current { get; set; }
        public IEnumerable<Player> All { get; set; } = Array.Empty<Player>();
        public bool IsMayBeStart { get; set; } // Добавить
    }
}

Next, we will have to change the WaitHypermedia hypermedia model of player waiting. We need to add a new method for starting the game, a sign for starting the game, and modify the Model() method. Make changes according to the following listing.

Listing: Modified WaitHypermedia hypermedia model

using System.Linq;
using System.Web;
using SpyOnlineGame.Data;
using SpyOnlineGame.Models;
using SpyOnlineGame.Web.Models;

namespace SpyOnlineGame.Web.Hypermedia
{
    public class WaitHypermedia
    {
        private readonly HttpRequestBase _request;
        private readonly int _id;
        private readonly Player _current;
        
        public bool IsHtmx => _request.Headers.AllKeys.Contains("hx-request");

        public bool IsNotFound => _current is null;

        public bool IsNoContent => IsHtmx && HasOldData();

        public bool IsMayBeStart => PlayersRepository.All.Count() >= 3 && 
            PlayersRepository.All.All(x => x.IsReady) 
            && !IsGameStarted; // Добавить

        public bool IsGameStarted =>
            PlayersRepository.All.Any(p => p.IsPlay); // Добавить

        public WaitHypermedia(HttpRequestBase request, int id)
        {
            _request = request;
            _id = id;

            _current = PlayersRepository.GetById(_id);
        }

        public bool HasOldData()
        {
            if (_current?.IsNeedUpdate != true) return true;
            _current.IsNeedUpdate = false;
            return false;
        }

        public void SwitchReady()
        {
            if (_current is null) return;
            _current.IsReady = !_current.IsReady;
            PlayersRepository.IsNeedAllUpdate();
        }

        public void SetName(string name)
        {
            if (_current is null || _current.Name == name) return;
            _current.Name = name;
            PlayersRepository.IsNeedAllUpdate();
        }

        public void Kick(int playerId)
        {
            PlayersRepository.Remove(playerId);
        }

        public void Logout()
        {
            PlayersRepository.Remove(_id);
        }

        public void Start() // Добавить новый метод
        {
            if (!IsMayBeStart) return;
            foreach (var each in PlayersRepository.All)
            {
                each.IsPlay = true;
            }
            PlayersRepository.IsNeedAllUpdate();
        }

        public WaitWebModel Model()
        {
            return new WaitWebModel
            {
                Id = _id,
                Current = _current ?? new Player(),
                All = PlayersRepository.All,
                IsMayBeStart = IsMayBeStart, // Добавить
            };
        }
    }
}

Now modify the WaitPartial.cshtml partial view to display the start game button.

Listing: Modified partial view Wait/WaitPartial.cshtml

@model WaitWebModel

<div class="my-1">
    <p class="form-label">Ваша готовность:</p>
    <button class="btn @(Model.Current.IsReady ? "btn-success" : "btn-danger")"
            hx-get="@Url.Action("SwitchReady", new { Model.Id })"
            hx-target="#wait">
        @(Model.Current.IsReady ? "Готов" : "Не готов")
    </button>
</div>

<p>Все зарегистрированные игроки:</p>
<table class="table table-striped table-bordered">
    <thead>
        <tr><th>Id</th><th>Имя</th><th>Готовность</th>
          <th class="col-1"></th></tr>
    </thead>
    <tbody>
        @foreach (var each in Model.All)
        {
            <tr>
                <th scope="row" class="align-middle">@each.Id</th>
                <td class="align-middle">@each.Name</td>
                <td class="align-middle text-white @(each.IsReady 
                  ? "bg-success" : "bg-danger")">
                    @(each.IsReady ? "Готов" : "Не готов")
                </td>
                <td>
                    @if (each.Id != Model.Id)
                    {
                        <button class="btn btn-warning"
                                hx-get="@Url.Action("Kick", 
                                new { Model.Id, playerId = each.Id })"
                                hx-target="#wait">
                            Выгнать
                        </button>
                    }
                </td>
            </tr>
        }
    </tbody>
</table>

@if (Model.IsMayBeStart) // Добавить весь новый блок
{
    <button class="btn btn-success"
            hx-get="@Url.Action("Start", new { Model.Id })"
            hx-swap="none">Начать игру</button>
}

And last but not least, you need to make changes to the Wait controller. You need to add a game start method to it and add a redirection to another controller when the game starts in the Index method.

Listing: Modified Wait controller

using System.Net;
using System.Web.Mvc;
using SpyOnlineGame.Web.Hypermedia;

namespace SpyOnlineGame.Controllers
{
    public class WaitController : Controller
    {
        public ActionResult Index(int id)
        {
            var hypermedia = new WaitHypermedia(Request, id);
            if (hypermedia.IsNotFound)
            {
                if (!hypermedia.IsHtmx) return RedirectToAction("Index", "Home");
                Response.Headers.Add("hx-redirect", Url.Action("Index", "Home"));
            }
            if (hypermedia.IsGameStarted) // Добавить весь блок
            {
                Response.Headers.Add("hx-redirect", 
                  Url.Action("Index", "Game", new { id }));
            }
            if (hypermedia.IsNoContent) 
                return new HttpStatusCodeResult(HttpStatusCode.NoContent);

            if (hypermedia.IsHtmx)
            {
                return PartialView("Partial/WaitPartial", hypermedia.Model());
            }
            return View(hypermedia.Model());
        }
…
        public ActionResult Logout(int id)
        {
            var hypermedia = new WaitHypermedia(Request, id);
            hypermedia.Logout();

            return RedirectToAction("Index", "Home");
        }

        public void Start(int id) // Добавить новый метод
        {
            var hypermedia = new WaitHypermedia(Request, id);
            hypermedia.Start();
        }
    }
}

Now you can launch the application and check the game launch. You need to open two additional browser tabs, register in the game and enable the ready state. After pressing the start game button, all three tabs will be redirected to the non-existent Game controller.

The Index() method redirects to the Index() action method of the Game controller with the boundary operator of checking the game's running status. Such a controller and method do not yet exist. Add it and fill it with the contents from the following listing.

Listing: Game Controller Game

using System.Web.Mvc;

namespace SpyOnlineGame.Controllers
{
    public class GameController : Controller
    {
        public ActionResult Index(int id)
        {
            return View();
        }
    }
}

Add a new visual representation with empty content to display a blank game page.

Listing: Visual representation without content Views/Game/Index.cshtml

@{
    ViewBag.Title = "Игра";
    Layout = "~/Views/Shared/_GameLayout.cshtml";
}

<h4>@ViewBag.Title</h4>

Next, we will add a ban on registering new participants when the game is already running. To do this, we will add a new boundary operator to the Registration() method of the Home controller. If there is at least one user in the repository with the flag of participation in the game, we will redirect to the main page of the game. This will prohibit further registration of players.

Listing: Modified Registration() action method of Home controller

…
       [HttpPost]
       public ActionResult Registration(RegistrationWebModel model)
       {
           if (PlayersRepository.All.Any(p => p.IsPlay)) // Добавить
             return RedirectToAction("Index"); // Добавить
           var player = model.Map();
           var id = PlayersRepository.Add(player);
           return RedirectToAction("Index", "Wait", new { id });
       }
…

Run the application in debug mode, open two more additional windows and register three game participants. After setting all three participants to the “Ready” status, a game start button will appear. After clicking this button, all three game participants will be redirected to the game page.

The following screenshot shows the three running application windows at the top before the game is launched. And the bottom part of the window shows the same windows after clicking the “Start Game” button.

We have successfully completed the implementation of the page for waiting for participants to register. We can proceed to the implementation of the next page, with the functionality of the game itself.

Game Page

Let's get down to the main part of the application. We'll start implementing the game page with the simplest thing – displaying the participant's name. Then we'll add the initial initialization of the game. Then we'll gradually fill this page with interactive game elements.

Create a new game web model called GameWebModel. This model will pass data for the visual representation of the game page. Fill it with the content of the following listing.

Listing: Initial web model GameWebModel

using SpyOnlineGame.Models;

namespace SpyOnlineGame.Web.Models
{
    public class GameWebModel
    {
        public int Id { get; set; }
        public Player Current { get; set; }
    }
}

Next, create a new hypermedia model of the game GameHypermedia. This hypermedia model will contain all the logic from the controller action method. It will be very similar to another hypermedia model. We will not refactor hypermedia models and form an abstract base hypermedia model due to the inappropriateness of such refactoring. Fill the hypermedia model class with the contents of the following listing.

Listing: GameHypermedia Hypermedia Model

using System.Linq;
using System.Web;
using SpyOnlineGame.Data;
using SpyOnlineGame.Models;
using SpyOnlineGame.Web.Models;

namespace SpyOnlineGame.Web.Hypermedia
{
    public class GameHypermedia
    {
        private readonly HttpRequestBase _request;
        private readonly int _id;
        private readonly Player _current;

        public bool IsHtmx => _request.Headers.AllKeys.Contains("hx-request");

        public bool IsNotFound => _current is null;

        public GameHypermedia(HttpRequestBase request, int id)
        {
            _request = request;
            _id = id;

            _current = PlayersRepository.GetById(_id);
        }

        public GameWebModel Model()
        {
            return new GameWebModel
            {
                Id = _id,
                Current = _current ?? new Player(),
            };
        }
    }
}

Now let's change the contents of the Game controller's action method so that it passes the payload to the view and also returns a partial view with the voting table in case of a hypermedia request. Add the new contents as shown in the following listing.

Listing: Modified Game Controller Game

using System.Web.Mvc;

namespace SpyOnlineGame.Controllers
{
    public class GameController : Controller
    {
        public ActionResult Index(int id) // полностью заменить содержимое
        {
            var hypermedia = new GameHypermedia(Request, id);
            if (hypermedia.IsNotFound)
            {
                if (!hypermedia.IsHtmx) return RedirectToAction("Index", "Home");
                Response.Headers.Add("hx-redirect", Url.Action("Index", "Home"));
            }

            if (hypermedia.IsHtmx)
            {
                return PartialView("Partial/VotingPartial", hypermedia.Model());
            }
            return View(hypermedia.Model());
        }
    }
}

Create a new voting partial view Views/Game/Partial/VotingPartial.cshtml and fill it with empty content. We will fill it with content later.

Listing: Partial visual representation Views/Game/Partial/VotingPartial.cshtml

@model GameWebModel

<h6>Открытое голосование за определение шпиона</h6>

Change the visual representation of the game page as in the following listing.

Listing: Visual representation Views/Game/Index.cshtml

@model GameWebModel // Добавить
@{
    ViewBag.Title = "Игра";
    Layout = "~/Views/Shared/_GameLayout.cshtml";
}

<h4>@ViewBag.Title</h4>

<p>Ваше имя: <strong>@Model.Current.Name</strong></p> // Добавить

@Html.Partial("Partial/VotingPartial", Model) // Добавить

So far this visual representation doesn't show anything useful other than the current participant's name. However, this is the main bulk of the application. Next we need to add the functionality of game initialization.

Initializing the game

This functionality is triggered only once after launching from the waiting page for participants to register. For the sake of simplicity of this example, we will not implement a reset of the game. The main goal of this article is to get acquainted with hypermedia systems. Therefore, we will focus primarily on the interactive visual elements of the pages. However, before adding further hypermedia systems, we must create the logic of the initial initialization of the game. Without a correctly initialized game, there is no point in moving further.

First, let's create a location source. The purpose of this source is to randomly select a location from the list of available locations. Create a new LocationsSource class in the Models folder and fill it with the content from the following listing.

Listing: LocationsSource list of available locations

using System;

namespace SpyOnlineGame.Models
{
    public static class LocationsSource
    {
        private static Random _rand = new Random();

        private static string[] _locations =
        {
            "Банк",
            "Казино",
            "Больница",
            "Офис",
            "Казино",
        };

        public static string GetRandomLocation()
        {
            var locationNum = _rand.Next(_locations.Length);

            return _locations[locationNum];
        }
    }
}

You can add any number of your unique places. The number of places in this class, as you can see, is not limited, the main thing is that the spy could guess about them.

Add a field to store the assigned role to the player in the Player class as in the following listing. During the initialization of the game, one of the participants must be assigned the role of a spy. This participant must not be shown a secretly selected random location. Only peaceful players must know about the location.

Create a new enumerated type RoleCode in the Models folder. Fill it with the contents from the following listing.

Listing: Player Roles RoleCode

namespace SpyOnlineGame.Models
{
    public enum RoleCode
    {
        Honest,
        Spy,
    }
}

Add a usage of this new type to the Player class.

Listing: Player game participant model

namespace SpyOnlineGame.Models
{
    public class Player
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool IsReady { get; set; }
        public bool IsPlay { get; set; }
        public RoleCode Role { get; set; } // Добавить
        public bool IsNeedUpdate { get; set; }
    }
}

Add new properties to the game's web model to convey information about the selected location and who asked the question first to the visual representation.

Listing: GameWebModel web model with new properties

using SpyOnlineGame.Models;

namespace SpyOnlineGame.Web.Models
{
    public class GameWebModel
    {
        public int Id { get; set; }
        public Player Current { get; set; }
        public string Location { get; set; } // Добавить
        public string FirstName { get; set; } // Добавить
    }
}

In the hypermedia model GameHypermedia, first of all, a new method for initializing a new game must be added. Additionally, another useful property must be added – a sign of the need to launch the game.

Listing: Updated hypermedia model GameHypermedia

using System;
using System.Linq;
using System.Web;
using SpyOnlineGame.Data;
using SpyOnlineGame.Models;
using SpyOnlineGame.Web.Models;

namespace SpyOnlineGame.Web.Hypermedia
{
    public class GameHypermedia
    {
        private static string _location; // Добавить
        private static string _firstName; // Добавить
        private readonly Random _rand = new Random(); // Добавить
        private readonly HttpRequestBase _request;
        private readonly int _id;
        private readonly Player _current;

        public bool IsHtmx => _request.Headers.AllKeys.Contains("hx-request");
        public bool IsNotFound => _current is null;

        public bool IsNeedInit => string.IsNullOrEmpty(_location) &&
          PlayersRepository.All.Any(p => p.IsPlay); // Добавить

        public GameHypermedia(HttpRequestBase request, int id)
        {
            _request = request;
            _id = id;
            _current = PlayersRepository.GetById(_id);
        }

        public void Init() // Добавить метод
        {
            if (!IsNeedInit) return;
            var all = PlayersRepository.All.ToArray();
            var firstNum = _rand.Next(all.Length);
            _firstName = all[firstNum].Name;
            _location = LocationsSource.GetRandomLocation();
            var spyNum = _rand.Next(all.Length);
            all[spyNum].Role = RoleCode.Spy;
        }

        public GameWebModel Model()
        {
            return new GameWebModel
            {
                Id = _id,
                Current = _current ?? new Player(),
                Location = _location, // Добавить
                FirstName = _firstName, // Добавить
            };
        }
    }
}

In the Index() action method of the Game controller, you need to add a boundary operator to check whether the game needs to be launched, as in the following screenshot. If launching is necessary, you need to call the game initialization method.

Listing: Updated Index() method of Game controller

…
        public ActionResult Index(int id)
        {
            var hypermedia = new GameHypermedia(Request, id);
            if (hypermedia.IsNotFound)
            {
                if (!hypermedia.IsHtmx) return RedirectToAction("Index", "Home");
                Response.Headers.Add("hx-redirect", Url.Action("Index", "Home"));
            }
            if (hypermedia.IsNeedInit) hypermedia.Init(); // Добавить

            if (hypermedia.IsHtmx)
            {
                return PartialView("Partial/VotingPartial", hypermedia.Model());
            }
            return View(hypermedia.Model());
        }
…

In this method, the game is initialized only once. It is performed as soon as any first participant goes to the game page from the waiting page for players.

Let's finish adding this new functionality of displaying game information on the game page. After that, you can run the application and check that the game is initialized correctly. Edit the visual representation of the game as in the following listing.

Listing: Visual representation Views/Game/Index.cshtml

@model GameWebModel
@{
    ViewBag.Title = "Игра";
    Layout = "~/Views/Shared/_GameLayout.cshtml";
}

<h4>@ViewBag.Title</h4>

<p>Ваше имя: <strong>@Model.Current.Name</strong></p>

<div class="my-1"> // Добавить новый блок
    <span>Загадано место: <strong>@Model.Location</strong>.</span>
</div>

<div class="my-1"> // Добавить новый блок
    @if (Model.Current.Name == Model.FirstName)
    {
        <p>Вы <strong>первым</strong> задаете вопрос любому другому игроку.</p>
    }
    else
    {
          Первым вопрос задаёт <p><strong>@Model.FirstName</strong>.</p>
    }
</div>

@Html.Partial("Partial/VotingPartial", Model)

Now run the application in debug mode. Run two more browser windows, register three users at once, set the status to ready and run the game. You should see the result shown in the following screenshot. It should display a randomly selected location and information about who asked the question first. Each participant in the game should be assigned a role – there should be only one spy.

If everything is OK with the game initialization, then you can proceed to filling this page with interactive game elements.

Completion of the second part

This concludes the second part of my article. In this part, you refactored, filled the waiting page for game participants to register with interactive elements, and created the game page itself. In the next part, we will fill this page with interactive elements and complete the development of this small game application.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *