Научете обектно-ориентирано програмиране в JavaScript, като създадете Tetris. (7)

В този кръг ние рефакторираме MinoGfx и осъзнаваме, че можем да правим промени безопасно при ООП.

Ето връзка към статии от тази серия: Предишна статия / Следваща статия

Първо, ще можем да открием дали завъртяно Мино е ударило стени. За да постигнем това, въвеждаме масива _fposRotates, който съдържа формите на Мино във всяка от четирите посоки. Например, ако беше T-Mino, щеше да е както следва.

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

Променяме L76, 77, 81, 89 от Mino и добавяме createRotates и this.rotate като следния списък. Ще прикача целия списък в долната част на тази статия.

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 приема 1 за дясно завъртане и -1 за ляво завъртане като свой аргумент. При изчисляването на dirUpdating във втория ред отгоре бихме искали да използваме просто (_fposDir + dir) % 4, но резултатът от -1 % 4 е -1, така че го коригираме, като добавим 4.

createRotates създава масива _fposRotates, описан по-горе, и не се извиква отвън на Mino, така че се дефинира чрез използване на function, а не чрез формата на this.createRotates. Методът за изчисляване на стойностите, които ще се съхраняват в масива, ще използва същия метод на изчисление, както е описано в четвъртата статия.

След това добавяме 6 реда към gGame, както следва. След това можем да завъртим Мино наляво с клавиша z и надясно с клавиша x.

На пръв поглед изглежда, че програмата работи правилно, когато направим някои промени по-горе.

Въпреки това, след като Mino падне на дъното и следващият Mino се появи отгоре, програмата ще се държи неправилно. Причината за грешката е, че стойностите на координатите, съхранени в _pxpos8 от MinoGfx, все още се завъртат, когато следващото Mino се появи в горната част.

Сега нека преработим MinoGfx, за да имаме _pxposRotates подобно на _fposRotates от Mino. Ако масивът _pxposRotates е получен от аргумент, когато обектът MinoGfx е конструиран, неговата инициализация се опростява, както следва.

Променяме MinoGfx.draw и MinoGfx.erase както следва.

Можем да преработим двата метода MinoGfx.rotateR и MinoGfx.rotateL в един метод MinoGfx.rotate, както следва.

Горният рефакторинг прави MinoGfx много прост, както следва.

Дори при такива значителни промени за MinoGfx, въздействието е ограничено само до Mino. Това илюстрира важността на ООП капсулирането.

И накрая, в отговор на промените, направени в MinoGfx, ще направим някои модификации в Mino. Ние променяме инициализацията на Mino както следва.

createRotates се променя, както следва.

Mino.rotate променя само един ред.

С горните модификации програмата вече ще работи правилно. Препоръчвам ви да стартирате вашата програма и да потвърдите нейната работа.

В тази статия се опитахме да направим основен рефакторинг на програмата и се надявам, че сте открили, че можете да правите промени безопасно. Благодаря, че прочетохте тази статия.

// 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();