Click here to Skip to main content
15,879,535 members
Articles / Web Development / HTML5

Falling Blocks on Canvas

Rate me:
Please Sign up or sign in to vote.
4.97/5 (66 votes)
8 Feb 2019MIT15 min read 143.8K   3.4K   101   74
V.7.5.1: Derived work: customizable Falling Blocks with pure HTML + JavaScript + Canvas, using strict mode, complete with help and all classic Falling Blocks operations

Image 1

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

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.

Falling Blocks 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:
    JavaScript
    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:

JavaScript
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:

JavaScript
// 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:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
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?

JavaScript
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:

JavaScript
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:

JavaScript
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.

[Update]

Interactive on-line editor of game settings and using Web local storage for permanent data storage is implemented in v. 7.0.

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.

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.

7.1: February 5, 2019: Fixed the problem with browsers not allowing localStorage (DOM Storage) — implemented the fallback: playing works, but not storing of custom data in local storage.
The problem was revealed by testing on: Microsoft Edge 42.17134.1.0, EdgeHTML 17.17134, 2018.

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


Written By
Architect
United States United States
Physics, physical and quantum optics, mathematics, computer science, control systems for manufacturing, diagnostics, testing, and research, theory of music, musical instruments… Contact me: https://www.SAKryukov.org

Comments and Discussions

 
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
mvaSergey 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
mvaSergey Alexandrovich Kryukov24-Feb-15 11:15 
AnswerSpeaking of resize... I re-created life preview. Pin
Sergey Alexandrovich Kryukov17-Aug-17 16:47
mvaSergey Alexandrovich Kryukov17-Aug-17 16:47 
GeneralMy vote of 5 Pin
Humayun Kabir Mamun19-Feb-15 18:07
Humayun Kabir Mamun19-Feb-15 18:07 
GeneralRe: My vote of 5 Pin
Sergey Alexandrovich Kryukov19-Feb-15 18:58
mvaSergey Alexandrovich Kryukov19-Feb-15 18:58 
QuestionThe irony... Pin
gstolarov19-Feb-15 17:00
gstolarov19-Feb-15 17:00 
First I wanted to rate it 5* and warn you about pending notice for the license violation - The original Tetris inventor - Alexey Pajitnov - became a patent troll and prosecutes anyone who publishes anything related to Tetris - I guess so he can clear up a field for the people willing to pay for the license - Sony and EA likes. The reason I know is that about 15 years ago I also published a Tetris game on CodeProject just to be notified within couple of month of license violation and have my article removed.

And then scanning the comments I noticed you warning someone else about them violating YOUR license...

The irony...
AnswerRe: The irony... Pin
Sergey Alexandrovich Kryukov19-Feb-15 17:31
mvaSergey Alexandrovich Kryukov19-Feb-15 17:31 
GeneralRe: The irony... Pin
gstolarov20-Feb-15 9:55
gstolarov20-Feb-15 9:55 
GeneralRe: The irony... Pin
Sergey Alexandrovich Kryukov20-Feb-15 10:20
mvaSergey 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
mvaSergey 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
mvaSergey Alexandrovich Kryukov16-Feb-15 15:39 
GeneralRe: is it published? Pin
Harm-Jan17-Feb-15 23:10
professionalHarm-Jan17-Feb-15 23:10 
GeneralPlease remove this copy for now; it violates the license Pin
Sergey Alexandrovich Kryukov18-Feb-15 12:25
mvaSergey Alexandrovich Kryukov18-Feb-15 12:25 
GeneralRe: Please remove this copy for now; it violates the license Pin
Harm-Jan18-Feb-15 21:59
professionalHarm-Jan18-Feb-15 21:59 
GeneralRe: Please remove this copy for now; it violates the license Pin
Sergey Alexandrovich Kryukov19-Feb-15 5:47
mvaSergey Alexandrovich Kryukov19-Feb-15 5:47 
GeneralLife playing on this page Pin
Sergey Alexandrovich Kryukov23-Feb-15 7:13
mvaSergey Alexandrovich Kryukov23-Feb-15 7:13 
AnswerLife playing on this page Pin
Sergey Alexandrovich Kryukov23-Feb-15 7:12
mvaSergey Alexandrovich Kryukov23-Feb-15 7:12 
GeneralRe: Life playing on this page Pin
User 1106097924-Feb-15 1:17
User 1106097924-Feb-15 1:17 
GeneralRe: Life playing on this page Pin
Sergey Alexandrovich Kryukov24-Feb-15 2:44
mvaSergey Alexandrovich Kryukov24-Feb-15 2:44 
GeneralRe: Life playing on this page Pin
User 1106097924-Feb-15 5:04
User 1106097924-Feb-15 5:04 
GeneralRe: Life playing on this page Pin
Sergey Alexandrovich Kryukov24-Feb-15 5:24
mvaSergey 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.