Building and testing a realtime game with Angular, Protractor and Deepstream

This article explores Angular and Deepstream from the perspective of a Connect Four game built with these technologies. The source code is available on Github.

The first section takes an opinionated look at Angular and Deepstream and considers the pros and cons of using them to develop the game.

In the next part, we discuss the implementation of the social login feature.

Next we look at the mechanics of keeping web clients in sync and consider the question of how to integrate a realtime library into our app. I put forth the argument that it is best to isolate Deepstream from the rest of the application by wrapping it into an Angular service.

Next we look at the end-to-end testing capabilities made possible by Protractor. Protractor enables us to run several browser instances in parallel, simultaneously testing the app from different user perspectives. This test scenario is vital for a two-player (or multi-player ) game.

Finally, if you are interested in AI and game theory, there is also a short section where we discuss the game engine.

Let’s begin.

The stack

All the while I was writing Connect Four, I wondered what would be the best way to go about it.  Do Angular and Deepstream represent the best choice? Should I maybe try React or Vue? Would vanilla JavaScript work better? What about PHP?

I still don’t have a definite answer, but I think both Angular/Protractor and Deepstream did the job pretty well and I’m happy for the choice.

The development experience has been smooth and nice, but there were bumps in the road as well. Let’s see what the pros and cons are, starting with Angular.

When we build an Angular app – as opposed to a vanilla Javascript app – we are programming to a framework. This aspect informs the core structure of our code.

Angular requires the programmer to think in terms of web components. With Angular, we use a component to design the aspect and user interaction for a specific patch of the screen, called view. A component is a bundle consisting of a Html template, a class and optionally a CSS file. Components enable us to pack a complex application feature into a single unit of code.

This is great, but there is more in the Angular toolkit: we have services, property and event bindings, directives, pipes, routing and navigation etc.

This translates to a wonderfully concise way of doing things. Let’s look at the Connect Four board as an example. All it takes to code the component template is a couple of nested *ngFor loops.

<form>
  <div id="board">
    <div id="field">
      <div class="grid">
        <ng-container *ngFor="let i of columns">
          <div class="column">
            <ng-container *ngFor="let j of rows">
              <input type="radio" name="{{j * 10 + i}}" tabindex="-1" (click)="onClickInput($event)"
                (mouseover)="onMouseoverInput($event.target.name)" (mouseleave)="onMouseleaveInput($event.target.name)">
              <div class="disc disc-initial" id="{{j * 10 + i}}"></div>
            </ng-container>
          </div>
        </ng-container>
      </div>
    </div>
    <div id="front"></div>
  </div>
</form>

Angular is also built with efficiency in mind. A skeleton project can be bootstraped in a few seconds and Angular CLI also generates the boilerplate for a class, component, service or pipe.

ng g service AuthService
ng g service RealtimeService

Angular integrates the Karma and Protractor test frameworks, which is major plus.

TypeScript, which brings a rich type system to JavaScript applications, is another positive with Angular. So is RxJS, a library using the Observable pattern for asynchronous operations.

All of the above are good solid reasons to use Angular for a non-trivial web app, as opposed to vannila JavaScript.

On the other hand, a framework increases code complexity and forces us to deal with additional complications.

For me, one such problem involved the condition that Protractor waits for Angular to become stable before doing anything (e.g. searching for an element on the web page ). If the app is not stable, end-to-end tests timeout or exit with error.

If something is killing the stability of your app, you gotta dig in and find the culprit. And it can be a surprisingly innocent thing, like a timer component, or an open WebSockets connection.

Surprisingly, this is the intended behavior under certain conditions.

When programming with Angular, you may also encounter the issue that suddenly the view is out of sync with the app. For some mysterious reason, you have reached an inconsistent state and dropped our of the Angular flow. Usually it’s a trivial mistake in our code, but not always. In some cases, such as the one described in this issue, this unusual behavior is a tough nut to crack.

One solution may involve the trick to perform some operations outside of the Angular zone. For instance, we can execute the timer/view update job outside of Angular Zone or initialize the WebSockets connection outside of the Angular zone. By doing so, we can get isStable to emit and our tests pass.

Deepstream

An online gaming application has to sync data between online clients. Deepstream.io can synchronize network devices in real-time.

The platform provides the open source Deepstream Server and client SDKs for JavaScript and Java.

Deepstream is a reliable and fast solution with good documentation. I hardly found any bugs in version 2.3.0 and the framework does exactly what you’d expect.

Perhaps one limitation of Deepstream is that the capabilities of the server cannot be extended. A Java EE Application Server for instance can be extended with a Web Service. We cannot do similar thing with Deepstream.

This implies that our application logic must be entirely implemented on the client side. Deepstream Server simply forwards messages to network clients.

WebSockets

One layer below, WebSockets make the magic happen.

WebSockets protocol doesn’t incur the overhead of HTTP headers and cookies. For this reason, WebSockets are ideal for applications that require fast realtime communication.

A WebSocket connection starts with an HTTP  request called handshake.

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

The server response:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

According to Wikipedia

For more about web sockets, you can check the Mozilla docs.

Authentication

We want users to authenticate to our multiplayer game. The (lazy) way to do this is to implement a social login feature.

Angular programmers can use Firebase for this purpose. However, as a result our application would depend on Firebase service for authentication.

As an alternative we can directly integrate the OAuth providers we want to support ( Google, Facebook, Twitter etc. ).

We can thus define an AuthProvider interface; classes implementing this interface will handle obtaining a User from a particular source, providing also a signout method.

interface AuthProvider { 
  getUser: () => Observable<User>; 
  signout: () => void; 
}

That’s all we care about.

import { Injectable, NgZone } from '@angular/core';
import { Subject, Observable } from 'rxjs';

import { User } from './util/models';
import { environment } from '../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  public login$: Subject<undefined> = new Subject();
  private _user: User;
  private authProvider: AuthProvider;

  constructor(private zone: NgZone) {
    if (environment.production) {
      this.registerProvider(new FacebookAuth());
      this.registerProvider(new GoogleAuth());
    } else {
      this.registerProvider(new MockUserAuth());
    }
  }

  get user() {
    return this._user;
  }

  signout() {
    this.authProvider.signout();
  }

  private registerProvider(authProvider: AuthProvider) {
    authProvider.getUser().subscribe((user: User) => {
      this.zone.run(() => {
        this._user = user;
        this.authProvider = authProvider;
        this.login$.next();
      });
    });
  }
}

AuthService exposes an Observable called login$, which emits when somebody logs into our app. At this points, client components can access the _user property, which, as a precaution against modification attempts, is exposed through a getter.

Classes implementing AuthProvider are a proxy. They interact with an actual OAuth provider and construct a user from public profile data. We’ll skip the details.

/* Entry points to OAuth APIs */
declare const gapi: any;
declare const FB: any;

class GoogleAuth implements AuthProvider {
  private GoogleAuth: any;

  getUser(): Observable<User> {
    // implementation
  }

  signout() {
    this.GoogleAuth.signOut().then(() => setTimeout(() => location.assign('/login'), 0));
  }
}

class MockUserAuth implements AuthProvider {

  getUser(): Observable<User> {
    return new Observable(subscriber => {
      if (!environment.production) {
        subscriber.next(<User>{
          id: localStorage.getItem('id'),
          name: localStorage.getItem('username'),
          imgUrl: 'assets/img/user.png',
          status: 'Online'
        });
      }
    });
  }

  signout() {
    setTimeout(() => location.assign('/login'), 0);
  }
}

In development mode, the user is constructed from data saved in browser local storage.

Data-sync service

Deepstream is a realtime database of Records.

According to the documentation: records are documents in deepstreamHub’s realtime data-store. They are atomic bits of JSON data that can be manipulated and observed. Any change to a record is instantly synced across all connected clients.

const client = deepstream('https://localhost:6020').login();
const user = { name: 'Jane', status: 'online' };
const record = client.record.getRecord(user.name);
record.set(user);
record.subscribe('status', (status: string) => {
       // update the view 
});

Besides records, we have Lists. Lists are collections of record names (not their actual data). Just like records, lists can be manipulated and observed.

const list = client.record.getList('users');
list.on('entry-added', (user: User) => {
   // update the view over the list of online users
}

While records and lists represent persistent data, non-persistent data is represented by Events. Events are topic-based messages sent between clients.

You can do a lot with records, lists and events and Deepstream also provides remote procedure calls.

If you are curious to learn more visit https://deepstream.io. As there are better resources to learn about Deepstream, I won’t delve too far into details here.

Let me just add a quick observation before we finish this section.

In Connect Four, various components and parts of the view receive input from ( remote ) users and process asynchronous updates.

The question then is what is the best way to use Deepstream? Do we embed Deepstream client code directly into our components or should we wrap the Deepstream library into an Angular service?

Having tried both approaches, I think it’s much better to isolate Deepstream. Really much better.

One reason is reduced coupling.

Secondly, wrapping Deepstream logic in a service prevents code duplication and connects well with the idea of not doing heavy work in components. Components should only be concerned with data presentation, leaving the details of data fetching to a service.

Thirdly, by hiding application logic into API methods, we specify our policy in an abstract way and leave open the possiblity to switch the implementation.

End-to-end tests

Angualar/Protractor enables us to run multiple browser instances during a test.

const browser2: ProtractorBrowser = browser.forkNewDriverInstance();

This one line of code enabled me to automate e2e tests for Connect Four.

The problem is that Protractor waits for Angular to become stable. We might blissfully ignore that our app is unstable until we run an e2e test and it hits us in the face.

Protractor automatically calls waitForAngular() at each process tick, enabling the programmer to write the test without putting in sleeps and waits. But synchronization also causes the tests to timeout if Protractor fails to detect Angular on the page. This can happen even though Angular code is obviously there.

So when is Angular stable?

According to the docs, when there are no pending items in the microtask queue and Angular is about to relinquish the VM turn.

If  Angular is unstable, there must be a pending task somewhere. It can be something as simple as a timer component regularly updating the view, or your app polling an HTTP endpoint, or even an open WS connection. Angular/Protractor waits for that task to be done and the test times out.

Protractor offers a workaround allowing us to opt out of Angular synchronization. This is done with one of the following methods.

beforeEach(async function () {
  browser.ignoreSynchronization = true; 
  browser.waitForAngular(false); 
  browser.waitForAngularEnabled(false);
}

However, this leads to flaky tests and should probably be avoided.  The solution in my case was to change the app in two different places.

On the one hand, I rewrote the timer component so that updates are now performed outside of Angular, by directly manipulating the DOM with JavaScript.

Secondly, I refactored to RealtimeService so as to run the Deepstream client initialization code outside of Angular.

import { Injectable, NgZone } from '@angular/core';

import { environment } from '../environments/environment';
import { AuthService } from './auth.service';
import { User } from './util/user';

declare var deepstream: any;
@Injectable({
  providedIn: 'root'
})
export class DeepstreamService {
  private client: deepstreamIO.Client;

  constructor(auth: AuthService, ngZone: NgZone) {
    // this.init(auth.user); // Don't! Tests exit with error. 
    ngZone.runOutsideAngular(this.init.bind(this, auth.user)); // OK
  }

  private init(user: User) {
    this.client = deepstream(environment.deepstreamUrl, { maxReconnectAttempts: 5 });
    this.client.login({ username: user.name });
  }
}

After I applied this fix e2e tests started to exit normally even with Angular synchronization enabled, which was a huge relief. On the downside, I had to put in some plumbing code, as shown.

Another possible solution could be to subscribe to isStable and perform sensitive tasks, such as initializing the WS connection, asynchronously, when the observable emits. I have yet to try that.

Finally note that an additional issue with Protractor is that for Angular apps (not AngularJS), the binding and model locators are not supported. We need to use by.css.

Game AI

Connect Four is a perfect information game. Players have complete information about the game at any stage in the game.

Connect Four is also a zero sum game. In zero sum games, a player’s gain of utility requires an equivalent loss of utility by the other players. Dividing a cake between people is a zero sum game.

To solve the problem of generating optimal moves for Connect Four, we can use MiniMax algorithm. As it turns out, MiniMax ( or MaxMin ) is one of the earliest algorithms in artificial intelligence.

To learn more MiniMax you can go to Wikipedia or your read this excellent article by Lauri Hartikka on writing a Chess engine in JavaScript.

Here, because I’m in a rush this finish this article, I’ll just outline the main steps.

1. Evaluation Function

First we need to write an algorithm to evaluate the board in a certain state of the game. The algorithm employs heuristics to asses the current state of the board and returns a number in the range of [-INFINITY, +INFINITY].

Victory for the Minimizing Player is assumed to be negative INFINITY and victory for the Maximizing Player is positive INFINITY.

The return value of the evaluation function thus indicates which player has the stronger position in the game.

2. Game Tree

In the next step we build and evaluate each node in a game search tree. In the search tree, the origin node is the current state of the game and children nodes represent, well, children states.

Overall Connect Four has many game states: 4,531,985,219,092. Simply consider that the total number of nodes increases by a factor of 7 after each turn. To avoid running out of memory we must limit the tree depth to a reasonable value, say 4.

3. Move selection

With the help of the evaluation function, we evaluate the nodes in the tree, starting with the leaves or terminal nodes and going back recursively until the origin is reached.

To select between sibling nodes on the same level we use the following rule: we pick the node with the minimum value if the current turn is the Minimizing Player’s and conversely the node with the maximum value on the Maximizing Player’s turn. This represents the “worst case” scenario.

To recap, MiniMax guarantees the minimum possible loss under a worst case scenario.

4. Optimization

We can further improve the performance of the MiniMax with alpha-beta pruning.

Alpha-beta pruning stops evaluating a move when at least one possibility has been found that proves the move to be worse than a previously examined move. This eliminates certain subtrees from the search, pruning the number of nodes to be evaluated from n to √n.

Final thoughts

I hope you liked this article and learned something useful out of it.

If you are a junior dev like myself, I heartily recommend trying Connect Four as a programming exercise. The task is super fun and will force you to think through many interesting problems.

But try not to rush it. The more time you spend improving your app, the higher the profit 😉