CSS&JS/👀Study and Copy

2048 클론코딩

arancia_ 2022. 6. 7. 13:32

Web Dev Simpified : If You Want To Be An Advanced Game Developer Build This Project

본 포스팅은 위 유튜브 튜토리얼 영상을 보고 쓴 클론코딩(깃허브)입니다.
가독성을 위해서 기타 일반 함수로 작성된 부분도 클래스 안에 넣고, event 부분도 따로 파일로 빼서 클래스로 작성하였습니다.
점수 표시도 추가하였습니다.

새로 배웠던 개념

애니메이션은 전부 CSS의 사용자지정속성(--x 이런거) 과  transition으로 처리하고 있음. js로 사용자 지정 속성을 동적으로 수정하고 반영함으로써 움직이는 효과를 구현하고 있음.

아직 제대로 이해 못한거 : moveTiles 부분의 cellsByColumn과 cellsByRow...
이 개념이..머릿속에 잘 안 잡힘 reduce로 하는게

HTML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>2048 mine</title>
    <link rel="stylesheet" type="text/css" href="./css/main.css"/>
    <script src="./js/main.js" type="module"></script>
</head>
<body>
    <h1>2048</h1>
    <h2 id="score">점수</h2>
    <section id="game-board">
    </section><!-- game-board -->
</body>
</html>
cs

CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@charset "utf-8";
:root{
    --bg:#EEF1F7;
    --font:Arial;
    --fSize:7.5vmin;
    
    --bdr:min(12px,2vmin);
    
    --grid-bg:rgb(218, 222, 233);
    --cell-bg:rgb(168, 174, 191);
}
*{margin:0;padding:0;box-sizing:border-box;}
html,body{
    display:flex; flex-flow:column nowrap;
    justify-content:center; align-items:center;
    gap:3vmin;
    width:100%;min-height:100vh;
    background:var(--bg);
    font-family:var(--font); font-size:var(--fSize);
}
h1{font-size:3vmin;}
#score{color:hsl(200,25%,50%);}
 
#game-board{
    /* --grid-size:4;
    --cell-size:15vmin;
    --cell-gap:1.5vmin; */
    display:grid; overflow:hidden;
    grid-template-columns:repeat(var(--grid-size),var(--cell-size));
    grid-template-rows:repeat(var(--grid-size),var(--cell-size));
    grid-gap:var(--cell-gap);
    position:relative;
    padding:var(--cell-gap);
    background-color:var(--grid-bg);
    border-radius:var(--bdr);
}
 
.cell{
    background:var(--cell-bg);
    border-radius:var(--bdr);
}
 
.tile{
    /* --x:0; --y:0; */
    /* --cell-bg-lightness:20%; */
    /* --cell-text-lightness:80%; */
    position:absolute;
    width:var(--cell-size); aspect-ratio:1/1;
    border-radius:var(--bdr);
    text-align:center;line-height:var(--cell-size); font-weight:bold;
    left:calc(
        var(--x) * 
        (var(--cell-size) + var(--cell-gap)) + 
        var(--cell-gap));
    top:calc(
        var(--y) * 
        (var(--cell-size) + var(--cell-gap)) + 
        var(--cell-gap));
    background:hsl(200,50%,var(--cell-bg-lightness));
    color:hsl(200,25%,var(--cell-text-lightness));
    animation:show 200ms ease-in-out;
    transition:100ms ease-in-out;
    box-shadow:2px 2px 0 rgba(0,0,0,.2);
}
 
@keyframes show {
    from{opacity:0; transform:scale(0);}
    to{opacity:1;transform:scale(1)}}
cs

 

main.js

1
2
3
4
5
6
7
8
9
10
11
12
import Grid from "./Grid.js";
import Play from "./Play.js";
import Tile from "./Tile.js";
 
const $board = document.getElementById('game-board');
const GRID = new Grid($board);
 
GRID.randomEmptyCell().tile = new Tile($board);
GRID.randomEmptyCell().tile = new Tile($board);
 
const PLAY = new Play($board, GRID);
PLAY.setupInput();
cs

 

Grid.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import Cell from "./Cell.js";
 
export default class Grid{
    #CELLS
 
    constructor($board){
        this.$board = $board;
        this.SETTING = Object.freeze({
            gridSize : 4,
            cellSize : 15,
            cellGap : 1.5
        });
        this.init();
    }//constructor
 
    /* 💥01. 그리드 그려주고 Cell 클래스 개별 생성 관련 */
    /* init all */
    init(){
        const CELLS = this.draw_grid();
 
        this.#CELLS = CELLS.map(($cell,idx)=>{
            const X = idx % this.SETTING.gridSize;
            const Y = Math.floor(idx / this.SETTING.gridSize);
            return new Cell($cell,X,Y);
        });
    }//init
 
    /* draw grid */
    draw_grid(){
        const {gridSize,cellSize,cellGap} = this.SETTING;
        const CELLS = [];
 
        this.$board.style.setProperty('--grid-size',gridSize);
        this.$board.style.setProperty('--cell-size',`${cellSize}vmin`);
        this.$board.style.setProperty('--cell-gap',`${cellGap}vmin`);
 
        const $frag = document.createDocumentFragment();
        for(let i=0; i < gridSize ** 2; i++){
            const $cell = document.createElement('DIV');
            $cell.classList.add('cell');
            $frag.appendChild($cell);
            CELLS.push($cell);
        }//for
 
        this.$board.appendChild($frag);
 
        return CELLS;
    }//draw_grid
 
    /* get a random EMPTY tile */
    randomEmptyCell(){
        const rIdx = Math.floor(Math.random() * this.#EmptyCells.length);
        const emptyCell= this.#EmptyCells[rIdx];
        return emptyCell;
    }//randomEmptyCell
 
    /* GETTER, SETTER */
    get #EmptyCells(){
        return this.#CELLS.filter($cell => $cell.tile == null);
    }//#EmptyCells
 
    /* 💥02. 키보드 이벤트 핸들러 관련 */
    get cellsByColumn(){
        return this.#CELLS.reduce((cellGrid,cell)=>{
            console.log(cellGrid,cell);
            cellGrid[cell.x] = cellGrid[cell.x] || [];
            cellGrid[cell.x][cell.y] = cell;
            return cellGrid;
        },[]);
    }//cellsByColumn
 
    get cellsByRow(){
        return this.#CELLS.reduce((cellGrid,cell)=>{
            cellGrid[cell.y] = cellGrid[cell.y] || [];
            cellGrid[cell.y][cell.x] = cell;
            return cellGrid;
        },[]);
    }//cellsByRow
 
    get cells(){return this.#CELLS;}
}//class-Grid
cs

 

Cell.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
export default class Cell{
    #CELL
    #X
    #Y
    #TILE
    #MERGETILE
 
    constructor($cell,X,Y){
        this.#CELL = $cell;
        this.#X = X;
        this.#Y = Y;
    }//constructor
 
    /* 💥 01.Cell의 기본 세팅 - Tile 값에 영향 */
    /* [GETTER, SETTER] */
    get x(){return this.#X}
    get y(){return this.#Y}
 
    get tile(){
        // console.log('class:',this.#TILE);
        return this.#TILE;}
 
    set tile(newTile){
        this.#TILE = newTile;
        if(newTile == null){return;}
        this.#TILE.x = this.#X;
        this.#TILE.y = this.#Y;
    }//tile
 
    /* 💥02. 키보드 이벤트 관련 */
    get mergeTile(){return this.#MERGETILE;}
 
    set mergeTile(tile){
        this.#MERGETILE = tile;
        if(tile == null){return;}
        this.#MERGETILE.x = this.#X;
        this.#MERGETILE.y = this.#Y;
    }
    
    canAccept(tile){
        return (
            this.tile == null ||
            (this.mergeTile == null && this.tile.value === tile.value)
        );
    }//canAccept
 
    mergeTiles(PLAY){
        if(this.tile == null || this.mergeTile == null){return;}
 
        this.tile.value  = this.tile.value + this.mergeTile.value;
        PLAY.score += this.tile.value;
        this.mergeTile.remove();
        this.mergeTile = null;
    }//mergeTiles
 
}//class Cell
cs

 

Tile.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
const bgColor = "--cell-bg-lightness";
const txtColor = "--cell-text-lightness";
 
export default class Tile{
    #TILE
    #x
    #y
    #VALUE
 
    constructor($board, vnum = Math.random() > 0.5 ? 2 : 4){
        this.$board = $board;
        this.#TILE = this.addTileElement();
        this.value = vnum; //set에서 실행됨
    }//constructor
 
    /* 💥 01. 랜덤한 타일 2개 생성 관련 */
    addTileElement(){
        const $tile = document.createElement('DIV');
        $tile.classList.add('tile');
        this.$board.appendChild($tile);
        return $tile;
    }//addTileElement
 
    /* [GETTER, SETTER] */
    set x(xnum){
        this.#x = xnum;
        this.#TILE.style.setProperty('--x',xnum);
    }//set-x
 
    set y(ynum){
        this.#x = ynum;
        this.#TILE.style.setProperty('--y',ynum);
    }//set-x
 
    get value(){return this.#VALUE;}
 
    set value(vnum){
        //숫자 설정
        this.#VALUE = vnum;
        this.#TILE.textContent = vnum;
 
        //배경색 및 글씨색
        const power = Math.log2(vnum);
        const bgLight = 100 - (power * 9);
 
        this.#TILE.style.setProperty(bgColor,`${bgLight}%`);
        this.#TILE.style.setProperty(txtColor,`${bgLight <= 50 ? 90 : 10}%`);
    }//set-value
 
    /* 💥 02. 이벤트 관련 */
    remove(){this.#TILE.remove();}
 
    waitForTransition(animation = false){
        return new Promise(res=>{
            const evt = animation ? "animationend" : "transitionend";
            this.#TILE.addEventListener(evt,res,{once:true});
        });
    }//waitForTransition
}//class-Tile
cs

 

Play.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import Tile from "./Tile.js";
 
export default class Play{
    #SCORE
    constructor($board,GRID){
        this.$board = $board;
        this.GRID = GRID;
        this.KEYS = Object.freeze({
            up : "ArrowUp",
            down : "ArrowDown",
            left : "ArrowLeft",
            right : "ArrowRight",
        });
        this.$score = document.getElementById('score');
        this.score = 0;
    }//constructor
 
    setupInput(){
        window.addEventListener('keydown'this.handler, {once:true});
    }//setupInput
 
    handler = async(e)=>{
        const {up,down,left,right} = this.KEYS;
        
        //Move Tiles
        switch(e.key){
            case up :
                if(!this.canMoveUp()){this.setupInput(); return;}
                await this.moveUp();
                break;
                
            case down :
                if(!this.canMoveDown()){this.setupInput(); return;}
                await this.moveDown();
                break;
                
            case left :
                if(!this.canMoveLeft()){this.setupInput(); return;}
                await this.moveLeft();
                break;
                
            case right :
                if(!this.canMoveRight()){this.setupInput(); return;}
                await this.moveRight();
                break;
 
            default:
                this.setupInput();
                return;
        }//switch
 
        //Merge
        this.GRID.cells.forEach($cell => $cell.mergeTiles(this));
 
        //Make New Random Tile
        const newTile = new Tile(this.$board);
        this.GRID.randomEmptyCell().tile = newTile;
 
        //Game Over?
        if(!this.canMoveUp() && !this.canMoveDown() && !this.canMoveLeft() && !this.canMoveRight()){
            newTile.waitForTransition(true).then(()=>{
                console.log('패배');
                alert('YOU LOST');
            });
            return;
        }//if
 
        //reset-setupInput
        this.setupInput();
    }//handler
 
    /* [움직일 수 있는가?] */
    canMove(cells){
        return cells.some(colOrRow =>{
            return colOrRow.some(($cell,idx)=>{
                if(idx === 0return false;
                if($cell.tile == nullreturn false;
                const $moveToCell = colOrRow[idx - 1];
                return $moveToCell.canAccept($cell.tile);
            });//some- colOrRow
        });//some-cells
    }//canMove
 
    canMoveUp(){return this.canMove(this.GRID.cellsByColumn);}
    canMoveDown(){return this.canMove(this.GRID.cellsByColumn.map(col=>[...col].reverse()));}
    canMoveLeft(){return this.canMove(this.GRID.cellsByRow);}
    canMoveRight(){return this.canMove(this.GRID.cellsByRow.map(row=>[...row].reverse()));}
 
    /* [움직이기] */
    slideTiles(cells){
        return Promise.all(
            cells.flatMap(group => {
                const promises = [];
                
                for(let i=1; i<group.length; i++){
                    const $cell= group[i];
                    if($cell.tile == nullcontinue;
                    let $lastValidCell;
 
                    for(let j=i-1; j>=0; j--){
                        const moveToCell = group[j];
                        if(!moveToCell.canAccept($cell.tile)) break;
                        $lastValidCell = moveToCell;
                    }//for-j
 
                    if($lastValidCell != null){
                        promises.push($cell.tile.waitForTransition());
 
                        if($lastValidCell.tile != null){
                            $lastValidCell.mergeTile = $cell.tile;
                        }else{
                            $lastValidCell.tile = $cell.tile;
                        }//if-else
 
                        $cell.tile = null;
                    }//if
                }//for-i
 
                return promises;
            })//flatMap
        );//return-Promise
    }//slideTiles
 
    moveUp(){return this.slideTiles(this.GRID.cellsByColumn);}
    moveDown(){
        return this.slideTiles(this.GRID.cellsByColumn.map(col=>[...col].reverse()));}
    moveLeft(){
        return this.slideTiles(this.GRID.cellsByRow);}
    moveRight(){
        return this.slideTiles(this.GRID.cellsByRow.map(row=>[...row].reverse()));}
 
    /* 💥03. 점수 */
    get score(){return this.#SCORE;}
    set score(num){
        this.#SCORE = num;
        this.$score.textContent = this.#SCORE;
    }//set-scroe
}//class-Play
cs