Изучите объектно-ориентированное программирование на JavaScript, создав тетрис. (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
во 2-й строке сверху мы хотели бы использовать просто (_fposDir + dir) % 4
, но результатом -1 % 4
является -1
, поэтому мы исправляем его, добавляя 4
.
createRotates
создает массив _fposRotates
, описанный выше, и он не вызывается снаружи Mino
, поэтому он определяется с помощью function
, а не формы this.createRotates
. Метод расчета значений, которые будут храниться в массиве, будет использовать тот же метод расчета, который описан в четвертой статье.
Далее мы добавляем 6 строк к gGame
следующим образом. Затем мы можем повернуть Mino влево с помощью клавиши z и вправо с помощью клавиши x.
На первый взгляд кажется, что программа работает правильно, когда мы вносим некоторые изменения выше.
Однако, как только Мино упадет на дно, а следующий Мино появится сверху, программа будет вести себя неправильно. Причина ошибки в том, что значения координат, хранящиеся в _pxpos8
из MinoGfx
, все еще поворачиваются, когда следующий Мино появляется сверху.
Теперь давайте рефакторим 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();