23.01.2009

Javascript Tetris Pt 3: Infrastructure

Full source code can be downloaded from project home at kjeldahlnilsson.net.

We’ll start by creating some basic infrastructure - just enough to give us a good running start. We need the bare minimum only: personally, I like to evolve my projects to actual needs as I go along. I don’t want to invest huge amounts of time in build scripts, test setups, frameworks and support code before I know what I actually need.

Note: The industrious reader may wish to follow along and reimplement the game laid out in this article. Be advised that the source code included in this article is incomplete, and that code snippets sometimes refers to code which is defined further ahead - refer to the full source code if you want the whole picture. I will reference the files containing the code as we move forward.

Unit tests

We are going to write at least some unit tests as we go along, so we need support for writing and running them in our environment. Now, of course, there are several popular unit test frameworks to choose from… but since the goal of our tiny project is to learn Javascript, we’ll simply roll our own tiny framework.

We don’t need much - some way to assert that tests fail or succeed, some way of defining test cases, and a function to launch and run all the tests.

util.js:

// Assert methods needed by test framework
function assertTrue(boolean, errorMsg) {
    if (boolean === false) {
        throw (errorMsg);
    }
    return;
}

function assertFalse(boolean, errorMsg) {
    if (boolean === true) {
        throw (errorMsg);
    }
    return;
}

test.js:
var Test = {

    runSuite: function() {
        // Call all methods/testcases in suite
        for (var testFunc in this.Suite) {
            if (this.Suite.hasOwnProperty(testFunc)) { // Don't call any inherited methods
                try {
                    this.Suite[testFunc]();
                }
                catch(err) {
                    alert(testFunc + "() failed: " + err);
                    return;
                }
            }
        }

        Graphics.drawString("--All tests in suite passed--", 400, 400);
    },

    Suite: {

        // Add test cases here
        testAsserts: function() {
            assertTrue(2 === 2, "This should never fail");
            assertFalse(2 === 3, "This should always fail");
        },

};

Finally we need some sort of testrunner application. I like having a “test bench” when I develop low level graphical functionality; a context for manually running and observing isolated visual regression tests. We’ll create a testbench web page, with a separate button for running our test suite.

test.html:



QuickTetris test bench

 
 

 
 
 
 
 
 
 





Programmatic tests


Visual tests











You can run it yourself here. Clicking the top button calls Test.runTestSuite():

quicktetristestbench

I debugged the project using Apache on my own machine. Apache comes preinstalled in recent versions of Mac OS X, you simply need to enable it. Windows users need to download and run the binary installer. After starting Apache, simply dump the project in Apache’s /htdocs folder and point your browser to http://localhost/RELATIVE_PROJECT_PATH.

Abstract data types, syntactic sugar

Tetris is basically all about matrices - a grid of tiles where elements appear, move around, and disappear. We are going to store and manipulate a bunch of game state using two dimensional arrays. Javascript provides bare bones support by letting us define arrays of arrays, but we need a little more syntactic sugar for all the grid hopping we’re going to do.

I personally really like Ruby’s Enumerable idiom, so we want to wire each(), map() etc into the Javascript Array object. We are, of course, not the first people to think of this; the Prototype framework could supply much of this functionality instantly. But again: the object here is to learn the language, so we’ll write it ourselves.

The following tests articulate what we want from the Array object:

test.js:

        testArrDimensions: function() {
            var width = 3;
            var height = 2;
            var initValue = "x";
            var arr = get2dArray(width, height, initValue);

            assertTrue(arr.getWidth() === width, "Width of array not expected length");
            assertTrue(arr.getHeight() === height, "Height of array not expected length");

            for (var x in arr) {
                if (arr.hasOwnProperty(x)) { // Don't call any inherited methods
                    assertTrue(arr[x].length === height, "Height of array not expected length");
                }
            }
        },

        testArrEach: function() {
            var arr = [2, 4, 5, 2];
            var length = arr.length;

            var elementsVisited = 0;
            arr.each(function(element) {
                assertTrue(element !== null, "Expected all elements to be non-null");
                elementsVisited++;
            });

            assertTrue(elementsVisited === length, "Didn't visit " + length + " elements as expected");
        },

        testArrEach2d: function() {
            var width = 3;
            var height = 2;
            var initValue = "x";
            var arr = get2dArray(width, height, initValue);

            var elementsVisited = 0;
            arr.each(function(element) {
                assertTrue(element === initValue, "Not all slots in array was set to " + initValue);
                elementsVisited++;
            });

            assertTrue(elementsVisited === (width * height), "Didn't visit " + (width * height) + " elements as expected");
        },

        testArrEachRow: function() {
            var width = 3;
            var height = 4;
            var initValue = "x";
            var arr = get2dArray(width, height, initValue);

            var rowsVisited = 0;
            arr.eachRowWithIndex(function(row) {
                assertTrue(row.length === width, "Expected row to be " + width + " elements long");
                rowsVisited++;
            });

            assertTrue(rowsVisited === height, "Didn't get " + height + " rows as expected");
        },

        testArrMap: function() {
            var width = 2;
            var height = 2;
            var initValue = "2";
            var arr = get2dArray(width, height, initValue);

            var mappedArr = arr.map(function(element) {
                return element * 2
            });

            var elementsVisited = 0
            mappedArr.each(function(element) {
                assertTrue(element === initValue * 2, "Not all slots in mapped array were transformed to new value");
                elementsVisited++;
            });

            assertTrue(elementsVisited === width * height, "Mapped array not same size as original array");
        }

    }

We need to augment the Javascript Array object to support this functionality.

util.js:

// Returns two dimensional array, every element initiated to given value
function get2dArray(width, height, initValue) {
    var arr2d = [];

    for (var x = 0; x < width; x++) { // For each row
        arr2d[x] = [];
    }

    for (x = 0; x < width; x++) {
        for (var y = 0; y < height; y++) {
            arr2d[x][y] = initValue;
        }
    }

    return arr2d;
}

// D. Crockford idiom for function mixin
Function.prototype.method = function(name, func) {
    this.prototype[name] = func;
    return this;
};

// Array mixins for 2d grid functionality
Array.method('getWidth',
function() {
    return this.length;
});

Array.method('getHeight',
function() {
    return this[0].length;
});

Array.method('isTwoDimensional',
function() {
    return (this[0].constructor == Array);
});

Array.method('each',
function(appliedFunction) {
    for (var x = 0; x < this.getWidth(); x++) {
        if (this.isTwoDimensional()) {
            for (var y = 0; y < this.getHeight(); y++) {
                appliedFunction(this[x][y]);
            }
        }
        else {
            appliedFunction(this[x]);
        }
    }
});

Array.method('eachWithIndexes',
function(appliedFunction) {
    for (var x = 0; x < this.getWidth(); x++) {
        if (this.isTwoDimensional()) {

            for (var y = 0; y < this.getHeight(); y++) {
                appliedFunction(this[x][y], x, y);
            }
        }
        else {
            appliedFunction(this[x], x);
        }
    }
});

Array.method('eachRowWithIndex',
function(appliedFunction) {
    for (rowCount = 0; rowCount < this.getHeight(); rowCount++) {
        var row = [];
        for (columnCount = 0; columnCount < this.getWidth(); columnCount++) {
            row[columnCount] = this[columnCount][rowCount];
        }
        appliedFunction(row, rowCount);
    }
});

Array.method('map',
function(appliedFunction) {
    var mappedArr = null;
    if (this.isTwoDimensional()) {
        mappedArr = get2dArray(this.getWidth(), this.getHeight(), null);
    }
    else {
        mappedArr = [];
    }

    for (var x = 0; x < this.getWidth(); x++) {
        if (this.isTwoDimensional()) {
            for (var y = 0; y < this.getHeight(); y++) {
                mappedArr[x][y] = appliedFunction(this[x][y], x, y);
            }
        }
        else {
            mappedArr[x] = appliedFunction(this[x]);
        }
    }

    return mappedArr;
});

A little later I found that Array.map() actually already exists in Javascript. It was, however, still a useful exercise to implement a variant of it myself. It’s probably usually not a great idea to monkeypatch over existing core functionality, though. :)

Build environment

We want to set up some sort of automated code verification - especially important since this is a newbie project. Enter JsLint, the closest thing you get to compile-time error checking for Javascript. I chose to run it using the Rhino version (download here). This is our Rake task for running it:

Rakefile:

desc "Run JSLint audit on code and markup"
task :jslint do

  lintCommand = "java -classpath ./lib/jsLint/js.jar "+
                  "org.mozilla.javascript.tools.shell.Main ./lib/jsLint/jslint.js";


PreviousNext