Научете обектно-ориентирано програмиране в 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();