├── 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 | 
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 | 
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 |
233 | Share Custom Tetris
234 |
FB -
235 |
TW -
236 |
G+
237 |
238 |
239 |
240 |
241 |
242 |
Score
243 |
244 |
245 |
246 |
247 | Rank
248 | Name
249 | Score
250 | Lines
251 |
252 | {local:$i}
253 | {php}$i=0;{/php}
254 | {fores $score as $s}
255 |
256 | #{!++$i}
257 | {$s.TName}
258 | {!$s.TScore}
259 | {!$s.TLine}
260 |
261 | {/fores}
262 |
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 |
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 |
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 |
--------------------------------------------------------------------------------