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

Ravi da N.
7 min readMay 9, 2023

In this round, we refactor MinoGfx and realize that we can make changes with safety under OOP.

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

First, we’ll be able to detect whether a rotated Mino has hit walls. To achieve that, we introduce the array _fposRotates which holds the shapes of the Mino in each of the four directions. For example, if it were a T-Mino, it would be like as follows.

_fposRotates[0] = [1, 12, 13, 14]
_fposRotates[1] = [14, 1, 13, 25]
_fposRotates[2] = [25, 14, 13, 12]
_fposRotates[3] = [12, 25, 13, 1]

We change L76, 77, 81, 89 of Mino and add createRotates and this.rotate as the following list. I’ll attach the entire list in the bottom of this article.

this.rotate = (dir) => {
const dirUpdating = (_fposDir + dir + 4) % 4;
if (gFieldJdg.chkToPut(_fposLeftTop, _fposRotates[dirUpdating]) == false) {
return;
}
_fposDir = dirUpdating;

_minoGfx.erase();
(dir > 0) ? _minoGfx.rotateR() : _minoGfx.rotateL();
_minoGfx.draw();
}

function createRotates(blkpos8) {
const ret_fpos = [];
for (let r = 0; r < 4; r++) {
const fpos4 = [];
for (let idx = 0; idx < 8; idx += 2) {
fpos4.push(blkpos8[idx] + blkpos8[idx + 1] * g.PCS_FIELD_COL);
}
ret_fpos.push(fpos4);

for (let idx = 0; idx < 8; idx += 2) {
const old_x = blkpos8[idx];
blkpos8[idx] = 2 - blkpos8[idx + 1];
blkpos8[idx + 1] = old_x;
}
}
return ret_fpos;
}

rotate takes 1 for right rotation and -1 for left rotation as its argument. In the calculation of dirUpdating in the 2nd line from the top, we would like to use (_fposDir + dir) % 4 simply, but the result of -1 % 4 is -1, so we correct it by adding 4.

createRotates produces the array _fposRotates described above, and it is not called from the outside of Mino, so it is defined by using function, not by the form of this.createRotates. The method for calculating the values to be stored in the array will use the same calculation method as described in the fourth article.

Next, we add 6 lines to gGame as follows. Then we can rotate a Mino leftward by the z key and rightward by the x key.

Please add 6 highlighted lines.

At first glance, the program appears to be working correctly when we make some changes above.

The rotated Mino cannot pass through the wall.

However, once a Mino falls to the bottom and the next Mino appears on the top, the program will behave incorrectly. The reason for the error is that the coordinate values stored in _pxpos8 of MinoGfx are still rotated when the next Mino appears on the top.

Fig.1: In setToStartPos, _pxpos8 is not set to its initial values.

Now let’s refactor MinoGfx to have a _pxposRotates similar to _fposRotates of Mino. If the array _pxposRotates is received by an argument when the MinoGfx object is constructed, its initialization is simplified as follows.

Compare with Fig.1.

We change MinoGfx.draw and MinoGfx.erase as follows.

We can refactor the two methods MinoGfx.rotateR and MinoGfx.rotateL into one method MinoGfx.rotate as follows.

The above refactoring make MinoGfx very simple, as follows.

Even with such significant changes for MinoGfx, the impact is only limited to within Mino. That illustrates the importance of OOP encapsulation.

Finally, in response to the changes made to MinoGfx, we will make some modifications in Mino. We change the initialization of Mino as follows.

Please change two highlighted lines.

createRotates changes as follows.

Please change three highlighted lines.

Mino.rotate changes only one line.

Please change one highlighted line.

With the above modifications, the program will now work correctly. I encourage you to run your program and confirm its operation.

In this article, we have attempted a major refactoring of the program, and I hope you have found that you can make changes with safety. 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,

MSEC_GAME_INTERVAL: 1000,
}

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;
this.canvas = canvas;
}

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 [_fposRotates, pxposRotates] = createRotates(blkpos8);
const _minoGfx = new MinoGfx(color, pxposRotates);

let _fposLeftTop = 0;
let _fposDir = 0;

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

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

this.move = (dx, dy) => {
const posUpdating = _fposLeftTop + dx + dy * g.PCS_FIELD_COL;
if (gFieldJdg.chkToPut(posUpdating, _fposRotates[_fposDir]) == false) {
return false;
}
_fposLeftTop = posUpdating;

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

this.rotate = (dir) => {
const dirUpdating = (_fposDir + dir + 4) % 4;
if (gFieldJdg.chkToPut(_fposLeftTop, _fposRotates[dirUpdating]) == false) {
return;
}
_fposDir = dirUpdating;

_minoGfx.erase();
_minoGfx.rotate(dirUpdating);
_minoGfx.draw();
}

function createRotates(blkpos8) {
const ret_fpos = [];
const ret_pxpos = [];

for (let r = 0; r < 4; r++) {
const fpos4 = [];
for (let idx = 0; idx < 8; idx += 2) {
fpos4.push(blkpos8[idx] + blkpos8[idx + 1] * g.PCS_FIELD_COL);
}
ret_fpos.push(fpos4);
ret_pxpos.push([...blkpos8].map(x => x * g.Px_BLOCK));

for (let idx = 0; idx < 8; idx += 2) {
const old_x = blkpos8[idx];
blkpos8[idx] = 2 - blkpos8[idx + 1];
blkpos8[idx + 1] = old_x;
}
}
return [ret_fpos, ret_pxpos];
}

function MinoGfx(color, pxposRotates) {
const _ctx = gFieldGfx.context2d;
const _color = color;
const _pxposRotates = pxposRotates;
let _x, _y, _pxposCur;

this.setToStartPos = () => {
_x = 4 * g.Px_BLOCK;
_y = 0;
_pxposCur = _pxposRotates[0];
}

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

this.rotate = (dir) => {
_pxposCur = _pxposRotates[dir];
}

this.draw = () => drawIn(_color);
this.erase = () => drawIn('black');

function drawIn(color) {
_ctx.fillStyle = color;
for (let idx = 0; idx < 8; idx += 2) {
_ctx.fillRect(_x + _pxposCur[idx] + 1, _y + _pxposCur[idx + 1] + 1
, g.Px_BLOCK_INNER, g.Px_BLOCK_INNER);
}
}
}
}


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 'z':
_curMino.rotate(-1);
break;

case 'x':
_curMino.rotate(1);
break;

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

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

case 'ArrowDown':
if (_curMino.move(0, 1)) {
_timeNextDown = Date.now() + g.MSEC_GAME_INTERVAL;
}
break;
}
}

let _timeNextDown;
let _isQuit = false;

this.run = async () => {
_timeNextDown = Date.now() + g.MSEC_GAME_INTERVAL;

for (;;) {
await new Promise(r => setTimeout(r, _timeNextDown - Date.now()));

if (_isQuit) { break; }
if (Date.now() < _timeNextDown) { continue; }

if (_curMino.move(0, 1) == false) {
_curMino.drawAtStartPos();
}
_timeNextDown += g.MSEC_GAME_INTERVAL;
}
}

this.quit = () => {
_isQuit = true;
}
}

gFieldGfx.canvas.onclick = gGame.quit;
gGame.run();

--

--

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!