├── style.css ├── index.html └── main.js /style.css: -------------------------------------------------------------------------------- 1 | canvas { 2 | border: solid 1px; 3 | image-rendering: pixelated; 4 | } 5 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Tetris 9 | 10 | 11 | 12 |
0
13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /*_____ _ _ 2 | | __ \| || | GOVNO 3 | | |__) | || |_ CODE 4 | | _ /|__ _| STUDIOS 5 | | | \ \ | | 6 | |_| \_\ |_|*/ 7 | 8 | /* 9 | Список того что нужно сделать: 10 | 1). Рандомная генерация фигур ✅ 11 | 2). Повороты фигур ✅ 12 | 3). Управление свайпами ✅ 13 | 4). Функции для софтдропа ✅ 14 | 5). Функция для харддропа ✅ 15 | 6). Коллизия по бокам фигуры ✅ 16 | 7). Начисление очков ✅ 17 | 8). Увеличение сложности по ходу игры ✅ 18 | 9). Стирание нижних рядов при заполнении ✅ 19 | 10). Доделать обесцвечивание закрепившихся блоков ✅ 20 | 11). Сделать ghost pieces ❌ 21 | 12). Сделать Game Over 22 | */ 23 | 24 | //Graphics 25 | let block = new Image(); 26 | block.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAAXNSR0IArs4c6QAAAC1JREFUKFNjZGBg+M+ABzCCFMSH+2NVsnDlRga4AhAHGYA00VsBNlfC3YDPmwCq1SYB/MvrCgAAAABJRU5ErkJggg=="; 27 | let empty = new Image(); 28 | empty.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAAXNSR0IArs4c6QAAACJJREFUKFNj/H9m1X8GPIARpCCheylWJQtKoxmGjgJ83gQA/twtqU9deHcAAAAASUVORK5CYII="; 29 | let ghost = new Image(); 30 | ghost.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAAXNSR0IArs4c6QAAACFJREFUKFNjZGBg+M+ABzCCFMSH+2NVsnDlRoahowCfNwFtuxoBXWlpLgAAAABJRU5ErkJggg=="; 31 | 32 | //Pieces creation 33 | let pieceSpawnCenter = { 34 | "T" : -2, 35 | "O" : -1, 36 | "L" : -2, 37 | "J" : -1, 38 | "S" : -1, 39 | "Z" : -1, 40 | "I" : -1 41 | }; 42 | 43 | pieces = { 44 | "T" : [ 45 | 46 | [1,1,1], 47 | [0,1,0], 48 | [0,0,0] 49 | ], 50 | "O": [ 51 | [1, 1], 52 | [1, 1] 53 | ], 54 | "L": [ 55 | [0, 1, 0], 56 | [0, 1, 0], 57 | [0, 1, 1] 58 | ], 59 | "J": [ 60 | [0, 1, 0], 61 | [0, 1, 0], 62 | [1, 1, 0] 63 | ], 64 | "S": [ 65 | [0, 1, 1], 66 | [1, 1, 0], 67 | [0, 0, 0] 68 | ], 69 | "Z": [ 70 | [1, 1, 0], 71 | [0, 1, 1], 72 | [0, 0, 0] 73 | ], 74 | "I": [ 75 | [0,1,0,0], 76 | [0,1,0,0], 77 | [0,1,0,0], 78 | [0,1,0,0] 79 | ], 80 | }; 81 | 82 | //Initialization 83 | let c = document.getElementById("canvas"); 84 | let ctx = c.getContext("2d"); 85 | let debug = document.getElementById("debug"); 86 | let scoreText = document.getElementById("scoreText"); 87 | //Scaler 88 | let gameScale = 2; 89 | //c.width = c.width * gameScale; 90 | //c.height = c.height * gameScale; 91 | c.width = window.innerWidth - 20; 92 | c.height = window.innerHeight -80; 93 | ctx.imageSmoothingEnabled = false; 94 | //Dimensions 95 | let levelWidth = 10; 96 | let levelHeight = 20; 97 | let blockWidth = c.width / levelWidth; 98 | let blockHeight = c.height / levelHeight; 99 | 100 | let currentPiece = pickRandomPiece(); 101 | let originalRotation = pieces[currentPiece]; 102 | let nextPiece = pickRandomPiece(); 103 | let X = Math.floor((levelWidth/2)) + pieceSpawnCenter[currentPiece], Y = 0; 104 | let ghostX = X, ghostY = levelHeight - pieces[currentPiece].length; 105 | let canRotate = true; 106 | let hardDrop = false; 107 | let score = 0; 108 | let level = 1; 109 | let linesForLevelUp = 10; 110 | let maximumLevel = 28; 111 | let maximumSpeed = 40; 112 | let linesTotal = 0; 113 | 114 | //Arena creation 115 | let arena = []; 116 | for (column = 0; column < levelHeight; column++) { 117 | arena[column] = []; 118 | for (row = 0; row < levelWidth; row++) { 119 | arena[column][row] = 0; 120 | if(column === levelHeight-1) { 121 | arena[column][row] = 2; 122 | } 123 | } 124 | }; 125 | 126 | ///Controls 127 | 128 | let swipeStartX = 0; 129 | let swipeStartY = 0; 130 | let swipeEndX = 0; 131 | let swipeEndY = 0; 132 | let swipeDuration = 0; 133 | let swipedLeft = false, swipedRight = false; 134 | c.addEventListener("touchstart", function(e) { 135 | swipeStartX = e.touches[0].clientX; 136 | swipeStartY = e.touches[0].clientY; 137 | }); 138 | c.addEventListener("touchmove", function(e) { 139 | swipeEndX = e.touches[0].clientX; 140 | swipeEndY = e.touches[0].clientY; 141 | swipeDuration += 1; 142 | }); 143 | c.addEventListener("touchend", function(e) { 144 | e.preventDefault(); 145 | if (!hardDrop && !isGameOver()) { 146 | if (swipeEndX < swipeStartX-20 && swipeDuration> 1) { 147 | X -= 1; 148 | ghostX -= 1; 149 | if (pieceIsOutOfBounds(pieces[currentPiece]) === "left") { 150 | X += 1; 151 | ghostX += 1; 152 | } 153 | if (collision() === "withPiece") { 154 | X += 1 155 | ghostX += 1; 156 | }; 157 | swipeDuration = 0; 158 | draw(); 159 | } else if(swipeEndX > swipeStartX+20 && swipeDuration > 1) { 160 | //Swipe right 161 | X += 1; 162 | ghostX += 1; 163 | if (pieceIsOutOfBounds(pieces[currentPiece]) === "right") { 164 | X -= 1; 165 | ghostX -= 1; 166 | } 167 | if(collision() === "withPiece") { 168 | X -= 1 169 | ghostX -= 1; 170 | }; 171 | swipeDuration = 0; 172 | draw(); 173 | } else if (swipeEndY < swipeStartY - 20 && swipeDuration > 1) { 174 | //Swipe up 175 | hardDrop = true; 176 | swipeDuration=0; 177 | } 178 | else if (swipeEndY > swipeStartY + 20 && swipeDuration > 1) { 179 | //Swipe down 180 | //dropPiece(); 181 | dropPiece(); 182 | score += 1; 183 | gameTimer = 0; 184 | draw(); 185 | //timeStart = 0; 186 | swipeDuration=0; 187 | } else if (swipeDuration <= 1 && canRotate) { 188 | //Regular touch 189 | let buffer = rotate(pieces[currentPiece]); 190 | while(pieceIsOutOfBounds(buffer) === "left") { 191 | X+= 1; 192 | } 193 | while (pieceIsOutOfBounds(buffer) === "right") { 194 | X -= 1; 195 | } 196 | if (!rotationCollision(buffer) ) 197 | pieces[currentPiece] = buffer; 198 | draw(); 199 | swipeDuration = 0; 200 | } 201 | } 202 | } 203 | ); 204 | 205 | //Functions 206 | function draw() { 207 | //Clear screen 208 | ctx.clearRect(0, 0, c.width, c.height); 209 | //Rendering arena 210 | for (y = 0; y < levelHeight; y++) { 211 | for (x = 0; x < levelWidth; x++) { 212 | if (arena[y][x] === 1) { 213 | ctx.drawImage(block, x * blockWidth, y * blockHeight, blockWidth, blockHeight); 214 | fadeBlock(x*blockWidth,y*blockHeight); 215 | } 216 | if (arena[y][x] === 0 || 217 | arena[y][x] === 2) { 218 | ctx.drawImage(empty, x * blockWidth, y * blockHeight, blockWidth, blockHeight) 219 | } 220 | } 221 | } 222 | //Rendering dropping piece 223 | for (i = 0; i < pieces[currentPiece].length; i++) { 224 | for (j = 0; j < pieces[currentPiece][0].length; j++) { 225 | if (pieces[currentPiece][i][j] !== 0) { 226 | ctx.drawImage(block, 227 | (X*blockWidth) + (j*blockWidth), 228 | (Y*blockHeight) + (i*blockHeight), 229 | blockWidth, blockHeight); 230 | } 231 | } 232 | } 233 | }; 234 | 235 | function random(min, max) { 236 | let rand = min + Math.random() * (max + 1 - min); 237 | rand = Math.floor(rand); 238 | return rand; 239 | }; 240 | 241 | function pickRandomPiece() { 242 | let piece = ""; 243 | let pieces = ["T","O","L","J","S","Z","I"]; 244 | for (i = 0; i < pieces.length; i++) { 245 | piece = pieces[random(0,pieces.length-1)]; 246 | } 247 | return piece; 248 | }; 249 | 250 | function merge() { 251 | for (i = 0; i < pieces[currentPiece].length; i++) { 252 | for (j = 0; j < pieces[currentPiece][0].length; j++) { 253 | if (pieces[currentPiece][i][j] === 1) { 254 | arena[Y+i][X+j] = 1; 255 | } 256 | } 257 | } 258 | }; 259 | 260 | function rotationCollision(buffer) { 261 | for (i = 0; i < buffer.length; i++) { 262 | for (j = 0; j < buffer[0].length; j++) { 263 | if (buffer[i][j] === 1) { 264 | if (arena[Y + i][(X + j)] === 1) { 265 | return true; 266 | } 267 | } 268 | } 269 | } 270 | }; 271 | 272 | function collision() { 273 | for (i = 0; i < pieces[currentPiece].length; i++) { 274 | for (j = 0; j < pieces[currentPiece][0].length; j++) { 275 | if (pieces[currentPiece][i][j] === 1) { 276 | if (arena[Y + i][(X + j)] === 1) { 277 | return "withPiece"; 278 | } 279 | if (arena[Y + i][(X + j)] === 2) { 280 | return "withBottom"; 281 | } 282 | } 283 | } 284 | } 285 | }; 286 | 287 | function pieceIsOutOfBounds(buffer) { 288 | let middle = levelWidth/2; 289 | 290 | if (X < middle) { 291 | for (i = 0; i < buffer.length; i++) { 292 | for (j = 0; j < buffer[0].length; j++) { 293 | if (buffer[i][j] === 1) { 294 | if(arena[Y+i][X+j] === undefined) { 295 | return "left"; 296 | } 297 | } 298 | } 299 | } 300 | } 301 | 302 | if (X > middle) { 303 | for (i = 0; i < buffer.length; i++) { 304 | for (j = 0; j < buffer[0].length; j++) { 305 | if (buffer[i][j] === 1) { 306 | if (X+j === levelWidth) { 307 | return "right"; 308 | } 309 | } 310 | } 311 | } 312 | } 313 | }; 314 | 315 | function isGameOver() { 316 | for (i = 0; i < pieces[currentPiece].length; i++) { 317 | for (j = 0; j < pieces[currentPiece][0].length; j++) { 318 | if (pieces[currentPiece][i][j] === 1) { 319 | if (arena[0][X + j] === 1) { 320 | return true; 321 | } 322 | } 323 | } 324 | } 325 | }; 326 | 327 | function dropPiece() { 328 | canRotate = false; 329 | if (isGameOver()) 330 | console.log("GAME OVER"); 331 | Y += 1; 332 | if (hardDrop) 333 | score += 2; 334 | if(collision() === "withPiece") { 335 | Y -= 1; 336 | merge(); 337 | hardDrop = false; 338 | clearLines(); 339 | pieces[currentPiece] = originalRotation; 340 | currentPiece = nextPiece; 341 | nextPiece = pickRandomPiece(); 342 | while (nextPiece === currentPiece) 343 | nextPiece = pickRandomPiece(); 344 | originalRotation = pieces[currentPiece]; 345 | Y = 0; 346 | X = Math.floor((levelWidth/2)) + pieceSpawnCenter[currentPiece]; 347 | } 348 | if (collision() === "withBottom") { 349 | merge(); 350 | hardDrop = false; 351 | clearLines(); 352 | pieces[currentPiece] = originalRotation; 353 | currentPiece = nextPiece; 354 | nextPiece = pickRandomPiece(); 355 | while (nextPiece === currentPiece) 356 | nextPiece = pickRandomPiece(); 357 | originalRotation = pieces[currentPiece]; 358 | Y = 0; 359 | X = Math.floor((levelWidth/2)) + pieceSpawnCenter[currentPiece]; 360 | } 361 | canRotate = true; 362 | }; 363 | 364 | function ghostPiece() { 365 | if(collision() ) { 366 | ghostY = ghostY-pieces[currentPiece].length; 367 | } else { 368 | ghostY = levelHeight - pieces[currentPiece].length; 369 | } 370 | }; 371 | 372 | function fadeBlock(x, y) { 373 | let buffer = ctx.getImageData(x, y, blockHeight, blockWidth); 374 | let data = buffer.data; 375 | 376 | for (var i = 0; i < data.length; i += 4) { 377 | data[i] = data[i] -30; // red 378 | data[i + 1] = data[i + 1] -30; // green 379 | data[i + 2] = data[i + 2] -30; // blue 380 | } 381 | ctx.putImageData(buffer, x, y); 382 | }; 383 | 384 | function rotate(matrix) { 385 | const N = matrix.length - 1; 386 | const result = matrix.map((row, i) => 387 | row.map((val, j) => matrix[N - j][i]) 388 | ); 389 | return result; 390 | }; 391 | 392 | function clearLines() { 393 | let lines = []; 394 | 395 | for (var y = levelHeight-1; y >= 0; y--) { 396 | let numberOfBlocks = 0; 397 | 398 | for (var x = 0; x < levelWidth; x++) { 399 | if(arena[y][x] !== 0 && arena[y][x] !== 2) { 400 | numberOfBlocks += 1; 401 | } 402 | } 403 | 404 | if (numberOfBlocks === 0) { 405 | break; 406 | } else if (numberOfBlocks < levelWidth) { 407 | continue; 408 | } else if (numberOfBlocks === levelWidth) { 409 | lines.unshift(y); 410 | } 411 | } 412 | 413 | 414 | for (let index of lines) { 415 | arena.splice(index,1); 416 | arena.unshift(new Array(levelWidth).fill(0)); 417 | } 418 | 419 | for (x = 0; x < levelWidth; x++) { 420 | if(arena[levelHeight-1][x] === 0) 421 | arena[levelHeight-1][x] = 2; 422 | } 423 | 424 | for (let index of lines) { 425 | linesTotal += 1; 426 | } 427 | if ((linesTotal > 0 && linesTotal % linesForLevelUp === 0) || linesTotal > linesForLevelUp) { 428 | level += 1; 429 | linesTotal -= linesForLevelUp; 430 | currentSpeed = currentSpeed - (maximumSpeed / maximumLevel); 431 | } 432 | console.log(`Lines cleared: ${linesTotal} :: Current level: ${level} :: Current speed: ${currentSpeed}`) 433 | 434 | if (lines.length === 1) { 435 | score = score + (100*level); 436 | } else if(lines.length === 2) { 437 | score = score + (300*level); 438 | } else if(lines.length === 3) { 439 | score = score + (500*level); 440 | } else if(lines.length === 4) { 441 | score = score + (800*level); 442 | } 443 | }; 444 | draw(); 445 | //Main game loop 446 | let timeStart = 0; 447 | let gameTimer = 0; 448 | let gameSpeed = 40; 449 | let currentSpeed = 40; 450 | //60 max, 28 levels 451 | function update(timestamp) { 452 | if (!isGameOver()) { 453 | scoreText.innerHTML = `Score: ${score}     Level: ${level}
Current Piece: ${currentPiece}
Next piece: ${nextPiece}`; 454 | 455 | if (hardDrop) 456 | gameSpeed = 1; 457 | else gameSpeed = currentSpeed; 458 | gameTimer++; 459 | 460 | //if (timestamp - timeStart >= 500) { 461 | if (gameTimer >= gameSpeed) { 462 | dropPiece(); 463 | draw(); 464 | gameTimer = 0; 465 | timeStart = timestamp; 466 | } 467 | requestAnimationFrame(update); 468 | } 469 | }; 470 | 471 | update(); 472 | --------------------------------------------------------------------------------