Sunteți pe pagina 1din 8

2/2/2019 Tetris on Canvas - CodeProject

Tetris on Canvas
Sergey Alexandrovich Kryukov, 1 Feb 2019

V.7.0: Derived work: customizable Tetris with pure HTML + JavaScript + Canvas, using strict mode, complete with help and all
classic Tetris operations

Download source code, HTML+JavaScript — 23.2 KB

Table of Contents
1. Introduction
2. Derived Work
3. New Features
4. Customization
5. General Code Design
6. Low-Level Tetromino
7. Tetromino Constructor and Prototype
8. Inner Functions and Event Handling: Passing "this"
9. Future Development?
10. Versions
11. Live Play
12. Conclusions
https://www.codeproject.com/Articles/876475/Tetris-on-Canvas?display=Print 1/8
2/2/2019 Tetris on Canvas - CodeProject

1. Introduction
This is my very first complete work on JavaScript, not counting something totally trivial. I cannot say I never tried anything in the
field of pure entertainment before, but, again, nothing worth mentioning.

Tetris is a legendary game with unique combination of extreme simplicity and attractiveness.

Unfortunately, I haven't seen a really playable implementation of the game for a long time. No, I'm not a gamer and did not see
much. First of all, I would not risk anything without open source, but what I saw wasn't really playable, due to the lack of most
important features and proper look and feel, making no match with the old-time implementations for DOS.

At the same time, HTML with JavaScript using new HTML (HTML5) canvas element is the most attractive platform for simple
games. It does not require anything except a browser, should work on all platforms and always comes with source code. So, when
I, just be some chance, came across such implementation, I was much pleased. It was apparent that the incomplete work I found
was written by quite a qualified author. At the same time, it wasn't yet playable and the quality of code did not satisfy me at the level
of its general design, despite of its clarity and general correctness. This was mostly due to the lack of flexibility already put in the
initial design and lack of important features. But how can it be a problem if you have a neat source code? So I decided to rewrite it
from scratch.

2. Derived Work
This is the original work by Jake Gordon which caught my attention:

http://codeincomplete.com,
http://codeincomplete.com/posts/2011/10/10/javascript_tetris,
https://github.com/jakesgordon/javascript-tetris

I re-wrote nearly 100% of the code from scratch, but I used all the low-level algorithms developed by Jake and followed most of his
algorithmic ideas, as well as the general design of the application, decomposition into major blocks: basic helper methods, game
with its event queue, rendering with invalidation mechanism and main application. The original Jake's work was incomplete, but
some initial design feature already led the development in wrong direction, and the lack of at least one feature made the game not
really playable. But I liked the basics of the solution and really wanted to fix it all, to create a really operational and well maintained
product.

I did not follow the original code design with the further decomposition of the game into such parts as "constants", "variable",
"logics" and so on. Instead, I designed the separation of the settings objects places in a separate file "settings.js", introduced a
separate constructor with prototype methods representing Tetromino elements and other structural elements formally expressed as
a set of separate JavaScript objects, such as simple FSM and layout.

So, first of all, it allows to easily customize things which were totally rigid in the original work, first of all, the size of the game in
blocks can be modified in reasonable limits. Even playing on the board of the size of some 100 x 100 blocks became quite possible
(but, by the way, really irritating :-)).

3. New Features
I mentioned the lack of the feature which rendered the implementation of the game not playable. Unfortunately, the lack of this
feature is typical for most of the implementations I saw. What is it? There should be a key press (space bar, originally) which should
drop a current tetromino to the bottom, where it still can be moved, if there is a room for that. So, I added this important feature.

I completely changed the layout of the page. Original Jake's design was based on the fixed set of predefined layouts for different
page sizes. Probably he thought it would be simpler, but it wasn't. Not only it added superfluous CSS code, but looked ugly. Now,
the game looks symmetrical on the Web page of any size. A user can adjust the page size at any time, even during the game play.
The layout is recalculated according to the window.innerHeight and the size of the game board in blocks. The
recalculation is made to keep the size of the block to an integer (not fractional value), so the relative size of the board compared to
the inner height of the page vary to keep all the aspects ratio values at the expense of variable game area margins. In other words,
it is designed in the style of a well-resized desktop application.

More importantly, the game can be customized. First of all, the size of the game board in block could not be changed, due to the
layout and aspect ratio problems I mentioned above. Now, as I mentioned before, it can be changed in a separate file, as well as
the block colors and even shapes. I'll describe it in next section of the article. I actually changed the colors and original orientation,
to make the game more playable and closer to its original design.

https://www.codeproject.com/Articles/876475/Tetris-on-Canvas?display=Print 2/8
2/2/2019 Tetris on Canvas - CodeProject
I also added help showing on the same page at any moment of time.

Internally, I created a different thoroughly structured code design, used JavaScript strict mode and exception handling, and
improved performance. I'll briefly describe this design in section 5, but first will describe what can be customized.

4. Customization
The customizable part of the game is placed in a separate file "settings.js".

1. Game size in blocks can be changed, due to the changes in the layout described above. This declaration can be changed:

const gameSizeInBlocks = { x:10, y:20 }

2. Key assignment can be changed in the object key. The properties of this object are named by function, not by key name.
By default, [Enter] is used to start/pause/continue the game, [Esc] stops currently played game, arrow keys move the
current tetromino element ("up" key rotates it), blank space drops it down, [F1] shows and hides help.

3. Timing of the game can be changed in the object delays. This object defines the delays in seconds before moving a
tetromino element by a line: initial, minimal and decrement of the delay applied for acceleration of the game as the user
progress. The delay is incremented by a constant value as total number of lines, according to the game rule, grows.

4. Score rules can be changed in the object scoreRules. The rules define the added score on the drop of each tetromino
and when some rows are removed. The rules can be any user-defined functions calculated the added score depending on
current count of removed lines, score and the number of lines to be removed at once. By default, a fixed amount of points is
added for each dropped line, and the amount of points added for removed lines grows as a power function of the number of
lines removed at once. This is done according the original game design, where the player is given the incentive to collect
numbers of incomplete rows and then complete up to 4 of them at once.

5. Finally, tetromino colors and shapes can be changed. I'll explain it in section 6.

5. General Code Design


The central unit of the game is the Tetromino constructor and two methods of its prototype object described in section 7.

The code starts with the file "settings.js" including in HTML first, and the main code is in "application.js".

The object layout gets main DOM elements and implements original layout and the layout behavior on the change of the
window size. Next object, game, defines the game logic abstracted from the graphical rendering, which is delegated to the object
rendering, which uses HTML5 Canvas feature,
A set of few simple basic utility functions is put below all that, followed by the game's main anonymous function, which is
implemented in the IIFE form, which helps to keep all local functions inaccessible from outside the main function. This pattern is
used throughout the code. (Please see this article on the IIFE JavaScript design pattern, "Immediately-invoked function
expression".)

Another problem elegantly solved by this design pattern is the resolution of the requirements of JavaScript strict mode. It helps to
use inner functions and, at the same time, sandwich the main code in the try-catch block, which is important, especially for
development.

Main function initialize the game, installs event handlers and starts the first frame; other frames are requested through
window.requestAnimationFrame. The use of exception catching is limited to the very top level: on each event
handler and main function, according to the structural exception handling philosophy.

As a next step, I'll describe the most interesting part of the code: the code of the algorithms and its implementation.

6. Low-Level Tetromino
This is a fragment if bitwise definition of tetromino shapes:

function TetrominoShape(size, blocks, color) {


this.size = size; this.blocks = blocks; this.color = color;

https://www.codeproject.com/Articles/876475/Tetris-on-Canvas?display=Print 3/8
2/2/2019 Tetris on Canvas - CodeProject
}
const tetrominoSet = [
new TetrominoShape(4, [0x0F00, 0x2222, 0x00F0, 0x4444], tetrominoColor.I),
//...
];

This is the description of the binary representation of each shape object:

// blocks: each element represents a rotation of the piece (0, 90, 180, 270)
// each element is a 16 bit integer where the 16 bits represent
// a 4x4 set of blocks, e.g. "J"-shaped tetrominoSet[1].blocks[1] = 0x44C0
//
// 0100 = 0x4 << 3 = 0x4000
// 0100 = 0x4 << 2 = 0x0400
// 1100 = 0xC << 1 = 0x00C0
// 0000 = 0x0 << 0 = 0x0000
// ------
// 0x44C0

7. Tetromino Constructor and Prototype


I introduced the Tetromino constructor object for some very good reasons: to improve performance of the code and
maintainability at the same time. Related JavaScript features are often referred to as "OOP" and "class", but these are very
misleading or at least controversial terms; JavaScript prototype-based object machinery is principally different from "OOP with
classes".

Here is the constructor:

function Tetromino(shape, x, y, orientation) {


this.shape = shape; //TetrominoShape
this.x = x;
this.y = y;
this.orientation = orientation;
} //Tetromino

And two method are added to its prototype:

Tetromino.prototype = {
// fn(x, y), accepts coordinates of each block, returns true to break
first: function(x0, y0, orientation, fn, doBreak) {
let row = 0, col = 0, result = false,
blocks = this.shape.blocks[orientation];
for(let bit = 0x8000; bit > 0; bit = bit >> 1) {
if (blocks & bit) {
result = fn(x0 + col, y0 + row);
if (doBreak && result)
return result;
} //if
if (++col === 4) {
col = 0;
++row;
} //if
} //loop
return result;
}, //Tetromino.prototype.first
all: function(fn) { // fn(x, y), accepts coordinates of each block
this.first(this.x, this.y, this.orientation, fn, false); // no break
} //Tetromino.prototype.all
} //Tetromino.prototype

These two methods is the heart of the low-level algorithms: they implement well-known "first of" and "all" patterns. They traverse all
the blocks in the tetromino shape and the first one breaks the search when some condition supplied by the function argument
becomes true.

This break one of the major performance improvements as original code traversed all blocks of a given shape in all cases. (Also
note, that the first and all are created only once; that's why this prototype assignment is done outside of the constructor.)

This is how the function first is used in the game logic:


https://www.codeproject.com/Articles/876475/Tetris-on-Canvas?display=Print 4/8
2/2/2019 Tetris on Canvas - CodeProject

willHitObstacle: function(tetromino, x0, y0, orientation) {


// tentative move is blocked with some obstacle
return tetromino.first(x0, y0, orientation, function(x, y) {
if ((x < 0)
|| (x >= gameSizeInBlocks.x)
|| (y < 0)
|| (y >= gameSizeInBlocks.y)
|| game.getBlock(x,y))
return true;
}, true);
}, //willHitObstacle

As soon as the anonymous function passed to tetromino.first returs true, the function first also returns true
immediately, breaking from the loop traversing the tetromino blocks. This indicates that the first obstacle has been encountered,
which could be one of the walls or another block. Detecting the very first obstacle makes further consideration of obstacle
redundant, so the function willHitObstacle returns true at this point.

The use of the function Tetromino.all is simpler: all blocks of the shape are traversed. This is used, in particular, for
drawing the tetromino elements on the HTML canvas.

8. Inner Functions and Event Handling: Passing "this"


Let's look at one more interesting detail: now an event handlers are added. It's enough to consider just one. This is how it can be
done:

function someFunction(event) { /* ... */ }


document.onkeydown = someFunction;

Will it work? One little problem is that the handler function is implemented as a member of the game object. So will the below code
also work?

document.onkeydown = game.keydown;

Not quite. The problem is that the keydown function uses not only the event argument, but also the implicit argument this,
which is used to access other members of the game object. If the event handler is added the way showed above, this this
argument will still be passed, as always, but it will, not too surprisingly, reference… document object. Didn't I created some
artificial problem for myself? Not at all. This problem is easily solved this way:

document.onkeydown = function(event) { game.keydown(event); };

Note that the event argument should be explicitly passed.

Similar story goes with inner functions. Look at the short fragment of the object rendering:

const rendering = {

// ...

promptText: element("prompt"),
rowsText: element("rows"),
pausedText: element("paused"),
invalid: { board: true, upcoming: true, score: true, rows: true, state: true },
// ...

// ...

draw: function() {
const drawRows = function() {
if (!this.invalid.rows) return;
setText(this.rowsText, game.rows);
this.invalid.rows = false;
}; //drawRows
const drawState = function() {
if (!this.invalid.state) return;
setText(statusVerb, game.states.current === game.states.paused ? "continue" :
"start");

https://www.codeproject.com/Articles/876475/Tetris-on-Canvas?display=Print 5/8
2/2/2019 Tetris on Canvas - CodeProject
setVisibility(this.pausedText, game.states.current === game.states.paused);
setVisibility(this.promptText, game.states.current != game.states.playing);
this.invalid.state = false;
}; //drawState
// ...
drawRows.call(this);
drawState.call(this);
// compare:
// drawState(); //won't work
// drawState(this); //won't work
// ...
} //draw

} //rendering

In the beginning of my design, several drawing methods like drawRows or drawState were defined as rendering properties,
until I figured out that they won't be used anywhere but in draw, so it's better to hide them from outside context by making them
inner functions. From the code fragment shown above, one can see that they use implicit this argument to access members of
the object rendering. Why direct calls (commented out in the code sample) won't work? In JavaScript, this argument
passed to an inner function would be the outer function object, draw instead of rendering. The work-around is to use the
function's call function, which simply passes this explicitly: https://developer.mozilla.org/en-
US/docs/Web/JavaScript/Reference/Global_Objects/Function/call

Of course, it would be possible to add an explicit argument and pass rendering, but using already-always-passing implicit
this argument is a natural and economic solution. After all, I wanted to explain a very general technique for dealing with inner
functions.

9. Future Development?
I have no plan on adding mouse/touch support to the game. I actually implemented experimental version with mouse control and
decided to remove it: it basically worked in different variants, but playing with mouse is really awkward, inconvenient.

It would be really more practical to add the support for the accelerometer and gyroscope for client computers having this
equipment. But this rather would be the job for native Windows, Linux or Android. As to the use of any Web technologies, I find it
important to… wait. I strongly believe that the use of any devices should be incorporated in the public applications only when they
become standardized with W3 Consortium, even so widespread devices as Web cameras or fingerprint readers. In my opinion, all
such devices can be used when appropriate W3 drafts or just proposals evolve into standards and get implemented in the major
browsers. Please see, for example:

http://www.w3.org/TR/orientation-event
http://www.w3.org/TR/html-media-capture.

I don't want to break JavaScript isolation and interact with file system for the sake of storing the setting. By some very good
reasons, this is considered unsafe. As I designed the application rather for "home use", not for playing online, it would quite
possible and pretty easy to generate a setting file from UI on the fly and make it downloadable, so the user could replace it
manually.

I think the only legitimate way would be using Web local storage for the game setting. Apparently, this is would be one additional
feature to implement. Another one would be the option to populate the game with blocks when it starts, with chosen average
density and up to certain height. This is the popular feature of the original game which I would love to implement, as I think this is
the most interesting way to play it. By a number of reasons, it is less trivial than the rest, so I'm only thinking about it.

I invite anyone to send any suggestions or spin-off any kind of derived work.

10. Versions
1.0: February 15, 2015: First fully-functional version, as described in the article.

1.1: February 19, 2015: Functionally the same version, with version information, links to the information on the game, license,
contributors and original publication, added to the help box. This is done for the possibility to publish the product on a stand-along
Web page, apart from this article, still showing this legally sensitive information.

2.0: February 19, 2015: Known browser compatibility issues fixed.

https://www.codeproject.com/Articles/876475/Tetris-on-Canvas?display=Print 6/8
2/2/2019 Tetris on Canvas - CodeProject

3.0: September 20, 2015: Modernized JavaScript code, text-based help show/close button replaced with SVG image, added a note
for incompatible browsers.

4.0: January 20, 2019: Fixed behavior after a tetromino is dropped down (with blank space key): now its location freezes, so it
cannot be moved anymore; move keys affect next tetromino element.

4.1: January 23, 2019: Implemented more advanced handling of Space character.
Now it drops down current tetromino only if the key is not auto-repeat space or if Ctrl+Space is pressed.
KeyboardEvent.repeat may be not implemented in all browsers, so this property is simulated using
game.repeatedKeyDropDown property. Help is updated accordingly.

7.0: February 1, 2019: Many new features.

Added "Download source code" and "Settings" commands.


New "Settings" page provides interactive and convenient way to customize game size in blocks, timing (speed and speed
growth), tetromino colors and key assignments, as well as "clutter". Custom data is saved in a browser's local storage and
can be removed at any time.
"Clutter" is the feature typical for best old implementations of classical Tetris which adds interest to the game. The game
field is cluttered with random tetrominoes up to certain height (specified by the user in percents via settings or immediately
before the game). Then the user can try to clean up the clutter.
Many convenience feature and better help. In particular, custom key assignments are reflected in help.

11. Live Play


The game can be played live here.

12. Conclusions
For some good reasons, JavaScript is sometimes claimed to be the world's most misunderstood language:
http://javascript.crockford.com/javascript.html
https://yow.eventer.com/yow-2013-1080/the-world-s-most-misunderstood-programming-language-by-douglas-crockford-1377.

One important lesson I learned from this exercise is: It's very important to derive the right practices by looking into the very
fundamental features and stay away from the illusions which are too easy to overcome the mind which is not properly cleared. It is
very important not to fall into the distractions created by hypes and plainly incompetent but pretty convincing people. What kind of
distractions? Some of those described here: http://davidwalsh.name/javascript-objects-distractions.

I think all that myth busting if very useful, but this is… a whole different story.

License
This article, along with any associated source code and files, is licensed under The MIT License

About the Author


Sergey Alexandrovich KryukovNo Biography provided
Architect
United States

https://www.codeproject.com/Articles/876475/Tetris-on-Canvas?display=Print 7/8
2/2/2019 Tetris on Canvas - CodeProject

Comments and Discussions


60 messages have been posted for this article Visit https://www.codeproject.com/Articles/876475/Tetris-on-Canvas to post
and view comments on this article, or click here to get a print view with messages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile Article Copyright 2015 by Sergey Alexandrovich Kryukov
Web04 | 2.8.190129.1 | Last Updated 1 Feb 2019 Everything else Copyright © CodeProject, 1999-2019

https://www.codeproject.com/Articles/876475/Tetris-on-Canvas?display=Print 8/8

S-ar putea să vă placă și