Chapter 3. Creating the First Game

HTML5 Game Skeleton

The Standard Skeleton

Listing 3-1. The Initial HTML5 Skeleton
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <style>
    </style>
    <script>
        function init() {
        }
    </script>
</head>
<body onload="init()">

</body>
</html>
Listing 3-2. Viewport Meta Tag: Setting Zooming Options for a Web Page
...
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, target-densitydpi=device-dpi"/>
<style>
...
Listing 3-3. CSS for the Page: No Scrollbars, Margins, Paddings, or Borders
<style>
    html, body {
        overflow: hidden;
        width: 100%;
        height: 100%;
        margin:0;
        padding:0;
        border: 0;
    }
</style>
Listing 3-4. Listening to Browser Events to Resize the Canvas Once the Page Is Resized
function init() {
    var canvas = initFullScreenCanvas("mainCanvas");
}

/**
 * Resizes the canvas element once the window is resized.
 * @param canvasId – string id of the canvas element
 */
function initFullScreenCanvas(canvasId) {
    var canvas = document.getElementById(canvasId);
    resizeCanvas(canvas);
    window.addEventListener("resize", function() {
        resizeCanvas(canvas);
    });
    return canvas;
}
Listing 3-5. resizeCanvas, the Function That Finds the New Size of a Page and Updates the Canvas to Fit It
/**
* Does the actual resize
*/
function resizeCanvas(canvas) {
    canvas.width = document.width || document.body.clientWidth;
    canvas.height = document.height || document.body.clientHeight;
    // Notify the main game class that canva is resized
}
Listing 3-6. HTML5 Skeleton Modified for Game Development Needs
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport"
        content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, target-densitydpi=device-dpi"/>

    <style>
        html, body {
            overflow: hidden;
            width: 100%;
            height: 100%;
            margin:0;
            padding:0;
            border: 0;
        }
    </style>
    <script>
    function init() {
        var canvas = initFullScreenCanvas("mainCanvas");
    }

    function initFullScreenCanvas(canvasId) {
        var canvas = document.getElementById(canvasId);
        resizeCanvas(canvas);
        window.addEventListener("resize", function() {
            resizeCanvas(canvas);
        });
        return canvas;
    }

    function resizeCanvas(canvas) {
        canvas.width = document.width || document.body.clientWidth;
        canvas.height = document.height || document.body.clientHeight;
        // Notify the main game class that Canvas is resized
    }
    </script>
</head>
<body onload="init()">
    <canvas id="mainCanvas" width="100" height="100"></canvas>
</body>
</html>

Forced Orientation

Listing 3-7. Locking the Game’s Orientation
<script>
    var canvas;
    var ctx;
    function init() {
        canvas = initFullScreenCanvas("mainCanvas");
        ctx = canvas.getContext("2d");
        repaint();
    }

    function initFullScreenCanvas(canvasId) {
        var canvas = document.getElementById(canvasId);
        resizeCanvas(canvas);
        window.addEventListener("resize", function() {
            resizeCanvas(canvas);
        });
        return canvas;
    }

    function resizeCanvas(canvas) {
        canvas.width = document.width || document.body.clientWidth;
        canvas.height = document.height || document.body.clientHeight;
        // Paint something to see the effect of changed orientation
        repaint();
    }

    function repaint() {
        if (!ctx)
            return;

        // Clear background
        ctx.fillStyle = "white";
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        reorient();
        ctx.fillStyle = "darkgreen";
        ctx.fillRect(10, 10, 250, 30);
    }

    function reorient() {
        var angle = window.orientation;
        if (angle) {
            var rot = -Math.PI*(angle/180);
            ctx.translate(angle == -90 ? canvas.width : 0,
            angle == 90 ? canvas.height : 0);
            ctx.rotate(rot);
        }
    }
</script>

Making the Game

Rendering the Board

Listing 3-8. First Version of the BoardRenderer Constructor That Saves the Essential Parameters
function BoardRenderer(context, model) {
    this._ctx = context;
    this._model = model;
}

_p = BoardRenderer.prototype;
Listing 3-9. The BoardRenderer Constructor, All Variables Declared
function BoardRenderer(context, model) {
    this._ctx = context;
    this._model = model;

    // Save for convenience
    this._cols = model.getCols();
    this._rows = model.getRows();

    // top left corner of the board
    this._x = 0;
    this._y = 0;

    // Width and height of the board rectangle
    this._width = 0;
    this._height = 0;
}
Working with Different Screen Sizes
Listing 3-10. Calculating the Radius of Token and Gradient Offsets
// Token radius
var radius = cellSize*0.4;

// Center of the gradient
var gradientX = cellSize*0.1;
var gradientY = -cellSize*0.1;

var gradient = ctx.createRadialGradient(
    gradientX, gradientY, cellSize*0.1, // inner circle (glare)
    gradientX, gradientY, radius*1.2); // outer circle
Listing 3-11. Setting the Parameters of the Game UI: The Position of the Board Within a Canvas and the Size of a Cell
/**
 * Sets the new position and size for a board. Should call repaint to
 * see the changes
 * @param x the x coordinate of the top-left corner
 * @param y the y coordinate of the top-left corner
 * @param cellSize optimal size of the cell in pixels
 */
_p.setSize = function(x, y, cellSize) {
    this._x = x;
    this._y = y;
    this._cellSize = cellSize;
    this._width = this._cellSize*this._cols;
    this._height = this._cellSize*this._rows;
};
Rendering Board
Listing 3-12. Functions That Render the UI of the Board: A Background, a Grid, and a Token in a Given Cell
_p._drawBackground = function() {
    var ctx = this._ctx;

    // Background
    var gradient = ctx.createLinearGradient(0, 0, 0, this._height);
    gradient.addColorStop(0, "#fffbb3");
    gradient.addColorStop(1, "#f6f6b2");
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, this._width, this._height);

    // Drawing curves
    var co = this._width/6; // curve offset
    ctx.strokeStyle = "#dad7ac";
    ctx.fillStyle = "#f6f6b2";

    // First curve
    ctx.beginPath();
    ctx.moveTo(co, this._height);
    ctx.bezierCurveTo(this._width + co*3, -co,
                      -co*3, -co, this._width - co, this._height);
    ctx.fill();

    // Second curve
    ctx.beginPath();
    ctx.moveTo(co, 0);
    ctx.bezierCurveTo(this._width + co*3, this._height + co,
                     -co*3, this._height + co, this._width - co, 0);
    ctx.fill();
};

_p._drawGrid = function() {
    var ctx = this._ctx;
    ctx.beginPath();
    // Drawing horizontal lines
    for (var i = 0; i <= this._cols; i++) {
        ctx.moveTo(i*this._cellSize + 0.5, 0.5);
        ctx.lineTo(i*this._cellSize + 0.5, this._height + 0.5)
    }

    // Drawing vertical lines
    for (var j = 0; j <= this._rows; j++) {
        ctx.moveTo(0.5, j*this._cellSize + 0.5);
        ctx.lineTo(this._width + 0.5, j*this._cellSize + 0.5);
    }

    // Stroking to show them on the screen
    ctx.strokeStyle = "#CCC";
    ctx.stroke();
};


_p.drawToken = function(cellX, cellY) {
    var ctx = this._ctx;
    var cellSize = this._cellSize;
    var tokenType = this._model.getPiece(cellX, cellY);

    // Cell is empty
    if (!tokenType)
        return;

    var colorCode = "black";
    switch(tokenType) {
        case BoardModel.RED:
            colorCode = "red";
        break;
        case BoardModel.GREEN:
            colorCode = "green";
        break;
    }

    // Center of the token
    var x = this._x + (cellX + 0.5)*cellSize;
    var y = this._y + (cellY + 0.5)*cellSize;
    ctx.save();
    ctx.translate(x, y);

    // Token radius
    var radius = cellSize*0.4;

    // Center of the gradient
    var gradientX = cellSize*0.1;
    var gradientY = -cellSize*0.1;

    var gradient = ctx.createRadialGradient(
        gradientX, gradientY, cellSize*0.1, // inner circle (glare)
        gradientX, gradientY, radius*1.2); // outer circle

    gradient.addColorStop(0, "yellow"); // the color of the "light"
    gradient.addColorStop(1, colorCode); // the color of the token
    ctx.fillStyle = gradient;

    ctx.beginPath();
    ctx.arc(0, 0, radius, 0, 2*Math.PI, true);
    ctx.fill();
    ctx.restore();
};
Listing 3-13. The Repaint Function Renders the Whole Board from Scratch
_p.repaint = function() {
    this._ctx.save();
    this._ctx.translate(this._x, this._y);
    this._drawBackground();
    this._drawGrid();
    this._ctx.restore();

    for (var i = 0; i < this._cols; i++) {
        for (var j = 0; j < this._rows; j++) {
            this.drawToken(i, j);
        }
    }
};

Game State and Logic

Listing 3-14. The BoardModel Constructor
function BoardModel(cols, rows) {
    this._cols = cols || 7;
    this._rows = rows || 6;
    this._data = [];

    this._currentPlayer = BoardModel.RED;
    this._totalTokens = 0;

    this.reset();
}

_p = BoardModel.prototype;
Listing 3-15. The Code That Uses “Constants” Instead of Numbers Are Easier to Read
BoardModel.EMPTY = 0;
BoardModel.RED = 1;
BoardModel.GREEN = 2;
Listing 3-16. Resetting the Game Board to the Initial State
_p.reset = function() {
        this._data = [];
        for (var i = 0; i < this._rows; i++) {
                this._data[i] = [];
                for (var j = 0; j < this._cols; j++) {
                        this._data[i][j] = BoardModel.EMPTY;
                }
        }

        this._currentPlayer = BoardModel.RED;
        this._totalTokens = 0;
};