It’s not like we at work don’t have things to do, but suddenly we decided to play Heroes of Might and Magic. So we created a virtual machine, installed the game and started a hot-seat game via RDP.

HoMM queue

For notifying about next players turn we created a Slack channel. But soon enough it became annoying to announce next turns manually, so I created a simple web-application for that.

Implementing the queue

I decided to implement the queue with .NET Core MVC, although it can be any other web-framework really.

So, where to store players and current turn? Database would be an overkill, so I went with a JSON file in the web-root (/queue.json):

{
    "players": [
        {
            "name": "Chad",
            "slackID": "U4JDL9QBC",
            "playing": true,
            "currentTurn": false
        },
        {
            "name": "Martin",
            "slackID": "U4JLODQ3F",
            "playing": true,
            "currentTurn": true
        },
        "..."
    ]
}

Here’s how to read a JSON file using Newtonsoft.Json:

public class HomeController : Controller
{
    private readonly IHostingEnvironment _hostingEnvironment;
    private readonly string queueFile = "queue.json";

    public HomeController(IHostingEnvironment hostingEnvironment)
    {
        _hostingEnvironment = hostingEnvironment;
        queueFile = Path.Combine(_hostingEnvironment.WebRootPath, queueFile);
    }
    
    // ...

    private JObject ReadQueueFile()
    {
        using (StreamReader reader = new StreamReader(queueFile))
        {
            return (JObject)JToken.ReadFrom(new JsonTextReader(reader));
        }
    }
}

View the queue

Player class:

namespace homm_queue.Models
{
    public class Player
    {
        public string Name { get; set; }
        public bool CurrentTurn { get; set; }
    }
}

Controller code:

public IActionResult Index()
{
    List<Player> queue = new List<Player>();
    JObject queueJson = ReadQueueFile();
    
    foreach(var player in queueJson["players"])
    {
        if (Convert.ToBoolean(player["playing"]))
        {
            queue.Add(new Player()
            {
                Name = Convert.ToString(player["name"]),
                CurrentTurn = Convert.ToBoolean(player["currentTurn"])
            });
        }
    }

    return View(queue);
}

View code:

@model IEnumerable<homm_queue.Models.Player>

<div class="heading"><h3>Total players: @(Model.Count())</h3></div>
@foreach (var item in Model)
{
    @if (item.CurrentTurn)
    {
        <div class="player turn">@item.Name</div>
    }
    else
    {
        <div class="player">@item.Name</div>
    }
}

Make a turn

Every time next button is clicked, current player’s currentTurn is set to false, and for the next player it is set to true. If there is no next player (end of the list), then it just starts from the first player in the list.

[HttpPost("/MakeTurn")]
public JsonResult MakeTurn()
{
    string nextPlayerID = "unknown";
    bool setCurrentPlayerTurn = false;
    
    JObject queueJson = ReadQueueFile();
    
    foreach(var player in queueJson["players"])
    {
        if (Convert.ToBoolean(player["playing"]))
        {
            if (setCurrentPlayerTurn)
            {
                player["currentTurn"] = true;
                nextPlayerID = Convert.ToString(player["slackID"]);
                setCurrentPlayerTurn = false;
                break;
            }
            if (Convert.ToBoolean(player["currentTurn"]))
            {
                player["currentTurn"] = false;
                setCurrentPlayerTurn = true;
            }
        }
    }
    if (setCurrentPlayerTurn)
    {
        queueJson["players"][0]["currentTurn"] = true;
        nextPlayerID = Convert.ToString(queueJson["players"][0]["slackID"]);
    }

    using (StreamWriter file = new StreamWriter(queueFile, false))
    using (JsonTextWriter writer = new JsonTextWriter(file))
    {
        queueJson.WriteTo(writer);
    }

    PostToSlackChannel(nextPlayerID);

    return Json("OK");
}

Players order in JSON file obviously has to match the players order you have in game.

How to call this method from view:

<script>
    function makeTurn()
    {
        let xhr = new XMLHttpRequest();
        xhr.responseType = "json";
        xhr.open("POST", "/MakeTurn");
        xhr.send();

        xhr.onload = function()
        {
            if (xhr.status != 200) { alert("some error"); }
            else { location.reload(true); }
        };
    }
</script>

Get current player

And a simple REST API for getting the current player:

HttpGet("/GetCurrentTurn")]
public JsonResult GetCurrentTurn()
{
    JObject queueJson = ReadQueueFile();
    try
    {
        return Json(
            queueJson.SelectToken("$.players[?(@.currentTurn == true)]")["name"]
            );
    }
    catch
    {
        return Json("unknown");
    }
}

Slack notifications

Slack notifications are done via Slack Bot. You only need to set an incoming webhook for the channel:

Slack bot incoming webhook

And then you can post notifications using HttpClient:

async void PostToSlackChannel(string userSlackID)
{
    using (var httpClient = new HttpClient())
    {
        httpClient.BaseAddress = new Uri("https://hooks.slack.com/");

        HttpRequestMessage request = new HttpRequestMessage(
            HttpMethod.Post,
            "services/YOUR/SLACK/WEBHOOK"
            );
        StringBuilder msg = new StringBuilder()
            .Append("{\"text\":\"")
            .Append($"<@{userSlackID}>'s turn!")
            .Append("\"}");
        request.Content = new StringContent(
            msg.ToString(),
            Encoding.UTF8,
            "application/json"
            );

        try
        {
            var httpResponse = await httpClient.SendAsync(request);
            var httpContent = await httpResponse.Content.ReadAsStringAsync();
            Console.WriteLine($"[DEBUG] {(int)httpResponse.StatusCode} - {httpContent}");
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
    }
}

Full source code of the project is here.