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

Ravi da N.
7 min readMay 6, 2023

In this round, we introduce a new object that judges whether a Mino hit the wall or not.

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

A Mino can move through walls in the current stage we have created. How would you approach to improve this situation?

Well, to talk about something a bit different, suppose you were playing a game of football, basketball and so on. How do you determine whether the rules are being followed in the game at that time? You would need a game referee. The first question is also resolved in the same way. When using OOP, program components are created while imagining actual roles in human society. Let’s hire the object gFieldJdg to determine whether a Mino hit the wall or not.

We will create the object gFieldJdg which has an array that stores the true or false values. The total number of elements in the array is 252, considering that there are 12 columns and 21 rows. The elements of it are set to true if there is a block, or false if it is empty. For example, if the 14th element is false, it indicates that the place highlighted in red in the figure below is empty.

Add the following code under gFieldGfx. I am aware that it will take some work, but I hope that you will enjoy creating the Tetris program.

const gFieldJdg = new function() {
const _field = [];

for (let y = 0; y < 20; y++) {
_field.push(true);
for (let x = 0; x < 10; x++) {
_field.push(false);
}
_field.push(true);
}
for (let x = 0; x < 12; x++) {
_field.push(true);
}

this.ChkToPut = (fposLeftTop, fpos4) => {
for (let i = 0; i < 4; i++) {
if (_field[fposLeftTop + fpos4[i]]) { return false; }
}
return true;
}
}

In the first half of the program, it is set to true where the left and right end walls and the bottom wall are located.

ChkToPut expects to receive an array fpos4 with four numbers. The four numbers represent where the Mino blocks are located. For instance, for a T-Mino, it would be [1, 12, 13, 14]. Another number fposLeftTop indicates where you want the mino to be moved to. If fposLeftTop was 24, it indicates that you want to move the T-mino to [25, 36, 37, 38]. ChkToPut returns true if the mino can be put at the specified location, false otherwise.

JavaScript arrays are unique in that they do not require a length to be specified. The usual usage is to declare that it is an array and add elements by push as shown below. It is also possible to access any element of your choice without prior preparation.

const a = [];
a.push(10);
a.push(20); // a == [10, 20]

const b = [];
b[2] = 10; // b == [undefined, undefined, 10]

Next, create Mino as below that manages the information to be passed to gFieldJdg.ChkToPut. Add the following code under gFieldJdg.

function Mino(blkpos8) {
let _fposLeftTop = 0;
const _fpos4 = [];

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

this.move = (dx, dy) => {
const posUpdating = _fposLeftTop + dx + dy * g.PCS_FIELD_COL;
if (gFieldJdg.ChkToPut(posUpdating, _fpos4) == false) {
return false;
}

_fposLeftTop = posUpdating;
return true;
}
}

blkpos8 in the 1st line is the same as that passed to MinoGfx. _fpos4 in the 3rd line holds four values to be passed to gFieldJdg.ChkToPut, which are like [25, 36, 37, 38]. Mino.move returns true if the Mino can be move to the specified direction, false otherwise.

Let’s test the two added objects to see if they work properly. Add 5 lines L141, 142, 148, 160, 172 to gGame, as shown in the following list. If you rotate the Mino, they will no longer work properly, but if you move it without rotating it, you can find that they are working correctly.

A mino no longer pass through the wall.

Consider the list above. _curMino manages the location of the Mino and _curMinoGfx is in charge of drawing it. So we consider _curMino to be principal object and _curMinoGfx to be its member. Rebuilding the program to make it that way, we get the following.

function Mino(color, blkpos8) {
const _minoGfx = new MinoGfx(color, blkpos8);

// omitted

this.move = ...


function MinoGfx(color, blkpos8) {

// omitted

this.move = ...
this.draw = ...
this.erase = ...
this.rotateR = ...
this.rotateL = ...
}
}

As you get used to OOP mindset, it becomes more natural to confine information as much as possible to the inside. So we confine MinoGfx to Mino. It is a common situation in daily life. For example, you will feel that it is natural for your stationery to be in the drawer of your desk. If you do so, you will naturally have less trouble with your stationery. The same is true for programs. If you adopt the program above, you will have less trouble with MinoGfx.

There is another benefit to be gained by rebuilding the program as described above. We cannot access anything defined with const, so we cannot access functions of MinoGfx. So only move function is accessible from outside for Mino object, which reduces the burden on the programmer to think. Remember that one of the goals of OOP is to reduce the burden on the programmer by limiting information.

In order to clearly indicate how it has been changed, I’ll show the list highlighting the changes made. In total, 14 lines were changed. For some convenience, I’ve added two member functions, which are drawAtStartPos of Mino and setToStartPos of MinoGfx.

Because of rebuilding Mino, gGame is simplified as follows.

const gGame = new function() {
let _curMino = new Mino('magenta', [1, 0, 0, 1, 1, 1, 2, 1]);
_curMino.drawAtStartPos();

document.onkeydown = (e) => {
switch (e.key)
{
case 'ArrowLeft':
_curMino.move(-1, 0);
break;

case 'ArrowRight':
_curMino.move(1, 0);
break;

case 'ArrowDown':
_curMino.move(0, 1);
break;
}
}
}

I’ll show the full list below. Thank you for taking the time to read 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;
}

const gFieldJdg = new function() {
const _field = [];

for (let y = 0; y < 20; y++) {
_field.push(true);
for (let x = 0; x < 10; x++) {
_field.push(false);
}
_field.push(true);
}
for (let x = 0; x < 12; x++) {
_field.push(true);
}

this.ChkToPut = (fposLeftTop, fpos4) => {
for (let i = 0; i < 4; i++) {
if (_field[fposLeftTop + fpos4[i]]) { return false; }
}
return true;
}
}

function Mino(color, blkpos8) {
const _minoGfx = new MinoGfx(color, blkpos8);

let _fposLeftTop = 0;
const _fpos4 = [];

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

this.drawAtStartPos = () => {
_fposLeftTop = 4;

_minoGfx.setToStartPos();
_minoGfx.draw();
}

this.move = (dx, dy) => {
const posUpdating = _fposLeftTop + dx + dy * g.PCS_FIELD_COL;
if (gFieldJdg.ChkToPut(posUpdating, _fpos4) == false) {
return false;
}

_fposLeftTop = posUpdating;

_minoGfx.erase();
_minoGfx.move(dx, dy);
_minoGfx.draw();
return true;
}

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.setToStartPos = () => {
_x = 4 * g.Px_BLOCK;
_y = 0;
}

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 _curMino = new Mino('magenta', [1, 0, 0, 1, 1, 1, 2, 1]);
_curMino.drawAtStartPos();

document.onkeydown = (e) => {
switch (e.key)
{
case 'ArrowLeft':
_curMino.move(-1, 0);
break;

case 'ArrowRight':
_curMino.move(1, 0);
break;

case 'ArrowDown':
_curMino.move(0, 1);
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!