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:
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>QuickTetris test bench</title>
<!-- Load external script -->
<script src="lib/jQuery/jquery-1.2.6.js"></script>
<script src="lib/jQuery/jquery-ui-1.6rc4.js"></script>
<script src="lib/soundmanager/script/soundmanager2.js"></script>
<script src="src/graphics.js"></script>
<script src="src/sound.js"></script>
<script src="src/piece.js"></script>
<script src="src/field.js"></script>
<script src="src/test.js"></script>
<script src="src/util.js"></script>
<script src="src/main.js"></script>
<script>
window.onload = function() {
document.getElementById("test0").onclick = function(){ Test.runSuite(); };
document.getElementById("test1").onclick = function(){ Test.testDrawSingleSquare(); };
document.getElementById("test2").onclick = function(){ Test.testDetectKeys(); };
document.getElementById("test3").onclick = function(){ Test.testMoveSquare(); };
document.getElementById("test4").onclick = function(){ Test.testDrawPlayingField(); };
document.getElementById("test5").onclick = function(){ Test.testPieceCollision(); };
document.getElementById("test6").onclick = function(){ Test.testPlaySound(); };
document.getElementById("test7").onclick = function(){ Test.testPlaySoundLooped(); };
document.getElementById("test8").onclick = function(){ Test.testPlayMusic(); };
document.getElementById("test9").onclick = function(){ Test.testExplodeAnimation(); };
};
</script>
</head>
<body>
<h3>Programmatic tests</h3>
<input id="test0" type="submit" value="Run test suite"><br/>
<h3>Visual tests</h3>
<input id="test1" type="submit" value="Draw single square"><br/>
<input id="test2" type="submit" value="Detect keyboard input"><br/>
<input id="test3" type="submit" value="Move square based on input"><br/>
<input id="test4" type="submit" value="Draw playing field"><br/>
<input id="test5" type="submit" value="Piece collision"><br/>
<input id="test6" type="submit" value="Play sound"><br/>
<input id="test7" type="submit" value="Play looped sound"><br/>
<input id="test8" type="submit" value="Play music"><br/>
<input id="test9" type="submit" value="Run explosion animation"><br/>
<div id="gameContainer">
</div>
</body>
</html>
You can run it yourself here. Clicking the top button calls Test.runTestSuite():

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"; # Check the .html files in root dir lintTargets = Dir.entries(".").reject! do |direntry| if(direntry !~ /(\.html)\z/ ) then true end end lintTargets.each do |filename| echo "Running JSLint on: "+filename puts %x{ #{lintCommand} #{filename} }; end # Check the .js files in /src dir lintTargets = Dir.entries("./src").reject! do |direntry| if(direntry !~ /(\.js)\z/ ) then true end end lintTargets.each do |filename| echo "Running JSLint on: "+filename puts %x{ #{lintCommand} ./src/#{filename} }; end echo "Done running JSLint" end def echo(msg) puts(" <Rake build>: "+msg); end
Running it, we see that we have some issues in the code we wrote above:

Easily fixed. Now we have a basic infrastructure in place. In the next part we start looking at graphics and user input.
Comments
Comment from Nox
Date: February 2, 2009, 8:55 pm
You also have forEach
https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Array#Iteration_methods
It might also be noteworthy that semicolons are not required by the parser as long as you use whitespace correctly.
Comment from Thomas Kjeldahl Nilsson
Date: February 2, 2009, 9:31 pm
About the semicolon; I know they’re not required, but I like the semicolon style (blame it on X years of Java programming) – relying on whitespace feels a little fragile to me personally.
Comment from Sean McArthur
Date: February 3, 2009, 1:05 am
Abuot the the forEach method, that’s unfortunately not in all Javascript implementations. IE does not have that method. You’re better off keeping your own written method.
Comment from Mark Story
Date: February 3, 2009, 3:44 pm
Not using semi-colons can easily lead to semicolon injection, and cause a myriad of hard to find problems. I always find it best to use all your braces, and all the semi colons. Sure the interpreter works without them, but I feel better, knowing things are going work exactly as I want with them.
For example:
if (foo == ‘bar’)
foo += ‘baz’
and
if (foo == ‘bar’)
foo += ‘baz’
foo += ‘gizmo’
work very differently and its hard to tell at a glance how they are going to work.
Comment from Nox
Date: February 3, 2009, 11:04 pm
No semicolons are no problems at all as long as you make good use of whitespace in a sane way.
I do agree about braces. Seriously, it’s not like one additional line is going to kill someone… though I don’t agree with your example, again whitespace solves that problem nicely.
Comment from willurd
Date: February 3, 2009, 11:20 pm
Thanks for the tutorials! Javascript is a great language, as I’m sure you’re finding out. It’s one of my favorites. I, however, know almost nothing about game development so this is a big help
Anyway, if you’re using firefox, you should take a look at firebug (https://addons.mozilla.org/en-US/firefox/addon/1843). Firebug is probably the single most useful web developer tool, including a way to log debug/error messages from javascript (console.log(‘your message’)), a dom explorer and a javascript debugger. It’s absolutely fantastic.
Comment from Thomas Kjeldahl Nilsson
Date: February 3, 2009, 11:42 pm
Willurd-
I’m glad you liked it!
Regarding Firebug: I actually used it all along (see part 8 for tool references). And yes it’s a terrific aid for debugging.
Comment from Greg Salisbury
Date: February 4, 2009, 8:19 pm
Adding prototypes onto top-level javascript objects is generally not a good idea.
When you add a prototype to the Function object, it affects all declared functions.
This can have consequences if other functions declare a method of the same name.
All the best, Greg


Write a comment