joriszwart.nl

I code dreams™

Tetris

I am so sorry, this article is in Dutch only :p however Google Translate does a pretty good job.

Introductie

Duotris is een spel voor twee spelers waarmee Tetris gespeeld kan worden over internet. Het doel is om je tegenspeler uit te schakelen door zelf zoveel mogelijk lijnen te vereffenen en daarmee het je tegenstander lastig te maken. Deze ontvangt namelijk een gerandomiseerde lijn als je 4 lijnen in 1x weet weg te spelen. Deze variant doet me denken aan vervlogen tijden...

Als onderdeel van een assessment voor een klus heb ik hier een PoC voor gebouwd.

Het is een port van een eerder door mij gebouwd .NET 4.5 project en geporteerd naar .NET Core 2.0. Het één en ander is ontwikkeld onder Visual Studio Code op een Mac en draait ook onder Microsoft Windows.

Inhoud

Doel van het Proof of Concept

Het doel van het PoC is tweeledig en bestaat uit het aantonen van de volgende zaken:

  1. porteren van .NET 4.5 naar .NET Core 2.0;
  2. toepassen van Dependency Injection.

Andere zaken zoals de front-end vallen buiten het PoC .

Portering

Zoals verwacht vormden externe dependencies het grootste struikelblok. Met name SignalR heeft een andere API gekregen én het was lastig om de juiste versie te vinden; ook omdat het – voor mij – niet onmiddellijk duidelijk was dat .NET Core 2.0 in tegenstelling tot .NET Core 2.1 nog geen SignalR aan boord had. Uiteindelijk heb ik de pre-release SignalR 1.0.0-rc1-final gebruikt. Het is nl. net iets te vroeg om .NET Core 2.1 onder MacOS te draaien.

Daar tegenover staat dat het aantal expliciete dependencies van vijf naar één is teruggelopen. Dit komt grotendeels doordat OWIN onderdeel is geworden van Microsoft.AspNetCore.All. Met de komst van .NET Core 2.1 zal dit aantal zelfs teruglopen tot nul omdat SignalR dan ook onderdeel van Microsoft.AspNetCore.All is geworden. Hou je ogen op je mailbox voor een update!

Update!

We schrijven inmiddels begin 2019. De beloofde portering heeft plaatsgevonden. Minder dependencies, minder zorgen. Het project zelf is van dotnetcore2.0 naar dotnetcore2.2 geüpgraded. Microsoft.AspNetCore.SignalR hoeft niet meer apart gerefereerd te worden. Ook is het minder uitgebreide Microsoft.AspNetCore.App i.p.v. Microsoft.AspNetCore.All gebruikt. De browser client is geüpgraded naar SignalR 1.1.0.

Update to .NET 7.0

Apart from a [SignalR issue](https://github.com/dotnet/aspnetcore/issues/48690), it was pretty easy to upgrade.

Dependency Injection

Voor een dergelijke kleine applicatie is het eigenlijk overkill om DI toe te passen. Het ontwerp van SignalR maakt het echter wenselijk om toch DI te hanteren.

De SignalR hubs worden nl. per request geïnstantieerd. Hierdoor is het verleidelijk om de game state als static members in de SignalR hub op te nemen of zelfs globaal te declareren. Niet goed. Dit schendt natuurlijk werkelijk elk concept van S.O.L.I.D. op meerdere manieren!

Ik heb er voor gekozen om de classes GameList en PlayerQueue in DuotrisHub te injecteren als singletons (wah!) om dit op te lossen. Een refactorslag die nog zou kunnen plaatsvinden is het losser koppelen van DuotrisHub en de spellogica (methods QueueForMatch(), Move(), GameOver() etc.). Bijvoorbeeld door het mediator pattern toe te passen. Ook het splitsen van beweeg- en spellogica is een optie. Dit valt buiten de scope van het PoC.

services.AddSingleton<IGameList, GameList>();
services.AddSingleton<IPlayerQueue, PlayerQueue>();
Duotris UML class diagram
Mandatory UML class diagram

Over het gebruik van SignalR

Er is gebruik gemaakt van SignalR om real-time communicatie te laten plaatsvinden tussen de game server en (browser-)clients. De spelers worden gepaard voor het spelbegin. Hiertoe worden er op de server twee lijsten bijgehouden; de eerdergenoemde game state.

  1. wachtende spelers; PlayerQueue
  2. lopende spellen; GameList

Als er genoeg wachtende spelers zijn (nl. twee stuks) worden deze van de wachtende spelers naar de lopende spellen verplaatst en begint het spel.

Opmerking: er had gebruik gemaakt kunnen worden van SignalR groups om spelers bij elkaar te houden, maar het groepenconcept in SignalR kent wat beperkingen; het is mogelijk om spelers aan een groep toe te voegen of te verwijderen. Het opvragen van spelers in een groep of zelfs het aantal spelers bepalen is echter niet mogelijk.

SignalR in de back-end

Het configureren van SignarlR is een breeze in ASP.NET Core.

app.UseSignalR(routes =>
{
    routes.MapHub<DuotrisHub>("/duotrishub");
});

Over de back-end

In de klasse DuotrisHub is de spellogica te vinden. Enkele relevante snippets:

private void QueueForMatch(string player)
{
    lock (playerQueue)
    {
        Console.WriteLine($"Player {player} is waiting for a game.");
        playerQueue.Enqueue(player);
    }
}

private void TryMakeMatch()
{
    lock (playerQueue)
    {
        if (playerQueue.Count >= 2)
        {
            string player1 = playerQueue.Dequeue();
            string player2 = playerQueue.Dequeue();

            Console.WriteLine($"Starting a match for player {player1} and {player2}.");

            // add players to a game and notify them
            matches.Add(player1, player2);

            Clients.Client(player1).SendAsync("Start");
            Clients.Client(player2).SendAsync("Start");

            Clients.Client(player1).SendAsync("Message", "Game started!");
            Clients.Client(player2).SendAsync("Message", "Game started!");
        }
    }
}

Over de front-end

De front-end is gebouwd in plain JavaScript en een snelle hack bovenop JavaScript Tetris in 1.5 kB (2004). Don't look at it too closely :-) Andere keer... een TypeScript-versie komt er aanis beschikbaar!

SignalR in de front-end

Het initialiseren van SignalR in de front-end werkt als volgt:

const connection = new signalR.HubConnectionBuilder()
    .withUrl('/duotrishub')
    .build()
    // ...

connection.on('move', (bucket, score, lines) => {
    // ...
}

connection.start()    

Vervolgens kunnen er boodschappen naar de SignalR Hub verstuurd worden.

connection.send('Move', bucket, score, lines)

Screenshot

Build en run

Voor het bouwen en runnen is .NET Core 2.2 vereist.

Gebruik het volgende commando op een command-line om de applicatie te bouwen en te starten:

dotnet run

Navigeer vervolgens in twee aparte browser-vensters naar localhost:5000 om een spel te starten.

Download

Git repository: duotris.