Cacomania: Let's develop an Entanglement Clone using D3.js and TypeScript

Cacomania

Let's develop an Entanglement Clone using D3.js and TypeScript

Guido Krömer - 27. March 2015 - Tags: , , , ,

I am a big fan of Entanglement and it's clone Weave. The game mechanics are simple, you start with a hexagon which can be rotated. A hexagon has six lines crossing each other, each line has a random entry and exit point at it sides. The first hexagon has a fixed point which marks the line currently connected to this point. After placing the first hexagon, a new hexagon gets placed at the side the previous hexagon's connected line ended. Each hexagon can be crossed, by clever rotating and placing, up to six times. The game ends if the line touches the border.

Especially Weave, which uses nice Vector graphics and some decent animations, inspired my writing my own clone of this clone. Since I wanted to improve my D3.js skills I chose the unorthodox way using this library for a game. At first glance it might sounds odd why I choose D3.js for creating a HTML5 game, this is because D3 is normally used for visualizing data by drawing a graph. I do not see D3 as a graphing library it is more like a colorful bouquet full with algorithms like geographic data related ones, DOM manipulation, query selection, transitions, SVG convenience methods and much more. Hence, it so versatile you will never find a tutorial discussing more than a fraction of its functionality. My tutorial make here no difference everything you will see from D3 is just a tickle.

I think it's no secret that I am a TypeScript fan, and especially the last update to version 1.3 with the long awaited support of protected members let me choose this nice language for this project, too.

This little animated gif shows the game, discussed here, in action.
A animated gif showing the game.

Drawing a simple hexagon

Drawing a hexagon begins with calculating the geometry data defining a hexagon, the class HexagonGeometry does the job. The calculations are based on the provided side length, with the side length the values height and distance can be calculated.

The image below shows what distance and height are actually are.
Hexagon geometry, description of sideLength, height and distance.

With those base values a path can be generated, I choose the most d3.js native way storing a path. Hence, it is a simple array storing a list of points in the desired drawing order. Each point is just an array holding the x and y coord of a point.

The code below shows the most interesting parts of the hexagon geometry calculating class, besides the path itself some convenience fields like halfRectHeight.

class HexagonGeometry {
    public sideLength: number = 0.0;

    public height: number = 0.0;

    public distance: number = 0.0;

    public rectHeight: number = 0.0;

    public rectWidth: number = 0.0;

    public halfRectHeight: number = 0.0;

    public halfRectWidth: number = 0.0;

    public path: Array<Point> = null;

    constructor(sideLength?: number) {
        this.calculate(sideLength);
    }

    public calculate(sideLength: number = 10.0): void {
        var thirtyDegree = 30.0 * (Math.PI / 180.0);

        this.sideLength     = sideLength;
        this.height         = Math.sin(thirtyDegree) * this.sideLength;
        this.distance       = Math.cos(thirtyDegree) * this.sideLength;
        this.rectHeight     = sideLength + 2.0 * this.height;
        this.rectWidth      = 2.0 * this.distance;
        this.halfRectHeight = this.rectHeight / 2.0;
        this.halfRectWidth  = this.distance;

        this.path = [ // clockwise
            [this.distance,  0.0],                           // top-middle
            [this.rectWidth, this.height],                   // top-right
            [this.rectWidth, this.rectHeight - this.height], // bottom-right
            [this.distance,  this.rectHeight],               // bottom-middle
            [0.0,            this.rectHeight - this.height], // bottom-left
            [0.0,            this.height],                   // top-left
            [this.distance,  0.0],                           // top-middle
        ];
    }

    …
}

With the points stored in the path array D3.js can create a SVG path, the generator d3.svg.line() returns a function converting the raw path data into SVG commands for drawing a polygon. Those commands get assigned to the d attribute of a path element.

<path class="hexagon hexagon-color-b" d="M34.64101615137755,0L69.2820323027551,19.999999999999996L69.2820323027551,60L34.64101615137755,80L0,60L0,19.999999999999996L34.64101615137755,0"></path>

The SVG code above has been generated through the Hexagon class init(…) method. The line function generated by D3.js has already been created during the initialization of the Hexagon object. The path elements get appended to the selection this.svgElement, a selection can be compared with a jQuery object like $('.foo') or $(myDomElement). Handing attributes and adding a child element is comparable with the use of jQuery.

class Hexagon extends SVGGameObject {

    protected line: (path: Array<Point>) => string;

    protected svgElementHexagon: D3.Selection;

    …

    protected hexagonGeometry: HexagonGeometry;

    …

    constructor(hexagonGeometry: HexagonGeometry, …) {
        super();
        this.hexagonGeometry  = hexagonGeometry;
        this.line = d3.svg.line().interpolate('linear');
        …
    }

    public init(gameManager: GameManager<D3.Selection>, renderingContext: D3.Selection): void {
        super.init(gameManager, renderingContext);

        this.svgElementHexagon = this.svgElement.append('path')
            .attr('class', 'hexagon')
            .attr('d', this.line(this.hexagonGeometry.path));
    }

    …
}

The hexagon path gets appended to the browsers SVG element as child. But it still looks very ugly. Styling can be done using D3.js by setting attributes like fill … , but in opinion it is more comfortable using CSS for styling SVG elements, wherever possible.

I used Less instead of pure CSS, since the use of variables and mixins makes it more comfortable changing the complete look and feel without annoying search and replace sessions. Another interesting feature is the import of other less files, I used this to swap out the color variables into a separate Less file named colors.less.

@color-a: rgb(72,72,62);
@color-b: rgb(108,199,44);
@color-c: rgb(250,39,114);
@color-d: rgb(102,217,238);
@color-e: rgb(254,151,32);
@color-f: rgb(147,88,254);
@color-g: rgb(118,113,94);

The syntax of Less is self-explaining, the @ allows defining variables like the color constants which can be used for setting the fill color for example. A mixin allows defining CSS classes which can be used in other classes, which gives the Less file a touch of inheritance. One mixin is used for defining a base hexagon-color class which contains all color transitions which are equal in all hexagon color classes.

@import "colors";

…

.hexagon {
  stroke: darken(@color-a, 10%);
  stroke-width: 1px;
}

.hexagon-color {
  transition-property: fill;
  …
}

.hexagon-color-a {
  .hexagon-color;
  fill: @color-a;
}

…

.hexagon-color-g {
  .hexagon-color;
  fill: @color-g;
}

Simple green SVG Hexagon drawed with D3.js and some TypeScript.
This basic hexagon has been drawn with the code above and styled with Less..

Adding the entangled paths to the hexagon

The first hexagon looks nice, but it needs those entangled paths the player can connect by placing and rotating new hexagons.

A hexagon with entangled lines/paths done with D3.js and TypeScript.
The target is building a hexagon which looks like this one.

Each line/path has an unique entry and exit point, for drawing the paths curved a basis interpolation method provided by D3.js is used. Hence, the tricky part is not drawing a nice spline, the trick is how to define the path's control and start/end points. Let's start with the start/end points, the hexagon has four sloped and two straight sides, which means eight points on sloped lines has to be placed. The points on the straight ones are no problem. Since it is a hexagon we know the angle of each sloped line, by using an imaginary triangle and the sine rule it's easy placing the start/end points on the hexagon borders. calculateConnectionsPoints() is the method which fills the connectionPoints list.

A hexagon with the connections and helper points for drawing paths.

After the connection points are placed the control/helper points has to be created, which are used for the interpolated path alignment. These points are created really easily by building a copy of the hexagon "border" path point and rotating this copy around the next connection point by 90 or 270 degree, depending on the connection points next side. Creating this list of helperPoints is performed by the calculateHelperPoints() method.

class EntangledHexagonGeometry extends HexagonGeometry {

    public connectionPoints: Array<Point> = null;

    public helperPoints: Array<Point> = null;

    constructor(sideLength?: number) {
        super(sideLength);
        this.calculate(sideLength);
    }

    public calculate(sideLength?: number): void {
        super.calculate(sideLength);
        this.calculateConnectionsPoints();
        this.calculateHelperPoints();
    }

    /**
     * Calculates the connection points using the sine rule.
     */
    protected calculateConnectionsPoints(): void {
        var sideLengthQuater      = this.sideLength / 4.0;
        var sideLengthThreeQuarter = sideLengthQuater * 3.0;
        var pY   = sideLengthQuater / 2.0;
        var pX   = (Math.sqrt(3) * sideLengthQuater) / 2.0;
        var pY2  = sideLengthThreeQuarter / 2.0;
        var pX2  = (Math.sqrt(3) * sideLengthThreeQuarter) / 2.0;
        var pY3  = sideLengthQuater + this.height;
        var pX3  = 0.0;

        this.connectionPoints = [ // clockwise
            [this.rectWidth - pX2, this.height - pY2 ],                  // top-right-a
            [this.rectWidth - pX,  this.height - pY ],                   // top-right-b
            [pX3 + this.rectWidth, pY3 ],                                // middle-right-a
            [pX3 + this.rectWidth, this.rectHeight - pY3 ],              // middle-right-b
            [this.rectWidth - pX,  pY + this.rectHeight - this.height],  // bottom-right-a
            [this.rectWidth - pX2, pY2 + this.rectHeight - this.height], // bottom-right-b
            [pX2,                  pY2 + this.rectHeight - this.height], // bottom-left-a
            [pX,                   pY + this.rectHeight - this.height],  // bottom-left-b
            [pX3,                  this.rectHeight - pY3 ],              // middle-left-a
            [pX3,                  pY3 ],                                // middle-left-b
            [pX,                   this.height - pY ],                   // top-left-a
            [pX2,                  this.height - pY2 ],                  // top-left-b
          ];
    }

    protected calculateHelperPoints(): void {
        var twoHundredSeventyDegree = 270.0 * (Math.PI / 180.0);
        var ninetyDegree            =  90.0 * (Math.PI / 180.0);

        this.helperPoints = [ // clockwise
            Utils.rotateAround(this.path[0], this.connectionPoints[0],  twoHundredSeventyDegree),
            Utils.rotateAround(this.path[1], this.connectionPoints[1],  ninetyDegree),
            Utils.rotateAround(this.path[1], this.connectionPoints[2],  twoHundredSeventyDegree),
            Utils.rotateAround(this.path[2], this.connectionPoints[3],  ninetyDegree),
            Utils.rotateAround(this.path[2], this.connectionPoints[4],  twoHundredSeventyDegree),
            Utils.rotateAround(this.path[3], this.connectionPoints[5],  ninetyDegree),
            Utils.rotateAround(this.path[3], this.connectionPoints[6],  twoHundredSeventyDegree),
            Utils.rotateAround(this.path[4], this.connectionPoints[7],  ninetyDegree),
            Utils.rotateAround(this.path[4], this.connectionPoints[8],  twoHundredSeventyDegree),
            Utils.rotateAround(this.path[5], this.connectionPoints[9],  ninetyDegree),
            Utils.rotateAround(this.path[5], this.connectionPoints[10], twoHundredSeventyDegree),
            Utils.rotateAround(this.path[6], this.connectionPoints[11], ninetyDegree),
        ];
    }
}

With the conectionPoints and helperPoints lists created by the EntangledHexagonGeometry class we are able to draw the "entangled hexagon". For drawing the six paths per hexagon a D3.js line generator is used. This line generator uses the basis interpolation for drawing nice rounded paths, I recommend this link if you want to play with the available interpolation methods.

The init(…) method adds six paths with unique connection points to the hexagon's SVG. For generating unique random numbers within a defined range the class RandomNumberPool is used. addEntangledLine() adds the SVG path to the hexagon with a start and end point. Each path stars and ends with a connection point followed by a helper point used for controlling the spline plus the hexagons center point which has a similar task likes the helper points.

The point path is used for adding two SVG paths, one for the outline and on for the path itself. The class below is stripped down to contain only the drawing logic described here.

class EntangledHexagon extends Hexagon {
    protected entangledLine: (path: Array<Point>) => string;

    protected randomNumberPool: RandomNumberPool;

    …

    constructor(hexagonGeometry: EntangledHexagonGeometry, position: Point, randomNumberPool: RandomNumberPool) {
        super(hexagonGeometry, position);
        this.entangledLine    = d3.svg.line().interpolate('basis');
        this.randomNumberPool = randomNumberPool;
        …
    }

    public init(gameManager: GameManager<D3.Selection>, renderingContext: D3.Selection): void {
        super.init(gameManager, renderingContext);

        this.randomNumberPool.reset();
        for (var i: number = 0; i < 6; i++) {
            var start: number = this.randomNumberPool.getNumber();
            var end: number   = this.randomNumberPool.getNumber();

            this.addEntangledLine(start, end);
        }
    }

    …

    private addEntangledLine(start: number, end: number): void {
        var hexagonGeometry = <EntangledHexagonGeometry> this.hexagonGeometry;

        var line: string = this.entangledLine([
            hexagonGeometry.connectionPoints[start],
            hexagonGeometry.helperPoints[start],
            [hexagonGeometry.halfRectWidth, hexagonGeometry.halfRectHeight], //hexagon middle point
            hexagonGeometry.helperPoints[end],
            hexagonGeometry.connectionPoints[end],
        ]);

        this.svgElement.append('path')
            .attr('class', 'line-border')
            .attr('connectionPoints', line);

        var svgLine: D3.Selection = this.svgElement.append('path')
            .attr('class', 'line-inactive')
            .attr('d', line);

        …
    }
}

Like the hexagon the paths are styled using CSS/Less, too. .line is a mixin defining the entangled line base properties. A CSS transition is used for animating the color change among different line states. The line state .line-active is used for highlighting the already "walked" path.

.line {
    fill: none;
    stroke-width: 3px;
    transition-property: stroke;
    transition-duration: @transition-duration;
    transition-timing-function: @transition-timing-function;
}

.line-border {
  .line;
  stroke: darken(@color-a, 20%);
  stroke-width: 5px;
}

.line-inactive {
  .line;
  stroke: lighten(@color-g, 20%);
}

.line-active {
  .line;
  stroke: @color-c;
}

.line-preview {
  .line;
  stroke: @color-e;
}

Rotating and moving with SVG transitions

For rotating, moving… the SVG elements I created a general class for all common transformation tasks. Each class which extends SVGGameObject has the following methods: setPosition(…), setRotation(…), setZoom(…) and setOpacity(…) which can be used for manipulating the game objects SVG.

The hexagon nested groups for rotating, positioning… in chrome's inspector.
The hexagon's DOM with all nested groups for positioning, rotating … in the chrome inspector. By the use of this nested groups it is possible to change the position and rotation independently.

class SVGGameObject extends GameObject<D3.Selection> {
    protected svgElementPositionGroup: D3.Selection;

    protected svgElementRotationGroup: D3.Selection;

    protected svgElementZoomGroup: D3.Selection;

    protected svgElementOpacityGroup: D3.Selection;

    protected svgElement: D3.Selection;

    protected position: Point = <Point>[0, 0];

    protected center: Point = <Point>[0, 0];

    protected rotation: number = 0;

    protected opacity: number = 1.0;

    protected zoomFactor: number = 1.0;

    …

    public init (gameManager: GameManager<D3.Selection>, renderingContext: D3.Selection): void {
        super.init(gameManager, renderingContext);

        this.svgElementPositionGroup = renderingContext
            .append('g')
            .attr('class', this.tag + '-position');

        this.svgElementRotationGroup = this.svgElementPositionGroup
            .append('g')
            .attr('class', this.tag + '-rotation');

        this.svgElementZoomGroup = this.svgElementRotationGroup
            .append('g')
            .attr('class', this.tag + '-zoom');

        this.svgElementOpacityGroup = this.svgElementZoomGroup
            .append('g')
            .attr('class', this.tag + '-opacity');

        this.svgElement = this.svgElementOpacityGroup.append('g')
            .attr('class', this.tag);
    }

    public setPosition (newPosition: Point, duration: number = 500, delay: number = 0, ease: string = 'cubic'): void {
        var oldPosition: Point = this.position;
        this.position = newPosition;

        if (duration === 0) {
            this.svgElementPositionGroup
                .attr('transform', 'translate(' + newPosition[0] + ',' + newPosition[1] + ')');

            return;
        }

        this.svgElementPositionGroup.transition()
            .duration(duration)
            .delay(delay)
            .ease(ease)
            .attrTween('transform', function () {
                           return d3.interpolateTransform(
                               'translate(' + oldPosition[0] + ',' + oldPosition[1] + ')',
                               'translate(' + newPosition[0] + ',' + newPosition[1] + ')'
                           );
                       });
    }

    …

    public setRotation (angle: number, duration: number = 250, delay: number = 0, ease: string = 'cubic'): void {
        var rotationStart: number = this.rotation;
        this.rotation = angle;
        var rotationEnd: number = this.rotation;

        …

        var center = this.center;
        this.svgElementRotationGroup.transition()
            .delay(delay)
            .duration(duration)
            .ease(ease)
            .attrTween('transform', function () {
                           return d3.interpolateString( // interpolateTransform is ugly when rotating around centroid
                               'rotate(' + rotationStart + ',' + center[0] + ',' + center[1] + ')',
                               'rotate(' + rotationEnd + ',' + center[0] + ',' + center[1] + ')'
                           );
                       });
    }

    …
}

The setPosition(…) method moves an element to the provided position, if the duration parameter is equal zero no transition is needed and the element can be placed directly by modifying the svgElementPositionGroup's transform attribute which is quite straightforward.

If the duration is greater than zero D3's transition functionality gets used, a transition has a duration, an optional delay and an interpolation method, which is cubic by default.

Since a transition is an interpolation of a value over a defined time span (the duration) an interpolator method has to be defined which returns the transform's translate value at the given translation progress. D3.js provides a nice set of generators which build interpolation functions for the most common use cases, d3.interpolateTransform(a, b) for example generates a tween function. If you want to move a SVG element from the position 100, 100 to its new position 1337, 1337 within one second the code could look like this:

svgElementPositionGroup.transition()
    .duration(duration)
    .attrTween('transform', function () {
                   return d3.interpolateTransform(
                       'translate(100, 100)',
                       'translate(1337, 1337)'
                   );
               });

D3 provides a very general interpolator called d3.interpolateString(a, b), this one interpolates the numbers found in string A to the numbers found in string B. I used this one for interpolating a rotation of an element around it's centroid. The result is a nicer looking rotation as interpolateTransform() provides for this use case. I was too lazy creating an animated gif showing this behavior, but believe me it looked really odd.

As already mentioned, I made the decision using CSS transitions for smooth color changes over d3.js transitions, which was the easier way since all colors are defined in the style sheet. But if I would have chosen the way defining the colors in the source code, using d3.js's transitions would have been an option, too.

Drawing the background

The background or play field is a tile based map of hexagons, tile based maps have the advantage that it only needs a two dimensional array for saving the whole map. The default map displays hexagon like in the original Entanglement and gets used if the second constructor parameter is undefined.

Drawing a tile based map is simple, too. A single tile/hexagon can be drawn when iterating over the two dimensional array's rows and columns. The HexMap class inherits from SVGGameObject and it therefore a game object managed by the game manager and has its own parent SVG element. All hexagons get appended to this SVG Element when iterating over the map tiles: hexagon.init(gameManager, this.svgElement). The color code gets taken from the tile's numeric value. The position a hexagon gets calculated using the HexagonGeometry's getPositionOnMap(row, column) method which converts the current row and column index into a position as Point.

class HexMap extends SVGGameObject {
    …

    constructor(public hexagonGeometry: HexagonGeometry, public hexMap?: Array<Array<number>>) {
        super();
        this.tag = 'map';

        if (typeof this.hexMap === 'undefined') {
            this.hexMap = [
                [0, 0, 1, 1, 1, 1, 1, 0, 0],
                [0, 0, 1, 2, 2, 2, 2, 1, 0],
                [0, 1, 2, 2, 2, 2, 2, 1, 0],
                [0, 1, 2, 2, 2, 2, 2, 2, 1],
                [1, 2, 2, 2, 1, 2, 2, 2, 1],
                [0, 1, 2, 2, 2, 2, 2, 2, 1],
                [0, 1, 2, 2, 2, 2, 2, 1, 0],
                [0, 0, 1, 2, 2, 2, 2, 1, 0],
                [0, 0, 1, 1, 1, 1, 1, 0, 0],
            ];
        }
    }

    public init(gameManager: GameManager<D3.Selection>, renderingContext: D3.Selection) {
        super.init(gameManager, renderingContext);
        var middlePoint: Point = this.hexagonGeometry.getPositionOnMap(this.hexMap.length / 2, this.hexMap[0].length / 2);

        var transitionSpeed: number = 2500;
        for (var row = this.hexMap.length - 1; row >= 0; row--) {
            for (var column = this.hexMap.length - 1; column >= 0; column--) {
                var tile = this.hexMap[row][column];

                if (tile === 0) {
                    continue;
                }

                var position: Point = this.hexagonGeometry.getPositionOnMap(row, column);

                var hexagon = new Hexagon(this.hexagonGeometry, middlePoint);
                hexagon.init(gameManager, this.svgElement);
                hexagon.setPosition(position, transitionSpeed, 0, 'elastic');
                hexagon.setColor(HexMap.colorCodes[tile]);
            }
        }

        …
    }

    …
}

There is a nice tutorial at gamedev.net about coordinates in hexagon based tile maps, I took the following code from there, the method is used at several parts in the game for placing hexagon on the map.

class HexagonGeometry {
    …

    public getPositionOnMap(row: number, column: number): Point {
        if (row % 2 === 0) { // even or odd check
            return <Point>[
                column * this.rectWidth + this.distance,
                row * (this.height + this.sideLength)
            ];
        }

        return <Point>[
            column * this.rectWidth,
            row * (this.height + this.sideLength)
        ];
    }

    …
}

Some game logic internals

Since the game mechanics are quite simple, I am going to explain only most interesting parts. Therefore, I skip some steps like finding the right exit point in rotated hexagons here, for example.

The mechanics like reacting on user inputs, regardless how the user triggered an action and placing new hexagons are located in the GameLogic class. This class inherits from the abstract GameObject class and is managed by the global GameManager. The game manager's main tasks are initializing new game objects and controlling the message flow among the registered game objects. So any user input class likes Keyboard or TouchControl can send messages like rotate-left, rotate-right, place … to the game logic class which reacts to the message commands.

Let's assume the player pressed the right arrow key, a new message arrives the game logic component where it gets processed by the onMessage method. By calling the active hexagons's rotateRight() method the hexagon rotates itself and changes its connection points state. The active hexagon is the currently by the player controllable game object. Once it has been rotated the rotated() methods gets called which updates and resets the placement preview line and "draws" a new one by placing the preview hexagon. The preview hexagon marks the point the new entangled line will end.

The placePreviewHexagon(…) function walks along the entangled line, by visiting each connected hexagon recursively, and marks the entangled line by calling setConnection(…) for each visited hexagon. placePreviewHexagon(…) stops walking if no hexagon connected to the path was found, at this position the preview hexagon gets moved by setting the position.

The place action is very similar to a rotate action, when the player pressed the place key the active hexagon gets placed and the entangled path resulting will be "drawn". After a hexagon has been placed the preview hexagon's position has to be updated, too. The function placeNextHexagon(…) works similar in comparison with placePreviewHexagon(…), it walks recursive down the path, hexagon by hexagon, and marks the path pieces as ACTIVE. When the path touches the map border tile, which has the numeric value of 1, the game ends by calling finish().

class GameLogic extends SVGGameObject {

    protected activeHexagon: EntangledHexagon = null;

    protected spareHexagon: EntangledHexagon = null;

    protected previewHexagon: Hexagon = null;

    …

    public onMessage(message: GameMessage): void {
        switch (message.type) {

            …

            case 'rotate-right':
                if (!this.finished) {
                    this.activeHexagon.rotateRight();
                    this.rotated();
                    this.gameManager.sendMessage(new GameMessageToAll('hexagon-rotated', this.getId()));
                }
                break;
            case 'place':
                this.place();
                break;

            …
        }
    }

    …

    protected rotated() {
        this.resetMapPreview();
        this.activeHexagon.setConnection(this.activePoint, LineState.PREVIEW);
        this.placePreviewHexagon(this.activeHexagon, this.activePoint);
    }

    private place(): void {
        if (this.finished) {
            return;
        }

        …
        this.activeHexagon.setConnection(this.activePoint, LineState.ACTIVE);
        this.placeNextHexagon(this.activeHexagon, this.activePoint);
        this.placePreviewHexagon(this.activeHexagon, this.activePoint);
        …
    }

    protected placeNextHexagon(hexagon: EntangledHexagon, nextConnectionPoint: number): void {
        var exitPoint: number  = hexagon.getExitPoint(nextConnectionPoint);
        var exitSide: number   = Math.floor(exitPoint / 2);
        var mapPosition: Point = hexagon.getMapPosition();
        var previewPosition: Point  = this.entangledHexagonGeometry.getNextPositionBySide(mapPosition[0], mapPosition[1], exitSide);
        var hexagonAtPreviewPosition: EntangledHexagon = this.placedHexagons[previewPosition[0]][previewPosition[1]];
        var newConnectionPoint = this.exitPointToNewConnectionPoint(exitPoint);

        if (hexagonAtPreviewPosition === null || typeof hexagonAtPreviewPosition === 'undefined') {
            this.gameManager.sendMessage(new GameMessageToTag('score-display', 'increase-score', this.getId(), this.scoreMultiplier));
            this.scoreMultiplier = 1;

            this.pathFollowingPoint.add(<EntangledHexagon>hexagon, nextConnectionPoint);
            if (this.hexMap.getTile(this.previewHexagon.getMapPosition()[0], this.previewHexagon.getMapPosition()[1]) === 1) {
                this.finish();
                return;
            }

            this.addNewActiveHexagon(this.previewHexagon.getMapPosition(), newConnectionPoint);
            return;
        }

        hexagonAtPreviewPosition.setConnection(newConnectionPoint, LineState.ACTIVE);

        this.pathFollowingPoint.add(hexagon, nextConnectionPoint);
        this.placeNextHexagon(hexagonAtPreviewPosition, newConnectionPoint);
        this.scoreMultiplier++;
        this.gameManager.sendMessage(new GameMessageToTag('score-display', 'increase-score', this.getId(), this.scoreMultiplier));
    }

    protected placePreviewHexagon(hexagon: EntangledHexagon, nextConnectionPoint: number): void {
        if (this.finished) {
            return;
        }

        var exitPoint: number  = hexagon.getExitPoint(nextConnectionPoint);
        var exitSide: number   = Math.floor(exitPoint / 2);
        var mapPosition: Point = hexagon.getMapPosition();
        var previewPosition: Point  = this.entangledHexagonGeometry.getNextPositionBySide(mapPosition[0], mapPosition[1], exitSide);
        var placedHexagon: EntangledHexagon = this.placedHexagons[previewPosition[0]][previewPosition[1]];
        var newConnectionPoint = this.exitPointToNewConnectionPoint(exitPoint);

        if (placedHexagon === null || typeof placedHexagon === 'undefined') {
            this.previewHexagon.setMapPosition(previewPosition, 500, 250);
            return;
        }

        placedHexagon.setConnection(newConnectionPoint, LineState.PREVIEW);
        this.placePreviewHexagon(placedHexagon, newConnectionPoint);
    }

    protected addNewActiveHexagon(position: Point, connectionPoint: number): void {
        if (this.activeHexagon !== null) {
            this.activeHexagon.setColor('d');
        }

        this.activeHexagon = new EntangledHexagon(
            this.entangledHexagonGeometry,
            this.entangledHexagonGeometry.getPositionOnMap(4, 4),
            this.randomNumberPool
        );
        this.gameManager.addGameObject(this.activeHexagon, true, this.svgElement);
        this.activeHexagon.setColor('c');
        this.activeHexagon.setMapPosition(position);
        this.activeHexagon.setConnection(connectionPoint, LineState.PREVIEW);
        this.activePoint = connectionPoint;
        this.placedHexagons[position[0]][position[1]] = this.activeHexagon;
    }

    …
}

Adding touch control support

My game uses Hammer.JS, which makes it easy integrating gestures like swipes into an existing application. Only new instance of Hammer.JS with the desired HTML element has to be crated and new events like swipeleft or tap must be registered and Voila the basic touch support is working. Working with an external JavaScript library gets more comfortable with a TypeScript definition file from DefinitelyTyped like this one for Hammer.JS. Those definition files enable code completion in your IDE like PHPStorm and allows the TypeScript compiler to make assumptions about the used libraries instead of using the any type. A definition file has to be registered using the reference path statement.

/// <reference path="../vendor/hammer.d.ts" />

…
class TouchControl extends GameObject<D3.Selection> {

    …

    protected static sendMessage(type: string, senderId: number, gameManager: GameManager<D3.Selection>): void {
        gameManager.sendMessage(new GameMessageToTag(
            'game-logic',
            type,
            senderId
        ));
    }

    public init(gameManager: GameManager<D3.Selection>, renderingContext: D3.Selection): void {
        var senderId = this.getId();
        var mc = new Hammer(document.getElementById('game-screen'));

        mc.on('swipeleft', function() {
            TouchControl.sendMessage('rotate-left', senderId, gameManager);
        });

        mc.on('swiperight', function() {
            TouchControl.sendMessage('rotate-right', senderId, gameManager);
        });

        mc.on('tap', function() {
            TouchControl.sendMessage('place', senderId, gameManager);
        });

        mc.on('press', function() {
            TouchControl.sendMessage('switch', senderId, gameManager);
        });
    }
}

Playing sounds

The game has currently no sound support. Since it has got a messaging system, adding sound support by creating a game object listening to the global messages would be easy. There is already a dummy class called AudioPlayer which logs each message to the browser's console instead of playing some sound files.

A hard nut

A simple looking and rather subtle effect was tough to implement, inspired by Weave I wanted a dot following the path after placing the current active hexagon.

Every time a hexagon gets placed the PathFollowingPoint instance gets reseted and after placeNextHexagon() has been called the animation will be started through its start() method.

private place() {
    …

    this.pathFollowingPoint.reset();
    this.activeHexagon.setConnection(this.activePoint, LineState.ACTIVE);
    this.placeNextHexagon(this.activeHexagon, this.activePoint);
    this.placePreviewHexagon(this.activeHexagon, this.activePoint);
    this.pathFollowingPoint.start();
    this.gameManager.sendMessage(new GameMessageToAll('hexagon-placed', this.getId()));
}

Every time a path element gets marked as ACTIVE the hexagon with its current connection point gets added to the game logic's pathFollowingPoint instance, the hexagons are stored internal into a linked list.

protected placeNextHexagon(hexagon: EntangledHexagon, nextConnectionPoint: number): void {
    …

    if (hexagonAtPreviewPosition === null || typeof hexagonAtPreviewPosition === 'undefined') {
        …

        this.pathFollowingPoint.add(<EntangledHexagon>hexagon, nextConnectionPoint);
        if (this.hexMap.getTile(this.previewHexagon.getMapPosition()[0], this.previewHexagon.getMapPosition()[1]) === 1) {
            this.finish();
            return;
        }

        this.addNewActiveHexagon(this.previewHexagon.getMapPosition(), newConnectionPoint);
        return;
    }

    …

    this.pathFollowingPoint.add(hexagon, nextConnectionPoint);

    …
}

The currentElement, which type is HexagonWithStartPoint, stores the reference of its predecessor if available. Once start() gets called, the list of hexagons gets converted into a list of points, which are needed for the animation . Since each HexagonWithStartPoint instance knows the hexagon and it's start point, the matching entangled path SVG element can be fetched. By using the method getPointAtLength(…), which is a native SVG function and is not a part for D3.js, a list of points can be created from a path. The positions of this points are relative to hexagon position, hence the hexagon position has to be added to each single point.

follow(…) creates this list of points representing the path, by using D3.js this list gets converted into an invisible SVG path. Following this path using a potion of D3.js transition magic is now very easy, thanks to the powerful transition system again. There is a D3.js block where I stole the animation code used here.

class PathFollowingPoint extends SVGGameObject {

    private static PATH_RESOLUTION: number = 25;

    protected currentElement: HexagonWithStartPoint = null;

    …

    public start (): void {
        …

        if (this.currentElement !== null) {
            var combinedPath: Array<Point> = [];

            this.follow(this.currentElement, combinedPath);
            var lineGen = d3.svg.line().interpolate('linear');
            var pathD3 = this.svgElement.append('path')
                .attr('class', 'invisible')
                .attr('d', lineGen(combinedPath));

            var path = <SVGPathElement>pathD3.node();
            var length: number = path.getTotalLength();

            this.svgElement
                .append('circle')
                .attr('r', 4).transition()
                .duration(500 * (this.currentElement.getLength() + 1))
                .ease('linear')
                .attrTween('transform', function () {
                                           return function (t: number) {
                                               var p: SVGPoint = path.getPointAtLength(t * length);
                                               return 'translate(' + p.x + ',' + p.y + ')';
                                           };
                                       });
        }
    }

    public add (hexagon: EntangledHexagon, startPoint: number): void {
        if (this.currentElement === null) {
            this.currentElement = new HexagonWithStartPoint(hexagon, startPoint);
            return;
        }

        this.currentElement = new HexagonWithStartPoint(hexagon, startPoint, this.currentElement);
    }

    …

    protected follow (hexagon: HexagonWithStartPoint, combinedPath: Array<Point>): void {
        if (hexagon.previous) {
            this.follow(hexagon.previous, combinedPath);
        }

        var entangledHexagon = hexagon.entangledHexagon;
        var line: Line = entangledHexagon.renameMeGetLineByPoint(hexagon.startPoint);
        var rotation: number = entangledHexagon.getRotation();
        var center: Point = entangledHexagon.getCentroid();
        var position: Point = entangledHexagon.getPosition();

        var pointList: Array<Point> = this.pathToPointList(<SVGPathElement>line.svgLine.node(), rotation, center, position);

        if (hexagon.startPoint !== line.start) {
            pointList.reverse();
        }

        for (var i: number = 0; i < PathFollowingPoint.PATH_RESOLUTION; i++) {
            combinedPath.push(pointList[i]);
        }
    }

    protected pathToPointList(path: SVGPathElement, rotation: number, centroid: Point, position: Point): Array<Point> {
        var length: number = path.getTotalLength();
        var pointList: Array<Point> = [];
        var degToRad: number = Math.PI / 180.0;

        for (var i: number = 0; i < PathFollowingPoint.PATH_RESOLUTION; i++) {
            var originalPoint: SVGPoint = path.getPointAtLength((i / PathFollowingPoint.PATH_RESOLUTION) * length);
            var point: Point = <Point>[originalPoint.x, originalPoint.y];
            point = Utils.rotateAround(point, centroid, rotation * degToRad);
            point[0] += position[0];
            point[1] += position[1];

            pointList.push(point);
        }

        return pointList;
    }
}

Conclusion

I hope you enjoyed my little tutorial and maybe I was able to change your view about D3.js. Feel free to leave a comment if you have a question or annotation.

The complete source code is available at GitHub and here is a running demo if you want to get entangled a round or two.