Learn Object-Oriented Programming In JavaScript by Creating Tetris (4)

Ravi da N.
6 min readMay 5, 2023

In this round, our goal is to understand encapsulation which is one of the key aspects of OOP.

Here is a link to articles in this series: Previous Article / Next Article

We will make a few changes to MinoGfx. In move of MinoGfx, we rewritten the value of _pxpos8. Watch the highlighted lines in the following list.

In order to calculate the coordinates of the rotated mino, it is better to keep the value of _pxpos8. So that the mino can be moved while keeping the value of it, we introduce the member variables _x and _y which represent the current position of the mino. We will change the five lines L50, 58, 59, 65, 73 as following list. I attach the entire code at the bottom of this article.

It is important to realize that any change in the usage of _pxpos8 will only affect the inside of MinoGfx since _pxpos8 is confined by the block { } of MinoGfx. As shown in the list below, we cannot access the variables and functions defined using const or let within { } of function, and we can access them defined using this.

const z = new function() {
const a = 1;
this.b = 2;
this.c = 3;

const f = (x, y) => { return x + y; }
this.g = (x, y) => { return x + y; }
}

//const d = z.a + z.b; // we cannot get the value of a in z.
const e = z.b + z.c; // OK

//const p = z.f(1, 2); // we cannot access the function f in z.
const q = z.g(1, 2); // OK

If we made _pxpos8 accessible from the outside of the block { } as this._pxpos8, it would be necessary to check the impact of changes to _pxpos8 throughout the entire program. The OOP approach makes a clear distinction between what is used only inside the object and what is also used outside of it. This mechanism for restricting access to variables and functions is called encapsulation.

When you are working on a program at work, changes in specifications often happen. At that time, it is very important that how safely the program can be rewritten.

MinoGfx only exposes move, draw, and erase to the outside. So, if the behavior of these three functions remains the same, then no matter how much you change the code inside MinoGfx, the code outside of it will work correctly without any modification.

Do you notice that this is the same thing as your social life? Suppose you asked the assistant to do the same job every day. If the assistant changes the way he or she does it, but the result of it remains the same, no one other than the assistant needs to change their acts. Only the results of the job affect the outside world, is that correct?

Next, let’s rotate the mino. Although it seems difficult to calculate the coordinates of the rotated mino, it is actually quite simple. When the mino fits into 3 by 3 cells, it is easy to see how the rotation is calculated by tracking the change in the upper left coordinate of the cells. We use a different method for the I-mino with 4 cells in a straight line, and we have in mind that the O-mino does not rotate.

The figure below illustrates the case where the size of a cell is considered to be 10 pixels with 90-degrees rightward rotation.

Note that the size of a cell is considered to be 10 pixels.

We can calculate the coordinates after rotating 90 degrees to the right as follows.

x_after_R_rotating = 2 * cell_size - old_y

y_after_R_rotating = old_x

By thinking in the same way, we can also easily find the calculation for leftward rotation.

x_after_L_rotating = old_y

y_after_L_rotating = 2 * cell_size - old_x

OOP makes it easy to imagine where to add the code. Now let’s add 2 functions to MinoGfx.

function MinoGfx(color, blkpos8) {
const _ctx = gFieldGfx.context2d;
const _color = color;
const _pxpos8 = [];
let _x =0, _y = 0;

// omitted

this.rotateR = () => {
for (let idx = 0; idx < 8; idx += 2) {
const old_x = _pxpos8[idx];
_pxpos8[idx] = 2 * g.Px_BLOCK - _pxpos8[idx + 1];
_pxpos8[idx + 1] = old_x;
}
}

this.rotateL = () => {
for (let idx = 0; idx < 8; idx += 2) {
const old_x = _pxpos8[idx];
_pxpos8[idx] = _pxpos8[idx + 1];
_pxpos8[idx + 1] = 2 * g.Px_BLOCK - old_x;
}
}
}

We will also make a few changes to gGame. If the shift key is pressed, the mino is to be rotated. If you want to rotate using the Z key or similar, you can easily do so by using case 'z':.

const gGame = new function() {

// omitted

document.onkeydown = (e) => {
switch (e.key)
{
case 'ArrowLeft':
_curMinoGfx.erase();
if (e.shiftKey) {
_curMinoGfx.rotateL();
} else {
_curMinoGfx.move(-1, 0);
}
_curMinoGfx.draw();
break;

case 'ArrowRight':
_curMinoGfx.erase();
if (e.shiftKey) {
_curMinoGfx.rotateR();
} else {
_curMinoGfx.move(1, 0);
}
_curMinoGfx.draw();
break;

// omitted
}
}
}

While this article focuses on understanding OOP, I believe that making Tetris is also fun in itself. I hope you will also enjoy creating the Tetris program itself. Thanks for reading this article.

// tetris.js
'use strict';

const divTitle = document.createElement('div');
divTitle.textContent = "TETRIS";
document.body.appendChild(divTitle);

const g = {
Px_BLOCK: 30,
Px_BLOCK_INNER: 28,

PCS_COL: 10,
PCS_ROW: 20,
PCS_FIELD_COL: 12,
}

const gFieldGfx = new function() {
const pxWidthField = g.Px_BLOCK * g.PCS_FIELD_COL;
const pxHeightField = g.Px_BLOCK * (g.PCS_ROW + 1);

const canvas = document.createElement('canvas');
canvas.width = pxWidthField;
canvas.height = pxHeightField;
document.body.appendChild(canvas);

const _ctx = canvas.getContext('2d');
_ctx.fillStyle = "black";
_ctx.fillRect(0, 0, pxWidthField, pxHeightField);

const yBtmBlk = g.Px_BLOCK * g.PCS_ROW;
const xRightBlk = pxWidthField - g.Px_BLOCK + 1;

_ctx.fillStyle = 'gray';
for (let y = 1; y < yBtmBlk; y += g.Px_BLOCK) {
_ctx.fillRect(1, y, g.Px_BLOCK_INNER, g.Px_BLOCK_INNER);
_ctx.fillRect(xRightBlk, y, g.Px_BLOCK_INNER, g.Px_BLOCK_INNER);
}

for (let x = 1; x < pxWidthField; x += g.Px_BLOCK) {
_ctx.fillRect(x, yBtmBlk + 1, g.Px_BLOCK_INNER, g.Px_BLOCK_INNER);
}

this.context2d = _ctx;
}

function MinoGfx(color, blkpos8) {
const _ctx = gFieldGfx.context2d;
const _color = color;
const _pxpos8 = [];
let _x =0, _y = 0;

for (let idx = 0; idx < 8; idx += 2) {
_pxpos8[idx] = blkpos8[idx] * g.Px_BLOCK;
_pxpos8[idx + 1] = blkpos8[idx + 1] * g.Px_BLOCK;
}

this.move = (dx, dy) => {
_x += dx * g.Px_BLOCK;
_y += dy * g.Px_BLOCK;
}

this.draw = () => {
_ctx.fillStyle = _color;
for (let idx = 0; idx < 8; idx += 2) {
_ctx.fillRect(_x + _pxpos8[idx] + 1, _y + _pxpos8[idx + 1] + 1
, g.Px_BLOCK_INNER, g.Px_BLOCK_INNER);
}
}

this.erase = () => {
_ctx.fillStyle = 'black';
for (let idx = 0; idx < 8; idx += 2) {
_ctx.fillRect(_x + _pxpos8[idx] + 1, _y + _pxpos8[idx + 1] + 1
, g.Px_BLOCK_INNER, g.Px_BLOCK_INNER);
}
}

this.rotateR = () => {
for (let idx = 0; idx < 8; idx += 2) {
const old_x = _pxpos8[idx];
_pxpos8[idx] = 2 * g.Px_BLOCK - _pxpos8[idx + 1];
_pxpos8[idx + 1] = old_x;
}
}

this.rotateL = () => {
for (let idx = 0; idx < 8; idx += 2) {
const old_x = _pxpos8[idx];
_pxpos8[idx] = _pxpos8[idx + 1];
_pxpos8[idx + 1] = 2 * g.Px_BLOCK - old_x;
}
}
}

const gGame = new function() {
let _curMinoGfx = new MinoGfx('magenta', [1, 0, 0, 1, 1, 1, 2, 1]);
_curMinoGfx.move(4, 0);
_curMinoGfx.draw();

document.onkeydown = (e) => {
switch (e.key)
{
case 'ArrowLeft':
_curMinoGfx.erase();
if (e.shiftKey) {
_curMinoGfx.rotateL();
} else {
_curMinoGfx.move(-1, 0);
}
_curMinoGfx.draw();
break;

case 'ArrowRight':
_curMinoGfx.erase();
if (e.shiftKey) {
_curMinoGfx.rotateR();
} else {
_curMinoGfx.move(1, 0);
}
_curMinoGfx.draw();
break;

case 'ArrowDown':
_curMinoGfx.erase();
_curMinoGfx.move(0, 1);
_curMinoGfx.draw();
break;
}
}
}

--

--

Ravi da N.

C++, C# developer. I'm happy to help so many people enjoy programming. I'd also like to share a lot of knowledge about the program!