Angular + .NET Core: Drinking Game With SignalR

Angular + .NET Core: Drinking Game With SignalR

This idea came on one of the Angular Hack Days where I wanted to create something to present by the end of the day where I could get everyone involved. The game is quite simple, everyone will load the page and as people connect to the site more glasses of beer will show up on the screen, as you tap or click the amount of beer in your glass will decrease, the first person to empty their glass wins the game.

Check out the updated version of this post here

That's what you see when you have the app running with 3 people connected to it:

Of course, I could create this solution with .NET 4.6 and jQuery only, but what would be the fun of it? I wanted to use the latest and greatest as a proof of concept, so Angular and .NET Core it is.

The source code covered in this post is available here. Feel free to contribute

The application is configured to be continuously deployed to http://drinkinggame.azurewebsites.net

Some resources I've used to put this app together

So here's what I did:

Create .NET Core App with Angular

For this project, I've used the dotnet new angular template. If you haven't used the dotnet CLI before or have no idea where to get the angular template, please follow this post by Jason Taylor. It shows step-by-step how to get it done.

Add SignalR dependencies

You can either install it using npm:
npm install signalr-server --registry https://dotnet.myget.org/f/aspnetcore-ci-dev/npm/

Or you can add a package source to your local or global Nuget.config https://dotnet.myget.org/F/aspnetcore-ci-dev/api/v3/index.json then in Manage Nuget Packages look for Microsoft.AspNetCore.SignalR.Server and install it.

Configure SignalR

2 things we need to do here, both of them in the Startup.cs file.

In the ConfigureServices method, add the line below:

services.AddSignalR(options => options.Hubs.EnableDetailedErrors = true);

In the Configure method, add the line below. Just make sure it comes before app.UseMvc:

app.UseSignalR();

You can test if SignalR is configured correctly by running the application and navigating to /signalr/js. You should see a script, at this point nothing special about it as we haven't created any Hubs.

Create Hub

Just for the sake of this example, I created a Hub.cs file in the root of the project folder. And to start with I overrode two methods OnConnected and OnDisconntected.

using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Hubs;

[HubName("BeerHub")]
public class BeerHub : Hub
{
    public override Task OnConnected()
    {
        return base.OnConnected();
    }

    public override Task OnDisconnected(bool stopCalled)
    {
        return base.OnDisconnected(stopCalled);
    }
}

Now I have to work further on both methods so I can store the connected clients and also how they are doing with their beers.

First I added a couple of static variables into my hub:

//This list will store all the connect clients
private static List<string> clients = new List<string>();
//This dictionary will store the glass of every client
private static Dictionary<string, int> glasses = new Dictionary<string, int>();
//This will store the winner
private static string winnerId = null;

Now I have to update the OnConnected method to add the client to the clients list and also initiate his/her glass:

public override Task OnConnected()
{
    if (!clients.Contains(Context.ConnectionId))
    {
        clients.Add(Context.ConnectionId);
        glasses.Add(Context.ConnectionId, 100);
        //Glass.Project is a simple projection to transform the way the data is presented
        Clients.Clients(clients).broadcastMessage(glasses.Select(Glass.Project));
    }
    return base.OnConnected();
}

Also, need to update the OnDisconnected method to remove the client from the lists.

public override Task OnDisconnected(bool stopCalled)
{

    if (clients.Contains(Context.ConnectionId))
    {
        clients.Remove(Context.ConnectionId);
        glasses.Remove(Context.ConnectionId);
        Clients.Clients(clients).broadcastMessage(glasses.Select(Glass.Project));
    }
    return base.OnDisconnected(stopCalled);
}

The last thing is to create a new public Drink method that will decrease the amount of beer in the glass for the current client:

public void Drink()
{
    try
    {
        //once there's a winner, the game is finished
        if (string.IsNullOrEmpty(winnerId))
        {
            if (clients.Contains(Context.ConnectionId))
            {
                glasses[Context.ConnectionId]--;
                if (glasses[Context.ConnectionId] <= 0)
                {
                    winnerId = Context.ConnectionId;
                    //this will send a message to the winner
                    Clients.Caller.sendMessage("You're the winner");
                }
                Clients.Clients(clients).broadcastMessage(glasses.Select(Glass.Project));
            }
        }
    }
    catch (Exception ex)
    {
    }
}

Reference Scripts

In the Home/Index.cshtml file, I included the references to both jQuery and SignalR

<script src="https://code.jquery.com/jquery-2.x-git.min.js" asp-append-version="true"></script>
<script src="http://ajax.aspnetcdn.com/ajax/signalr/jquery.signalr-2.2.0.min.js"></script>
<script src="~/signalr/js" asp-append-version="true"></script>

Create Beer Service

In my app folder, I created a beer.service.ts file.


@Injectable()
export class BeerService {

    messageSubject = new Subject<any>();

    //workaround to deal with jQuery
    window: any = (<any>window);

    constructor() {
        this.start();
    }

    drink() {
        this.server.drink();
    }
    
    start(): void {
        if (this.window.$.signalR && this.window.$.signalR.BeerHub && this.window.$.signalR.BeerHub.connection) {
            
            this.hubConnection = this.window.$.signalR.BeerHub.connection;
            this.hubConnection.url = `/signalr`;
            this.client = this.window.$.connection.BeerHub.client;
            this.server = this.window.$.connection.BeerHub.server;

            //this will be called every time Clients.Clients(clients).broadcastMessage is called from the Hub
            this.client.broadcastMessage = (msg) => {
                 this.messageSubject.next(msg);
            };

            //this will be called every time Clients.Caller.sendMessage is called from the Hub
            this.client.sendMessage = (msg) => {
                alert(msg);
            };

            this.hubConnection.start()
                .done(() => {
                    this.startingSubject.next();
                })
                .fail((error: any) => {
                    this.startingSubject.error(error);
                });
        }
    }
}

Wire the Component

Here are the changes I made to my home.component.ts:

@Component({
    selector: 'home',
    templateUrl: './home.component.html'
})
export class HomeComponent implements OnInit {

    private glasses: any;

    constructor(
        private beerService: BeerService,
        private ngZone: NgZone) {
    }

    ngOnInit(): void {
        this.beerService.messageSubject.subscribe(data => {
            //pretty ugly workaround. Will try to solve this differently
            //doing that because it wasn't being triggered by the change detection
            this.ngZone.run(() => {
                setTimeout(() => {
                    this.glasses = data;
                }, 10);
            });
        });

    }

    drink() {
        this.beerService.drink();
    }
}

Here are the changes I made to my home.component.html

<div class="row">
    <div class="col-md-2" *ngFor="let glass of glasses">
        <div class="beer-container" (click)="drink()">
            <img src="/images/glass.png" />
            <div class="beer" [style.height]="glass.Number+'%'"></div>
        </div>
    </div>
</div>

That's it. It does seem more complex than it is. Keep in mind that I created this in a couple of hours in a hack day. So some hacks are allowed. Again, feel free to contribute to the solution. I'm still going to work further on the project.

Next Steps

2018-02-26: All these features are now available here

  • Wire up Gravatar
  • Ability to create drinking group
  • Join a drinking group
  • Start drinking when everyone has joined the group
  • Broadcast winner to group