본문 바로가기
프로젝트

[JS] 테트리스를 간단히 만들어보며.

by ISA(류) 2022. 3. 20.

바닐라 js로 간단하게 만들어 볼 수 있는 것들을 만들어 보는 사이드 프로젝트인 js-util repo의 8번째 기능으로 간단한 테트리스를 구현해보았다.

 

https://github.com/yoonjonglyu/js-util

 

GitHub - yoonjonglyu/js-util: html, css, js, vanillajs, animation, module & util

html, css, js, vanillajs, animation, module & util - GitHub - yoonjonglyu/js-util: html, css, js, vanillajs, animation, module & util

github.com

당연한 말이지만 바닐라 js로 직접 만든 모듈 로더와 css in js 모듈 외에는 순수한 html, css, js만을 사용해서 만들었는데 개인적으로는 바닐라 js로 만든 간단한 게임의 경우 이번이 처음이 아니라 공 튀기기와 벽돌깨기 정도를 js와 canvas를 사용해서 만들어본 경험이 있었고, 그렇기에 테트리스 자체를 canvas를 사용해서 만들 경우 그렇게 어려울 게 없었다. 자연히 해당 소재는 실용성도 없고 너무 재미가 없어서 만들 필요가 없다고 생각이 들었는데 문득 웹 canvas를 쓰지 않고 브라우저 DOM 조작만을 통해서 테트리스 게임을 만들 수 있을까?라는 의문이 들었고 이에 흥미를 느꼈다. 그래서 그를 실제로 확인해보고 그 과정에서 어떤 어려움이 있을지 확인해보고자 간단히 만들어본 결과물이다.

 

먼저 처음 막연히 컨셉을 잡을 때는 html tag와 css를 통해서 테트리스 블록을 모두 직접 애니메이션이나 트랜스폼 등을 통해서 블록 모양을 어떻게 잡을지? 또 그것들의 충돌 이벤트를 어떻게 구현할지 고민해보았는데 곰곰이 고민해보니 state와 render Tree 자체를 구분할 필요성이 있다고 느꼈고, 내가 초기에 고려했던 dom을 css로 블록 모양으로 만들고 그것을 이동시키는 방식으로 게임을 구성한 후 충돌 이벤트를 구현할 경우(canvas처럼) 웹 그래픽에 적합한 방법이지 브라우저 dom객체에는 적합한 방향성이 아니라고 느꼈다. (리플로우가 많이 일어난다.) 뭐 사실 테트리스 하나만을 간단히 구현할 경우 크게 무리가 되는 자원 소비는 아니라고 생각했지만, 실제로 web canvas를 사용하지 않고 웹에서 테트리스를 구현할 경우 그렇게 구현할 사람이 있을지?라는 생각을 해보니 그럴 사람은 없다는 생각이 들더라.

 

다시 고민을 해본결과 어차피 테트리스의 경우 격자 블록들로 이루어진 몇 가지 블록과 그것들의 판을 통해서 게임의 UI적인 요소를 모두 표현하고 있으니 DOM을 통해서 그런 격자무늬 Board를 만들고 그를 통해서 리플로우가 아닌 배경색 정도 수정하는 게 렌더링 자원을 아낄 수 있지 않을까?라는 아이디어가 떠올랐고 곰곰이 검토해본 결과 합리적이라는 판단이 들어서 해당 방법으로 구현을 진행했다.

 

즉 canvas가 아닌 dom으로 그것도 table태그를 이용해서 테트리스 게임판을 만들고 그 판의 각 ceil의 배경색을 대응 하는 state에 맞게 변화를 주는 방식으로 테트리스 UI를 구현하기로 했다.

    /**
     * @description 테트리스의 판을 매순간 dom 조작하기에는 리플로우가 너무 많이 일어나므로 
     * 격자 방식의 board를 만들어 놓고 repaint(각 격자의 color 변경)와 state 조작만을 통해서 게임 진행 사항을 표시해준다.
     */
    createBoard() {
        const board = this.styled.table`
            width: 400px;
            height: 650px;
        `;

        this.state.table = this.state.board.map((row) => {
            const tr = this.styled.tr`
            `;

            const rows = row.map(() => {
                const cel = this.styled.td`
                    border: 1px solid red;
                `;
                tr.appendChild(cel);

                return cel;
            });
            board.appendChild(tr);

            return rows;
        });

        return board;
    }
    renderBoard = () => {
        this.state.board.forEach((row, rdx) => {
            row.forEach((col, cdx) => {
                this.state.table[rdx][cdx].style.background = this.blocks.getBlockColors(col);
            });
        });
    }

테트리스 Board

가장 큰 관건이던 UI를 어떻게 표현할지 결정이 났으니 테트리스 로직들을 만들어야 할 차례다. 일단 아무리 간단하게 만들더라도 하나의 완성품인 게임을 만들어야 하다 보니 코드를 적절한 관심사에 맞게 나눌 필요를 느꼈는데, 개인적으로 고민해 보았을 때 테트리스라는 게임 전체를 총괄하는 Tetris클래스와 테트리스 게임의 내부의 상태(점수, board, 현재 블록)를 관리할 TetrisState 클래스 또 테트리스 블록에 관한 모델을 보관할 TetrisBlock클래스 3가지 정도로 나누어보았다.

간단히 테트리스 블록 클래스의 경우 테트리스 블록의 모양과 그 색깔에 대한 정보를 가지고 있다.

* 개인적으로 테트리스를 전혀 즐기지 않기에 해당 정보들은 나무위키나 블로그 등을 참조해서 수집했다.

class TetrisBlock {
    constructor() {
        this.blocks = [
            [[1, 0, 0], [1, 1, 1], [0, 0, 0]],
            [[0, 0, 0, 0], [2, 2, 2, 2], [0, 0, 0, 0], [0, 0, 0, 0]],
            [[0, 0, 3], [3, 3, 3], [0, 0, 0]],
            [[4, 4], [4, 4]],
            [[0, 5, 5], [5, 5, 0], [0, 0, 0]],
            [[0, 6, 0], [6, 6, 6], [0, 0, 0]],
            [[7, 7, 0], [0, 7, 7], [0, 0, 0]]
        ];
        this.colors = [
            'none',
            '#0152b5',
            '#029dd9',
            '#fb6902',
            '#fcc900',
            '#56ad29',
            '#852587',
            '#da1e29',
        ];
    }
    getBlockColors = (type) => {
        return this.colors[type];
    }
    getNextBlock = () => {
        const type = parseInt(Math.random() * 7);
        const block = this.blocks[type];

        return block;
    }
}

테트리스 상태 클래스의 경우 테트리스 게임판에 대한 정보와, 그 사이즈 width,height 같은 정보들 또 점수와 현재 블록의 상태, 그리고 그 블록의 xy좌표값, 게임 진행에 따른 랭킹 정보들과 게임판 등 dom트리에 대한 정보, 또 그를 간단히 처리하는 로직들을 가지고 있다.

class TetrisState { // 이번에는 여러 클래스로 나누어서 코드를 짜본다.
    constructor(N) {
        this._board = this.initBoard(N);
        this._size = N;
        this._info = {};
        this._score = 0;
        this._ranks = [];
        this._nodeTable = [];
        this._target = [];
        this._xy = [];
        this.loadRanks();
    }
    get info() {
        return this._info;
    }
    set info(info) {
        this._info = {
            ...this._info,
            ...info
        };
    }
    get ranks() {
        return this._ranks;
    }
    set ranks(ranks) {
        ranks.sort((a, b) => b[1] - a[1]);
        if (ranks.length > 5) ranks.pop();
        this._ranks = ranks;
        localStorage.setItem('tetris-ranks', JSON.stringify(ranks));
    }
    get score() {
        return this._score;
    }
    set score(score) {
        this._score = score;
    }
    get width() {
        return this._size;
    }
    get height() {
        return this._size * 2;
    }
    get table() {
        return this._nodeTable;
    }
    set table(table) {
        this._nodeTable = table;
    }
    get board() {
        return this._board;
    }
    set board(board) {
        this._board = board;
    }
    get target() {
        return this._target;
    }
    set target(target) {
        this._target = target;
    }
    get xy() {
        return this._xy;
    }
    set xy(xy) {
        this._xy = xy;
    }
    resetScore() {
        this._score = 0;
    }
    resetBoard() {
        this._board = this.initBoard(this._size);
    }
    setBlock(block) {
        this.xy = [3, -1];
        this.target = block;
    }
    setBoard(isReset) {
        this.target.forEach((row, rdx) => {
            const [x, y] = this.xy;
            row.forEach((col, cdx) => {
                if (col && this.board[rdx + y] && this.board[rdx + y][cdx + x] !== undefined) {
                    this.board[rdx + y][cdx + x] = isReset ? 0 : col;
                }
            });
        });
    }
    initBoard(N) {
        return Array.from(
            {
                length: N * 2
            },
            () => new Array(N).fill(0)
        )
    }
    loadRanks() {
        const check = localStorage.getItem('tetris-ranks');
        check === null ?
            this.initScore() :
            this.ranks = JSON.parse(localStorage.getItem('tetris-ranks'));
    }
    initScore() {
        localStorage.setItem('tetris-ranks', JSON.stringify([]));
    }
}

로컬 스토리지와 getter setter 기능을 간단히 활용했다.

 

테트리스의 경우 기본적인 기능으로 점수와, 블록의 좌우와 하단 이동, 그리고 회전 정도를 볼 수 있는데, 이를 canvas나 dom으로 표현하기 위해서는 일정 주기마다, 해당 로직을 실행할 방법이 필요했고 web api인 requestAnimationFrame을 통해서 ui렌더링을 표현하고 일정 주기의 블록의 하단 이동의 경우 setInterval api를 통해서 표현해줬다.

매 state 변화마다 render 로직을 호출하지 않고 굳이 이렇게 나눈 이유는 state 변화에 따른 UI최신화와 state변화간의 관심사를 쪼개고 의존성을 줄이기 위해서 정도 있을 거 같다. (솔직히... 말하면 그냥 신경 쓰기 귀찮아서)

    playGame() {
        const startGame = (e) => {
            if (e.key === 'ArrowDown') {
                const isAnswer = confirm('새 게임을 시작하시겠어요?');
                if (isAnswer) {
                    document.removeEventListener('keydown', startGame);
                    this.game = setInterval(this.dropBlock.bind(this), 900);
                    this.renderGame();
                    this.controlBlock(true);
                    alert('묻고 더블로가!');
                } else {
                    alert('너 다음에 한판 더해');
                }
            }
        }
        document.addEventListener('keydown', startGame);
    }
    gameOver() {
        cancelAnimationFrame(this.isRender);
        clearInterval(this.game);
        alert('gameover');
        const isAnswer = prompt('랭킹에 등록 할 닉네임을 입력해주세요.', 'ASD');
        this.state.ranks = [
            ...this.state.ranks,
            [isAnswer, this.state.score]
        ];
        this.state.resetBoard();
        this.state.resetScore();
        this.controlBlock(false);
        this.renderRanks();
        this.playGame();
    }

    renderGame = () => {
        this.renderBoard();
        this.renderInformation();
        this.isRender = requestAnimationFrame(this.renderGame);
    }
    renderBoard = () => {
        this.state.board.forEach((row, rdx) => {
            row.forEach((col, cdx) => {
                this.state.table[rdx][cdx].style.background = this.blocks.getBlockColors(col);
            });
        });
    }
    renderInformation = () => {
        this.state.info.score.innerText = `내 점수 : ${this.state.score} 점`;
    }

나름의 철용좌 드립을 넣었지만 큰 의미는 없다. 게임 시작의 경우 키보드 이벤트를 받아서 시작하게 했고, 게임 과정의 state 변화는 위의 설명대로 render는 점수와 board상태 정도를 주기적으로 갱신하다가 게임이 끝나면 모든 이벤트나 state를 리셋한다. 덤으로 랭킹에 추가도 하고 말이다.

 

테트리스의 경우 블록이 내려가다 충돌되었을 때 가득 채운 라인을 정리하고 게임이 끝나거나 새로운 블록을 찾거나 하는 이벤트가 발생한다. 그 부분의 로직을 따로 분리해서 신경 써줬다.

    dropBlock() {
        this.moveBlock('down');
        if (this.isCrash) {
            if (this.state.xy[1] === -1) {
                this.gameOver();
                this.isCrash = false;
            } else {
                this.checkLine();
                this.state.setBlock(this.blocks.getNextBlock());
                this.isCrash = false;
            }
        }
    }

블록의 이동과 회전의 경우 키보드 입력 이벤트를 통해서 화살표 방향키를 통한 좌우 이동과 소프트 드롭, 회전을 구현해줬다.

    controlBlock(handle) {
        if (handle) {
            document.addEventListener('keydown', this.moveEvent);
            document.addEventListener('keyup', this.rotateEvent);
        } else {
            document.removeEventListener('keydown', this.moveEvent);
            document.removeEventListener('keyup', this.rotateEvent);
        }
    }
    moveEvent = (e) => {
        if (e.key === 'ArrowLeft') {
            this.moveBlock('left');
        } else if (e.key === 'ArrowRight') {
            this.moveBlock('right');
        } else if (e.key === 'ArrowDown') {
            this.state.score++;
            this.dropBlock();
        }
    }
    rotateEvent = (e) => {
        if (e.key === 'ArrowUp') this.moveBlock('rotate');
    }

블록 이동 4가지 경우의 경우(좌, 우, 아래, 회전) 따로 메서드를 추출해서 정리해주었다. 또 해당 이동을 검증하는 로직을 역시 따로 메서드를 추출해줬다.

    moveBlock(forward) {
        this.state.setBoard(true);
        switch (forward) {
            case 'down':
                const resize = Array.from(this.state.target);
                while (resize.length > 0 && !resize[resize.length - 1].find((col) => col > 0)) resize.pop();
                this.checkBoard(resize, 'down') ?
                    this.state.xy[1]++ :
                    this.isCrash = true;
                break;
            case 'left':
                if (this.checkBoard(this.state.target, 'left')) this.state.xy[0]--;
                break;
            case 'right':
                if (this.checkBoard(this.state.target, 'right')) this.state.xy[0]++;
                break;
            case 'rotate':
                const nextTarget = JSON.parse(JSON.stringify(this.state.target));
                nextTarget.forEach((_, rdx) => {
                    for (let cdx = 0; cdx < rdx; cdx++) {
                        [nextTarget[cdx][rdx], nextTarget[rdx][cdx]] =
                            [nextTarget[rdx][cdx], nextTarget[cdx][rdx]];
                    }
                });
                nextTarget.forEach((row) => row.reverse());
                if (this.checkBoard(nextTarget, 'rotate')) this.state.target = nextTarget;
                break;
            default:
                break;
        }
        this.state.setBoard(false);
    }
    checkBoard = (arr, forward) => {
        let result = true;
        const [x, y] = this.state.xy;
        arr.forEach((row, rdx) => {
            if (y + rdx < 0) rdx++;
            row.forEach((col, cdx) => {
                switch (forward) {
                    case 'down':
                        if (arr.length + y >= this.state.height) result = false;
                        if (col && this.state.board[y + rdx + 1] && this.state.board[y + rdx + 1][x + cdx]) result = false;
                        break;
                    case 'right':
                        if (col && x + cdx >= this.state.width - 1) result = false;
                        if (col && this.state.board[y + rdx][x + cdx + 1]) result = false;
                        break;
                    case 'left':
                        if (col && x + cdx < 1) result = false;
                        if (col && this.state.board[y + rdx][x + cdx - 1]) result = false;
                        break;
                    case 'rotate':
                        if (col && (x + cdx >= this.state.width || x + cdx < 0)) result = false;
                        if (col && (!this.state.board[y + rdx] || this.state.board[y + rdx][x + cdx])) result = false;
                        break;
                    default:
                        break;
                }
            });
        });
        return result;
    }

테트리스에는 이것 말고도 T-스핀이나 반대 방향 회전, 하드 드롭, 또 홀드 등의 기능 등이 더 있지만 사실 T-스핀을 제외하고는 로직상 별 차이도 없고 테트리스를 안 하다 보니 T-스핀은 뭔지 정확히 이해가 안 가는 것도 있고 흥미가 안가는 점 때문에 이 정도 수준에서 개발을 마쳤다.

간단히 만든 테트리스

이렇게 테트리스 공식 사이트와 여러 문서를 참고해서 js DOM조작을 통해서 간단히 테트리스를 만들어 보았다.

느낀 점을 몇 가지 꼽자면 테트리스 같은 간단한 게임도 실제로 만드는 데는 많은 공이 든다는 것과 행렬 등 수학적 지식이 게임 개발에서는 많이 필요할 거라는 것을 느꼈고 이럴 바에는 그냥 canvas를 쓰자 라는 생각이다.

테트리스 데모 링크와 깃허브 저장소, 또 테트리스 공식 사이트와 전체 코드를 남기니 관심이 있다면 확인해보길 바란다.

https://yoonjonglyu.github.io/js-util/tetris/

 

Tetris

 

yoonjonglyu.github.io

https://github.com/yoonjonglyu/js-util

 

GitHub - yoonjonglyu/js-util: html, css, js, vanillajs, animation, module & util

html, css, js, vanillajs, animation, module & util - GitHub - yoonjonglyu/js-util: html, css, js, vanillajs, animation, module & util

github.com

https://tetris.com/play-tetris

 

Play Tetris | Free Online Game

Play Tetris for free. Browser-based online Tetris game. No download required.

tetris.com

class Tetris {
    constructor(root, styled) {
        this.root = root;
        this.styled = styled;
        this.state = new TetrisState(10);
        this.blocks = new TetrisBlock();
        this.game = 0;
        this.isRender = 0;
        this.isCrash = false;
        this.state.setBlock(this.blocks.getNextBlock());
        this.createContainer();
        this.playGame();
    }

    createContainer() {
        const container = this.styled.main`
            display: flex;
            width: 600px;
            margin: 40px auto;
        `;

        container.appendChild(this.createBoard());
        container.appendChild(this.createScoreBoard());
        this.root.appendChild(container);
    }
    createScoreBoard() {
        const scoreBoard = this.styled.div`
            width: 160px;
            height: 600px;
            padding: 10px;
            margin-left: 20px;
            border: 1px solid blue;
            text-align: center;
        `;
        const scoreTitle = this.styled.h2`
        `;
        scoreTitle.innerText = 'SCORE';
        const score = this.styled.h3`
        `;
        this.state.info = { score: score };
        this.renderInformation();

        const help = this.styled.h4`
            color: tomato;
        `;
        help.innerText = '게임 시작은 키보드 방향키 아래 화살표로 시작합니다.';
        scoreBoard.appendChild(scoreTitle);
        scoreBoard.appendChild(score);
        scoreBoard.appendChild(help);
        this.createRanks(scoreBoard);

        return scoreBoard;
    }
    createRanks(scoreBoard) {
        const ranksTitle = this.styled.h2``;
        ranksTitle.innerText = 'RANKS';
        const ranks = this.styled.ul`
            padding: 0;
            list-style: none;
            text-align: center;
        `;
        this.state.info = { ranks: ranks };
        this.renderRanks();

        scoreBoard.appendChild(ranksTitle);
        scoreBoard.appendChild(ranks);
    }
    /**
     * @description 테트리스의 판을 매순간 dom 조작하기에는 리플로우가 너무 많이 일어나므로 
     * 격자 방식의 board를 만들어 놓고 repaint(각 격자의 color 변경)와 state 조작만을 통해서 게임 진행 사항을 표시해준다.
     */
    createBoard() {
        const board = this.styled.table`
            width: 400px;
            height: 650px;
        `;

        this.state.table = this.state.board.map((row) => {
            const tr = this.styled.tr`
            `;

            const rows = row.map(() => {
                const cel = this.styled.td`
                    border: 1px solid red;
                `;
                tr.appendChild(cel);

                return cel;
            });
            board.appendChild(tr);

            return rows;
        });

        return board;
    }
    playGame() {
        const startGame = (e) => {
            if (e.key === 'ArrowDown') {
                const isAnswer = confirm('새 게임을 시작하시겠어요?');
                if (isAnswer) {
                    document.removeEventListener('keydown', startGame);
                    this.game = setInterval(this.dropBlock.bind(this), 900);
                    this.renderGame();
                    this.controlBlock(true);
                    alert('묻고 더블로가!');
                } else {
                    alert('너 다음에 한판 더해');
                }
            }
        }
        document.addEventListener('keydown', startGame);
    }
    gameOver() {
        cancelAnimationFrame(this.isRender);
        clearInterval(this.game);
        alert('gameover');
        const isAnswer = prompt('랭킹에 등록 할 닉네임을 입력해주세요.', 'ASD');
        this.state.ranks = [
            ...this.state.ranks,
            [isAnswer, this.state.score]
        ];
        this.state.resetBoard();
        this.state.resetScore();
        this.controlBlock(false);
        this.renderRanks();
        this.playGame();
    }

    renderGame = () => {
        this.renderBoard();
        this.renderInformation();
        this.isRender = requestAnimationFrame(this.renderGame);
    }
    renderBoard = () => {
        this.state.board.forEach((row, rdx) => {
            row.forEach((col, cdx) => {
                this.state.table[rdx][cdx].style.background = this.blocks.getBlockColors(col);
            });
        });
    }
    renderInformation = () => {
        this.state.info.score.innerText = `내 점수 : ${this.state.score} 점`;
    }
    renderRanks = () => {
        this.state.info.ranks.innerText = '';
        this.state.ranks.forEach((rank, idx) => {
            const [name, score] = rank;
            const item = this.styled.li`
            `;
            item.innerText = `[${idx + 1} 등]${name} 님 : ${score} 점`;
            this.state.info.ranks.appendChild(item);
        });
        if (this.state.ranks.length === 0) this.state.info.ranks.innerText = '등록된 랭킹이 없습니다.';
    }

    dropBlock() {
        this.moveBlock('down');
        if (this.isCrash) {
            if (this.state.xy[1] === -1) {
                this.gameOver();
                this.isCrash = false;
            } else {
                this.checkLine();
                this.state.setBlock(this.blocks.getNextBlock());
                this.isCrash = false;
            }
        }
    }
    controlBlock(handle) {
        if (handle) {
            document.addEventListener('keydown', this.moveEvent);
            document.addEventListener('keyup', this.rotateEvent);
        } else {
            document.removeEventListener('keydown', this.moveEvent);
            document.removeEventListener('keyup', this.rotateEvent);
        }
    }
    moveEvent = (e) => {
        if (e.key === 'ArrowLeft') {
            this.moveBlock('left');
        } else if (e.key === 'ArrowRight') {
            this.moveBlock('right');
        } else if (e.key === 'ArrowDown') {
            this.state.score++;
            this.dropBlock();
        }
    }
    rotateEvent = (e) => {
        if (e.key === 'ArrowUp') this.moveBlock('rotate');
    }
    moveBlock(forward) {
        this.state.setBoard(true);
        switch (forward) {
            case 'down':
                const resize = Array.from(this.state.target);
                while (resize.length > 0 && !resize[resize.length - 1].find((col) => col > 0)) resize.pop();
                this.checkBoard(resize, 'down') ?
                    this.state.xy[1]++ :
                    this.isCrash = true;
                break;
            case 'left':
                if (this.checkBoard(this.state.target, 'left')) this.state.xy[0]--;
                break;
            case 'right':
                if (this.checkBoard(this.state.target, 'right')) this.state.xy[0]++;
                break;
            case 'rotate':
                const nextTarget = JSON.parse(JSON.stringify(this.state.target));
                nextTarget.forEach((_, rdx) => {
                    for (let cdx = 0; cdx < rdx; cdx++) {
                        [nextTarget[cdx][rdx], nextTarget[rdx][cdx]] =
                            [nextTarget[rdx][cdx], nextTarget[cdx][rdx]];
                    }
                });
                nextTarget.forEach((row) => row.reverse());
                if (this.checkBoard(nextTarget, 'rotate')) this.state.target = nextTarget;
                break;
            default:
                break;
        }
        this.state.setBoard(false);
    }
    checkLine() {
        for (let idx = this.state.xy[1] - 4; idx < this.state.xy[1] + 4; idx++) {
            if (this.state.board[idx] && !this.state.board[idx].includes(0)) {
                let count = idx;
                while (count >= 0 && this.state.board[count]) {
                    this.state.board[count] = this.state.board[count - 1];
                    count--;
                }
                this.state.board[0] = new Array(this.state.width).fill(0);
                this.state.score += 100;
            }
        }
    }
    checkBoard = (arr, forward) => {
        let result = true;
        const [x, y] = this.state.xy;
        arr.forEach((row, rdx) => {
            if (y + rdx < 0) rdx++;
            row.forEach((col, cdx) => {
                switch (forward) {
                    case 'down':
                        if (arr.length + y >= this.state.height) result = false;
                        if (col && this.state.board[y + rdx + 1] && this.state.board[y + rdx + 1][x + cdx]) result = false;
                        break;
                    case 'right':
                        if (col && x + cdx >= this.state.width - 1) result = false;
                        if (col && this.state.board[y + rdx][x + cdx + 1]) result = false;
                        break;
                    case 'left':
                        if (col && x + cdx < 1) result = false;
                        if (col && this.state.board[y + rdx][x + cdx - 1]) result = false;
                        break;
                    case 'rotate':
                        if (col && (x + cdx >= this.state.width || x + cdx < 0)) result = false;
                        if (col && (!this.state.board[y + rdx] || this.state.board[y + rdx][x + cdx])) result = false;
                        break;
                    default:
                        break;
                }
            });
        });
        return result;
    }
}
class TetrisState { // 이번에는 여러 클래스로 나누어서 코드를 짜본다.
    constructor(N) {
        this._board = this.initBoard(N);
        this._size = N;
        this._info = {};
        this._score = 0;
        this._ranks = [];
        this._nodeTable = [];
        this._target = [];
        this._xy = [];
        this.loadRanks();
    }
    get info() {
        return this._info;
    }
    set info(info) {
        this._info = {
            ...this._info,
            ...info
        };
    }
    get ranks() {
        return this._ranks;
    }
    set ranks(ranks) {
        ranks.sort((a, b) => b[1] - a[1]);
        if (ranks.length > 5) ranks.pop();
        this._ranks = ranks;
        localStorage.setItem('tetris-ranks', JSON.stringify(ranks));
    }
    get score() {
        return this._score;
    }
    set score(score) {
        this._score = score;
    }
    get width() {
        return this._size;
    }
    get height() {
        return this._size * 2;
    }
    get table() {
        return this._nodeTable;
    }
    set table(table) {
        this._nodeTable = table;
    }
    get board() {
        return this._board;
    }
    set board(board) {
        this._board = board;
    }
    get target() {
        return this._target;
    }
    set target(target) {
        this._target = target;
    }
    get xy() {
        return this._xy;
    }
    set xy(xy) {
        this._xy = xy;
    }
    resetScore() {
        this._score = 0;
    }
    resetBoard() {
        this._board = this.initBoard(this._size);
    }
    setBlock(block) {
        this.xy = [3, -1];
        this.target = block;
    }
    setBoard(isReset) {
        this.target.forEach((row, rdx) => {
            const [x, y] = this.xy;
            row.forEach((col, cdx) => {
                if (col && this.board[rdx + y] && this.board[rdx + y][cdx + x] !== undefined) {
                    this.board[rdx + y][cdx + x] = isReset ? 0 : col;
                }
            });
        });
    }
    initBoard(N) {
        return Array.from(
            {
                length: N * 2
            },
            () => new Array(N).fill(0)
        )
    }
    loadRanks() {
        const check = localStorage.getItem('tetris-ranks');
        check === null ?
            this.initScore() :
            this.ranks = JSON.parse(localStorage.getItem('tetris-ranks'));
    }
    initScore() {
        localStorage.setItem('tetris-ranks', JSON.stringify([]));
    }
}
class TetrisBlock {
    constructor() {
        this.blocks = [
            [[1, 0, 0], [1, 1, 1], [0, 0, 0]],
            [[0, 0, 0, 0], [2, 2, 2, 2], [0, 0, 0, 0], [0, 0, 0, 0]],
            [[0, 0, 3], [3, 3, 3], [0, 0, 0]],
            [[4, 4], [4, 4]],
            [[0, 5, 5], [5, 5, 0], [0, 0, 0]],
            [[0, 6, 0], [6, 6, 6], [0, 0, 0]],
            [[7, 7, 0], [0, 7, 7], [0, 0, 0]]
        ];
        this.colors = [
            'none',
            '#0152b5',
            '#029dd9',
            '#fb6902',
            '#fcc900',
            '#56ad29',
            '#852587',
            '#da1e29',
        ];
    }
    getBlockColors = (type) => {
        return this.colors[type];
    }
    getNextBlock = () => {
        const type = parseInt(Math.random() * 7);
        const block = this.blocks[type];

        return block;
    }
}
반응형