├── README.md
├── index.html
└── main.js
/README.md:
--------------------------------------------------------------------------------
1 | # Snake game in tab favicon.
2 |
3 | ## How to play
4 |
5 | - Use arrow keys to move the snake.
6 | - Eat the food to grow.
7 | - Don't hit yourself.
8 |
9 | ## How to run
10 |
11 | - Clone repository and open `index.html` in your browser or just try in [here](https://defernus.github.io/favicon-snake/).
12 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 🐍
8 |
9 |
31 |
32 |
33 |
34 |
35 | Look at favicon
36 | WASD to control
37 |
38 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | const WIDTH = 16;
2 | const HEIGHT = 16;
3 | const BACKGROUND_COLOR = 'white';
4 | const SNAKE_COLOR = 'black';
5 | const FRUIT_COLOR = 'red';
6 | const APPEND_SPEED_EVERY = 5;
7 |
8 | const TOP = 0;
9 | const RIGHT = 1;
10 | const BOTTOM = 2;
11 | const LEFT = 3;
12 |
13 | document.head ??= document.getElementsByTagName('head')[0];
14 |
15 | const sleep = (s) => new Promise((resolve) => setTimeout(resolve, s * 1000));
16 |
17 | const setFavicon = (canvas) => {
18 | const src = canvas.toDataURL();
19 | const link = document.createElement('link');
20 | const oldLink = document.getElementById('dynamic-favicon');
21 | link.id = 'dynamic-favicon';
22 | link.rel = 'shortcut icon';
23 | link.href = src;
24 | if (oldLink) {
25 | document.head.removeChild(oldLink);
26 | }
27 | document.head.appendChild(link);
28 | };
29 |
30 | const setTitle = (titleStr) => {
31 | document.title = titleStr;
32 | };
33 |
34 | let gameSpeed = 1;
35 |
36 | let score = 0;
37 |
38 | let fruitX = 0;
39 | let fruitY = 0;
40 |
41 | const canvas = document.createElement('canvas');
42 | canvas.width = WIDTH;
43 | canvas.height = HEIGHT;
44 |
45 | const ctx = canvas.getContext('2d');
46 |
47 | let headX = 0;
48 | let headY = HEIGHT / 2;
49 |
50 | let dir = TOP;
51 | let bodyParts = [];
52 |
53 | let dirWasChanded = false;
54 |
55 | const trueMod = (a, b) => ((a % b) + b) % b;
56 |
57 | const respawnFruit = () => {
58 | const posToSpawn = [];
59 | for (let x = 0; x < WIDTH; x++) {
60 | for (let y = 0; y < HEIGHT; y++) {
61 | if (x === headX && y === headY) {
62 | continue;
63 | }
64 | if (bodyParts.some((part) => part.x === x && part.y === y)) {
65 | continue;
66 | }
67 | posToSpawn.push({ x, y });
68 | }
69 | }
70 | const pos = posToSpawn[Math.floor(Math.random() * posToSpawn.length)];
71 | fruitX = pos.x;
72 | fruitY = pos.y;
73 | };
74 |
75 | respawnFruit();
76 |
77 | const changeDir = (newDir) => {
78 | if (dirWasChanded) {
79 | return;
80 | }
81 | if (dir === newDir) {
82 | return;
83 | }
84 | if (dir === TOP && newDir === BOTTOM) {
85 | return;
86 | }
87 | if (dir === RIGHT && newDir === LEFT) {
88 | return;
89 | }
90 | if (dir === BOTTOM && newDir === TOP) {
91 | return;
92 | }
93 | if (dir === LEFT && newDir === RIGHT) {
94 | return;
95 | }
96 | dirWasChanded = true;
97 | dir = newDir;
98 | }
99 |
100 | const handleKey = (e) => {
101 | switch (e.keyCode) {
102 | case 38:
103 | case 87:
104 | changeDir(TOP);
105 | break;
106 | case 68:
107 | case 39:
108 | changeDir(RIGHT);
109 | break;
110 | case 83:
111 | case 40:
112 | changeDir(BOTTOM);
113 | break;
114 | case 65:
115 | case 37:
116 | changeDir(LEFT);
117 | break;
118 | }
119 | };
120 |
121 | document.addEventListener('keydown', handleKey);
122 |
123 | const restart = () => {
124 | headX = 0;
125 | headY = HEIGHT / 2;
126 | dir = TOP;
127 | bodyParts = [];
128 | gameSpeed = 5;
129 | score = 0;
130 | respawnFruit();
131 | };
132 |
133 | restart();
134 |
135 | const handleFrame = async () => {
136 | dirWasChanded = false;
137 |
138 | ctx.fillStyle = BACKGROUND_COLOR;
139 | ctx.fillRect(0, 0, WIDTH, HEIGHT);
140 |
141 | ctx.fillStyle = SNAKE_COLOR;
142 | ctx.fillRect(headX, headY, 1, 1);
143 |
144 | let fruitEated = false;
145 | if (headX === fruitX && headY === fruitY) {
146 | respawnFruit();
147 | fruitEated = true;
148 | ++score;
149 |
150 | if (score % APPEND_SPEED_EVERY === 0) {
151 | ++gameSpeed;
152 | }
153 | setTitle(`${score} 🍎`);
154 | }
155 |
156 | bodyParts.forEach((part) => {
157 | ctx.fillRect(part.x, part.y, 1, 1);
158 | });
159 |
160 | ctx.fillStyle = FRUIT_COLOR;
161 | ctx.fillRect(fruitX, fruitY, 1, 1);
162 |
163 | bodyParts.push({ x: headX, y: headY });
164 |
165 | if (!fruitEated) {
166 | bodyParts.splice(0, 1);
167 | }
168 |
169 | if (dir === TOP) {
170 | headY -= 1;
171 | } else if (dir === RIGHT) {
172 | headX += 1;
173 | } else if (dir === BOTTOM) {
174 | headY += 1;
175 | } else if (dir === LEFT) {
176 | headX -= 1;
177 | }
178 |
179 | headX = trueMod(headX, WIDTH);
180 | headY = trueMod(headY, HEIGHT);
181 |
182 | if (bodyParts.some((part) => part.x === headX && part.y === headY)) {
183 | setTitle(`💀: ${score} 🍎`);
184 | restart();
185 | }
186 |
187 | setFavicon(canvas);
188 |
189 | await sleep(1 / gameSpeed);
190 |
191 | requestAnimationFrame(() => handleFrame());
192 | };
193 |
194 | handleFrame();
195 |
--------------------------------------------------------------------------------