├── 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 | --------------------------------------------------------------------------------