├── README.md ├── tetris.html └── tetris.js /README.md: -------------------------------------------------------------------------------- 1 | # HTML5 Tetris 2 | 3 | Has the world ever seen such a thing? [**Tetris in HTML5!!!**](https://raw.org/demo/html5-tetris-with-ai/) Admittedly, tetris isn't as cool as [2048](http://gabrielecirulli.github.io/2048/) nowadays. However, tetris has some very interesting problems to study. More can be found [here](http://www.colinfahey.com/tetris/tetris.html). 4 | 5 | My focus was on implementing *optimal* algorithms, an Tetris AI and playing around with some new web-technologies. The features of Tetris include: 6 | 7 | * Customizable Tetris Tiles/Board 8 | * A Tetris AI 9 | * Gamepad API 10 | * Devicemotion API 11 | * Animated Favicon 12 | 13 | The Game 14 | --- 15 | The colors of the game board are a little orientated on n-blox - I hope you can forgive me, but I really like them. 16 | 17 | You can participate in the highscore, hosted on my site as long as you don't change the tiles and the view of the board (and haven't used the AI ;-) ). 18 | 19 | ![](http://oi61.tinypic.com/ev88kh.jpg =350x) 20 | 21 | 22 | Tetris AI 23 | --- 24 | Just press **a** on your keyboard and enjoy the screensaver-esque thing. After increasing the speed, it's much more fun. 25 | 26 | Gamepad API 27 | --- 28 | You got a PS3 or XBOX? Connect the controller to your computer and try it in your favorite browser. There is an [experimental API](http://www.w3.org/TR/2014/WD-gamepad-20140225/) for game controllers in modern browsers, which works pretty well after some hacking. Just activate it in the Tetris edit menu and enjoy - I didn't play tetris on a gamepad as well, but it's cool. 29 | 30 | 31 | Tetris Tiles 32 | --- 33 | It's possible to customize the tetris tiles - and to send the custom tetris game to your friend via the edit menu on the right hand side. The URL is changed and can be copied/pasted. 34 | 35 | ![](http://oi57.tinypic.com/k4tnoy.jpg =350x) 36 | 37 | The [Hacker Emblem](http://www.catb.org/hacker-emblem/) was added to make it a little more difficult for the AI and also for you, if you want to participate in the highscore table. 38 | 39 | -------------------------------------------------------------------------------- /tetris.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HTML5 Tetris 5 | 6 | 7 | 8 | 9 | 10 | {literal} 11 | 201 | {/literal} 202 | 203 | 204 | 205 |

HTML5 TETRIS

206 | 207 | 208 | 209 | 210 | 211 |
212 |
Edit
213 | 214 |
X Tiles
215 |
Tile-Size
216 |
Border
217 |
Speed Delay
218 | 219 |
Pause (p)
220 |
Autopilot (a)
221 |
Shadow (s)
222 |
Preview (w)
223 |
Favicon (f)
224 |
Gamepad
225 | 226 |
Active shadow and preview reduces the score per tile!
227 | 228 |
229 | 230 |
231 | 232 | 238 | 239 |
240 | 241 |
242 |
Score
243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | {local:$i} 253 | {php}$i=0;{/php} 254 | {fores $score as $s} 255 | 256 | 257 | 258 | 259 | 260 | 261 | {/fores} 262 |
RankNameScoreLines
#{!++$i}{$s.TName}{!$s.TScore}{!$s.TLine}
263 | 264 |
265 | 266 |
267 | 268 |
Enter your name
269 | 270 | 271 | 272 | go 273 | 274 |
275 | 276 |
277 | Click to restart 278 |
279 | 280 |
281 | 282 |
283 | Highscore participation 284 |
285 |
286 | Score: 0
287 | Lines: 0
288 |
289 | 290 | 291 | by Robert Eisele (Twitter)
Source & Description (Github)
292 | 293 |
294 | 295 | 296 | 297 | 298 | xarg open projects 299 | 300 | 301 | 302 | -------------------------------------------------------------------------------- /tetris.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Tetris v1.0.0 08/04/2014 3 | * http://www.xarg.org/project/tetris/ 4 | * 5 | * Copyright (c) 2014, Robert Eisele (robert@xarg.org) 6 | * Dual licensed under the MIT or GPL Version 2 licenses. 7 | **/ 8 | 9 | (function(window) { 10 | 11 | var document = window['document']; 12 | var location = window['location']; 13 | var navigator = window['navigator']; 14 | 15 | var canvas = document.getElementById('canvas'); 16 | var preview = document.getElementById('preview'); 17 | 18 | var favicon = document.getElementById('favicon'); 19 | var fav = document.getElementById('fav'); 20 | 21 | var divBest = document.getElementById('best'); 22 | var divEdit = document.getElementById('edit'); 23 | var divOpen = document.getElementById('open'); 24 | var divOpenScore = document.getElementById('open2'); 25 | 26 | var divScore = document.getElementById('score'); 27 | var divLines = document.getElementById('lines'); 28 | 29 | var divTables = document.getElementById('tables'); 30 | 31 | var highscore = document.getElementById('highscore'); 32 | var submit = document.getElementById('submit'); 33 | var nick = document.getElementById('nick'); 34 | 35 | var sFB = document.getElementById('sFB'); 36 | var sTW = document.getElementById('sTW'); 37 | var sGP = document.getElementById('sGP'); 38 | 39 | var ctx = canvas.getContext('2d'); 40 | var ptx = preview.getContext('2d'); 41 | var ftx = favicon.getContext('2d'); 42 | 43 | var originalFavicon = fav['href']; 44 | 45 | 46 | /** 47 | * Game Speed 48 | * 49 | * @type number 50 | */ 51 | var speed = 200; 52 | 53 | 54 | /** 55 | * Score for current speed 56 | * 57 | * @type number 58 | */ 59 | var speedScore = 5; 60 | 61 | /** 62 | * Somehow cheated? Entering the highscore isn't possible anymore 63 | * 64 | * @type {boolean} 65 | */ 66 | var expelled = false; 67 | 68 | 69 | /** 70 | * Game score 71 | * 72 | * @type number 73 | */ 74 | var score = 0; 75 | 76 | /** 77 | * Number of lines cleared 78 | * 79 | * @type number 80 | */ 81 | var clearedLines = 0; 82 | 83 | /** 84 | * Tile border width 85 | * 86 | * @type number 87 | */ 88 | var tileBorder = 2; 89 | 90 | /** 91 | * Number of tiles on the board in X direction 92 | * 93 | * @type number 94 | */ 95 | var tilesX = 21; 96 | 97 | /** 98 | * Number of tiles on the board in Y direction 99 | * 100 | * @type number 101 | */ 102 | var tilesY = 35; 103 | 104 | /** 105 | * The inner tile size - border exclusive 106 | * 107 | * @type number 108 | */ 109 | var tileSize = 16; 110 | 111 | 112 | /** 113 | * Game status types, enum doesn't fold properly :/ 114 | */ 115 | /** 116 | * 117 | * @type {number} 118 | * @const 119 | */ 120 | var STATUS_INIT = 0; 121 | /** 122 | * @type {number} 123 | * @const 124 | */ 125 | var STATUS_PLAY = 1; 126 | /** 127 | * @type {number} 128 | * @const 129 | */ 130 | var STATUS_PAUSE = 2; 131 | /** 132 | * @type {number} 133 | * @const 134 | */ 135 | var STATUS_GAMEOVER = 3; 136 | /** 137 | * @type {number} 138 | * @const 139 | */ 140 | var STATUS_WAIT = 4; 141 | 142 | /** 143 | * The actual game status 144 | * 145 | * @type number 146 | */ 147 | var gameStatus = STATUS_INIT; 148 | 149 | /** 150 | * Has the window lost the foucs? 151 | * 152 | * @type boolean 153 | */ 154 | var leftWindow = false; 155 | 156 | /** 157 | * Is the AI playing? 158 | * 159 | * @type boolean 160 | */ 161 | var autoMode = false; 162 | 163 | /** 164 | * Is the the helping shadow visible? 165 | * 166 | * @type boolean 167 | */ 168 | var showShadow = true; 169 | 170 | /** 171 | * Is the favicon animated? 172 | * 173 | * @type boolean 174 | */ 175 | var showFavicon = !!favicon.toDataURL; 176 | 177 | 178 | /** 179 | * Is the preview box visible? 180 | * 181 | * @type boolean 182 | */ 183 | var showPreview = true; 184 | 185 | /** 186 | * The actual game board to work on (a Y/X matrix) 187 | * 188 | * @type Array 189 | */ 190 | var board; 191 | 192 | /** 193 | * The highest Y positions of all columns 194 | * 195 | * @type Array 196 | */ 197 | var topY; 198 | 199 | /** 200 | * The actual piece X position 201 | * 202 | * @type number 203 | */ 204 | var curX; 205 | 206 | /** 207 | * The actual piece Y position 208 | * @type number 209 | */ 210 | var curY; 211 | 212 | /** 213 | * Game piece description, enum doesn't fold properly :/ 214 | */ 215 | 216 | /** 217 | * @type {number} 218 | * @const 219 | */ 220 | var PIECE_PROBABILITY = 0; 221 | /** 222 | * @type {number} 223 | * @const 224 | */ 225 | var PIECE_ROTATABLE = 1; 226 | /** 227 | * @type {number} 228 | * @const 229 | */ 230 | var PIECE_COLOR = 2; 231 | /** 232 | * @type {number} 233 | * @const 234 | */ 235 | var PIECE_SHAPE = 3; 236 | 237 | 238 | /** 239 | * The actual piece direction 240 | * @type number 241 | */ 242 | var direction = PIECE_SHAPE; 243 | 244 | 245 | /** 246 | * Is Edit menu currently closed? 247 | * 248 | * @type boolean 249 | */ 250 | var menuOpen = false; 251 | 252 | 253 | /** 254 | * 255 | * @type number 256 | */ 257 | var pixelRatio = window['devicePixelRatio'] || 1; 258 | 259 | 260 | /** 261 | * All available piece definitions, see PIECE enum 262 | * @type Array 263 | */ 264 | var pieces = [ 265 | [ 266 | 1.0, // probability 267 | 1, // rotatable 268 | [202, 81, 249], // pink 269 | [0, -1, -1, 0, 0, 0, 1, 0] 270 | ], [ 271 | 1.0, // probability 272 | 1, // rotatable 273 | [255, 102, 0], // orange 274 | [0, -1, 0, 0, 0, 1, 1, 1] 275 | ], [ 276 | 1.0, // probability 277 | 1, // rotatable 278 | [0, 255, 0], // green 279 | [0, -1, 0, 0, -1, 0, 1, -1] 280 | ], [ 281 | 1.0, // probability 282 | 1, // rotatable 283 | [255, 0, 0], // red 284 | [0, -1, 0, 0, -1, 0, -1, 1] 285 | ], [ 286 | 1.0, // probability 287 | 1, // rotatable 288 | [102, 204, 255], // light blue 289 | [-1, 0, 0, 0, 1, 0, 2, 0] 290 | ], [ 291 | 0.2, // probability 292 | 1, // rotatable 293 | [255, 255, 255], // white - the haxx0r one 294 | [-1, 1, 0, 1, 1, 1, 1, 0, 0, -1] 295 | ], [ 296 | 1.0, // probability 297 | 1, // rotatable 298 | [0, 0, 255], // blue 299 | [-1, -1, -1, 0, 0, 0, 1, 0] 300 | ], [ 301 | 0.8, // probability 302 | 0, // rotatable 303 | [255, 255, 0], // yellow 304 | [0, 0, 1, 0, 1, 1, 0, 1] 305 | ]/*, [ 306 | 0.1, // probability 307 | 0, // rotatable 308 | [255, 0, 0], // red 309 | [ 310 | -2, -6, 311 | -1, -6, 312 | 0, -6, 313 | 1, -6, 314 | 2, -6, 315 | -2, -5, 316 | -1, -5, 317 | 0, -5, 318 | 1, -5, 319 | 2, -5, 320 | -2, -4, 321 | 0, -4, 322 | 2, -4, 323 | -2, -3, 324 | -1, -3, 325 | 0, -3, 326 | 1, -3, 327 | 2, -3, 328 | -2, -2, 329 | -1, -2, 330 | 0, -2, 331 | 1, -2, 332 | 2, -2, 333 | -2, -1, 334 | -1, -1, 335 | 0, -1, 336 | 1, -1, 337 | 2, -1, 338 | -2, 0, 339 | -1, 0, 340 | 0, 0, 341 | 1, 0, 342 | 2, 0, 343 | -2, 1, 344 | -1, 1, 345 | 0, 1, 346 | 1, 1, 347 | 2, 1, 348 | -2, 2, 349 | -1, 2, 350 | 0, 2, 351 | 1, 2, 352 | 2, 2, 353 | -2, 3, 354 | -1, 3, 355 | 0, 3, 356 | 1, 3, 357 | 2, 3, 358 | -2, 4, 359 | -1, 4, 360 | 0, 4, 361 | 1, 4, 362 | 2, 4, 363 | -2, 5, 364 | -1, 5, 365 | 0, 5, 366 | 1, 5, 367 | 2, 5, 368 | -2, -7, 369 | -2, -8, 370 | 2, -7, 371 | 2, -8, 372 | -3, -8, 373 | 3, -8, 374 | -4, -8, 375 | 4, -8, 376 | -5, -8, 377 | 5, -8, 378 | -6, -8, 379 | 6, -8, 380 | -4, -9, 381 | 4, -9, 382 | -5, -9, 383 | 5, -9, 384 | -4, -7, 385 | 4, -7, 386 | -5, -7, 387 | 5, -7, 388 | 3, -1, 389 | 3, 0, 390 | 3, 1, 391 | 3, 2, 392 | 3, 3, 393 | 3, 4, 394 | 3, 5, 395 | 3, 6, 396 | 3, 7, 397 | -2, 6, 398 | 2, 6, 399 | -3, -1, 400 | -3, 0, 401 | -3, 1, 402 | -3, 2, 403 | -3, 3, 404 | -3, 4, 405 | -3, 5, 406 | -3, 6, 407 | -3, 7, 408 | 4, 1, 409 | 4, 2, 410 | 4, 3, 411 | 4, 4, 412 | 4, 5, 413 | -4, 1, 414 | -4, 2, 415 | -4, 3, 416 | -4, 4, 417 | -4, 5, 418 | 5, 0, 419 | 5, 1, 420 | 5, 2, 421 | -5, 0, 422 | -5, 1, 423 | -5, 2, 424 | 5, 4, 425 | 5, 5, 426 | -5, 4, 427 | -5, 5, 428 | 6, 4, 429 | 7, 4, 430 | 6, 5, 431 | 7, 5, 432 | -6, 4, 433 | -7, 4, 434 | -6, 5, 435 | -7, 5, 436 | 7, 2, 437 | 7, 3, 438 | -7, 3, 439 | -7, 2, 440 | 8, 1, 441 | 8, 2, 442 | -8, 1, 443 | -8, 2, 444 | 9, 0, 445 | 9, 1, 446 | -9, 0, 447 | -9, 1, 448 | 10, 0, 449 | -10, 0, 450 | 4, 7, 451 | 4, 8, 452 | -4, 7, 453 | -4, 8, 454 | -5, 8, 455 | 5, 8, 456 | -6, 8, 457 | 6, 8, 458 | -6, 7, 459 | 6, 7 460 | 461 | ] 462 | ]*/ 463 | ]; 464 | 465 | /** 466 | * The fastest timer we can get 467 | * 468 | * @type {Function} 469 | */ 470 | var NOW; 471 | 472 | /** 473 | * The time when a game started 474 | * 475 | * @type {Date} 476 | */ 477 | var startTime = new Date; 478 | 479 | /** 480 | * The current piece flying around 481 | * 482 | * @type Array 483 | */ 484 | var curPiece; 485 | 486 | /** 487 | * The next piece in the queue 488 | * 489 | * @type Array 490 | */ 491 | var nextPiece; 492 | 493 | /** 494 | * The timeout ID of the running game 495 | * 496 | * @type number 497 | */ 498 | var loopTimeout; 499 | 500 | /** 501 | * The time of the flash effect in ms 502 | * @type number 503 | * @const 504 | */ 505 | var flashTime = 350; 506 | 507 | 508 | /** 509 | * Generates a rotated version of the piece 510 | * 511 | * @param {Array} form the original piece 512 | * @returns {Array} The rotated piece 513 | */ 514 | var getRotatedPiece = function(form) { 515 | 516 | var newForm = new Array(form.length); 517 | for (var i = 0; i < newForm.length; i+= 2) { 518 | newForm[i] = -form[i + 1]; 519 | newForm[i + 1] = form[i]; 520 | } 521 | return newForm; 522 | }; 523 | 524 | 525 | /** 526 | * Get a new weighted random piece 527 | * 528 | * @returns {Array} 529 | */ 530 | var getNextPiece = function() { 531 | 532 | var rnd = Math.random(); 533 | for (var i = pieces.length; i--; ) { 534 | if (rnd < pieces[i][PIECE_PROBABILITY]) 535 | return pieces[i]; 536 | rnd-= pieces[i][PIECE_PROBABILITY]; 537 | } 538 | return pieces[0]; 539 | }; 540 | 541 | 542 | /** 543 | * Take the next piece 544 | */ 545 | var newPiece = function() { 546 | 547 | curPiece = nextPiece; 548 | nextPiece = getNextPiece(); 549 | 550 | calcInitCoord(); 551 | 552 | updatePreview(); 553 | }; 554 | 555 | 556 | /** 557 | * Calculate the initial coordinate of a new piece 558 | */ 559 | var calcInitCoord = function() { 560 | 561 | var minY = -10; 562 | 563 | var cur = curPiece[direction]; 564 | 565 | direction = PIECE_SHAPE + Math.random() * 4 | 0; 566 | 567 | for (var i = 0; i < cur.length; i+= 2) { 568 | 569 | minY = Math.max(minY, cur[i + 1]); 570 | } 571 | curX = tilesX >> 1; 572 | curY = -minY; 573 | }; 574 | 575 | 576 | /** 577 | * Take the URL hash and set the initial settings 578 | */ 579 | var prepareUrlHash = function(hash) { 580 | 581 | if (!hash) { 582 | return; 583 | } 584 | 585 | try { 586 | hash = JSON.parse(window['atob'](hash.slice(1))); 587 | } catch (e) { 588 | return; 589 | } 590 | 591 | // No highscore participation 592 | setExpelled(true); 593 | 594 | if (hash['P']) { 595 | pieces = hash['P']; 596 | } 597 | 598 | if (hash['X']) { 599 | tilesX = hash['X']; 600 | } 601 | 602 | if (hash['Y']) { 603 | tilesY = hash['Y']; 604 | } 605 | 606 | if (hash['S']) { 607 | tileSize = hash['S']; 608 | } 609 | 610 | if (hash['B']) { 611 | tileBorder = hash['B']; 612 | } 613 | 614 | if (hash['Q']) { 615 | speed = hash['Q']; 616 | } 617 | }; 618 | 619 | 620 | /** 621 | * Try if a move vertical move is valid 622 | * 623 | * @param {number} newY The new Y position to try 624 | * @returns {boolean} Indicator if it's possible to move 625 | */ 626 | var tryDown = function(newY) { 627 | 628 | var cur = curPiece[direction]; 629 | 630 | for (var i = 0; i < cur.length; i+= 2) { 631 | 632 | var x = cur[i] + curX; 633 | var y = cur[i + 1] + newY; 634 | 635 | if (y >= tilesY || board[y] !== undefined && board[y][x] !== undefined) { 636 | return false; 637 | } 638 | } 639 | curY = newY; 640 | return true; 641 | }; 642 | 643 | 644 | /** 645 | * Try if a horizontal move is valid 646 | * 647 | * @param {number} newX The new X position to try 648 | * @param {number} dir The direction to try 649 | * @returns {boolean} Indicator if it's possible to move 650 | */ 651 | var tryMove = function(newX, dir) { 652 | 653 | var cur = curPiece[dir]; 654 | 655 | for (var i = 0; i < cur.length; i+= 2) { 656 | 657 | var x = cur[i] + newX; 658 | var y = cur[i + 1] + curY; 659 | 660 | if (x < 0 || x >= tilesX || y >= 0 && board[y][x] !== undefined) { 661 | return false; 662 | } 663 | } 664 | curX = newX; 665 | direction = dir; 666 | return true; 667 | }; 668 | 669 | 670 | /** 671 | * Integrate the current piece into the board 672 | */ 673 | var integratePiece = function() { 674 | 675 | var cur = curPiece[direction]; 676 | 677 | for (var i = 0; i < cur.length; i+= 2) { 678 | 679 | // Check for game over 680 | if (cur[i + 1] + curY <= 0) { 681 | gameOver(); 682 | break; 683 | } else { 684 | board[cur[i + 1] + curY][cur[i] + curX] = curPiece[PIECE_COLOR]; 685 | topY[cur[i] + curX] = Math.min(topY[cur[i] + curX], cur[i + 1] + curY); 686 | } 687 | } 688 | 689 | if (gameStatus === STATUS_GAMEOVER) { 690 | pauseLoop(); 691 | } else { 692 | checkFullLines(); 693 | } 694 | 695 | updateScore(speedScore); 696 | }; 697 | 698 | 699 | /** 700 | * Show the game over overlay 701 | */ 702 | var gameOver = function() { 703 | 704 | gameStatus = STATUS_GAMEOVER; 705 | 706 | if (expelled) { 707 | 708 | } else { 709 | highscore.style.display = 'block'; 710 | nick.focus(); 711 | } 712 | }; 713 | 714 | 715 | /** 716 | * Ultimately remove lines from the board 717 | * 718 | * @param {Array} remove A stack of lines to be removed 719 | */ 720 | var removeLines = function(remove) { 721 | 722 | var rp = remove.length - 1; 723 | var wp = remove[rp--]; 724 | var mp = wp - 1; 725 | 726 | for (; mp >= 0; mp--) { 727 | 728 | if (rp >= 0 && remove[rp] === mp) { 729 | rp--; 730 | } else { 731 | board[wp--] = board[mp]; 732 | } 733 | } 734 | 735 | while (wp >= 0) { 736 | board[wp--] = new Array(tilesX); 737 | } 738 | 739 | for (mp = tilesX; mp--; ) { 740 | 741 | topY[mp]+= remove.length; 742 | 743 | // It's not possible to simply add remove.length, because you can clear lines in arbitrary order 744 | while (topY[mp] < tilesY && board[topY[mp]][mp] === undefined) { 745 | topY[mp]++; 746 | } 747 | } 748 | 749 | // Calculate line scoring 750 | clearedLines+= remove.length; 751 | updateScore(remove.length * 20); 752 | }; 753 | 754 | 755 | /** 756 | * Check for full lines and drop them using removeLines() 757 | */ 758 | var checkFullLines = function() { 759 | 760 | var flashColor = ['#fff', '#fff', '#fff']; 761 | 762 | var remove = []; 763 | 764 | for (var x, y = 0; y < tilesY; y++) { 765 | 766 | for (x = tilesX; x--; ) { 767 | 768 | if (board[y][x] === undefined) { 769 | break; 770 | } 771 | } 772 | 773 | if (x < 0) { 774 | remove.push(y); 775 | } 776 | } 777 | 778 | if (remove.length > 0) { 779 | 780 | if (flashTime > 0) { 781 | 782 | gameStatus = STATUS_WAIT; 783 | pauseLoop(); 784 | 785 | animate(flashTime, function(pos) { 786 | 787 | var cond = pos * 10 & 1; 788 | 789 | // Simply paint a flash effect over the current tiles 790 | for (var i = 0; i < remove.length; i++) { 791 | 792 | for (var x = tilesX; x--; ) { 793 | 794 | if (cond) { 795 | drawTile(ctx, x, remove[i], flashColor); 796 | } else if (board[remove[i]][x] !== undefined) { 797 | drawTile(ctx, x, remove[i], board[remove[i]][x]); 798 | } 799 | } 800 | } 801 | 802 | }, function() { 803 | 804 | removeLines(remove); 805 | 806 | newPiece(); 807 | 808 | draw(); 809 | gameStatus = STATUS_PLAY; 810 | loop(); 811 | 812 | }, flashTime / 10); 813 | 814 | } else { 815 | 816 | removeLines(remove); 817 | 818 | newPiece(); 819 | 820 | draw(); 821 | } 822 | 823 | } else { 824 | newPiece(); 825 | } 826 | }; 827 | 828 | 829 | /** 830 | * The main loop of the game 831 | */ 832 | var loop = function() { 833 | 834 | // If AI 835 | if (autoMode) { 836 | 837 | if (findOptimalSpot()) { 838 | integratePiece(); 839 | } 840 | 841 | } else if (!tryDown(curY + 1)) { 842 | integratePiece(); 843 | } 844 | 845 | draw(); 846 | 847 | // AI or normal game 848 | if (gameStatus === STATUS_PLAY) { 849 | loopTimeout = window.setTimeout(loop, speed); 850 | } 851 | }; 852 | 853 | 854 | /** 855 | * Pause the main loop 856 | */ 857 | var pauseLoop = function() { 858 | 859 | window.clearTimeout(loopTimeout); 860 | }; 861 | 862 | 863 | /** 864 | * Update the score 865 | * 866 | * @param {number} n The number of points to add to the actual score 867 | */ 868 | var updateScore = function(n) { 869 | 870 | score+= n; 871 | 872 | divScore.innerHTML = score; 873 | divLines.innerHTML = clearedLines; 874 | }; 875 | 876 | 877 | /** 878 | * Find the optimal spot of a tile 879 | * 880 | * @returns {boolean} Indicator if we found the spot already (false to indicate a small step) 881 | */ 882 | var findOptimalSpot = function() { 883 | 884 | /** 885 | * @type number 886 | */ 887 | var minCost = 100; 888 | 889 | /** 890 | * @type number 891 | */ 892 | var minDir; 893 | 894 | /** 895 | * @type number 896 | */ 897 | var minX; 898 | 899 | for (var o = PIECE_SHAPE; o < PIECE_SHAPE + 4; o++) { 900 | 901 | for (var x = tilesX; x--; ) { 902 | 903 | if (tryMove(x, o)) { 904 | 905 | var cost = calcCost(x, o); 906 | 907 | if (cost < minCost) { 908 | minCost = cost; 909 | minDir = o; 910 | minX = x; 911 | } 912 | } 913 | } 914 | 915 | } 916 | 917 | curX = minX; 918 | direction = minDir; 919 | 920 | while (tryDown(curY + 1)) {} 921 | 922 | return true; 923 | }; 924 | 925 | 926 | /** 927 | * Calculate the cost to set the new element at the curX and rotation position 928 | * 929 | * @param {number} curX The position to be checked 930 | * @param {number} rotation The rotation to be checked 931 | * @returns {number} The actual cost of the position 932 | */ 933 | var calcCost = function(curX, rotation) { 934 | 935 | var cur = curPiece[rotation]; 936 | 937 | // Calculate the height 938 | var dist = tilesY; 939 | for (var i = 0; i < cur.length; i+= 2) { 940 | dist = Math.min(dist, topY[curX + cur[i]] - curY - cur[i + 1]); 941 | } 942 | 943 | var minY = tilesY; 944 | for (var i = 0; i < cur.length; i+= 2) { 945 | minY = Math.min(minY, cur[i + 1] + curY + dist - 1); 946 | } 947 | 948 | if (minY < 0) 949 | return tilesY; // Something big 950 | 951 | // Count existing holes 952 | var holes = 0; 953 | for (var i = topY[curX + cur[i]]; i < tilesY; i++) { 954 | holes+= board[curX + cur[i]][i] === undefined; 955 | } 956 | 957 | // Count holes we're creating now 958 | var newHoles = 0; 959 | 960 | for (var i = 0; i < cur.length; i+= 2) { 961 | 962 | // Shadow-Tile position 963 | var x = cur[i] + curX; 964 | var y = cur[i + 1] + curY + dist - 1; 965 | var take = true; 966 | 967 | // Ignore tiles in the same column that are higher 968 | for (var j = 0; j < cur.length; j+= 2) { 969 | 970 | if (i !== j) { 971 | 972 | if (cur[i] === cur[j] && cur[i + 1] < cur[j + 1]) { 973 | take = false; 974 | break; 975 | } 976 | } 977 | } 978 | 979 | if (take) { 980 | 981 | for (j = y + 1; j < tilesY && board[j][x] === undefined; j++) { 982 | newHoles++; 983 | } 984 | } 985 | } 986 | 987 | return (1 / minY + holes + newHoles); 988 | }; 989 | 990 | 991 | /** 992 | * Draw a single tile on the screen 993 | * 994 | * @param {CanvasRenderingContext2D} ctx The context to be used 995 | * @param {number} x X position on the grid 996 | * @param {number} y Y position on the grid 997 | * @param {Array} color - A RGB array 998 | */ 999 | var drawTile = function(ctx, x, y, color) { 1000 | 1001 | ctx.save(); 1002 | 1003 | ctx.translate(tileBorder + x * (tileBorder + tileSize), tileBorder + y * (tileBorder + tileSize)); 1004 | 1005 | // Draw the tile border 1006 | ctx.fillStyle = "#000"; 1007 | ctx.fillRect(-tileBorder, -tileBorder, tileSize + tileBorder + tileBorder, tileSize + tileBorder + tileBorder); 1008 | 1009 | // Draw a light inner border 1010 | ctx.fillStyle = color[2]; 1011 | ctx.fillRect(0, 0, tileSize, tileSize); 1012 | 1013 | // Draw a dark inner border 1014 | ctx.fillStyle = color[1]; 1015 | ctx.beginPath(); 1016 | ctx.moveTo(0, 0); 1017 | ctx.lineTo(0, tileSize); 1018 | ctx.lineTo(tileSize, tileSize); 1019 | ctx.closePath(); 1020 | ctx.fill(); 1021 | 1022 | // Draw the actual tile 1023 | ctx.fillStyle = color[0]; 1024 | ctx.fillRect(tileBorder, tileBorder, tileSize - 2 * tileBorder, tileSize - 2 * tileBorder); 1025 | 1026 | ctx.restore(); 1027 | 1028 | if (showFavicon) { 1029 | ftx.fillStyle = color[0]; 1030 | ftx.fillRect(x * favicon.width / tilesX, y * favicon.width / tilesY, 1, 1); 1031 | } 1032 | }; 1033 | 1034 | 1035 | /** 1036 | * Draw a single tile in shadow color 1037 | * 1038 | * @param {CanvasRenderingContext2D} ctx The context to be used 1039 | * @param {number} x X position on the grid 1040 | * @param {number} y Y position on the grid 1041 | */ 1042 | var drawShadow = function(ctx, x, y) { 1043 | 1044 | ctx.save(); 1045 | 1046 | ctx.translate(tileBorder + x * (tileBorder + tileSize), tileBorder + y * (tileBorder + tileSize)); 1047 | 1048 | ctx.fillStyle = "#b7c7e4"; 1049 | ctx.fillRect(0, 0, tileSize, tileSize); 1050 | 1051 | ctx.restore(); 1052 | }; 1053 | 1054 | 1055 | /** 1056 | * Draw a text on the screen 1057 | * 1058 | * @param text The text to be drawn 1059 | */ 1060 | var drawTextScreen = function(text) { 1061 | 1062 | ctx.font = "60px Lemon"; 1063 | 1064 | // Background layer 1065 | ctx.fillStyle = "rgba(119,136,170,0.5)"; 1066 | ctx.fillRect(0, 0, canvas.width, canvas.height); 1067 | 1068 | var size = ctx.measureText(text); 1069 | 1070 | ctx.fillStyle = "#fff"; 1071 | ctx.fillText(text, (canvas.width - size.width) / 2, canvas.height / 3); 1072 | }; 1073 | 1074 | 1075 | /** 1076 | * Initialize the game with a countdown 1077 | */ 1078 | var init = function() { 1079 | 1080 | var cnt = 4; 1081 | 1082 | prepareBoard(); 1083 | 1084 | curPiece = getNextPiece(); 1085 | nextPiece = getNextPiece(); 1086 | 1087 | calcInitCoord(); 1088 | 1089 | updatePreview(); 1090 | 1091 | gameStatus = STATUS_INIT; 1092 | 1093 | score = clearedLines = 0; 1094 | 1095 | animate(4000, function() { 1096 | 1097 | cnt--; 1098 | 1099 | if (!cnt) { 1100 | cnt = 'Go'; 1101 | ctx.fillStyle = "#0d0"; 1102 | } else { 1103 | ctx.fillStyle = "#fff"; 1104 | } 1105 | 1106 | ctx.clearRect(0, 0, canvas.width, canvas.height); 1107 | 1108 | // Set the font once 1109 | ctx.font = "60px Lemon"; 1110 | 1111 | var size = ctx.measureText(cnt); 1112 | 1113 | ctx.fillText(cnt, (canvas.width - size.width) / 2, canvas.height / 3); 1114 | 1115 | }, function() { 1116 | 1117 | gameStatus = STATUS_PLAY; 1118 | loop(); 1119 | 1120 | }, 1000); 1121 | }; 1122 | 1123 | 1124 | /** 1125 | * Pause or unpause the game, according to gameStatus 1126 | */ 1127 | var pause = function() { 1128 | 1129 | if (gameStatus === STATUS_PAUSE) { 1130 | gameStatus = STATUS_PLAY; 1131 | document.getElementById('Cpause').checked = false; 1132 | loop(); 1133 | } else if (gameStatus === STATUS_PLAY) { 1134 | gameStatus = STATUS_PAUSE; 1135 | document.getElementById('Cpause').checked = true; 1136 | pauseLoop(); 1137 | } 1138 | draw(); 1139 | }; 1140 | 1141 | 1142 | /** 1143 | * Update the social links 1144 | */ 1145 | var updateSocialLinks = function() { 1146 | 1147 | var fb = 'https://www.facebook.com/sharer/sharer.php?u='; 1148 | var tw = 'http://twitter.com/share?text=Check%20out%20my%20custom%20HTML5%20Tetris%20(made%20by%20%40RobertEisele)&url='; 1149 | var gp = 'https://plus.google.com/share?url='; 1150 | 1151 | var P = []; 1152 | 1153 | for (var i = pieces.length; i--; ) { 1154 | 1155 | P[i] = pieces[i].slice(0, 1 + PIECE_SHAPE); // Upper slice() bound is exclusive, so 1+x 1156 | P[i][PIECE_PROBABILITY] = 1; // We kill the probability for sake of string length. Maybe we'll find a better solution 1157 | 1158 | P[i][PIECE_COLOR] = P[i][PIECE_COLOR][0].substring(4, P[i][PIECE_COLOR][0].length - 1).split(','); 1159 | } 1160 | 1161 | try { 1162 | 1163 | // See prepareUrlHash() as the opposite endpoint 1164 | location.hash = window['btoa'](JSON.stringify({ 1165 | 'P': P, 1166 | 'X': tilesX, 1167 | 'Y': tilesY, 1168 | 'S': tileSize, 1169 | 'B': tileBorder, 1170 | 'Q': speed 1171 | })); 1172 | 1173 | } catch (e) { 1174 | return; 1175 | } 1176 | 1177 | var url = encodeURIComponent(location.href); 1178 | sFB.setAttribute('href', fb + url); 1179 | sTW.setAttribute('href', tw + url); 1180 | sGP.setAttribute('href', gp + url); 1181 | }; 1182 | 1183 | /** 1184 | * Draw all components on the screen 1185 | */ 1186 | var draw = function() { 1187 | 1188 | // http://jsperf.com/ctx-clearrect-vs-canvas-width-canvas-width/3 1189 | // Should be fine and also the standard way to go 1190 | ctx.clearRect(0, 0, canvas.width, canvas.height); 1191 | 1192 | if (showFavicon) { 1193 | ftx.clearRect(0, 0, favicon.width, favicon.width); 1194 | } 1195 | 1196 | var cur = curPiece[direction]; 1197 | 1198 | for (var y = tilesY; y--; ) { 1199 | 1200 | // Draw board 1201 | for (var x = tilesX; x--; ) { 1202 | 1203 | if (board[y][x] !== undefined) { 1204 | drawTile(ctx, x, y, board[y][x]); 1205 | } 1206 | } 1207 | } 1208 | 1209 | if (showShadow && !autoMode) { 1210 | 1211 | var dist = tilesY; 1212 | for (var i = 0; i < cur.length; i+= 2) { 1213 | dist = Math.min(dist, topY[cur[i] + curX] - (curY + cur[i + 1])); 1214 | } 1215 | 1216 | for (var i = 0; i < cur.length; i+= 2) { 1217 | drawShadow(ctx, cur[i] + curX, cur[i + 1] + curY + dist - 1); 1218 | } 1219 | } 1220 | 1221 | // Draw current piece 1222 | for (var i = 0; i < cur.length; i+= 2) { 1223 | 1224 | drawTile(ctx, cur[i] + curX, cur[i + 1] + curY, curPiece[PIECE_COLOR]); 1225 | } 1226 | 1227 | if (showFavicon) { 1228 | 1229 | var s = favicon.width; 1230 | var v = s / 2; 1231 | 1232 | if (gameStatus === STATUS_GAMEOVER) { 1233 | 1234 | ftx.clearRect(0, 0, s, s); 1235 | 1236 | var p = 3 * pixelRatio; 1237 | 1238 | ftx.fillStyle = '#000'; 1239 | ftx.arc(v, v, v, 0, Math.PI * 2, false); 1240 | ftx.closePath(); 1241 | ftx.fill(); 1242 | 1243 | ftx.lineWidth = pixelRatio * 4; 1244 | ftx.strokeStyle = '#fff'; 1245 | ftx.beginPath(); 1246 | ftx.moveTo(p, p); 1247 | ftx.lineTo(s - p, s - p); 1248 | 1249 | ftx.moveTo(s - p, p); 1250 | ftx.lineTo(p, s - p); 1251 | ftx.stroke(); 1252 | 1253 | } else if (gameStatus === STATUS_PAUSE) { 1254 | 1255 | ftx.clearRect(0, 0, s, s); 1256 | 1257 | ftx.fillStyle = '#000'; 1258 | ftx.arc(v, v, v, 0, Math.PI * 2, false); 1259 | ftx.closePath(); 1260 | ftx.fill(); 1261 | 1262 | ftx.fillStyle = '#fff'; 1263 | ftx.fillRect(5 * v / 8 - 1, v / 2, v / 4 + 1, v); 1264 | ftx.fillRect(v + v / 8, v / 2, v / 4 + 1, v); 1265 | } 1266 | 1267 | setFavicon(); 1268 | } 1269 | 1270 | /* DEBUG LINES 1271 | for (var i = 0; i < tilesX; i++) { 1272 | ctx.save(); 1273 | ctx.fillStyle = "orange"; 1274 | ctx.translate(tileBorder + i * (tileBorder + tileSize), topY[i] * (tileBorder + tileSize) - tileBorder); 1275 | ctx.fillRect(0, 0, tileSize, 2); 1276 | 1277 | ctx.restore(); 1278 | } 1279 | */ 1280 | 1281 | // Draw text overlay 1282 | if (gameStatus === STATUS_PAUSE) { 1283 | drawTextScreen("PAUSE"); 1284 | } else if (gameStatus === STATUS_GAMEOVER) { 1285 | drawTextScreen("GAME OVER"); 1286 | 1287 | if (expelled) { 1288 | document.getElementById('restart').style.display = 'block'; 1289 | } 1290 | } 1291 | }; 1292 | 1293 | 1294 | /** 1295 | * Update the tiles on the preview monitor 1296 | */ 1297 | var updatePreview = function() { 1298 | 1299 | if (!showPreview) 1300 | return; 1301 | 1302 | ptx.clearRect(0, 0, preview.width, preview.height); 1303 | 1304 | var cur = nextPiece[direction]; 1305 | 1306 | for (var i = 0; i < cur.length; i+= 2) { 1307 | drawTile(ptx, cur[i] + 5, cur[i + 1] + 5, nextPiece[PIECE_COLOR]); 1308 | } 1309 | }; 1310 | 1311 | 1312 | /** 1313 | * Prepare the board 1314 | */ 1315 | var prepareBoard = function() { 1316 | 1317 | board = new Array(tilesY); 1318 | for (var y = tilesY; y--; ) { 1319 | board[y] = new Array(tilesX); 1320 | } 1321 | 1322 | topY = new Array(tilesX); 1323 | for (var i = tilesX; i--; ) { 1324 | topY[i] = tilesY; 1325 | } 1326 | 1327 | preview.width = /* void */ 1328 | preview.height = tileBorder + 11 * (tileBorder + tileSize); 1329 | 1330 | canvas.width = tileBorder + tilesX * (tileBorder + tileSize); 1331 | canvas.height = tileBorder + tilesY * (tileBorder + tileSize); 1332 | 1333 | favicon.width = /* void */ 1334 | favicon.height = 16 * pixelRatio; 1335 | }; 1336 | 1337 | 1338 | /** 1339 | * Prepare the pieces and caches some values 1340 | * 1341 | * @param {Array} pieces The array of pieces 1342 | */ 1343 | var preparePieces = function(pieces) { 1344 | 1345 | var sum = 0; 1346 | var opacity = 0.2; 1347 | 1348 | for (var i = pieces.length; i--; ) { 1349 | 1350 | // Pre-compute tile colors 1351 | var color = pieces[i][PIECE_COLOR]; 1352 | 1353 | color[0]|= 0; 1354 | color[1]|= 0; 1355 | color[2]|= 0; 1356 | 1357 | pieces[i][PIECE_COLOR] = [ 1358 | // Normal color 1359 | "rgb(" + color[0] + "," + color[1] + "," + color[2] + ")", 1360 | // Dark color 1361 | "rgb(" + Math.round(color[0] - color[0] * opacity) + "," + Math.round(color[1] - color[1] * opacity) + "," + Math.round(color[2] - color[2] * opacity) + ")", 1362 | // Light color 1363 | "rgb(" + Math.round(color[0] + (255 - color[0]) * opacity) + "," + Math.round(color[1] + (255 - color[1]) * opacity) + "," + Math.round(color[2] + (255 - color[2]) * opacity) + ")" 1364 | ]; 1365 | 1366 | // Add rotations 1367 | for (var j = PIECE_SHAPE; j < 4 - 1 + PIECE_SHAPE; j++) { 1368 | 1369 | if (pieces[i][PIECE_ROTATABLE]) 1370 | pieces[i][j + 1] = getRotatedPiece(pieces[i][j]); 1371 | else 1372 | pieces[i][j + 1] = pieces[i][PIECE_SHAPE].slice(0); 1373 | } 1374 | 1375 | // Calculate weight sum 1376 | sum+= pieces[i][PIECE_PROBABILITY]; 1377 | } 1378 | 1379 | // Adjust the weights 1380 | for (var i = pieces.length; i--; ) { 1381 | pieces[i][PIECE_PROBABILITY]/= sum; 1382 | 1383 | // Append tables to the menu 1384 | appendEditTable(divTables, pieces[i]); 1385 | } 1386 | }; 1387 | 1388 | 1389 | /** 1390 | * Set the actual rendered favicon 1391 | */ 1392 | var setFavicon = function() { 1393 | fav['href'] = favicon['toDataURL']('image/png'); 1394 | }; 1395 | 1396 | 1397 | /** 1398 | * A simple animation loop 1399 | * 1400 | * @param {number} duration The animation duration in ms 1401 | * @param {Function} fn The callback for every animation step 1402 | * @param {Function=} done The finish callback 1403 | * @param {number=} speed The speed of the animation 1404 | */ 1405 | var animate = function(duration, fn, done, speed) { 1406 | 1407 | var start = NOW(); 1408 | var loop; 1409 | 1410 | // We could use the requestAni shim, but yea...it's just fine 1411 | (loop = function() { 1412 | 1413 | var now = NOW(); 1414 | 1415 | var pct = (now - start) / duration; 1416 | if (pct > 1) 1417 | pct = 1; 1418 | 1419 | fn(pct); 1420 | 1421 | if (pct === 1) { 1422 | done(); 1423 | } else { 1424 | window.setTimeout(loop, speed || /* 1000 / 60*/ 16); 1425 | } 1426 | })(); 1427 | }; 1428 | 1429 | 1430 | /** 1431 | * Attach a new event listener 1432 | * 1433 | * @param {Object} obj DOM node 1434 | * @param {string} type The event type 1435 | * @param {Function} fn The Callback 1436 | */ 1437 | var addEvent = function(obj, type, fn) { 1438 | 1439 | if (obj.addEventListener) { 1440 | return obj.addEventListener(type, fn, false); 1441 | } else if (obj.attachEvent) { 1442 | return obj.attachEvent("on" + type, fn); 1443 | } 1444 | }; 1445 | 1446 | 1447 | /** 1448 | * Set the game mode to expelled, means highscore participation is disabled (because of custom game) 1449 | * 1450 | * @param {boolean=} diag Prevent the dialogue 1451 | */ 1452 | var setExpelled = function(diag) { 1453 | 1454 | if (!expelled && !diag) { 1455 | alert("This disables highscore participation."); 1456 | } 1457 | expelled = true; 1458 | displayHomeLink(); 1459 | }; 1460 | 1461 | /* 1462 | * Display the home link when needed 1463 | */ 1464 | var displayHomeLink = function() { 1465 | document.getElementById('home').style.display = 'block'; 1466 | }; 1467 | 1468 | 1469 | /** 1470 | * Add form edit tables 1471 | * 1472 | * @param {Object} root The root element 1473 | * @param {Array} piece The forms array 1474 | */ 1475 | var appendEditTable = function(root, piece) { 1476 | 1477 | var isSet = function(piece, x, y) { 1478 | 1479 | piece = piece[PIECE_SHAPE]; 1480 | 1481 | for (var i = 0; i < piece.length; i+= 2) { 1482 | if (piece[i] === x - 4 && piece[i + 1] === y - 4) 1483 | return i; 1484 | } 1485 | return -1; 1486 | }; 1487 | 1488 | var table = document.createElement('table'); 1489 | 1490 | for (var i = 0; i < 9; i++) { 1491 | 1492 | var tr = document.createElement('tr'); 1493 | 1494 | for (var j = 0; j < 9; j++) { 1495 | 1496 | var td = document.createElement('td'); 1497 | td.style.background = isSet(piece, j, i) >= 0 ? piece[PIECE_COLOR][0] : '#ccc'; 1498 | tr.appendChild(td); 1499 | 1500 | (function(x, y, td, piece) { 1501 | 1502 | addEvent(td, 'click', function() { 1503 | 1504 | var start; 1505 | 1506 | if (-1 === (start = isSet(piece, x, y))) { 1507 | td.style.background = piece[PIECE_COLOR][0]; 1508 | 1509 | // Add new coordinate 1510 | piece[PIECE_SHAPE].push(x - 4, y - 4); 1511 | 1512 | } else { 1513 | td.style.background = '#ccc'; 1514 | 1515 | // Delete coordinate 1516 | piece[PIECE_SHAPE].splice(start, 2); 1517 | } 1518 | 1519 | // Append new rotated pieces 1520 | for (var j = PIECE_SHAPE; j < 4 - 1 + PIECE_SHAPE; j++) { 1521 | 1522 | if (piece[PIECE_ROTATABLE]) 1523 | piece[j + 1] = getRotatedPiece(piece[j]); 1524 | else 1525 | piece[j + 1] = piece[PIECE_SHAPE].slice(0); 1526 | } 1527 | 1528 | setExpelled(); 1529 | updateSocialLinks(); 1530 | }); 1531 | 1532 | })(j, i, td, piece); 1533 | } 1534 | table.appendChild(tr); 1535 | 1536 | } 1537 | root.appendChild(table); 1538 | }; 1539 | 1540 | 1541 | /** 1542 | * Initialize the motion handling of mobile devices 1543 | */ 1544 | var initMotion = function() { 1545 | 1546 | var motionX = 0; 1547 | var motionY = 0; 1548 | var prevX = 0; 1549 | var prevY = 0; 1550 | 1551 | var ceil = function(n) { 1552 | 1553 | n = Math.round(n / 30); 1554 | 1555 | return (0 < n) - (n < 0); 1556 | }; 1557 | 1558 | addEvent(window, 'devicemotion', function(ev) { 1559 | 1560 | var acc = ev.rotationRate; 1561 | 1562 | var alpha = 0.05; 1563 | 1564 | motionX = motionX * (1 - alpha) + acc.alpha * alpha; 1565 | motionY = motionY * (1 - alpha) + acc.beta * alpha; 1566 | 1567 | var X = ceil(motionX); 1568 | var Y = ceil(motionY); 1569 | 1570 | if (prevX === X) { 1571 | X = 0; 1572 | } 1573 | prevX = X; 1574 | 1575 | if (prevY === Y) { 1576 | Y = 0; 1577 | } 1578 | prevY = Y; 1579 | 1580 | tryMove(curX + X, 3 + (direction + 1 + Y) % 4); 1581 | }); 1582 | }; 1583 | 1584 | 1585 | // Set the click handler for the submit button 1586 | addEvent(submit, 'click', function() { 1587 | 1588 | var name = nick.value; 1589 | 1590 | var img = document.createElement('img'); 1591 | 1592 | if (expelled) { 1593 | return; 1594 | } 1595 | 1596 | if (name) { 1597 | 1598 | img.onload = function() { 1599 | img.onload = null; 1600 | }; 1601 | 1602 | // Dafaq, it's tetris! Stop cheating and find a new hobby... 1603 | img.src = '/pixel.php?name=' + encodeURIComponent(name) + "&score=" + score + "&lines=" + clearedLines + "&date=" + Date.now(); 1604 | 1605 | highscore.style.display = 'none'; 1606 | 1607 | init(); 1608 | 1609 | } else { 1610 | nick.focus(); 1611 | } 1612 | }); 1613 | 1614 | 1615 | // Set the click handler for menu opening 1616 | var evTabOpen = function(ev) { 1617 | 1618 | var elm = ev.target.parentNode; 1619 | 1620 | if (elm === divEdit) { 1621 | divEdit.style.zIndex = 4; 1622 | divBest.style.zIndex = 2; 1623 | } else { 1624 | divEdit.style.zIndex = 2; 1625 | divBest.style.zIndex = 4; 1626 | } 1627 | 1628 | animate(600, function(k) { 1629 | /* 1630 | var pos = k === 1 ? 1 : 1 - Math.pow(2, -10 * k); 1631 | 1632 | pos = editClosed + pos * (1 - 2 * editClosed); 1633 | 1634 | edit.style.right = (-pos * 420 | 0) + 'px'; 1635 | */ 1636 | elm.style.right = (420 * ((k === 1 ? 1 : 1 - Math.pow(2, -10 * k)) * (1 - 2 * menuOpen) + menuOpen - 1) | 0) + 'px'; 1637 | }, function() { 1638 | menuOpen = !menuOpen; 1639 | }); 1640 | }; 1641 | addEvent(divOpen, 'click', evTabOpen); 1642 | addEvent(divOpenScore, 'click', evTabOpen); 1643 | 1644 | // Set keydown event listener 1645 | addEvent(window, "keydown", function(ev) { 1646 | 1647 | if (gameStatus !== STATUS_PLAY && ev.keyCode !== 80 && ev.keyCode !== 9) 1648 | return; 1649 | 1650 | switch (ev.keyCode) { 1651 | case 37: // left 1652 | tryMove(curX - 1, direction); 1653 | draw(); 1654 | break; 1655 | case 39: // right 1656 | tryMove(curX + 1, direction); 1657 | draw(); 1658 | break; 1659 | case 38: // up 1660 | tryMove(curX, PIECE_SHAPE + (direction - PIECE_SHAPE + 1) % 4); 1661 | draw(); 1662 | break; 1663 | case 40: // down 1664 | if (!tryDown(curY + 1)) 1665 | integratePiece(); 1666 | draw(); 1667 | break; 1668 | case 32: // space 1669 | while (tryDown(curY + 1)) { 1670 | } 1671 | integratePiece(); 1672 | draw(); 1673 | break; 1674 | case 80: // p 1675 | pause(); 1676 | break; 1677 | case 65: // a 1678 | autoMode = !autoMode; 1679 | document.getElementById('Cauto').checked = autoMode; 1680 | setExpelled(); 1681 | return; 1682 | case 83: // s 1683 | showShadow = !showShadow; 1684 | document.getElementById('Cshadow').checked = showShadow; 1685 | return; 1686 | case 9: 1687 | // fall to preventDefault, as we forbid tab selection (we have hidden input fields. chrome scrolls to them) 1688 | break; 1689 | default: 1690 | return; 1691 | } 1692 | ev.preventDefault(); 1693 | }); 1694 | 1695 | // Set window leave listener 1696 | addEvent(window, 'blur', function() { 1697 | 1698 | if (gameStatus !== STATUS_PLAY) 1699 | return; 1700 | 1701 | gameStatus = STATUS_PAUSE; 1702 | 1703 | leftWindow = true; 1704 | 1705 | pauseLoop(); 1706 | 1707 | draw(); 1708 | }); 1709 | 1710 | // Set comeback listener 1711 | addEvent(window, 'focus', function() { 1712 | 1713 | if (!leftWindow || gameStatus !== STATUS_PAUSE) { 1714 | return; 1715 | } 1716 | 1717 | gameStatus = STATUS_PLAY; 1718 | 1719 | leftWindow = false; 1720 | 1721 | loop(); 1722 | }); 1723 | 1724 | // Set canvas click handler (for restarting the game in custom mode) 1725 | addEvent(canvas, 'click', function() { 1726 | 1727 | if (expelled && gameStatus === STATUS_GAMEOVER) { 1728 | 1729 | document.getElementById('restart').style.display = 'none'; 1730 | 1731 | init(); 1732 | } 1733 | 1734 | }); 1735 | 1736 | 1737 | if (window['performance'] !== undefined && window['performance']['now'] !== undefined) { 1738 | NOW = function() { 1739 | return window.performance.now(); 1740 | }; 1741 | } else if (Date.now !== undefined) { 1742 | NOW = Date.now; 1743 | } else { 1744 | NOW = function() { 1745 | return new Date().valueOf(); 1746 | }; 1747 | } 1748 | 1749 | window['textBoxEdit'] = function(elm) { 1750 | 1751 | var value = parseInt(elm.value, 10); 1752 | 1753 | switch (elm.getAttribute('id')) { 1754 | 1755 | case 'border': 1756 | tileBorder = value; 1757 | setExpelled(); 1758 | break; 1759 | 1760 | case 'tilesX': 1761 | tilesX = value; 1762 | setExpelled(); 1763 | break; 1764 | 1765 | case 'tilesY': 1766 | tilesY = value; 1767 | setExpelled(); 1768 | break; 1769 | 1770 | case 'tilesSize': 1771 | tileSize = value; 1772 | setExpelled(); 1773 | break; 1774 | 1775 | case 'Cpreview': 1776 | showPreview = elm.checked; 1777 | if (showPreview) { 1778 | preview.style.display = 'block'; 1779 | } else { 1780 | preview.style.display = 'none'; 1781 | } 1782 | return; 1783 | 1784 | case 'Cpause': 1785 | pause(); 1786 | return; 1787 | 1788 | case 'Cauto': 1789 | autoMode = elm.checked; 1790 | setExpelled(); 1791 | return; 1792 | 1793 | case 'Cshadow': 1794 | showShadow = elm.checked; 1795 | return; 1796 | 1797 | case 'speedDelay': 1798 | speed = parseFloat(elm.value); 1799 | 1800 | /** 1801 | * Find a function for the following "speed : speedScore" mapping 1802 | * 1803 | 1000 = 1 1804 | 200 = 5 1805 | 40 = 20 1806 | 1807 | We need a function such that 1808 | speedScore = a * exp(b * speed) 1809 | 1810 | => log(speedScore) = log(a) + b * speed 1811 | => linear regression 1812 | 1813 | */ 1814 | speedScore = Math.max(1, Math.round(28.2632 * Math.exp(-0.00864879 * speed))); 1815 | return; 1816 | 1817 | case 'Cfavicon': 1818 | showFavicon = elm.checked; 1819 | if (!showFavicon) { 1820 | fav['href'] = originalFavicon; 1821 | } 1822 | return; 1823 | 1824 | case 'Cgamepad': 1825 | if (elm.checked) { 1826 | Gamepad.startPolling(); 1827 | } else { 1828 | Gamepad.stopPolling(); 1829 | } 1830 | return; 1831 | } 1832 | 1833 | updateSocialLinks(); 1834 | 1835 | prepareBoard(); 1836 | 1837 | //newPiece(); 1838 | calcInitCoord(); 1839 | 1840 | draw(); 1841 | 1842 | updatePreview(); 1843 | }; 1844 | 1845 | // Display the open buttons for the menus 1846 | window.setTimeout(function() { 1847 | 1848 | animate(400, function(pos) { 1849 | 1850 | var start = -442; 1851 | var end = -420; 1852 | 1853 | var p1 = Math.min(1, pos / 0.5); 1854 | var p2 = Math.max(0, (pos - 0.5) / 0.5); 1855 | 1856 | divEdit.style.right = (start + (p1 * (end - start))) + "px"; 1857 | divBest.style.right = (start + (p2 * (end - start))) + "px"; 1858 | 1859 | }, function() { }); 1860 | 1861 | }, 800); 1862 | 1863 | 1864 | if (!showFavicon) { 1865 | document.getElementById('showFavicon').parentNode.style.display = 'none'; 1866 | } 1867 | 1868 | /** 1869 | * Copyright 2012 Google Inc. All Rights Reserved. 1870 | * 1871 | * Licensed under the Apache License, Version 2.0 (the "License"); 1872 | * you may not use this file except in compliance with the License. 1873 | * You may obtain a copy of the License at 1874 | * 1875 | * http://www.apache.org/licenses/LICENSE-2.0 1876 | * 1877 | * Unless required by applicable law or agreed to in writing, software 1878 | * distributed under the License is distributed on an "AS IS" BASIS, 1879 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1880 | * See the License for the specific language governing permissions and 1881 | * limitations under the License. 1882 | */ 1883 | var Gamepad = { 1884 | // Heavily modified version of 1885 | // http://www.html5rocks.com/en/tutorials/doodles/gamepad/gamepad-tester/gamepad.js 1886 | 1887 | ticking: false, 1888 | gamepads: [], 1889 | prevRawGamepadTypes: [], 1890 | prevAxesTime: 0, 1891 | prevAxesHash: 0, 1892 | prevButtonTime: 0, 1893 | prevButtonHash: 0, 1894 | init: function() { 1895 | 1896 | if (navigator['getGamepads'] || 1897 | !!navigator['webkitGetGamepads'] || 1898 | !!navigator['webkitGamepads']) { 1899 | 1900 | if ('ongamepadconnected' in window) { 1901 | window.addEventListener('gamepadconnected', 1902 | Gamepad.onGamepadConnect, false); 1903 | window.addEventListener('gamepaddisconnected', 1904 | Gamepad.onGamepadDisconnect, false); 1905 | } 1906 | } 1907 | }, 1908 | onGamepadConnect: function(event) { 1909 | 1910 | Gamepad.gamepads.push(event['gamepad']); 1911 | 1912 | Gamepad.startPolling(); 1913 | }, 1914 | onGamepadDisconnect: function(event) { 1915 | 1916 | for (var i in Gamepad.gamepads) { 1917 | 1918 | if (Gamepad.gamepads[i]['index'] === event['gamepad']['index']) { 1919 | Gamepad.gamepads.splice(i, 1); 1920 | break; 1921 | } 1922 | } 1923 | 1924 | if (Gamepad.gamepads.length === 0) { 1925 | Gamepad.stopPolling(); 1926 | } 1927 | }, 1928 | startPolling: function() { 1929 | 1930 | document.getElementById('Cgamepad').checked = true; 1931 | 1932 | if (!Gamepad.ticking) { 1933 | Gamepad.ticking = true; 1934 | Gamepad.tick(); 1935 | } 1936 | }, 1937 | stopPolling: function() { 1938 | 1939 | document.getElementById('Cgamepad').checked = false; 1940 | 1941 | Gamepad.ticking = false; 1942 | }, 1943 | tick: function() { 1944 | Gamepad.pollStatus(); 1945 | Gamepad.nextTick(); 1946 | }, 1947 | nextTick: function() { 1948 | 1949 | if (Gamepad.ticking) { 1950 | if (window.requestAnimationFrame) { 1951 | window.requestAnimationFrame(Gamepad.tick); 1952 | } else if (window.mozRequestAnimationFrame) { 1953 | window.mozRequestAnimationFrame(Gamepad.tick); 1954 | } else if (window.webkitRequestAnimationFrame) { 1955 | window.webkitRequestAnimationFrame(Gamepad.tick); 1956 | } 1957 | } 1958 | }, 1959 | pollStatus: function() { 1960 | 1961 | // Let's get dirty! 1962 | 1963 | Gamepad.pollGamepads(); 1964 | var now = Date.now(); 1965 | 1966 | var gamepad = Gamepad.gamepads[0]; 1967 | 1968 | if (gamepad === undefined) { 1969 | //Gamepad.stopPolling(); 1970 | return; 1971 | } 1972 | 1973 | var hash = 0; 1974 | for (var j = gamepad['buttons'].length; j--; ) { 1975 | if (gamepad['buttons'][j]) 1976 | hash|= 1 << j; 1977 | } 1978 | 1979 | if (hash !== Gamepad.prevButtonHash) { 1980 | Gamepad.prevButtonHash = hash; 1981 | Gamepad.prevButtonTime = now; 1982 | } else if (hash !== 0) { 1983 | 1984 | if (now - Gamepad.prevButtonTime < 200) { 1985 | // Prevent 200ms after first kick 1986 | return; 1987 | } 1988 | 1989 | if ((now - Gamepad.prevButtonTime) % 50 >= 10) { // Math.floor((now - Gamepad.prevButtonTime) / 10) % 5 !== 0 1990 | // Now pass every 50ms 1991 | return; 1992 | } 1993 | } else { 1994 | 1995 | // Now test for the axes 1996 | hash = 0; 1997 | for (j = gamepad['axes'].length; j--; ) { 1998 | if (Math.abs(gamepad['axes'][j]) > 0.7) 1999 | hash|= 1 << j; 2000 | } 2001 | 2002 | if (hash !== Gamepad.prevAxesHash) { 2003 | Gamepad.prevAxesHash = hash; 2004 | Gamepad.prevAxesTime = now; 2005 | } else if (hash !== 0) { 2006 | 2007 | if (now - Gamepad.prevAxesTime < 100) { 2008 | // Prevent 100ms after first kick 2009 | return; 2010 | } 2011 | 2012 | if ((now - Gamepad.prevAxesTime) % 50 >= 10) { // Math.floor((now - Gamepad.prevAxesTime) / 10) % 5 !== 0 2013 | // Now pass every 50ms 2014 | return; 2015 | } 2016 | 2017 | } else { 2018 | // If no movement at all, exit here 2019 | return; 2020 | } 2021 | } 2022 | 2023 | Gamepad.updateMove(gamepad); 2024 | }, 2025 | pollGamepads: function() { 2026 | 2027 | var rawGamepads = 2028 | (navigator['getGamepads'] && navigator['getGamepads']()) || 2029 | (navigator['webkitGetGamepads'] && navigator['webkitGetGamepads']()); 2030 | 2031 | if (rawGamepads) { 2032 | 2033 | Gamepad.gamepads = []; 2034 | 2035 | for (var i = 0; i < rawGamepads.length; i++) { 2036 | 2037 | if (typeof rawGamepads[i] !== Gamepad.prevRawGamepadTypes[i]) { 2038 | 2039 | Gamepad.prevRawGamepadTypes[i] = typeof rawGamepads[i]; 2040 | } 2041 | 2042 | if (rawGamepads[i]) { 2043 | Gamepad.gamepads.push(rawGamepads[i]); 2044 | } 2045 | } 2046 | } 2047 | }, 2048 | updateMove: function(gamepad) { 2049 | 2050 | var y1 = gamepad['axes'][1]; 2051 | var y2 = gamepad['axes'][3]; 2052 | var x1 = gamepad['axes'][0]; 2053 | var x2 = gamepad['axes'][2]; 2054 | 2055 | if (gamepad['buttons'][9]) { 2056 | pause(); 2057 | return; 2058 | } 2059 | 2060 | // up 2061 | if (gamepad['buttons'][12] || gamepad['buttons'][3]) { 2062 | tryMove(curX, PIECE_SHAPE + (direction - PIECE_SHAPE + 1) % 4); 2063 | draw(); 2064 | } 2065 | 2066 | // left 2067 | if (gamepad['buttons'][14] || x1 < -0.5 || x2 < -0.5) { 2068 | tryMove(curX - 1, direction); 2069 | draw(); 2070 | } 2071 | 2072 | // right 2073 | if (gamepad['buttons'][15] || x1 > 0.5 || x2 > 0.5) { 2074 | tryMove(curX + 1, direction); 2075 | draw(); 2076 | } 2077 | 2078 | // down 2079 | if (gamepad['buttons'][13] || y1 > 0.5 || y2 > 0.5) { 2080 | if (!tryDown(curY + 1)) 2081 | integratePiece(); 2082 | draw(); 2083 | } 2084 | 2085 | // fall 2086 | if (gamepad['buttons'][0]) { 2087 | 2088 | while (tryDown(curY + 1)) { 2089 | } 2090 | integratePiece(); 2091 | draw(); 2092 | } 2093 | } 2094 | }; 2095 | 2096 | // Overwrite a custom setting with the defaults 2097 | prepareUrlHash(location['hash']); 2098 | 2099 | // Prepare the pieces and pre-calculate some caches 2100 | preparePieces(pieces); 2101 | 2102 | if (location['hash']) { 2103 | // If a URL was given, update social links 2104 | updateSocialLinks(); 2105 | } 2106 | 2107 | // Prepare the board 2108 | prepareBoard(); 2109 | 2110 | // Initialize the game 2111 | init(); 2112 | 2113 | // Initialize the gamepad 2114 | Gamepad.init(); 2115 | 2116 | // Initialize the motion handler for mobile devices 2117 | initMotion(); 2118 | 2119 | })(this); 2120 | --------------------------------------------------------------------------------