├── README.md
├── assets
├── bite.mp3
├── favicon.png
└── caret.svg
├── index.html
├── style.css
└── game.js
/README.md:
--------------------------------------------------------------------------------
1 | # https://2dsnake.netlify.app
2 |
--------------------------------------------------------------------------------
/assets/bite.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abhijeetSinghRajput/snake-game/HEAD/assets/bite.mp3
--------------------------------------------------------------------------------
/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abhijeetSinghRajput/snake-game/HEAD/assets/favicon.png
--------------------------------------------------------------------------------
/assets/caret.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | snake game
10 |
11 |
12 |
13 |
14 |
28 |
29 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
2 |
3 | *{
4 | margin: 0;
5 | padding: 0;
6 | box-sizing: border-box;
7 | font-family: "Press Start 2P", sans-serif;
8 | list-style: none;
9 | text-decoration: none;
10 | }
11 |
12 | body{
13 | width: 100vw;
14 | height: 100svh;
15 | background-color: #1f1f1f;
16 | color: greenyellow;
17 | display: flex;
18 | align-items: center;
19 | justify-content: center;
20 | overflow: hidden;
21 | }
22 | main{
23 | width: 100vmin;
24 | height: 100vmin;
25 | display: flex;
26 | flex-direction: column;
27 | align-items: center;
28 | justify-content: space-between;
29 | /* border: 1px solid white; */
30 | }
31 | header{
32 | text-align: center;
33 | margin-bottom: 20px;
34 | padding: 20px;
35 | width: 100%;
36 | }
37 | header h1{
38 | font-size: 22px;
39 | }
40 |
41 | .score-board{
42 | display: flex;
43 | align-items: center;
44 | justify-content: space-between;
45 | font-size: 12px;
46 | gap: 20px;
47 | margin-top: 20px;
48 | }
49 |
50 | .game-board{
51 | background-color: #333;
52 | aspect-ratio: 1;
53 | flex-grow: 1;
54 | display: grid;
55 | grid-template-columns: repeat(20, 1fr);
56 | grid-template-rows: repeat(20, 1fr);
57 | }
58 | .snake{
59 | background-color: greenyellow;
60 | border: .2vmin solid black;
61 | color: black;
62 | font-family: sans-serif;
63 | font-size: 10px;
64 | }
65 | .food{
66 | background-color: crimson;
67 | border: .2vmin solid black;
68 | }
69 | .head{
70 | border-top-right-radius: 100px;
71 | border-bottom-right-radius: 100px;
72 | position: relative;
73 | }
74 | .head::before,
75 | .head::after{
76 | content: '';
77 | position: absolute;
78 | border-radius: 50%;
79 | border: .3vmin solid white;
80 | background-color: black;
81 | width: 30%;
82 | aspect-ratio: 1;
83 | top: 0;
84 | right: 0;
85 | }
86 | .head::before{
87 | top: auto;
88 | bottom: 0;
89 | }
90 | .head.down{
91 | transform: rotate(90deg);
92 | }
93 | .head.up{
94 | transform: rotate(-90deg);
95 | }
96 | .head.left{
97 | transform: rotate(-180deg);
98 | }
99 | .head.right{
100 | transform: rotate(0deg);
101 | }
102 |
103 | .controlls{
104 | width: 100px;
105 | height: 100px;
106 | display: grid;
107 | grid-template-columns: 1fr 1fr;
108 | grid-gap: 5px;
109 | overflow: hidden;
110 | border-radius: 50%;
111 | transform: rotate(45deg);
112 | position: relative;
113 | z-index: 100;
114 | }
115 | button{
116 | cursor: pointer;
117 | background-color: greenyellow;
118 | border: none;
119 | outline: none;
120 | border-radius: 10px;
121 | }
122 |
123 | button#up img{
124 | transform: rotate(-45deg);
125 | }
126 | button#left img{
127 | transform: rotate(-135deg);
128 | }
129 | button#right img{
130 | transform: rotate(45deg);
131 | }
132 | button#down img{
133 | transform: rotate(135deg);
134 | }
135 |
136 |
--------------------------------------------------------------------------------
/game.js:
--------------------------------------------------------------------------------
1 | const gameBoard = document.querySelector('.game-board');
2 | let highScore = document.querySelector('.high-score .value');
3 | let score = document.querySelector('.score .value');
4 |
5 | highScore.textContent = localStorage.getItem('snakeHighScore') || 0;
6 | score.textContent = 0;
7 | let gridNum = 20;
8 | let scoreIncrement = 5;
9 |
10 | let snakeBody = [
11 | { x: 10, y: 13 },
12 | { x: 10, y: 12 },
13 | { x: 10, y: 11 },
14 | ];
15 |
16 | let snakeSpeed = 5;
17 | const expansionRate = 1;
18 | let snakeDirection = 'right';
19 | const directions = [
20 | { x: -1, y: 0 },
21 | { x: 0, y: 1 },
22 | { x: 1, y: 0 },
23 | { x: 0, y: -1 },
24 | ];
25 | const [up, right, down, left] = directions;
26 |
27 | let grid = [];
28 | for (let i = 1; i <= gridNum; ++i) {
29 | for (let j = 1; j <= gridNum; ++j) {
30 | grid.push({ x: i, y: j });
31 | }
32 | }
33 | let foodPosition = getRandomFood();
34 |
35 | let lastRenderTime = 0;
36 | function main(currTime) {
37 | let sec_delay = (currTime - lastRenderTime) / 1000;
38 | requestAnimationFrame(main);
39 | if (sec_delay < (1 / snakeSpeed)) return;
40 |
41 | moveSnake();
42 | drawSnake();
43 | drawFood();
44 | lastRenderTime = currTime;
45 | }
46 | requestAnimationFrame(main);
47 |
48 | function incScore() {
49 | let value = +score.textContent;
50 | let bonus = 0;
51 |
52 | //level up
53 | if(value > 200) bonus = 10, snakeSpeed = 9;
54 | else if(value > 150) bonus = 10, snakeSpeed = 8;
55 | else if(value > 100) bonus = 5, snakeSpeed = 7;
56 | else if(value > 50) bonus = 2, snakeSpeed = 6;
57 |
58 | score.textContent = value + scoreIncrement + bonus;
59 |
60 | if (+score.textContent > +highScore.textContent) {
61 | highScore.textContent = score.textContent;
62 | localStorage.setItem('snakeHighScore', highScore.textContent);
63 | }
64 | }
65 |
66 | function drawSnake() {
67 | gameBoard.innerHTML = '';
68 | snakeBody.forEach(({ x, y }, i) => {
69 | let snake = document.createElement('div');
70 | snake.className = "snake";
71 | snake.style.gridRowStart = x;
72 | snake.style.gridColumnStart = y;
73 | if (i === 0) snake.classList.add('head', snakeDirection);
74 | gameBoard.appendChild(snake);
75 | });
76 | }
77 |
78 | function drawFood() {
79 | let food = document.createElement('div');
80 | food.className = 'food';
81 | food.style.gridRowStart = foodPosition.x;
82 | food.style.gridColumnStart = foodPosition.y;
83 | gameBoard.appendChild(food);
84 | }
85 |
86 | const biteSound = new Audio('./assets/bite.mp3');
87 | function move(direction, str) {
88 | // prevent 180-degree turn
89 | if (str === 'up' && snakeDirection === 'down') return;
90 | if (str === 'left' && snakeDirection === 'right') return;
91 | if (str === 'right' && snakeDirection === 'left') return;
92 | if (str === 'down' && snakeDirection === 'up') return;
93 |
94 | snakeDirection = str;
95 | }
96 |
97 | function moveSnake() {
98 | let direction;
99 | switch (snakeDirection) {
100 | case 'up':
101 | direction = up;
102 | break;
103 | case 'down':
104 | direction = down;
105 | break;
106 | case 'left':
107 | direction = left;
108 | break;
109 | case 'right':
110 | direction = right;
111 | break;
112 | }
113 |
114 | for (let i = snakeBody.length - 1; i > 0; --i) {
115 | snakeBody[i] = { ...snakeBody[i - 1] };
116 | }
117 |
118 | snakeBody[0].x += direction.x;
119 | snakeBody[0].y += direction.y;
120 |
121 | // teleport
122 | if (snakeBody[0].x > gridNum) snakeBody[0].x = 1;
123 | if (snakeBody[0].y > gridNum) snakeBody[0].y = 1;
124 | if (snakeBody[0].x < 1) snakeBody[0].x = gridNum;
125 | if (snakeBody[0].y < 1) snakeBody[0].y = gridNum;
126 |
127 | if (checkIntersection()) {
128 | alert('game over');
129 | newGame();
130 | return;
131 | }
132 | if (isEqual(snakeBody[0], foodPosition)) {
133 | expand(expansionRate);
134 | biteSound.play();
135 | foodPosition = getRandomFood();
136 | incScore();
137 | }
138 | }
139 |
140 | function checkIntersection() {
141 | for (let i = 1; i < snakeBody.length; ++i) {
142 | if (isEqual(snakeBody[0], snakeBody[i])) {
143 | return true;
144 | }
145 | }
146 | return false;
147 | }
148 |
149 | function expand(expansionRate) {
150 | while (expansionRate--) {
151 | snakeBody.push({ ...snakeBody[snakeBody.length - 1] });
152 | }
153 | }
154 |
155 | function getRandomFood() {
156 | const remainingCell = grid.filter((cell) => {
157 | for (let i = 0; i < snakeBody.length; ++i) {
158 | if (isEqual(cell, snakeBody[i])) {
159 | return false;
160 | }
161 | }
162 | return true;
163 | });
164 | if (remainingCell.length === 0) {
165 | alert('you do not know how elite you are');
166 | newGame();
167 | }
168 | let randomIndex = Math.floor(Math.random() * remainingCell.length);
169 | return remainingCell[randomIndex];
170 | }
171 |
172 | function isEqual(pos1, pos2) {
173 | return pos1.x === pos2.x && pos1.y === pos2.y;
174 | }
175 |
176 | function newGame() {
177 |
178 | snakeBody = [
179 | { x: 10, y: 13 },
180 | { x: 10, y: 12 },
181 | { x: 10, y: 11 },
182 | ];
183 | snakeSpeed = 5;
184 | snakeDirection = 'right';
185 | foodPosition = getRandomFood();
186 | score.textContent = 0;
187 | }
188 |
189 | window.addEventListener('keydown', ({ key }) => {
190 | switch (key) {
191 | case 'ArrowUp':
192 | move(up, 'up');
193 | break;
194 | case 'ArrowDown':
195 | move(down, 'down');
196 | break;
197 | case 'ArrowLeft':
198 | move(left, 'left');
199 | break;
200 | case 'ArrowRight':
201 | move(right, 'right');
202 | break;
203 | default:
204 | break;
205 | }
206 | });
207 |
--------------------------------------------------------------------------------