CSS&JS/👀Study and Copy

WDS-Snake Game

arancia_ 2022. 8. 22. 17:04

유튜브 : https://www.youtube.com/watch?v=QTcIXok9wNY

미리보기(데모) : https://ohikmyeong.github.io/wds-snake-game/answer-with/

 

(Answer With)Snake Game

 

ohikmyeong.github.io

 

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>(Answer With)Snake Game</title>
    <link rel="stylesheet" type="text/css" href="./style.css"/>
    <script src="js/main.js" type="module"></script>
</head>
<body>
    <div id="score">0</div>
    <section id="game-board"></section>
 
    <div id="modal">GAME OVER..</div>
</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
@charset "utf-8";
:root{
    --bg:rgb(43, 43, 56);
    --game-board:rgb(193, 196, 211);
    --snake:linear-gradient(to bottom, rgb(56, 244, 184),rgb(0, 164, 112));
    --snake-head:conic-gradient(rgb(56, 244, 184), rgb(0, 96, 29));
    --food:linear-gradient(to bottom, rgb(255, 243, 16),rgb(255, 208, 0));
    --game-over:linear-gradient(to bottom, rgb(244, 56, 56),rgb(91, 0, 0));
    --game-over-head:conic-gradient(rgb(255,255,255), rgb(255, 89, 0), rgb(128, 0, 0));
    --border:.4vmin solid black;
    --grid-size:21;
}
*{margin:0;padding:0;box-sizing:border-box;}
 
html,body{
    display:flex; flex-flow:column nowrap;
    justify-content:center; align-items:center;
    gap:2rem;
    width:100%;height:100vh;
    background:var(--bg);}
 
/* [SCORE] */
#score{
    margin-top:1em;
    font-size:3vmax;font-weight:bold;color:#fff;}
/* [GAME BOARD] */
#game-board{
    display:grid;
    grid-template-columns:repeat(var(--grid-size),1fr);
    grid-template-rows:repeat(var(--grid-size),1fr);
    position:relative;
    width:80vmin;aspect-ratio:1/1;
    background:var(--game-board);}
 
.snake{
    background:var(--snake);
    border:var(--border);}
.snake.head{background:var(--snake-head);z-index:100;}
 
    /* game over */
    .snake.gameover{background:var(--game-over);}
    .snake.head.gameover{background:var(--game-over-head);}
 
.food{
    background:var(--food);
    border:var(--border);}
 
/* [MODAL] */
#modal{
    position:fixed;
    width:100%;
    top:50%;left:50%; transform:translate(-50%,0);
    padding:.5em 0 .6em;
    background:rgba(0,0,0,.3);
    backdrop-filter:blur(20px);
    text-align:center; font-weight:bold; font-size:10vmin; color:red;
    opacity:0;
    transition:all .3s;}
 
#modal.on{opacity:1; transform:translate(-50%,-50%);}
 
cs

main.js

1
2
3
4
import { Game } from "./game.js";
 
const GAME = new Game();
GAME.init();
cs

Game.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
import { Food } from "./food.js";
import { KeyInput } from "./keyboardInput.js";
import { Snake } from "./snake.js";
 
export class Game{
    constructor(){
        this.$board = document.getElementById('game-board');
        this.lastRenderTime = 0;
        this.playable = true;
        this.$modal = document.getElementById('modal');
        this.$score= document.getElementById('score');
        this.SCORE = 0;
        this.GRID_SIZE = 21;
        
        this.IPT = new KeyInput(this);
        this.SNAKE = new Snake(this);
        this.FOOD = new Food(this);
    }//constructor
 
    init(){
        requestAnimationFrame(this.repeat);
        this.IPT.add_key_event();
    }//init
 
    repeat = async (currTime) => {
        this.playable && requestAnimationFrame(this.repeat);
        if(!this.playable){
            this.game_over();
            if(await this.is_replay()) this.reset_all();
        }
        const secSinceLastRender = (currTime - this.lastRenderTime) / 1000;
        if(secSinceLastRender < 1 / this.SNAKE.SPEED) return;
 
        // console.log(secSinceLastRender);
        this.lastRenderTime = currTime;
        this.update();
        this.draw();
    }//repeat
 
    update(){
        this.SNAKE.update();
        this.playable = this.is_game_over();
        this.FOOD.update();
    }//update
 
    draw(){
        this.reset_board();
        this.SNAKE.draw();
        this.FOOD.draw();
    }//draw
 
    reset_board(){
        this.$board.innerHTML = '';
    }//reset_board
 
    update_score(){
        this.SCORE += (this.SNAKE.snakeBody.length* 10;
        this.$score.textContent = this.SCORE;
    }//update_score
 
    is_game_over(){
        const first =  this.SNAKE.snakeBody[0];
        if(first.x <= 0){
            first.x = 1;
            return false;}
        if(first.x > this.GRID_SIZE){
            first.x = this.GRID_SIZE;
            return false;}
        if(first.y <= 0){
            first.y = 1;
            return false;}
        if(first.y > this.GRID_SIZE){
            first.y = this.GRID_SIZE;
            return false;}
        if(this.SNAKE.is_snake_collide()){return false;}
        return true;
    }//is_game_over
 
    game_over(){
        this.$modal.classList.add('on');
        const $$snake = Array.from(this.$board.getElementsByClassName('snake'));
        $$snake.forEach($snake => $snake.classList.add('gameover'));
    }//game_over
 
    is_replay(){
        return new Promise(res =>{
            setTimeout(()=>{
                const result = confirm('Replay?');
                res(result)
            },500);
        })
    }//is_replay
 
    reset_all(){
        this.reset_board();
        this.$modal.classList.remove('on');
        this.lastRenderTime = 0;
        this.SCORE = 0;
        this.$score.textContent = this.SCORE;
 
        this.IPT.reset();
        this.SNAKE.reset();
        this.FOOD.reset();
        requestAnimationFrame(this.repeat);
    }//reset_all
}//class-Game 
cs

food.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
export class Food{
    constructor(GAME){
        this.GAME = GAME;
        this.SNAKE = this.GAME.SNAKE;
        this.foodPos = this.randomPos();
        this.EXPANSION_RATE = 5;
    }//constructor
 
    reset(){
        this.foodPos = this.randomPos();
    }//reset
 
    randomPos(){
        let foodPos
        while(!foodPos || this.SNAKE.is_on_snake(foodPos)){
            foodPos = {
                x : parseInt(Math.random() * this.GAME.GRID_SIZE + 1), 
                y: parseInt(Math.random() * this.GAME.GRID_SIZE + 1)
            };
        }
        // console.log("foodPos",foodPos);
        return foodPos;
    }//randomPos
 
    update(){
        if(!this.SNAKE.is_on_snake(this.foodPos)) return;
        this.SNAKE.expand_snake(this.EXPANSION_RATE);
        this.foodPos = this.randomPos();
        this.GAME.update_score();
    }//update
 
    draw(){
        const $food = document.createElement('DIV');
        $food.classList.add('food');
        $food.style.gridRowStart = this.foodPos.y;
        $food.style.gridColumnStart = this.foodPos.x;
        this.GAME.$board.appendChild($food);
    }//draw
 
}//class-Food
cs

snake.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
export class Snake{
    constructor(GAME){
        this.GAME = GAME;
        this.SPEED = 4;
        this.snakeBody = [
            {x:11,y:11},
        ];
        this.newSegments = 0;
    }//constructor
 
    reset(){
        this.snakeBody = [{x:11,y:11}];
        this.newSegments = 0;
    }
 
    update(){
        const len = this.snakeBody.length - 2
        // console.log('update');
        for(let i=len; i>=0; i--){
            // console.log(this.snakeBody[i+1], this.snakeBody[i]);
            this.snakeBody[i+1= {...this.snakeBody[i]};
        }//for
 
        const{x,y} = this.GAME.IPT.direction;
        this.snakeBody[0].x += x;
        this.snakeBody[0].y += y;
    }//update
 
    draw(){
        this.snakeBody.forEach((segment,idx) => {
            const $snake = this.make_snake(idx);            
            const {x,y} = segment;
            $snake.style.gridRowStart = y;
            $snake.style.gridColumnStart = x;
            this.GAME.$board.appendChild($snake);
        });
        // console.log(this.snakeBody);
    }//draw
 
    make_snake(idx){
        const $snake = document.createElement('DIV');
        $snake.classList.add('snake');
        if(idx==0) $snake.classList.add('head');
        return $snake;
    }//make_snake
 
    expand_snake(EXPANSION_RATE){
        this.newSegments += EXPANSION_RATE;
        // console.log('newSegment',this.newSegments, 'expansion',EXPANSION_RATE);
        this.add_snake();
    }//expand_snake
 
    is_on_snake(foodPos){
        return this.snakeBody.some(seg => is_same_pos(seg,foodPos));
    }//is_on_snake
 
    add_snake(){
        for(let i=0; i<this.newSegments; i++){
            this.snakeBody.push({...this.snakeBody[this.snakeBody.length - 1]});
        }//for
        console.log('body Length:',this.snakeBody.length);
        this.newSegments = 0;
    }//add_snake
 
    is_snake_collide(){
        const first = this.snakeBody[0];
        const rest = this.snakeBody.slice(1);
        return rest.some(seg => is_same_pos(seg,first));
    }//is_snake_collide
}//class-Snake
 
export function is_same_pos(pos1,pos2){
    return (pos1.x == pos2.x && pos1.y == pos2.y);
}//is_same_pos
cs

ipt.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
export class KeyInput{
    constructor(GAME){
        this.GAME = GAME;
        this.direction = {x:0, y:0};
        this.lastDirection = {x:0, y:0};
        this.DIR = {up:"ArrowUp",down:"ArrowDown",left:"ArrowLeft",right:"ArrowRight"};
    }//constructor
 
    reset(){
        this.direction = {x:0, y:0};
        this.lastDirection = {x:0, y:0};
    }//reset
 
    add_key_event(){
        window.addEventListener('keydown',this.on_key_down);
    }//add_key_event
 
    on_key_down = (e) => {
        if(this.CURR == e.key) return;
        this.CURR = e.key;
        switch(e.key){
            case this.DIR.up : 
                if (this.lastDirection.y !== 0break;
                this.direction.x = 0;
                this.direction.y = -1;
                break;
            case this.DIR.down : 
            if (this.lastDirection.y !== 0break;
                this.direction.x = 0;
                this.direction.y = 1;
                break;
            case this.DIR.left : 
                if (this.lastDirection.x !== 0break;
                this.direction.x = -1;
                this.direction.y = 0;
                break;
            case this.DIR.right : 
                if (this.lastDirection.x !== 0break;
                this.direction.x = 1;
                this.direction.y = 0;
                break;
            default : break;
        }//switch
        this.lastDirection = this.direction;
    }//on_key_down
}//class-KeyInput
cs