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

В этом раунде мы представляем новый объект, который определяет, ударился ли Мино о стену или нет.

Вот ссылка на статьи из этой серии: Предыдущая статья / Следующая статья

Мино может проходить сквозь стены на текущем этапе, который мы создали. Как бы вы подошли, чтобы улучшить эту ситуацию?

Ну, если говорить о чем-то немного другом, предположим, вы играете в футбол, баскетбол и так далее. Как вы определяете, соблюдаются ли правила в игре в это время? Вам понадобится судья игры. Таким же образом решается и первый вопрос. При использовании ООП программные компоненты создаются при воображении реальных ролей в человеческом обществе. Давайте наймем объект gFieldJdg, чтобы определить, попал Мино в стену или нет.

Мы создадим объект gFieldJdg, который имеет массив, в котором хранятся истинные или ложные значения. Общее количество элементов в массиве равно 252, учитывая, что в нем 12 столбцов и 21 строка. Его элементам присваивается значение true, если есть блок, или false, если он пуст. Например, если 14-й элемент ложный, это означает, что место, выделенное красным на рисунке ниже, пусто.

Добавьте следующий код под gFieldGfx. Я понимаю, что это потребует некоторой работы, но я надеюсь, что вам понравится создавать программу Tetris.

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;
  }
}

В первой половине программы устанавливается значение true там, где расположены левая и правая торцевые стенки и нижняя стенка.

ChkToPut ожидает получить массив fpos4 с четырьмя числами. Четыре числа представляют, где расположены блоки Мино. Например, для T-Mino это будет [1, 12, 13, 14]. Другое число fposLeftTop указывает, куда вы хотите переместить мино. Если fposLeftTop было 24, это означает, что вы хотите переместить T-мино на [25, 36, 37, 38]. ChkToPut возвращает true, если мино можно поместить в указанное место, иначе false.

Массивы JavaScript уникальны тем, что не требуют указания длины. Обычное использование состоит в том, чтобы объявить, что это массив, и добавить элементы по push, как показано ниже. Также возможен доступ к любому элементу по вашему выбору без предварительной подготовки.

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

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

Затем создайте Mino, как показано ниже, который управляет информацией, передаваемой в gFieldJdg.ChkToPut. Добавьте следующий код под 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 в 1-й строке совпадает с переданным в MinoGfx. _fpos4 в 3-й строке содержит четыре значения для передачи в gFieldJdg.ChkToPut, например [25, 36, 37, 38]. Mino.move возвращает true, если Mino можно переместить в указанном направлении, иначе false.

Давайте проверим два добавленных объекта, чтобы увидеть, правильно ли они работают. Добавьте 5 строк L141, 142, 148, 160, 172 к gGame, как показано в следующем списке. Если вы повернете Мино, они больше не будут работать должным образом, но если вы переместите его, не вращая, вы обнаружите, что они работают правильно.

Рассмотрим список выше. _curMino управляет местоположением Мино, а _curMinoGfx отвечает за его отрисовку. Итак, мы считаем _curMino главным объектом, а _curMinoGfx его членом. Пересобрав программу таким образом, мы получаем следующее.

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 = ...
  }
}

По мере того, как вы привыкаете к ООП-мышлению, становится более естественным ограничивать информацию как можно больше внутри. Таким образом, мы ограничиваем MinoGfx значением Mino. Это обычная ситуация в повседневной жизни. Например, вы почувствуете, что канцелярские принадлежности лежат в ящике письменного стола. Если вы сделаете это, у вас, естественно, будет меньше проблем с канцелярскими принадлежностями. То же самое верно и для программ. Если вы примете указанную выше программу, у вас будет меньше проблем с MinoGfx.

Есть еще одно преимущество, которое можно получить, перестроив программу, как описано выше. Мы не можем получить доступ к чему-либо, определенному с помощью const, поэтому мы не можем получить доступ к функциям MinoGfx. Таким образом, только функция move доступна извне для объекта Mino, что снижает нагрузку на программиста. Помните, что одна из целей ООП — уменьшить нагрузку на программиста за счет ограничения информации.

Чтобы четко указать, как это было изменено, я покажу список, выделив внесенные изменения. Всего было изменено 14 строк. Для удобства я добавил две функции-члена: drawAtStartPos из Mino и setToStartPos из MinoGfx.

Из-за перестроения Mino gGame упрощается следующим образом.

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;
    }
  }
}

Я покажу полный список ниже. Спасибо, что нашли время прочитать эту статью.

// 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;
    }
  }
}