Click here to Skip to main content
13,197,796 members (43,394 online)
Click here to Skip to main content
Add your own
alternative version

Stats

52.8K views
1.7K downloads
64 bookmarked
Posted 15 Feb 2015
MIT

Tetris on Canvas

Rate this:
Please Sign up or sign in to vote.
Derived work: customizable Tetris with pure HTML + JavaScript + Canvas, using strict mode, complete with help and all classic Tetris operations

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 Preview
  12. Conclusions

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:

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.

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;
}
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:

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"); 
         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:

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: First fully-functional version, as described in the article.

1.1: 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: Known browser compatibility issues fixed.

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

11. Live Preview

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

Share

About the Author

Sergey Alexandrovich Kryukov
Architect
United States United States
No Biography provided

You may also be interested in...

Comments and Discussions

 
SuggestionNice code, thanks for that, but gaming is boring because the tempo does not increase Pin
badmaster2-Oct-17 5:58
memberbadmaster2-Oct-17 5:58 
GeneralRe: Nice code, thanks for that, but gaming is boring because the tempo does not increase Pin
Sergey Alexandrovich Kryukov2-Oct-17 6:54
memberSergey Alexandrovich Kryukov2-Oct-17 6:54 
GeneralRe: Nice code, thanks for that, but gaming is boring because the tempo does not increase Pin
badmaster3-Oct-17 4:15
memberbadmaster3-Oct-17 4:15 
GeneralRe: Nice code, thanks for that, but gaming is boring because the tempo does not increase Pin
Sergey Alexandrovich Kryukov3-Oct-17 4:47
memberSergey Alexandrovich Kryukov3-Oct-17 4:47 
GeneralNeat Pin
Espen Harlinn8-Mar-16 14:51
mentorEspen Harlinn8-Mar-16 14:51 
GeneralRe: Neat Pin
Sergey Alexandrovich Kryukov8-Mar-16 20:06
mvpSergey Alexandrovich Kryukov8-Mar-16 20:06 
GeneralMy vote of 5 Pin
José Cintra4-Jan-16 10:21
memberJosé Cintra4-Jan-16 10:21 
GeneralRe: My vote of 5 Pin
Sergey Alexandrovich Kryukov4-Jan-16 10:26
mvpSergey Alexandrovich Kryukov4-Jan-16 10:26 
GeneralVery good... It's great... Pin
amarnath_gvt12-Oct-15 20:13
memberamarnath_gvt12-Oct-15 20:13 
GeneralRe: Very good... It's great... Pin
Sergey Alexandrovich Kryukov12-Oct-15 20:31
mvpSergey Alexandrovich Kryukov12-Oct-15 20:31 
Questionget away! Pin
Sem.Shekhovtsov19-Jul-15 11:49
professionalSem.Shekhovtsov19-Jul-15 11:49 
QuestionRe: get away! Pin
Jordan Wilde31-Aug-17 7:14
memberJordan Wilde31-Aug-17 7:14 
AnswerRe: get away! Pin
Sergey Alexandrovich Kryukov31-Aug-17 15:53
memberSergey Alexandrovich Kryukov31-Aug-17 15:53 
GeneralMy vote of 5 Pin
Mika Wendelius28-Feb-15 6:00
mentorMika Wendelius28-Feb-15 6:00 
GeneralRe: My vote of 5 Pin
Sergey Alexandrovich Kryukov28-Feb-15 6:48
mvpSergey Alexandrovich Kryukov28-Feb-15 6:48 
GeneralMy vote of 5 Pin
Agent__00725-Feb-15 21:11
professionalAgent__00725-Feb-15 21:11 
GeneralRe: My vote of 5 Pin
Sergey Alexandrovich Kryukov26-Feb-15 2:37
mvpSergey Alexandrovich Kryukov26-Feb-15 2:37 
GeneralAwesome... Pin
Raje_25-Feb-15 4:11
memberRaje_25-Feb-15 4:11 
GeneralRe: Awesome... Pin
Sergey Alexandrovich Kryukov25-Feb-15 4:58
mvpSergey Alexandrovich Kryukov25-Feb-15 4:58 
GeneralMy vote of 5 Pin
Maciej Los23-Feb-15 9:13
protectorMaciej Los23-Feb-15 9:13 
GeneralRe: My vote of 5 Pin
Sergey Alexandrovich Kryukov23-Feb-15 9:23
mvpSergey Alexandrovich Kryukov23-Feb-15 9:23 
GeneralRe: My vote of 5 Pin
Maciej Los23-Feb-15 9:26
protectorMaciej Los23-Feb-15 9:26 
GeneralRe: My vote of 5 Pin
Sergey Alexandrovich Kryukov23-Feb-15 9:28
mvpSergey Alexandrovich Kryukov23-Feb-15 9:28 
GeneralMy vote of 5 Pin
Afzaal Ahmad Zeeshan23-Feb-15 9:13
professionalAfzaal Ahmad Zeeshan23-Feb-15 9:13 
GeneralLife Preview was added today... Pin
Sergey Alexandrovich Kryukov23-Feb-15 9:22
mvpSergey Alexandrovich Kryukov23-Feb-15 9:22 
GeneralRe: Life Preview was added today... Pin
Afzaal Ahmad Zeeshan23-Feb-15 20:08
professionalAfzaal Ahmad Zeeshan23-Feb-15 20:08 
GeneralResize? Pin
Sergey Alexandrovich Kryukov24-Feb-15 2:48
mvpSergey Alexandrovich Kryukov24-Feb-15 2:48 
GeneralRe: Resize? Pin
Afzaal Ahmad Zeeshan24-Feb-15 3:09
professionalAfzaal Ahmad Zeeshan24-Feb-15 3:09 
GeneralRe: Resize? Pin
Sergey Alexandrovich Kryukov24-Feb-15 11:15
mvpSergey Alexandrovich Kryukov24-Feb-15 11:15 
AnswerSpeaking of resize... I re-created life preview. Pin
Sergey Alexandrovich Kryukov17-Aug-17 16:47
memberSergey Alexandrovich Kryukov17-Aug-17 16:47 
GeneralMy vote of 5 Pin
Humayun Kabir Mamun19-Feb-15 18:07
memberHumayun Kabir Mamun19-Feb-15 18:07 
GeneralRe: My vote of 5 Pin
Sergey Alexandrovich Kryukov19-Feb-15 18:58
mvpSergey Alexandrovich Kryukov19-Feb-15 18:58 
QuestionThe irony... Pin
gstolarov19-Feb-15 17:00
membergstolarov19-Feb-15 17:00 
AnswerRe: The irony... Pin
Sergey Alexandrovich Kryukov19-Feb-15 17:31
mvpSergey Alexandrovich Kryukov19-Feb-15 17:31 
GeneralRe: The irony... Pin
gstolarov20-Feb-15 9:55
membergstolarov20-Feb-15 9:55 
GeneralRe: The irony... Pin
Sergey Alexandrovich Kryukov20-Feb-15 10:20
mvpSergey Alexandrovich Kryukov20-Feb-15 10:20 
Questionis it published? Pin
Irina Pykhova16-Feb-15 10:11
professionalIrina Pykhova16-Feb-15 10:11 
AnswerRe: is it published? Pin
Sergey Alexandrovich Kryukov16-Feb-15 10:44
mvpSergey Alexandrovich Kryukov16-Feb-15 10:44 
GeneralRe: is it published? Pin
Irina Pykhova16-Feb-15 15:37
professionalIrina Pykhova16-Feb-15 15:37 
GeneralRe: is it published? Pin
Sergey Alexandrovich Kryukov16-Feb-15 15:39
mvpSergey Alexandrovich Kryukov16-Feb-15 15:39 
GeneralRe: is it published? Pin
Harm-Jan17-Feb-15 23:10
memberHarm-Jan17-Feb-15 23:10 
GeneralPlease remove this copy for now; it violates the license Pin
Sergey Alexandrovich Kryukov18-Feb-15 12:25
mvpSergey Alexandrovich Kryukov18-Feb-15 12:25 
GeneralRe: Please remove this copy for now; it violates the license Pin
Harm-Jan18-Feb-15 21:59
memberHarm-Jan18-Feb-15 21:59 
GeneralRe: Please remove this copy for now; it violates the license Pin
Sergey Alexandrovich Kryukov19-Feb-15 5:47
mvpSergey Alexandrovich Kryukov19-Feb-15 5:47 
GeneralLife playing on this page Pin
Sergey Alexandrovich Kryukov23-Feb-15 7:13
mvpSergey Alexandrovich Kryukov23-Feb-15 7:13 
AnswerLife playing on this page Pin
Sergey Alexandrovich Kryukov23-Feb-15 7:12
mvpSergey Alexandrovich Kryukov23-Feb-15 7:12 
GeneralRe: Life playing on this page Pin
Bruno Sprecher24-Feb-15 1:17
professionalBruno Sprecher24-Feb-15 1:17 
GeneralRe: Life playing on this page Pin
Sergey Alexandrovich Kryukov24-Feb-15 2:44
mvpSergey Alexandrovich Kryukov24-Feb-15 2:44 
GeneralRe: Life playing on this page Pin
Bruno Sprecher24-Feb-15 5:04
professionalBruno Sprecher24-Feb-15 5:04 
GeneralRe: Life playing on this page Pin
Sergey Alexandrovich Kryukov24-Feb-15 5:24
mvpSergey Alexandrovich Kryukov24-Feb-15 5:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.171020.1 | Last Updated 29 Sep 2017
Article Copyright 2015 by Sergey Alexandrovich Kryukov
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid