├── .github ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md ├── LICENSE ├── README.md ├── play.html └── src ├── assets └── tetris-favicon.jpg ├── css └── style.css └── js └── script.js /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the **JavaScript Tetris**, we, as project maintainers and members, commit to making participation in our community a harassment-free experience for everyone, regardless of age, personal appearance, ethnicity, gender identity, experience level, and socioeconomic status. 6 | 7 | ## Our Standards 8 | 9 | - Be kind and courteous to others. 10 | - Respect different viewpoints and experiences. 11 | 12 | ## Our Responsibilities 13 | 14 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 15 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to "JavaScript Tetris" 2 | 3 | Thank you for your interest in contributing to the "JavaScript Tetris"! Every contribution is appreciated. 4 | 5 | ## Quickstart 6 | 7 | 1. **Report Issues**: If you find a bug or have a suggestion, please open an issue [here](https://github.com/vontanne/javascript-tetris/issues). 8 | 2. **Submit Pull Requests**: Feel free to take on open issues or enhance the project. Fork the repo, make your changes, and submit a pull request. 9 | 10 | ## Guidelines 11 | 12 | - Ensure your code is well-documented and follows the project's style. 13 | - Include a clear and descriptive commit message. 14 | - Pull requests should be based on the latest master branch and should be conflict-free. 15 | 16 | ## Questions? 17 | 18 | If you have any questions or need further clarification, feel free to open an issue for discussion. 19 | 20 | Thank you for contributing to the "JavaScript Tetris"! 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Hovhannes Hovhannisyan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tetris Game 2 | 3 | This project is a simple yet fully functional implementation of the classic Tetris game using pure `HTML`, `CSS`, and `JavaScript`, with no external libraries. It's designed to be lightweight and straightforward, suitable for beginners looking to understand game development with web technologies. 4 | 5 | ## Live Demo 6 | 7 | You can play the Tetris game online without any installation by visiting: [Play Tetris Online](https://vontanne.github.io/javascript-tetris/) 8 | 9 | ## How to Play 10 | 11 | To play the game locally, follow these steps: 12 | 13 | 1. Clone the repository or download the ZIP file. 14 | 2. If downloaded as a ZIP, unzip the file. 15 | 3. Navigate to the project directory. 16 | 4. Double-click on `play.html` to open the game in your web browser. 17 | 18 | ### Game Controls 19 | 20 | - **Enter**: Start the game 21 | - **Arrow Left**: Move left 22 | - **Arrow Right**: Move right 23 | - **Arrow Up**: Rotate the piece 24 | - **Arrow Down**: Drop the piece faster 25 | - **Space**: Pause/Resume the game 26 | 27 | Enjoy the classic block-stacking game where the goal is to clear as many lines as possible by completing horizontal rows of blocks without any gaps. 28 | 29 | ## Features 30 | 31 | - Start, pause, and resume gameplay 32 | - Keyboard controls for moving and rotating Tetris blocks 33 | - Increasing difficulty as you progress in the game 34 | 35 | ## Contributing 36 | 37 | Your contributions are welcome! If you'd like to improve the Tetris game, please check out the [contributing guidelines](./.github/CONTRIBUTING.md). 38 | 39 | ## License 40 | 41 | This game is open-sourced under the [MIT license](./LICENSE). 42 | -------------------------------------------------------------------------------- /play.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 35 | 40 | 41 | Tetris 42 | 43 | 44 |
45 |

Tetris

46 |
47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/assets/tetris-favicon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vontanne/javascript-tetris/9a24f5dbbfad0503d1770e7d1eba15185872191e/src/assets/tetris-favicon.jpg -------------------------------------------------------------------------------- /src/css/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | background-color: #000; 9 | color: #fff; 10 | font-family: 'Courier New', Courier, monospace; 11 | } 12 | 13 | #root { 14 | text-align: center; 15 | } 16 | 17 | #root > h1 { 18 | margin: 0 10% 0 0; 19 | padding: 5px 0; 20 | color: #fff; 21 | } 22 | 23 | #root > canvas { 24 | display: block; 25 | margin: 20px auto; 26 | } 27 | -------------------------------------------------------------------------------- /src/js/script.js: -------------------------------------------------------------------------------- 1 | class Game { 2 | static points = { 3 | 1: 10, 4 | 2: 30, 5 | 3: 90, 6 | 4: 270, 7 | }; 8 | 9 | constructor() { 10 | this.reset(); 11 | } 12 | 13 | get level() { 14 | return Math.floor(this.lines * 0.04); 15 | } 16 | 17 | getState() { 18 | const playfield = this.createPlayfield(); 19 | const { y: pieceY, x: pieceX, blocks } = this.activePiece; 20 | 21 | for (let y = 0; y < this.playfield.length; y++) { 22 | playfield[y] = []; 23 | 24 | for (let x = 0; x < this.playfield[y].length; x++) { 25 | playfield[y][x] = this.playfield[y][x]; 26 | } 27 | } 28 | 29 | for (let y = 0; y < blocks.length; y++) { 30 | for (let x = 0; x < blocks[y].length; x++) { 31 | if (blocks[y][x]) { 32 | playfield[pieceY + y][pieceX + x] = blocks[y][x]; 33 | } 34 | } 35 | } 36 | 37 | return { 38 | score: this.score, 39 | level: this.level, 40 | lines: this.lines, 41 | nextPiece: this.nextPiece, 42 | playfield, 43 | isGameOver: this.topOut, 44 | }; 45 | } 46 | 47 | reset() { 48 | this.score = 0; 49 | this.lines = 0; 50 | this.topOut = false; 51 | this.playfield = this.createPlayfield(); 52 | this.activePiece = this.createPiece(); 53 | this.nextPiece = this.createPiece(); 54 | } 55 | 56 | createPlayfield() { 57 | const playfield = []; 58 | 59 | for (let y = 0; y < 20; y++) { 60 | playfield[y] = []; 61 | for (let x = 0; x < 10; x++) { 62 | playfield[y][x] = 0; 63 | } 64 | } 65 | 66 | return playfield; 67 | } 68 | 69 | createPiece() { 70 | const index = Math.floor(Math.random() * 7); 71 | const type = "IJLQSTZ"[index]; 72 | const piece = {}; 73 | 74 | switch (type) { 75 | case "I": 76 | piece.blocks = [ 77 | [0, 0, 0, 0], 78 | [1, 1, 1, 1], 79 | [0, 0, 0, 0], 80 | [0, 0, 0, 0], 81 | ]; 82 | break; 83 | case "J": 84 | piece.blocks = [ 85 | [0, 0, 0], 86 | [2, 2, 2], 87 | [0, 0, 2], 88 | ]; 89 | break; 90 | case "L": 91 | piece.blocks = [ 92 | [0, 0, 0], 93 | [3, 3, 3], 94 | [3, 0, 0], 95 | ]; 96 | break; 97 | case "Q": 98 | piece.blocks = [ 99 | [0, 0, 0, 0], 100 | [0, 4, 4, 0], 101 | [0, 4, 4, 0], 102 | [0, 0, 0, 0], 103 | ]; 104 | break; 105 | case "S": 106 | piece.blocks = [ 107 | [0, 0, 0], 108 | [0, 5, 5], 109 | [5, 5, 0], 110 | ]; 111 | break; 112 | case "T": 113 | piece.blocks = [ 114 | [0, 0, 0], 115 | [6, 6, 6], 116 | [0, 6, 0], 117 | ]; 118 | break; 119 | case "Z": 120 | piece.blocks = [ 121 | [0, 0, 0], 122 | [7, 7, 0], 123 | [0, 7, 7], 124 | ]; 125 | break; 126 | default: 127 | throw new Error("Oops!"); 128 | } 129 | 130 | piece.x = Math.floor((10 - piece.blocks[0].length) / 2); 131 | piece.y = -1; 132 | 133 | return piece; 134 | } 135 | 136 | movePieceLeft() { 137 | this.activePiece.x -= 1; 138 | if (this.hasCollision()) { 139 | this.activePiece.x += 1; 140 | } 141 | } 142 | 143 | movePieceRigth() { 144 | this.activePiece.x += 1; 145 | if (this.hasCollision()) { 146 | this.activePiece.x -= 1; 147 | } 148 | } 149 | 150 | movePieceDown() { 151 | if (this.topOut) return; 152 | 153 | this.activePiece.y += 1; 154 | 155 | if (this.hasCollision()) { 156 | this.activePiece.y -= 1; 157 | this.lockPiece(); 158 | const clearedLines = this.clearLines(); 159 | this.updateScore(clearedLines); 160 | this.updatePieces(); 161 | } 162 | 163 | if (this.hasCollision()) { 164 | this.topOut = true; 165 | } 166 | } 167 | 168 | rotatePiece() { 169 | this.rotateBlocks(); 170 | 171 | if (this.hasCollision()) { 172 | this.rotateBlocks(false); 173 | } 174 | } 175 | 176 | rotateBlocks(clockwise = true) { 177 | const blocks = this.activePiece.blocks; 178 | const length = blocks.length; 179 | const x = Math.floor(length / 2); 180 | const y = length - 1; 181 | 182 | for (let i = 0; i < x; i++) { 183 | for (let j = i; j < y - i; j++) { 184 | const temp = blocks[i][j]; 185 | 186 | if (clockwise) { 187 | blocks[i][j] = blocks[y - j][i]; 188 | blocks[y - j][i] = blocks[y - i][y - j]; 189 | blocks[y - i][y - j] = blocks[j][y - i]; 190 | blocks[j][y - i] = temp; 191 | } else { 192 | blocks[i][j] = blocks[j][y - i]; 193 | blocks[j][y - i] = blocks[y - i][y - j]; 194 | blocks[y - i][y - j] = blocks[y - j][i]; 195 | blocks[y - j][i] = temp; 196 | } 197 | } 198 | } 199 | } 200 | 201 | hasCollision() { 202 | const { y: pieceY, x: pieceX, blocks } = this.activePiece; 203 | for (let y = 0; y < blocks.length; y++) { 204 | for (let x = 0; x < blocks[y].length; x++) { 205 | if ( 206 | blocks[y][x] && 207 | (this.playfield[pieceY + y] === undefined || 208 | this.playfield[pieceY + y][pieceX + x] === undefined || 209 | this.playfield[pieceY + y][pieceX + x]) 210 | ) { 211 | return true; 212 | } 213 | } 214 | } 215 | 216 | return false; 217 | } 218 | 219 | lockPiece() { 220 | const { y: pieceY, x: pieceX, blocks } = this.activePiece; 221 | for (let y = 0; y < blocks.length; y++) { 222 | for (let x = 0; x < blocks[y].length; x++) { 223 | if (blocks[y][x]) { 224 | this.playfield[pieceY + y][pieceX + x] = blocks[y][x]; 225 | } 226 | } 227 | } 228 | } 229 | 230 | clearLines() { 231 | const rows = 20; 232 | const columns = 10; 233 | let lines = []; 234 | 235 | for (let y = rows - 1; y >= 0; y--) { 236 | let numberOfBlocks = 0; 237 | 238 | for (let x = 0; x < columns; x++) { 239 | if (this.playfield[y][x]) { 240 | numberOfBlocks += 1; 241 | } 242 | } 243 | 244 | if (numberOfBlocks === 0) { 245 | break; 246 | } else if (numberOfBlocks < columns) { 247 | continue; 248 | } else if (numberOfBlocks === columns) { 249 | lines.unshift(y); 250 | } 251 | } 252 | 253 | for (let index of lines) { 254 | this.playfield.splice(index, 1); 255 | this.playfield.unshift(new Array(columns).fill(0)); 256 | } 257 | 258 | return lines.length; 259 | } 260 | 261 | updateScore(clearedLines) { 262 | if (clearedLines > 0) { 263 | this.score += Game.points[clearedLines] * (this.level + 1); 264 | this.lines += clearedLines; 265 | } 266 | } 267 | 268 | updatePieces() { 269 | this.activePiece = this.nextPiece; 270 | this.nextPiece = this.createPiece(); 271 | } 272 | } 273 | 274 | class View { 275 | static colors = { 276 | 1: "#0000ff", 277 | 2: "#00ff00", 278 | 3: "#ff0000", 279 | 4: "#ff8000", 280 | 5: "#ff00ff", 281 | 6: "#ffff00", 282 | 7: "#00ffff", 283 | }; 284 | 285 | constructor(element, width, height, rows, columns) { 286 | this.element = element; 287 | this.width = width; 288 | this.height = height; 289 | 290 | this.canvas = document.createElement("canvas"); 291 | this.canvas.width = this.width; 292 | this.canvas.height = this.height; 293 | this.context = this.canvas.getContext("2d"); 294 | 295 | this.playfieldBorderWidth = 1; 296 | this.playfieldX = this.playfieldBorderWidth; 297 | this.playfieldY = this.playfieldBorderWidth; 298 | this.playfieldWidth = (this.width * 2) / 3; 299 | this.playfieldHeight = this.height; 300 | this.playfieldInnerWidth = 301 | this.playfieldWidth - this.playfieldBorderWidth * 2; 302 | this.playfieldInnerHeight = 303 | this.playfieldHeight - this.playfieldBorderWidth * 2; 304 | 305 | this.blockWidth = this.playfieldInnerWidth / columns; 306 | this.blockHeight = this.playfieldInnerHeight / rows; 307 | 308 | this.panelX = this.playfieldWidth + 10; 309 | this.panelY = 0; 310 | this.panelWidth = this.width / 3; 311 | this.panelHeight = this.height; 312 | 313 | this.element.appendChild(this.canvas); 314 | } 315 | 316 | renderMainScreen(state) { 317 | this.clearScreen(); 318 | this.renderPlayfield(state); 319 | this.renderPanel(state); 320 | } 321 | 322 | renderStartScreen() { 323 | this.context.fillStyle = "#fff"; 324 | this.context.font = "18px 'Courier New'"; 325 | this.context.textAlign = "center"; 326 | this.context.textBaseline = "middle"; 327 | this.context.fillText( 328 | "Press Enter to Start", 329 | this.width / 3, 330 | this.height / 2 331 | ); 332 | } 333 | 334 | renderPauseScreen() { 335 | this.context.fillStyle = "rgba(0, 0, 0, 0.8)"; 336 | this.context.fillRect(0, 0, this.width, this.height); 337 | 338 | this.context.fillStyle = "#fff"; 339 | this.context.font = "18px 'Courier New'"; 340 | this.context.textAlign = "center"; 341 | this.context.textBaseline = "middle"; 342 | this.context.fillText( 343 | "Press Enter to Resume", 344 | this.width / 3, 345 | this.height / 2 346 | ); 347 | } 348 | 349 | renderEndScreen({ score }) { 350 | this.clearScreen(); 351 | 352 | this.context.fillStyle = "#fff"; 353 | this.context.font = "18px 'Courier New'"; 354 | this.context.textAlign = "center"; 355 | this.context.textBaseline = "middle"; 356 | this.context.fillText("GAME OVER", this.width / 3, this.height / 2 - 48); 357 | this.context.fillText(`Score: ${score}`, this.width / 3, this.height / 2); 358 | this.context.fillText( 359 | "Press Enter to Restart", 360 | this.width / 3, 361 | this.height / 2 + 48 362 | ); 363 | } 364 | 365 | clearScreen() { 366 | this.context.clearRect(0, 0, this.width, this.height); 367 | } 368 | 369 | renderPlayfield({ playfield }) { 370 | for (let y = 0; y < playfield.length; y++) { 371 | for (let x = 0; x < playfield[y].length; x++) { 372 | const block = playfield[y][x]; 373 | 374 | if (block) { 375 | this.renderBlock( 376 | this.playfieldX + x * this.blockWidth, 377 | this.playfieldY + y * this.blockHeight, 378 | this.blockWidth, 379 | this.blockHeight, 380 | View.colors[block] 381 | ); 382 | } 383 | } 384 | } 385 | 386 | this.context.strokeStyle = "#fff"; 387 | this.context.lineWidth = this.playfieldBorderWidth; 388 | this.context.strokeRect(0, 0, this.playfieldWidth, this.playfieldHeight); 389 | } 390 | 391 | renderPanel({ level, score, lines, nextPiece }) { 392 | this.context.textAlign = "start"; 393 | this.context.textBaseline = "top"; 394 | this.context.fillStyle = "#fff"; 395 | this.context.font = "14px 'Courier New'"; 396 | 397 | this.context.fillText(`Score: ${score}`, this.panelX, this.panelY + 0); 398 | this.context.fillText(`Lines: ${lines}`, this.panelX, this.panelY + 24); 399 | this.context.fillText(`Level: ${level}`, this.panelX, this.panelY + 48); 400 | this.context.fillText("Next:", this.panelX, this.panelY + 96); 401 | 402 | for (let y = 0; y < nextPiece.blocks.length; y++) { 403 | for (let x = 0; x < nextPiece.blocks[y].length; x++) { 404 | const block = nextPiece.blocks[y][x]; 405 | 406 | if (block) { 407 | this.renderBlock( 408 | this.panelX + x * this.blockWidth * 0.5, 409 | this.panelY + 100 + y * this.blockHeight * 0.5, 410 | this.blockWidth * 0.5, 411 | this.blockHeight * 0.5, 412 | View.colors[block] 413 | ); 414 | } 415 | } 416 | } 417 | } 418 | 419 | renderBlock(x, y, width, height, color) { 420 | this.context.fillStyle = color; 421 | this.context.strokeStyle = "#000"; 422 | this.context.lineWidth = 2; 423 | 424 | this.context.fillRect(x, y, width, height); 425 | this.context.strokeRect(x, y, width, height); 426 | } 427 | } 428 | 429 | class Controller { 430 | constructor(game, view) { 431 | this.game = game; 432 | this.view = view; 433 | this.intervalId = null; 434 | this.isPlaying = false; 435 | 436 | document.addEventListener("keydown", this.handleKeyDown.bind(this)); 437 | document.addEventListener("keyup", this.handleKeyUp.bind(this)); 438 | this.view.renderStartScreen(); 439 | } 440 | 441 | update() { 442 | this.game.movePieceDown(); 443 | this.updateView(); 444 | } 445 | 446 | play() { 447 | this.isPlaying = true; 448 | this.startTimer(); 449 | this.updateView; 450 | } 451 | 452 | pause() { 453 | this.isPlaying = false; 454 | this.stopTimer(); 455 | this.updateView(); 456 | } 457 | 458 | reset() { 459 | this.game.reset(); 460 | this.play(); 461 | } 462 | 463 | updateView() { 464 | const state = this.game.getState(); 465 | 466 | if (state.isGameOver) { 467 | this.view.renderEndScreen(state); 468 | } else if (!this.isPlaying) { 469 | this.view.renderPauseScreen(); 470 | } else { 471 | this.view.renderMainScreen(state); 472 | } 473 | } 474 | 475 | startTimer() { 476 | const speed = 1000 - this.game.getState().level * 100; 477 | if (!this.intervalId) { 478 | this.intervalId = setInterval( 479 | () => { 480 | this.update(); 481 | }, 482 | speed > 0 ? speed : 100 483 | ); 484 | } 485 | } 486 | 487 | stopTimer() { 488 | if (this.intervalId) { 489 | clearInterval(this.intervalId); 490 | this.intervalId = null; 491 | } 492 | } 493 | 494 | handleKeyDown(event) { 495 | const state = this.game.getState(); 496 | switch (event.keyCode) { 497 | case 32: 498 | if (state.isGameOver) { 499 | this.reset(); 500 | } else if (this.isPlaying) { 501 | this.pause(); 502 | } else { 503 | this.play(); 504 | } 505 | break; 506 | case 13: 507 | if (state.isGameOver) { 508 | this.reset(); 509 | } else if (this.isPlaying) { 510 | this.pause(); 511 | } else { 512 | this.play(); 513 | } 514 | break; 515 | case 37: 516 | this.game.movePieceLeft(); 517 | this.updateView(); 518 | break; 519 | case 38: 520 | this.game.rotatePiece(); 521 | this.updateView(); 522 | break; 523 | case 39: 524 | this.game.movePieceRigth(); 525 | this.updateView(); 526 | break; 527 | case 40: 528 | this.stopTimer(); 529 | this.game.movePieceDown(); 530 | this.updateView(); 531 | break; 532 | } 533 | } 534 | 535 | handleKeyUp(event) { 536 | switch (event.keyCode) { 537 | case 40: 538 | this.startTimer(); 539 | break; 540 | } 541 | } 542 | } 543 | 544 | const game = new Game(); 545 | const view = new View(root, 480, 640, 20, 10); 546 | const controller = new Controller(game, view); 547 | --------------------------------------------------------------------------------