Hypermedia Systems on ASP.NET MVC 5. Part Three

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

Continuation of the second part

In the previous part, you refactored, filled the waiting page for game participants to register with interactive elements, and created the game page itself. In this part, we will complete the creation of the game application.

Game Page Interactivity

This game page needs to include a voting table for identifying the spy, a drop-down voting list, a voting button, a hidden display of the hidden location from the spy, and a display of the game rules. With the help of hypermedia systems, we can easily cope with these tasks. Let's start with the easiest – a hidden display of the hidden location.

Refactoring web models

Before adding new functionality, we will do a small refactoring, designed to make it easier for us to create the next new functionality. Functionally, the application will not change, but structurally it will improve a little. We will make our web models a little more responsible and shift some responsibilities from hypermedia models to them.

Create a new player web model PlayerWebModel and fill it with the contents of the following listing.

Listing: PlayerWebModel game participant web model

using SpyOnlineGame.Models;

namespace SpyOnlineGame.Web.Models
{
    public class PlayerWebModel
    {
        public static PlayerWebModel Default => new PlayerWebModel();


        public static PlayerWebModel Create(Player current)
        {
            return new PlayerWebModel
            {
                Id = current.Id,
                Name = current.Name,
                IsReady = current.IsReady,
                Role = current.Role,
            };
        }

        public int Id { get; private set; }
        public string Name { get; private set; }
        public bool IsReady { get; private set; }
        public RoleCode Role { get; private set; }
    }
}

With the help of this new web model of the game participant, it will become a little more convenient to transfer data to our visual representations of the game participants. Also in this file we maximally concentrate all the logic of working with this web model as from other parts of the application. In this web model, we created new static methods Create() and Default designed to transfer responsibility for creating the web model to the web model itself. For the same purpose, we specifically make the properties of this model inaccessible for writing from the outside. This makes our application a little cleaner and simpler in those places where this web model will be used.

Let's do a similar refactoring of all other web models. Make changes to the web model of game participants' expectations.

Listing: WaitWebModel participant waiting web model

using System;
using System.Collections.Generic;
using System.Linq; // Добавить
using SpyOnlineGame.Data; // Добавить
using SpyOnlineGame.Models;

namespace SpyOnlineGame.Web.Models
{
    public class WaitWebModel
    {
        public static WaitWebModel Create(int id, Player current, 
          bool isMayBeStart) // Добавить новый метод
        {
            return new WaitWebModel
            {
                Id = id,
                Current = PlayerWebModel.Create(current) 
                  ?? PlayerWebModel.Default,
                All = PlayersRepository.All.Select(PlayerWebModel.Create),
                IsMayBeStart = isMayBeStart,
            };
        }

        public int Id { get; private set; } // Изменить
        public PlayerWebModel Current { get; private set; } // Изменить
        public IEnumerable<PlayerWebModel> All { get; private set; } 
          = Array.Empty<PlayerWebModel>(); // Изменить
        public bool IsMayBeStart { get; private set; } // Изменить
    }
}

Then make similar changes to the web model of the game GameWebModel.

Listing: Web model of participants' expectations GameWebModel

using SpyOnlineGame.Models;

namespace SpyOnlineGame.Web.Models
{
    public class GameWebModel
    {
        public static GameWebModel Create(int id, Player current, 
          string location, string firstName) // Добавить новый метод
        {
            return new GameWebModel
            {
                Id = id,
                Current = PlayerWebModel.Create(current) 
                  ?? PlayerWebModel.Default,
                Location = location,
                FirstName = firstName,
            };
        }

        public int Id { get; private set; } // Изменить
        public PlayerWebModel Current { get; private set; } // Изменить
        public string Location { get; private set; } // Изменить
        public string FirstName { get; private set; } // Изменить
    }
}

Now we can remove responsibility from the hypermedia model of waiting for registration of participants of the WaitHypermdedia game, as in the following listing.

Listing: WaitHypermedia Registration Wait Model

…
        public void Start()
        {
            if (!IsMayBeStart) return;
            foreach (var each in PlayersRepository.All)
            {
                each.IsPlay = true;
            }
            PlayersRepository.IsNeedAllUpdate();
        }

        public WaitWebModel Model() =>
            WaitWebModel.Create(_id, _current, IsMayBeStart); // Изменить
…

In analogy with this model, also remove responsibility from the GameHypermedia game model, as in the following listing.

Listing: Hypermedia Game Model GameHypermedia

…
        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() =>
           GameWebModel.Create(_id, _current, _location, _firstName); // Изменить
…

This refactoring will make it a little easier for us to add a private preview of a hidden location to our application.

Closed viewing of the secret place

Now all the participants of the game are shown both their roles and the hidden place. We did this specifically to check the correctness of the initialization of the game. But this should not be according to the rules of the game. The game moment is such that peaceful players must secretly find out the hidden place, and the spy must find out his role instead of the place. We will implement this interactive function based on a hypermedia element, which will contain a show/hide button, a warning about stealth and secret text.

Let's start with the web model. Create a new web model called LocationWebModel. This model is only intended to pass data to a separate partial view of the location button display.

Listing: LocationWebModel location display webmodel

using SpyOnlineGame.Models;

namespace SpyOnlineGame.Web.Models
{
    public class LocationWebModel
    {
        public static LocationWebModel Create(int id, Player current, 
          string location, bool isShow)
        {
            return new LocationWebModel
            {
                Id = id,
                Role = current?.Role ?? RoleCode.Honest,
                Location = location,
                IsShow = isShow,
            };
        }

        public int Id { get; set; }
        public RoleCode Role { get; set; }
        public string Location { get; set; }
        public bool IsShow { get; set; }
    }
}

And in the GameWebModel web model, you should remove the location display property, since the new web model is now responsible for transmitting this information. Remove this property as in the following listing.

Listing: GameWebModel game web model

using SpyOnlineGame.Models;

namespace SpyOnlineGame.Web.Models
{
    public class GameWebModel
    {
        public static GameWebModel Create(int id, Player current, 
          string firstName) // Изменить
        {
            return new GameWebModel
            {
                Id = id,
                Current = PlayerWebModel.Create(current) 
                  ?? PlayerWebModel.Default,
                // Удалить
                FirstName = firstName,
            };
        }

        public int Id { get; private set; }
        public PlayerWebModel Current { get; private set; }
        // Удалить
        public string FirstName { get; private set; }
    }
}

The next step is to add a new method to the hypermedia model to help display the hidden location. Add the new method shown in the following listing.

Listing: New Method of Hypermedia Model GameHypermedia

…
        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 LocationWebModel Location(bool isShow) =>
            LocationWebModel.Create(_id, _current, _location, !isShow); // Замена

        public GameWebModel Model() =>
            GameWebModel.Create(_id, _current, _firstName); // Замена
…

Add a new action method called Location to the Game controller, as in the following listing.

Listing: New Game Controller Method

…
        public ActionResult Index(int id)
        {
…
        }

        public ActionResult Location(int id, bool isShow) // Добавить метод
        {
            var hypermedia = new GameHypermedia(Request, id);
            var model = hypermedia.Location(isShow);
            return PartialView("Partial/LocationPartial", model);
        }
…

This new Game controller action method returns a partial view that we haven't created yet. Create a new partial view View/Game/Partial/LocationPartial.cshtml and fill it with the following listing.

Listing: Partial view Views/Game/Partial/LocationPartial.cshtml

@using SpyOnlineGame.Models
@model LocationWebModel

@if (Model.IsShow)
{
    <p>Загадано место: <strong>@(Model.Role == RoleCode.Spy 
        ? "Вы шпион" : Model.Location)</strong>.</p>
}
else
{
    <p>Держите место в тайне от другого игрока. Возможно он шпион.</p>
}

<button class="btn btn-dark"
        hx-get="@Url.Action("Location", new { Model.Id, Model.IsShow })"
        hx-target="#location">
    @(Model.IsShow ? "Скрыть" : "Показать")
</button>

In the game page visual representation, replace the role and location display with this new partial view, as in the following listing.

Listing: Modified visual representation Views/Game/Index.cshtml

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

<h4>@ViewBag.Title</h4>

<div class="my-1">
    <p>Ваше имя: <strong>@Model.Current.Name</strong></p>
</div>

<div id="location" class="my-1"> // Добавить весь новый блок
    @Html.Partial("Partial/LocationPartial", 
    new LocationWebModel { Id = Model.Id })
</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)

Run the app to test and see how the new functionality works. Each time you click this button, the location information is shown or hidden, as intended.

Voting table

Let's add new properties to the game participant model intended for voting functionality, as in the following listing.

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 int VotePlayerId { get; set; } // Добавить
        public bool IsVoted { get; set; } // Добавить
        public bool IsNeedUpdate { get; set; }
    }
}

The first property added is to store the player's vote – the identifier of the player he considers a spy. And the second property is an indication that other players have voted for this player. Next, add new properties to the player's web model to convey this new voting information to the visual representations.

Listing: Modified web model of the game participant PlayerWebModel

using SpyOnlineGame.Data;
using SpyOnlineGame.Models;

namespace SpyOnlineGame.Web.Models
{
    public class PlayerWebModel
    {
        public static PlayerWebModel Default => new PlayerWebModel();
        public static PlayerWebModel Create(Player current)
        {
            return new PlayerWebModel
            {
                Id = current.Id,
                Name = current.Name,
                IsReady = current.IsReady,
                Role = current.Role,
                VotePlayerId = current.VotePlayerId, // Добавить
                VotePlayerName = PlayersRepository.GetById(current.VotePlayerId)?
                  .Name ?? "Нет", // Добавить
                IsVoted = current.IsVoted, // Добавить
            };
        }

        public int Id { get; private set; }
        public string Name { get; private set; }
        public bool IsReady { get; private set; }
        public RoleCode Role { get; private set; }
        public int VotePlayerId { get; set; } // Добавить
        public string VotePlayerName { get; set; } // Добавить
        public bool IsVoted { get; set; } // Добавить
    }
}

In this web model, we added another property, VotePlayerName, which is intended specifically for visual representation. After all, in the web model, you need to display not the player's ID, but his name. Change the web model of the game GameWebModel so that we can pass the list of game participants to the visual representation. We will act in order and first display the table of participants, then the drop-down list for voting, and only then the button to confirm the vote. Add new properties to the GameWebModel class, as in the following listing.

Listing: GameWebModel web model

using SpyOnlineGame.Models;
using System.Collections.Generic; // Добавить
using System; // Добавить
using System.Linq; // Добавить
using SpyOnlineGame.Data; // Добавить

namespace SpyOnlineGame.Web.Models
{
    public class GameWebModel
    {
        public static GameWebModel Create(int id, Player current, 
          string firstName)
        {
            var all = PlayersRepository.All.Where(p => p.IsPlay)
              .Select(PlayerWebModel.Create); // Добавить

            return new GameWebModel
            {
                Id = id,
                Current = PlayerWebModel.Create(current) 
                  ?? PlayerWebModel.Default,
                FirstName = firstName,
                All = all, // Добавить
            };
        }

        public int Id { get; private set; }
        public PlayerWebModel Current { get; private set; }
        public string FirstName { get; private set; }
        public IEnumerable<PlayerWebModel> All { get; set; } 
          = Array.Empty<PlayerWebModel>(); // Добавить
    }
}

This time, neither the hypermedia model of the game nor the controller need to be changed. Since they are exempt from the responsibility of forming the web model, no changes need to be made to them at this stage. At this stage, the first thing to do is to correctly draw the table of game participants together with the drop-down list of votes.

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

@model GameWebModel

<h6>Открытое голосование за определение шпиона</h6>
<table class="table table-striped table-bordered"> // Добавить новую таблицу
    <thead>
        <tr>
            <th>Игрок:</th>
            @foreach (var each in Model.All)
            {
                <th>@each.Name</th>
            }
        </tr>
    </thead>
    <tbody>
        <tr>
            <th scope="row" class="align-middle">Голос:</th>
            @foreach (var each in Model.All)
            {
                if (each.Id == Model.Id)
                {
                    <td>Список голосования</td>
                }
                else
                {
                    <td class="align-middle">@each.VotePlayerName</td>
                }
            }
        </tr>
    </tbody>
</table>

In this visual representation, you see text instead of a voting list. Run the application and check that it works correctly at this stage. The next stage is even more complex and you should definitely make sure that the application works correctly now.

The next step is to replace the fictitious implementation of the list with an obvious one. An obvious implementation involves instantly showing all participants any changes in the voting table. That is, as soon as someone changes their decision about who the spy is, this decision must be immediately shown to all other participants. And at the final stage, when all players become sure who the spy is, they must be shown a button to confirm the vote. And pressing this button will decide who won the game.

After completing all the steps to register three players and launch the game, you should see the result shown in the following screenshot.

In the screenshots provided, you should see a new table with the players' votes. Each participant will have the ability to vote via a drop-down list in their browser page. It's time to move on to the most complex part of our application.

Voting drop down list

At this point, we will finally implement the voting drop-down list. Any change to the selection in this drop-down list should be immediately transmitted to all other participants in the game. We will make the participant display table hypermedia, so that it periodically updates itself. And we will also make the drop-down list itself hypermedia. After any change to the selection in the list, we will send the selected item to the server. In this way, we link these two hypermedia elements together into one element – the voting hypermedia table.

Add a new helper class named GameHelpers to the new Common folder.

Listing: Common/GameHelpers Helper Class

using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using SpyOnlineGame.Data;
using SpyOnlineGame.Models;
using SpyOnlineGame.Web.Models;

namespace SpyOnlineGame.Common
{
    public static class GameHelpers
    {
        public static IEnumerable<SelectListItem> CreateVariantsForDropDown(
            this Player current, int id)
        {
            var othersLivesPlayer = CreateAllPlayers().Where(p => p.Id != id);
            var variants = new List<SelectListItem> { new SelectListItem
                { Value = "0", Text = "Сомневаюсь" } };
            variants.AddRange(othersLivesPlayer.Select(p =>
                new SelectListItem
                {
                    Value = p.Id.ToString(),
                    Text = p.Name,
                    Selected = p.Id == current.VotePlayerId,
                }));
            return variants;
        }

        public static bool CheckMayBeVote(int id)
        {
            return MaxVoteCount(id) >= CountOfLivesPlayers() - 1;
        }

        public static int MaxVoteCount(int id)
        {
            var grouped = GroupedByVotePlayers(id);
            if (!grouped.Any()) return 0;
            return grouped.Select(p => p.Count()).Max();
        }

        public static IGrouping<int, PlayerWebModel>[] GroupedByVotePlayers(
          int id)
        {
            var actualPlayers = CreateAllPlayers().Where(p => p.VotePlayerId != 0
              && p.VotePlayerId != id);
            var result = actualPlayers.GroupBy(p => p.VotePlayerId).ToArray();
            return result;
        }

        public static int CountOfLivesPlayers()
        {
            return CreateAllPlayers().Count(p => !p.IsVoted);
        }

        public static IEnumerable<PlayerWebModel> CreateAllPlayers()
        {
            return PlayersRepository.All.Where(p => p.IsPlay)
                .Select(PlayerWebModel.Create);
        }
    }
}

These helper class methods will be used by other classes in our application. They may look complicated, but they will greatly simplify other parts of the application. In the example of this article, I try to move all the complexity from the more important classes to the helper classes. This increases the chance of reusing existing code from the helper classes and reducing the overall amount of code in the application.

Before we start making changes to the hypermedia model, we need to prepare the web model of the game GameWebModel. We need to add a transfer to the visual representation of the list of players for rendering the drop-down list and a sign of rendering the voting confirmation button. Add changes as in the listing below.

Listing: Modified web model of the game GameWebModel

using SpyOnlineGame.Models;
using System.Collections.Generic;
using System;
using System.Web.Mvc; // Добавить
using SpyOnlineGame.Data;

namespace SpyOnlineGame.Web.Models
{
    public class GameWebModel
    {
        public static GameWebModel Create(int id, Player current, 
          string firstName)
        {
            return new GameWebModel
            {
                Id = id,
                Current = PlayerWebModel.Create(current) 
                          ?? PlayerWebModel.Default,
                FirstName = firstName,
                All = GameHelpers.CreateAllPlayers(), // Изменить
             PlayersVariants = current.CreateVariantsForDropDown(id), // Изменить
                IsMayBeVote = GameHelpers.CheckMayBeVote(id), // Изменить
            };
        }

        public int Id { get; private set; }
        public PlayerWebModel Current { get; private set; }
        public string FirstName { get; private set; }
        public IEnumerable<PlayerWebModel> All { get; private set; } 
            = Array.Empty<PlayerWebModel>();
        public IEnumerable<SelectListItem> PlayersVariants { get; private set; } 
            = Array.Empty<SelectListItem>(); // Добавить
        public bool IsMayBeVote { get; private set; } // Добавить
    }
}

Now we can move on to the hypermedia model of the game. To support regular updating of the participant table and support for the hypermedia drop-down list, we need to add changes to the hypermedia model GameHypermedia, as in the following listing.

Listing: Modified hypermedia model of the game GameHypermedia

…
        public bool IsNotFound => _current is null;

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

        public bool IsNeedInit => 
            string.IsNullOrEmpty(_location) && PlayersRepository.All.Any(p => p.IsPlay);

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

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

        public void Init()
        {
…
        }

        public void Select(int votePlayerId) // Добавить новый метод
        {
            _current.VotePlayerId = votePlayerId;
            PlayersRepository.IsNeedAllUpdate();
        }

        public LocationWebModel Location(bool isShow) =>
            LocationWebModel.Create(_id, _current, _location, !isShow);

        public GameWebModel Model() =>
            GameWebModel.Create(_id, _current, _firstName);
    }
}

Once these changes have been added to the hypermedia model, the game controller can be moved on. The new controller action method should use this new hypermedia model method.

Listing: Changes in the Game Controller Game

…
        public ActionResult Index(int id)
        {
            var hypermedia = new GameHypermedia(Request, id);
            if (hypermedia.IsNotFound)
            {
                return RedirectToAction("Index", "Home");
            }
            if (hypermedia.IsNeedInit) hypermedia.Init();
            if (hypermedia.IsNoContent) // Добавить
              return new HttpStatusCodeResult(HttpStatusCode.NoContent); // Добавить

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

        public ActionResult Location(int id, bool isShow)
        {
…
        }

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

Now let's change the visual presentation of the game page Views/Game/Index.cshtml. Replace the simple partial view display with a hypermedia element.

Listing: New element in visual view Views/Game/Index.cshtml

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

<div id="voting" class="my-1"
     hx-get="@Url.Action("Index", "Game", new { Model.Id })"
     hx-trigger="every 1s"> // Заменить новым блоком
    @Html.Partial("Partial/VotingPartial", Model)
</div>

We have added another hypermedia element of regular updating of the game participants table to this visual representation. It will periodically request a modified table from the server every second. If there are no changes, it will do nothing. Everything is similar to the page for waiting for registration of all players. All that remains is to change this partial representation of the display of game participants.

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

@model GameWebModel

<h6>Открытое голосование за определение шпиона</h6>
<table class="table table-striped table-bordered">
    <thead>
        <tr>
            <th>Игрок:</th>
            @foreach (var each in Model.All)
            {
                <th>@each.Name</th>
            }
        </tr>
    </thead>
    <tbody>
        <tr>
            <th scope="row" class="align-middle">Голос:</th>
            @foreach (var each in Model.All)
            {
                if (each.Id == Model.Id)
                {
                    <td>
                        // Добавить следую новый блок вместо текста
                        @Html.DropDownList("VotePlayerId", Model.PlayersVariants,
                          null, new
                        {
                            @class = "form-select",
                            hx_get = Url.Action("Select", "Game", 
                              new { Model.Id }),
                            hx_target = "#voting",
                        }) 
                    </td>
                }
                else
                {
                    <td class="align-middle">@each.VotePlayerName</td>
                }
            }
        </tr>
    </tbody>
</table>

@if (Model.IsMayBeVote) // Добавить новый блок
{
    <button class="btn btn-primary mt-2">
        Подтвердить голосование
    </button>
}

After replacing the dummy list designation with an obvious implementation, everything falls into place. Run the application in debug mode and check its functionality. You should see the result shown in the screenshot below, when two of the three participants decide who the spy is and select it from the drop-down list.

As you can see, as soon as two out of three participants decide who is the spy, a voting button will appear on the pages of the players who voted. Clicking on this button will not lead to anything – we have only added a fake implementation of it so far. We should work on the obvious implementation of this button.

Vote confirmation button

Let's create a new class of helper methods named VotedHelpers next to the GameHelpers class. This class will contain helper functions for voting. Fill it with the contents from the listing.

Listing: GameHelpers Helper Class

using System.Linq;
using SpyOnlineGame.Data;

namespace SpyOnlineGame.Common
{
    public static class VotedHelpers
    {
        public static int? GetVotedPlayerId()
        {
            var grouped = GameHelpers.GroupedByVotePlayers(0);
            var result = grouped
                .Select(p => new { p.Key, count = p.Count() })
                .OrderByDescending(p => p.count)
                .FirstOrDefault()?.Key;
            return result;
        }

        public static void VotedOfPlayer(int? votePlayerId)
        {
            if (votePlayerId == null) return;
            var votedPlayer = PlayersRepository.GetById((int)votePlayerId);
            votedPlayer.IsVoted = true;
            foreach (var each in PlayersRepository.All)
            {
                each.VotePlayerId = 0;
            }
            PlayersRepository.IsNeedAllUpdate();
        }
    }
}

These helper methods will be used by the application's hypermedia models. Add a new vote confirmation method to the GameHypermedia game model, as in the following listing.

Listing: New Method in the Hypermedia Game Model GameHypermedia

…
        public void Select(int votePlayerId)
        {
            _current.VotePlayerId = votePlayerId;
            PlayersRepository.IsNeedAllUpdate();
        }

        public void Confirm() // Новый метод
        {
            if (!GameHelpers.CheckMayBeVote(_id)) return;
            var votePlayerId = VotedHelpers.GetVotedPlayerId();
            VotedHelpers.VotedOfPlayer(votePlayerId);
        }

        public LocationWebModel Location(bool isShow) =>
            LocationWebModel.Create(_id, _current, _location, !isShow);

        public GameWebModel Model() =>
            GameWebModel.Create(_id, _current, _firstName);
…

Add a new small vote confirmation method to the Game controller.

Listing: New method in Game controller

…
        public ActionResult Select(int id, int votePlayerId)
        {
            var hypermedia = new GameHypermedia(Request, id);
            hypermedia.Select(votePlayerId);
            return Index(id);
        }

        public void Confirm(int id) // Новый метод
        {
            var hypermedia = new GameHypermedia(Request, id);
            hypermedia.Confirm();
        }
…

Now finally, Now finally, we can move on to the partial visual representation of the vote for the spy definition Views/Game/Partial/VotingPartial.cshtml. Replace it entirely with the contents of the following listing, as the changes are too extensive.

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

@using SpyOnlineGame.Models
@model GameWebModel

@if (Model.Current.IsVoted)
{
    <h6 class="bg-dark text-light p-2">Вы были убиты и теперь можете 
      только наблюдать</h6>
}
else
{
    <h6>Открытое голосование за определение шпиона</h6>
}
<div class="table-responsive">
    <table class="table table-striped table-bordered">
        <thead>
            <tr>
                <th>Игрок:</th>
                @foreach (var each in Model.All)
                {
                    if (each.IsVoted)
                    {
                        <th class="bg-dark text-light">@each.Name</th>
                    }
                    else
                    {
                        <th>@each.Name</th>
                    }
                }
            </tr>
        </thead>
        <tbody>
            <tr>
                <th scope="row" class="align-middle">Голос:</th>
                @foreach (var each in Model.All)
                {
                    if (each.IsVoted)
                    {
                        if (each.Role == RoleCode.Spy)
                        {
                            <td class="bg-danger text-light align-middle">
                              Шпион</td>
                        }
                        else
                        {
                            <td class="bg-success text-light align-middle">
                              Мирный</td>
                        }
                    }
                    else
                    {
                        if (each.Id == Model.Id)
                        {
                            <td>
                                @Html.DropDownList("VotePlayerId", 
                                  Model.PlayersVariants,
                                    null, new
                                    {
                                        @class = "form-select",
                                        hx_get = Url.Action("Select", "Game",
                                            new { Model.Id }),
                                        hx_target = "#voting",
                                    })
                            </td>
                        }
                        else
                        {
                            <td class="align-middle">@each.VotePlayerName</td>
                        }
                    }
                }
            </tr>
        </tbody>
    </table>
</div>

@if (Model.IsMayBeVote && !Model.Current.IsVoted)
{
    <button class="btn btn-primary mt-2"
            hx-get="@Url.Action("Confirm", new { Model.Id })">
        Уничтожить шпиона
    </button>
}

In this partial visualization, we added a change in the title and styling in the table depending on the player's state. The vote confirmation button should only be displayed to a player who is not being voted for and only when he is alive. When a player is no longer alive, his real role in this game should be displayed in the voting table.

Run the game and test the voting functionality, you should see something similar to the result shown in the following screenshot.

Make sure the voting works successfully. All that's left is to add the end of the game.

End of the game

According to the rules, the end of the game occurs in one of two cases:

  • In case of victory of the peaceful forces – when the spy is identified and successfully eliminated.

  • In case of a spy's victory – when the number of peaceful players becomes less than or equal to the number of spies.

We will again place such complex game rules in a special helper class. Add such a new class to the Common folder and name it RulesHelpers. Fill it with the contents from the following listing.

Listing: RulesHelpers Helper Class

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

namespace SpyOnlineGame.Common
{
    public static class RulesHelpers
    {
        public static bool CheckEndGame()
        {
            var lives = CreateLivesPlayers().ToArray();
            var spyCount = lives.Count(p => p.Role == RoleCode.Spy);
            var honestCount = lives.Count(p => p.Role == RoleCode.Honest);
            return spyCount == 0 || honestCount <= spyCount;
        }

        private static IEnumerable<PlayerWebModel> CreateLivesPlayers()
        {
            return GameHelpers.CreateAllPlayers().Where(p => !p.IsVoted);
        }
    }
}

We'll add a helper method from this class and a game-ending flag to the game's hypermedia model. Add the code shown in the following listing to the GameHypermedia class.

Listing: New code in the GameHypermedia hypermedia model

…
    public class GameHypermedia
    {
        private static string _location;
        private static string _firstName;
        private static bool _isEndGame; // Добавить
        private readonly Random _rand = new Random();
        private readonly HttpRequestBase _request;
        private readonly int _id;
        private readonly Player _current;
…
        public bool IsNeedInit => string.IsNullOrEmpty(_location) 
          && PlayersRepository.All.Any(p => p.IsPlay);

        public bool IsEndGame => _isEndGame; // Добавить

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

            _current = PlayersRepository.GetById(_id);
        }
…
        public void Select(int votePlayerId)
        {
            _current.VotePlayerId = votePlayerId;
            PlayersRepository.IsNeedAllUpdate();
        }

        public void Confirm()
        {
            if (!GameHelpers.CheckMayBeVote(_id)) return;
            var votePlayerId = VotedHelpers.GetVotedPlayerId();
            VotedHelpers.VotedOfPlayer(votePlayerId);
            if (RulesHelpers.CheckEndGame()) // Добавить
            {
                _isEndGame = true; // Добавить
            }
        }
…

In the Game controller, you need to add a redirect to the game results page in case the game ends.

Listing: Redirection in Game controller method

…
        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.IsEndGame) // Добавить блок кода
            {
                if (!hypermedia.IsHtmx) return RedirectToAction("Index", "End", 
                    new { id });
                Response.Headers.Add("hx-redirect", Url.Action("Index", "End", 
                    new { id }));
            }
            if (hypermedia.IsNoContent)
                return new HttpStatusCodeResult(HttpStatusCode.NoContent);

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

The next step is to add this new controller and an empty visual representation of the end of the game. Later, we will redesign it so that it displays the actual results of winning or losing the game to the participants. Fill the new EndController with the contents of the following listing.

Listing: End Game Controller

using System.Web.Mvc;

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

Listing: Visual representation Views/End/Index.cshtml

@{
    ViewBag.Title = "Конец игры";
}

<h2>Конец игры</h2>

Run the game in debug mode and play it for a bit. You should get something like the following screenshot. In this screenshot, the top portion shows three windows of three participants before the vote is confirmed. And after a successful vote, all three participants should be redirected to the action method of the newly added game over controller.

After the end of the game started working correctly in our game, we can move on to the final stage – displaying the game results on the end of the game page.

Game results

Currently, after the game is over, the participants are simply shown a page with information about the end of the game. The default Index() method of the End controller is passed the ID of the current player. Using this ID, we can show different visual representations to different participants depending on their team.

Add a new web model named EndWebModel and fill it with the contents of the following listing.

Listing: End of Game Web Model EndWebModel

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

namespace SpyOnlineGame.Web.Models
{
    public class EndWebModel
    {
        public static EndWebModel Create(Player current)
        {
            var honestPlayers = GameHelpers.CreateAllPlayers()
              .Where(p => p.Role == RoleCode.Honest);
            var spyPlayer = GameHelpers.CreateAllPlayers()
              .First(p => p.Role == RoleCode.Spy);
            var isWinOfHonestPlayers = spyPlayer.IsVoted;
            var isCurrentWin = current.Role == RoleCode.Honest &&
              isWinOfHonestPlayers || current.Role == RoleCode.Spy &&
              !isWinOfHonestPlayers;
            return new EndWebModel
            {
                IsWinOfHonestPlayers = isWinOfHonestPlayers,
                Current = PlayerWebModel.Create(current) ??
                  PlayerWebModel.Default,
                IsCurrentWin = isCurrentWin,
                HonestPlayers = honestPlayers,
                SpyPlayer = spyPlayer,
            };
        }
        // Победа мирных
        public bool IsWinOfHonestPlayers { get; private set; }
        // Текущий игрок
        public PlayerWebModel Current { get; private set; }
        // Текущий игрок - в команде победителей
        public bool IsCurrentWin { get; private set; }
        // Команда мирных игроков
        public IEnumerable<PlayerWebModel> HonestPlayers { get; private set; } =
          Array.Empty<PlayerWebModel>();
        // Шпион
        public PlayerWebModel SpyPlayer { get; private set; }
    }
}

This web model is quite complex, since its task is to correctly form data for visual representation of the end of the game. Such a complicated web model will allow us to get by with one visual representation, not four.

In this case, we don't need to create a hypermedia model, we can easily do with a regular end-of-game web service. Add a new folder \Web\Services and add a new web service class EndWebService to it. Fill it with the contents from the following listing.

Listing: End of game web service EndWebService

using SpyOnlineGame.Data;
using SpyOnlineGame.Models;
using SpyOnlineGame.Web.Models;

namespace SpyOnlineGame.Web.Services
{
    public class EndWebService
    {
        private readonly Player _current;

        public EndWebService(int id)
        {
            _current = PlayersRepository.GetById(id);
        }

        public EndWebModel Model() =>
            EndWebModel.Create(_current);
    }
}

Make changes to the main action method of the End game controller as in the following listing.

Listing: End Game Controller

using System.Web.Mvc;

namespace SpyOnlineGame.Controllers
{
    public class EndController : Controller
    {
        public ActionResult Index(int id)
        {
            var webService = new EndWebService(id); // Добавить
            return View(webService.Model()); // Добавить
        }
    }
}

Replace the contents of the visual representation completely to display the game results in it.

Listing: Visual representation Views/End/Index.cshtml

@model EndWebModel
@{
    ViewBag.Title = Model.IsCurrentWin ? "Победа" : "Проигрыш";
}

<div class="my-4 p-5 text-light rounded @(Model.IsCurrentWin 
    ? Model.IsWinOfHonestPlayers ? "bg-success" 
    : "bg-danger" : "bg-secondary")">
    <h1 class="text-center">@ViewBag.Title</h1>
    
    <p>Ваше имя: <strong>@Model.Current.Name</strong></p>

    @if (Model.IsWinOfHonestPlayers)
    {
        <p>Победа мирных.</p>
    }
    else
    {
        <p>Победа шпиона.</p>
    }
    @if (Model.IsCurrentWin)
    {
        <p>Вы выиграли в этой игре!</p>
    }
    else
    {
        <p>К сожалению, вы проиграли.</p>
    }

    <h5 class="mt-2">Мирные игроки:</h5>
    <ol class="list-group list-group-numbered">
        @foreach (var each in Model.HonestPlayers)
        {
            <li class="list-group-item">@each.Name</li>
        }
    </ol>
    
    <h5 class="mt-2">Шпион:</h5>
    <ol class="list-group list-group-numbered">
        <li class="list-group-item">@Model.SpyPlayer.Name</li>
    </ol>
</div>

Run the game again in debug mode and play it a little bit again. Try testing the app on several different options – spy win, civilian win, and increasing the number of players. Restart the app after each test.

You should get the correct game results every time, as in the screenshot below. This screenshot shows a simulation of a spy win, where a spy and a peaceful player vote to kick another peaceful player – this is the condition for the peaceful players to lose.

We have completed the creation of the online board game “Spy”. The application contains many hypermedia elements and can be easily expanded with new ones.

Continuation

As a continuation of the development of this game and consolidation of the acquired knowledge, you can independently implement several additional game features.

  1. Add display of game rules on the waiting and game pages. This can be implemented as a hypermedia button, clicking on which alternately displays/hides the rules of the board game “Spy” on the page.

  2. Implement a change in the number of spies. For example, by adding a hypermedia drop-down list to the game waiting page for regulating the number of spies. In this case, the limitation of the number of spies will depend on the number of registered players.

  3. Add a timer to the game page, after which the spies win.

These are all additional features that I will not cover in this article. I suggest you try to implement them yourself as a homework assignment.

Conclusion

In this article, we created a new MVC tutorial project on the legacy ASP.NET MVC 5 platform and used it to build a simple board game. Now you have a first idea about hypermedia systems based on the Htmx.js library. Also, you now have a basic knowledge of adding interactivity to a web application using hypermedia systems.

However, to simplify the material of the article, I decided not to consider many interesting aspects of using hypermedia systems. Validation of the input form using a hypermedia system was not considered. To simplify the material, a very simple method of communication between the browser and the server was used – pooling, which involves regularly sending requests to the server. To simplify the example, it was decided not to put the business logic in the application's domain models, but to leave it in the web and hypermedia models and services. Due to additional unnecessary complexity, it was decided not to add a dependency injection container to the project on the outdated ASP.NET MVC 5 platform.

For more complete theoretical information on hypermedia systems, you should refer to the original source on this topic. This is the book by Carson Gross “Hypermedia Development. Htmx and Hyperview” with examples in the Python programming language and the Flask web framework.

I think that the material of this article was useful to you, especially if you work with old web applications with outdated technologies. The knowledge gained will help you easily and quickly add various useful interactive elements directly to the existing application pages. You can directly copy whole pieces of code from this article and paste them into the code of your application. I will be very glad to know if the material of this article was useful to you, then please let me know. I wish you every success in working with your web projects and hope that you received useful knowledge from my article.

Similar Posts

Leave a Reply

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