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

Это будет последняя часть этой серии. Давайте завершим программу Tetris вместе!

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

Во-первых, давайте удостоверимся, что блоки складываются. Мы добавляем возможность для Mino отвечать на запросы о текущем местоположении блока следующим образом. Я прикреплю код всей готовой программы в конце этой статьи.

Используя функцию getCurPos, указанную выше, gFieldJdg записывает наличие блоков в массиве _field, как в списке ниже. Пожалуйста, добавьте метод this.merge в gFieldJdg.

const gFieldJdg = new function() {
  const _field = [];
  
  // (omitted)
  
  this.merge = (mino) => {
    const [fposLeftTop, fpos4] = mino.getCurFPos();
    for (let i = 0; i < 4; i++) {
      _field[fposLeftTop + fpos4[i]] = true;
    }
  }
}

Только с помощью дополнительного кода, приведенного выше, теперь можно записывать укладку блоков. Мы добавим только одну строку в gGame.run, чтобы Mino.merge вызывалась, когда Мино приземляется.

Теперь мы можем видеть, как блоки строятся следующим образом!

Кроме того, мы позаботимся о том, чтобы выйти из игрового цикла, когда куча Миноса достигнет вершины. Когда метод Mino.drawAtStartPos вызывается для рисования следующего Мино, мы заставим его возвращаемое значение указывать, может ли игра продолжаться. Добавьте одну строку, выделенную на рисунке ниже, к Mino.

Мы снова модифицируем только что измененный метод gGame.run следующим образом. Когда возвращаемое значение Mino.drawAtStartPos равно false, мы заставим программу выйти из игрового цикла.

Теперь игра заканчивается, когда блоки сложены вверх!

Затем, когда мы находим горизонтальные линии, заполненные тетримино, мы должны очистить их. Мы помещаем строки с завершенной горизонтальной линией в массив ret_rows, как в списке ниже, и заставляем gFieldJdg.merge возвращать этот массив.

const gFieldJdg = new function() {

  (omitted)

  this.merge = (mino) => {
    const [fposLeftTop, fpos4] = mino.getCurFPos();
    for (let i = 0; i < 4; i++) {
      _field[fposLeftTop + fpos4[i]] = true;
    }
    
    const ret_rows =  [];
    let row = Math.floor(fposLeftTop / g.PCS_FIELD_COL);
    for (let i = Math.min(4, g.PCS_ROW - row); i > 0; i--, row++) {
    
      const fpos_end = row * g.PCS_FIELD_COL + g.PCS_COL;
      for (let fpos = fpos_end - g.PCS_COL + 1;; fpos++) {
        if (_field[fpos] == false) { break; }
        if (fpos == fpos_end) {
          ret_rows.push(row);
          break;
        }
      }
    }
    return ret_rows;
  }
}

Мы должны создать метод стирания строк в gFieldJdg и gFieldGfx, какие строки указаны в массиве, полученном в качестве возвращаемого значения от gFieldJdg.merge.

const gFieldJdg = new function() {

  // (omitted)

  this.eraseRows = (rowsToErase) => {
    for (let row of rowsToErase) {
      _field.copyWithin(g.PCS_FIELD_COL, 0, row * g.PCS_FIELD_COL);
      _field.fill(false, 1, g.PCS_FIELD_COL - 1);
    }
  }
}

Подпись copyWithin - copyWithin(target, start, end). target — это индекс, в который копируется последовательность. start — это индекс, с которого нужно начать копирование элементов. end — это индекс, с которого следует закончить копирование элементов. Мы должны знать, что copyWithin копирует до end, но не включает его.

Подпись fill - fill(value, start, end). Что касается значения value и start, вы уже догадались. Значение end также примерно такое, как вы можете себе представить, но обратите внимание, что, как и end в copyWithin, как и раньше, fill заполняет до end, но не включает его. Например, результатом [0,1,2,3,4].fill(-1,1,3) будет [0,-1,-1,3,4].

const gFieldGfx = new function() {

  // (omitted)

  const _pxWidthFieldInner = g.Px_BLOCK * g.PCS_COL;
  this.eraseRows = (rowsToErase) => {
    _ctx.fillStyle = 'black';
    for (let row of rowsToErase) {
      const imgToMove = _ctx.getImageData(0, 0, pxWidthField, row * g.Px_BLOCK);
      _ctx.putImageData(imgToMove, 0, g.Px_BLOCK);
      _ctx.fillRect(g.Px_BLOCK, 0, _pxWidthFieldInner, g.Px_BLOCK);
    }
  }
}

Подпись getImageDatagetImageData(x, y, width, hight), а подпись putImageData — putImageData(imageData, x, y), где imageData — это объект, полученный с помощью getImageData.

Наконец, после вызова слияния в игровом цикле мы должны вызвать две только что созданные функции. Пожалуйста, добавьте выделенные строки, как в списке ниже.

Мы наконец завершили программу Тетрис! Я думаю, вы могли бы найти программу, состоящую примерно из 300 строк, которая была бы вам интересна. Буду рад, если опыт работы над этой программой сможет вам чем-то помочь. Спасибо за интерес к этой статье.

<!-- tetris.html -->
<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Tetris</title>
</head>
<body>
</body>
<script src="tetris.js"></script>
</html>
// 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 _pxWidthFieldInner = g.Px_BLOCK * g.PCS_COL;
  this.eraseRows = (rowsToErase) => {
    _ctx.fillStyle = 'black';
    for (let row of rowsToErase) {
      const imgToMove = _ctx.getImageData(0, 0, pxWidthField, row * g.Px_BLOCK);
      _ctx.putImageData(imgToMove, 0, g.Px_BLOCK);
      _ctx.fillRect(g.Px_BLOCK, 0, _pxWidthFieldInner, g.Px_BLOCK);
    }
  }
}

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

  this.merge = (mino) => {
    const [fposLeftTop, fpos4] = mino.getCurFPos();
    for (let i = 0; i < 4; i++) {
      _field[fposLeftTop + fpos4[i]] = true;
    }
    
    const ret_rows =  [];
    let row = Math.floor(fposLeftTop / g.PCS_FIELD_COL);
    for (let i = Math.min(4, g.PCS_ROW - row); i > 0; i--, row++) {
    
      const fpos_end = row * g.PCS_FIELD_COL + g.PCS_COL;
      for (let fpos = fpos_end - g.PCS_COL + 1;; fpos++) {
        if (_field[fpos] == false) { break; }
        if (fpos == fpos_end) {
          ret_rows.push(row);
          break;
        }
      }
    }
    return ret_rows;
  }
  
  this.eraseRows = (rowsToErase) => {
    for (let row of rowsToErase) {
      _field.copyWithin(g.PCS_FIELD_COL, 0, row * g.PCS_FIELD_COL);
      _field.fill(false, 1, g.PCS_FIELD_COL - 1);
    }
  }
}

const gMinos = new function() {
  const rotater3 = createRotater(3);
  const rotater4 = createRotater(4);
  
  const _minos = [
    new Mino('magenta', [1,0,  0,1,  1,1,  2,1], rotater3),
    new Mino('blue'   , [0,0,  0,1,  1,1,  2,1], rotater3),
    new Mino('orange' , [2,0,  0,1,  1,1,  2,1], rotater3),
    new Mino('green'  , [1,0,  2,0,  0,1,  1,1], rotater3),
    new Mino('red'    , [0,0,  1,0,  1,1,  2,1], rotater3),
    new Mino('cyan'   , [0,1,  1,1,  2,1,  3,1], rotater4),
    new Mino('yellow' , [1,0,  2,0,  1,1,  2,1], rotater0),
  ];
  this.getNextMino = () => _minos[Math.floor(Math.random() * 7)];
  
  function createRotater(minoSize)
  {
    return (blkpos8) => {
      for (let idx = 0; idx < 8; idx += 2) {
        const old_x = blkpos8[idx];
        blkpos8[idx] = minoSize - 1 - blkpos8[idx + 1];
        blkpos8[idx + 1] = old_x;
      }
    }
  }
  
  function rotater0(blkpos8) {}
}

function Mino(color, blkpos8, rotaterBlkpos8) {
  const [_fposRotates, pxposRotates] = createRotates(blkpos8, rotaterBlkpos8);
  const _minoGfx = new MinoGfx(color, pxposRotates);

  let _fposLeftTop = 0;
  let _fposDir = 0;
  this.getCurFPos = () => [_fposLeftTop, _fposRotates[_fposDir]];
  
  this.drawAtStartPos = () => {
    _fposLeftTop = 4;
    _fposDir = 0;

    _minoGfx.setToStartPos();
    _minoGfx.draw();
    
    return gFieldJdg.chkToPut(_fposLeftTop, _fposRotates[_fposDir]);
  };

  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, rotaterBlkpos8) {
    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));

      rotaterBlkpos8(blkpos8);
    }
    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 = gMinos.getNextMino();
  _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) {
        const rowsToErase = gFieldJdg.merge(_curMino);
        if (rowsToErase.length > 0) {
          gFieldJdg.eraseRows(rowsToErase);
          gFieldGfx.eraseRows(rowsToErase);
        }
        
        _curMino = gMinos.getNextMino();
        if (_curMino.drawAtStartPos() == false) {
          break;
        }
      }
      _timeNextDown += g.MSEC_GAME_INTERVAL;
    }
  }

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

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