1 /*
2 * PROJECT: JsTetris
3 * VERSION: 1.19
4 * LICENSE: BSD (revised)
5 * AUTHOR: (c) 2004-2009 Cezary Tomczak
6 * LINK: http://www.gosu.pl/tetris/
7 *
8 * This script can be used freely as long as all
9 * copyright messages are intact.
10 */
11
12 /**
13 * Tetris Game
14 * Initializes the buttons automatically, no additional actions required
15 *
16 * Score:
17 * 1) puzzle speed = 80+700/level
18 * 2) if puzzles created in current level >= 10+level*2 then increase level
19 * 3) after puzzle falling score is increased by 1000*level*linesRemoved
20 * 4) each down action increases score by 5+level
21 *
22 * API:
23 *
24 * public - method can be called outside of the object
25 * event - method is used as event, "this" refers to html object, "self" refers to javascript object
26 *
27 * class Tetris
28 * ------------
29 * public event void start()
30 * public event void reset()
31 * public event void pause()
32 * public event void gameOver()
33 * public event void up()
34 * public event void down()
35 * public event void left()
36 * public event void right()
37 * public event void space()
38 *
39 * class Window
40 * ------------
41 * event void activate()
42 * event void close()
43 * public bool isActive()
44 *
45 * class Keyboard
46 * --------------
47 * public void set(int key, function func)
48 * event void event(object e)
49 *
50 * class Stats
51 * -----------
52 * public void start()
53 * public void stop()
54 * public void reset()
55 * public event void incTime()
56 * public void setScore(int i)
57 * public void setLevel(int i)
58 * public void setLines(int i)
59 * public void setPuzzles(int i)
60 * public void setActions(int i)
61 * public int getScore()
62 * public int getLevel()
63 * public int getLines()
64 * public int getPuzzles()
65 * public int getActions()
66 *
67 * class Area
68 * ----------
69 * public Constructor(int unit, int x, int y, string id)
70 * public void destroy()
71 * public int removeFullLines()
72 * public bool isLineFull(int y)
73 * public void removeLine(int y)
74 * public mixed getBlock(int y, int x)
75 * public void addElement(htmlObject el)
76 *
77 * class Puzzle
78 * ------------
79 * public Constructor(object area)
80 * public void reset()
81 * public bool isRunning()
82 * public bool isStopped()
83 * public int getX()
84 * public int getY()
85 * public bool mayPlace()
86 * public void place()
87 * public void destroy()
88 * private array createEmptyPuzzle(int y, int x)
89 * event void fallDown()
90 * public event void forceMoveDown()
91 * public void stop()
92 * public bool mayRotate()
93 * public void rotate()
94 * public bool mayMoveDown()
95 * public void moveDown()
96 * public bool mayMoveLeft()
97 * public void moveLeft()
98 * public bool mayMoveRight()
99 * public void moveRight()
100 *
101 * class Highscores
102 * ----------------
103 * public Constructor(maxscores)
104 * public void load()
105 * public void save()
106 * public bool mayAdd(int score)
107 * public void add(string name, int score)
108 * public array getScores()
109 * public string toHtml()
110 * private void sort()
111 *
112 * class Cookie
113 * ------------
114 * public string get(string name)
115 * public void set(string name, string value, int seconds, string path, string domain, bool secure)
116 * public void del(string name)
117 *
118 * TODO:
119 * document.getElementById("tetris-nextpuzzle") cache ?
120 *
121 */
122 function Tetris()
123 {
124 var self = this;
125 this.stats = new Stats();
126 this.puzzle = null;
127 this.area = null;
128
129 this.unit = 20; // unit = x pixels
130 this.areaX = 20; // area width = x units
131 this.areaY = 20; // area height = y units
132
133 this.highscores = new Highscores(10);
134 this.paused = false;
135
136 /**
137 * @return void
138 * @access public event
139 */
140 this.start = function()
141 {
142 if (self.puzzle && !confirm('Are you sure you want to start a new game ?')) return;
143 self.reset();
144 self.stats.start();
145 document.getElementById("tetris-nextpuzzle").style.display = "block";
146 document.getElementById("tetris-keys").style.display = "none";
147 self.area = new Area(self.unit, self.areaX, self.areaY, "tetris-area");
148 self.puzzle = new Puzzle(self, self.area);
149 if (self.puzzle.mayPlace()) {
150 self.puzzle.place();
151 } else {
152 self.gameOver();
153 }
154 };
155
156 /**
157 * @return void
158 * @access public event
159 */
160 this.reset = function()
161 {
162 if (self.puzzle) {
163 self.puzzle.destroy();
164 self.puzzle = null;
165 }
166 if (self.area) {
167 self.area.destroy();
168 self.area = null;
169 }
170 document.getElementById("tetris-gameover").style.display = "none";
171 document.getElementById("tetris-nextpuzzle").style.display = "none";
172 document.getElementById("tetris-keys").style.display = "block";
173 self.stats.reset();
174 self.paused = false;
175 document.getElementById('tetris-pause').style.display = 'block';
176 document.getElementById('tetris-resume').style.display = 'none';
177 };
178
179 /**
180 * Pause / Resume.
181 * @return void
182 * @access public event
183 */
184 this.pause = function()
185 {
186 if (self.puzzle == null) return;
187 if (self.paused) {
188 self.puzzle.running = true;
189 self.puzzle.fallDownID = setTimeout(self.puzzle.fallDown, self.puzzle.speed);
190 document.getElementById('tetris-pause').style.display = 'block';
191 document.getElementById('tetris-resume').style.display = 'none';
192 self.stats.timerId = setInterval(self.stats.incTime, 1000);
193 self.paused = false;
194 } else {
195 if (!self.puzzle.isRunning()) return;
196 if (self.puzzle.fallDownID) clearTimeout(self.puzzle.fallDownID);
197 document.getElementById('tetris-pause').style.display = 'none';
198 document.getElementById('tetris-resume').style.display = 'block';
199 clearTimeout(self.stats.timerId);
200 self.paused = true;
201 self.puzzle.running = false;
202 }
203 };
204
205 /**
206 * End game.
207 * Stop stats, ...
208 * @return void
209 * @access public event
210 */
211 this.gameOver = function()
212 {
213 self.stats.stop();
214 self.puzzle.stop();
215 document.getElementById("tetris-nextpuzzle").style.display = "none";
216 document.getElementById("tetris-gameover").style.display = "block";
217 if (this.highscores.mayAdd(this.stats.getScore())) {
218 var name = prompt("Game Over !\nEnter your name:", "");
219 if (name && name.trim().length) {
220 this.highscores.add(name, this.stats.getScore());
221 }
222 }
223 };
224
225 /**
226 * @return void
227 * @access public event
228 */
229 this.up = function()
230 {
231 if (self.puzzle && self.puzzle.isRunning() && !self.puzzle.isStopped()) {
232 if (self.puzzle.mayRotate()) {
233 self.puzzle.rotate();
234 self.stats.setActions(self.stats.getActions() + 1);
235 }
236 }
237 };
238
239 /**
240 * @return void
241 * @access public event
242 */
243 this.down = function()
244 {
245 if (self.puzzle && self.puzzle.isRunning() && !self.puzzle.isStopped()) {
246 if (self.puzzle.mayMoveDown()) {
247 self.stats.setScore(self.stats.getScore() + 5 + self.stats.getLevel());
248 self.puzzle.moveDown();
249 self.stats.setActions(self.stats.getActions() + 1);
250 }
251 }
252 };
253
254 /**
255 * @return void
256 * @access public event
257 */
258 this.left = function()
259 {
260 if (self.puzzle && self.puzzle.isRunning() && !self.puzzle.isStopped()) {
261 if (self.puzzle.mayMoveLeft()) {
262 self.puzzle.moveLeft();
263 self.stats.setActions(self.stats.getActions() + 1);
264 }
265 }
266 };
267
268 /**
269 * @return void
270 * @access public event
271 */
272 this.right = function()
273 {
274 if (self.puzzle && self.puzzle.isRunning() && !self.puzzle.isStopped()) {
275 if (self.puzzle.mayMoveRight()) {
276 self.puzzle.moveRight();
277 self.stats.setActions(self.stats.getActions() + 1);
278 }
279 }
280 };
281
282 /**
283 * @return void
284 * @access public event
285 */
286 this.space = function()
287 {
288 if (self.puzzle && self.puzzle.isRunning() && !self.puzzle.isStopped()) {
289 self.puzzle.stop();
290 self.puzzle.forceMoveDown();
291 }
292 };
293
294 // windows
295 var helpwindow = new Window("tetris-help");
296 var highscores = new Window("tetris-highscores");
297
298 // game menu
299 document.getElementById("tetris-menu-start").onclick = function() { helpwindow.close(); highscores.close(); self.start(); this.blur(); };
300
301 // document.getElementById("tetris-menu-reset").onclick = function() { helpwindow.close(); highscores.close(); self.reset(); this.blur(); };
302
303 document.getElementById("tetris-menu-pause").onclick = function() { self.pause(); this.blur(); };
304 document.getElementById("tetris-menu-resume").onclick = function() { self.pause(); this.blur(); };
305
306 // help
307 document.getElementById("tetris-menu-help").onclick = function() { highscores.close(); helpwindow.activate(); this.blur(); };
308 document.getElementById("tetris-help-close").onclick = helpwindow.close;
309
310 // highscores
311 document.getElementById("tetris-menu-highscores").onclick = function()
312 {
313 helpwindow.close();
314 document.getElementById("tetris-highscores-content").innerHTML = self.highscores.toHtml();
315 highscores.activate();
316 this.blur();
317 };
318 document.getElementById("tetris-highscores-close").onclick = highscores.close;
319
320 // keyboard - buttons
321 //document.getElementById("tetris-keyboard-up").onclick = function() { self.up(); this.blur(); };
322 //document.getElementById("tetris-keyboard-down").onclick = function() { self.down(); this.blur(); };
323 //document.getElementById("tetris-keyboard-left").onclick = function () { self.left(); this.blur(); };
324 //document.getElementById("tetris-keyboard-right").onclick = function() { self.right(); this.blur(); };
325
326 // keyboard
327 var keyboard = new Keyboard();
328 keyboard.set(keyboard.n, this.start);
329 //keyboard.set(keyboard.r, this.reset);
330 keyboard.set(keyboard.p, this.pause);
331 keyboard.set(keyboard.up, this.up);
332 keyboard.set(keyboard.down, this.down);
333 keyboard.set(keyboard.left, this.left);
334 keyboard.set(keyboard.right, this.right);
335 keyboard.set(keyboard.space, this.space);
336 document.onkeydown = keyboard.event;
337
338 /**
339 * Window replaces game area, for example help window
340 * @param string id
341 */
342 function Window(id)
343 {
344 this.id = id;
345 this.el = document.getElementById(this.id);
346 var self = this;
347
348 /**
349 * Activate or deactivate a window - update html
350 * @return void
351 * @access event
352 */
353 this.activate = function()
354 {
355 self.el.style.display = (self.el.style.display == "block" ? "none" : "block");
356 };
357
358 /**
359 * Close window - update html
360 * @return void
361 * @access event
362 */
363 this.close = function()
364 {
365 self.el.style.display = "none";
366 };
367
368 /**
369 * @return bool
370 * @access public
371 */
372 this.isActive = function()
373 {
374 return (self.el.style.display == "block");
375 };
376 }
377
378 /**
379 * Assigning functions to keyboard events
380 * When key is pressed, searching in a table if any function has been assigned to this key, execute the function.
381 */
382 function Keyboard()
383 {
384 this.up = 38;
385 this.down = 40;
386 this.left = 37;
387 this.right = 39;
388 this.n = 78;
389 this.p = 80;
390 this.r = 82;
391 this.space = 32;
392 this.f12 = 123;
393 this.escape = 27;
394
395 this.keys = [];
396 this.funcs = [];
397
398 var self = this;
399
400 /**
401 * @param int key
402 * @param function func
403 * @return void
404 * @access public
405 */
406 this.set = function(key, func)
407 {
408 this.keys.push(key);
409 this.funcs.push(func);
410 };
411
412 /**
413 * @param object e
414 * @return void
415 * @access event
416 */
417 this.event = function(e)
418 {
419 if (!e) { e = window.event; }
420 for (var i = 0; i < self.keys.length; i++) {
421 if (e.keyCode == self.keys[i]) {
422 self.funcs[i]();
423 }
424 }
425 };
426 }
427
428 /**
429 * Live game statistics
430 * Updating html
431 */
432 function Stats()
433 {
434 this.level;
435 this.time;
436 this.apm;
437 this.lines;
438 this.score;
439 this.puzzles; // number of puzzles created on current level
440
441 this.actions;
442
443 this.el = {
444 "level": document.getElementById("tetris-stats-level"),
445 "time": document.getElementById("tetris-stats-time"),
446 "apm": document.getElementById("tetris-stats-apm"),
447 "lines": document.getElementById("tetris-stats-lines"),
448 "score": document.getElementById("tetris-stats-score")
449 }
450
451 this.timerId = null;
452 var self = this;
453
454 /**
455 * Start counting statistics, reset stats, turn on the timer
456 * @return void
457 * @access public
458 */
459 this.start = function()
460 {
461 this.reset();
462 this.timerId = setInterval(this.incTime, 1000);
463 };
464
465 /**
466 * Stop counting statistics, turn off the timer
467 * @return void
468 * @access public
469 */
470 this.stop = function()
471 {
472 if (this.timerId) {
473 clearInterval(this.timerId);
474 }
475 };
476
477 /**
478 * Reset statistics - update html
479 * @return void
480 * @access public
481 */
482 this.reset = function()
483 {
484 this.stop();
485 this.level = 1;
486 this.time = 0;
487 this.apm = 0;
488 this.lines = 0;
489 this.score = 0;
490 this.puzzles = 0;
491 this.actions = 0;
492 this.el.level.innerHTML = this.level;
493 this.el.time.innerHTML = this.time;
494 this.el.apm.innerHTML = this.apm;
495 this.el.lines.innerHTML = this.lines;
496 this.el.score.innerHTML = this.score;
497 };
498
499 /**
500 * Increase time, update apm - update html
501 * This func is called by setInterval()
502 * @return void
503 * @access public event
504 */
505 this.incTime = function()
506 {
507 self.time++;
508 self.el.time.innerHTML = self.time;
509 self.apm = parseInt((self.actions / self.time) * 60);
510 self.el.apm.innerHTML = self.apm;
511 };
512
513 /**
514 * Set score - update html
515 * @param int i
516 * @return void
517 * @access public
518 */
519 this.setScore = function(i)
520 {
521 this.score = i;
522 this.el.score.innerHTML = this.score;
523 };
524
525 /**
526 * Set level - update html
527 * @param int i
528 * @return void
529 * @access public
530 */
531 this.setLevel = function(i)
532 {
533 this.level = i;
534 this.el.level.innerHTML = this.level;
535 };
536
537 /**
538 * Set lines - update html
539 * @param int i
540 * @return void
541 * @access public
542 */
543 this.setLines = function(i)
544 {
545 this.lines = i;
546 this.el.lines.innerHTML = this.lines;
547 };
548
549 /**
550 * Number of puzzles created on current level
551 * @param int i
552 * @return void
553 * @access public
554 */
555 this.setPuzzles = function(i)
556 {
557 this.puzzles = i;
558 };
559
560 /**
561 * @param int i
562 * @return void
563 * @access public
564 */
565 this.setActions = function(i)
566 {
567 this.actions = i;
568 };
569
570 /**
571 * @return int
572 * @access public
573 */
574 this.getScore = function()
575 {
576 return this.score;
577 };
578
579 /**
580 * @return int
581 * @access public
582 */
583 this.getLevel = function()
584 {
585 return this.level;
586 };
587
588 /**
589 * @return int
590 * @access public
591 */
592 this.getLines = function()
593 {
594 return this.lines;
595 };
596
597 /**
598 * Number of puzzles created on current level
599 * @return int
600 * @access public
601 */
602 this.getPuzzles = function()
603 {
604 return this.puzzles;
605 };
606
607 /**
608 * @return int
609 * @access public
610 */
611 this.getActions = function()
612 {
613 return this.actions;
614 };
615 }
616
617 /**
618 * Area consists of blocks (2 dimensional board).
619 * Block contains "0" (if empty) or Html Object.
620 * @param int x
621 * @param int y
622 * @param string id
623 */
624 function Area(unit, x, y, id)
625 {
626 this.unit = unit;
627 this.x = x;
628 this.y = y;
629 this.el = document.getElementById(id);
630
631 this.board = [];
632
633 // create 2-dimensional board
634 for (var y = 0; y < this.y; y++) {
635 this.board.push(new Array());
636 for (var x = 0; x < this.x; x++) {
637 this.board[y].push(0);
638 }
639 }
640
641 /**
642 * Removing html elements from area.
643 * @return void
644 * @access public
645 */
646 this.destroy = function()
647 {
648 for (var y = 0; y < this.board.length; y++) {
649 for (var x = 0; x < this.board[y].length; x++) {
650 if (this.board[y][x]) {
651 this.el.removeChild(this.board[y][x]);
652 this.board[y][x] = 0;
653 }
654 }
655 }
656 };
657
658 /**
659 * Searching for full lines.
660 * Must go from the bottom of area to the top.
661 * Returns the number of lines removed - needed for Stats.score.
662 * @see isLineFull() removeLine()
663 * @return void
664 * @access public
665 */
666 this.removeFullLines = function()
667 {
668 var lines = 0;
669 for (var y = this.y - 1; y > 0; y--) {
670 if (this.isLineFull(y)) {
671 this.removeLine(y);
672 lines++;
673 y++;
674 }
675 }
676 return lines;
677 };
678
679 /**
680 * @param int y
681 * @return bool
682 * @access public
683 */
684 this.isLineFull = function(y)
685 {
686 for (var x = 0; x < this.x; x++) {
687 if (!this.board[y][x]) { return false; }
688 }
689 return true;
690 };
691
692 /**
693 * Remove given line
694 * Remove html objects
695 * All lines that are above given line move down by 1 unit
696 * @param int y
697 * @return void
698 * @access public
699 */
700 this.removeLine = function(y)
701 {
702 for (var x = 0; x < this.x; x++) {
703 this.el.removeChild(this.board[y][x]);
704 this.board[y][x] = 0;
705 }
706 y--;
707 for (; y > 0; y--) {
708 for (var x = 0; x < this.x; x++) {
709 if (this.board[y][x]) {
710 var el = this.board[y][x];
711 el.style.top = el.offsetTop + this.unit + "px";
712 this.board[y+1][x] = el;
713 this.board[y][x] = 0;
714 }
715 }
716 }
717 };
718
719 /**
720 * @param int y
721 * @param int x
722 * @return mixed 0 or Html Object
723 * @access public
724 */
725 this.getBlock = function(y, x)
726 {
727 if (y < 0) { return 0; }
728 if (y < this.y && x < this.x) {
729 return this.board[y][x];
730 } else {
731 throw "Area.getBlock("+y+", "+x+") failed";
732 }
733 };
734
735 /**
736 * Add Html Element to the area.
737 * Find (x,y) position using offsetTop and offsetLeft
738 * @param object el
739 * @return void
740 * @access public
741 */
742 this.addElement = function(el)
743 {
744 var x = parseInt(el.offsetLeft / this.unit);
745 var y = parseInt(el.offsetTop / this.unit);
746 if (y >= 0 && y < this.y && x >= 0 && x < this.x) {
747 this.board[y][x] = el;
748 } else {
749 // not always an error ..
750 }
751 };
752 }
753
754 /**
755 * Puzzle consists of blocks.
756 * Each puzzle after rotating 4 times, returns to its primitive position.
757 */
758 function Puzzle(tetris, area)
759 {
760 var self = this;
761 this.tetris = tetris;
762 this.area = area;
763
764 // timeout ids
765 this.fallDownID = null;
766 this.forceMoveDownID = null;
767
768 this.type = null; // 0..6
769 this.nextType = null; // next puzzle
770 this.position = null; // 0..3
771 this.speed = null;
772 this.running = null;
773 this.stopped = null;
774
775 this.board = []; // filled with html elements after placing on area
776 this.elements = [];
777 this.nextElements = []; // next board elements
778
779 // (x,y) position of the puzzle (top-left)
780 this.x = null;
781 this.y = null;
782
783 // width & height must be the same
784 this.puzzles = [
785 [
786 [0,0,1],
787 [1,1,1],
788 [0,0,0]
789 ],
790 [
791 [1,0,0],
792 [1,1,1],
793 [0,0,0]
794 ],
795 [
796 [0,1,1],
797 [1,1,0],
798 [0,0,0]
799 ],
800 [
801 [1,1,0],
802 [0,1,1],
803 [0,0,0]
804 ],
805 [
806 [0,1,0],
807 [1,1,1],
808 [0,0,0]
809 ],
810 [
811 [1,1],
812 [1,1]
813 ],
814 [
815 [0,0,0,0],
816 [1,1,1,1],
817 [0,0,0,0],
818 [0,0,0,0]
819 ]
820 ];
821
822 /**
823 * Reset puzzle. It does not destroy html elements in this.board.
824 * @return void
825 * @access public
826 */
827 this.reset = function()
828 {
829 if (this.fallDownID) {
830 clearTimeout(this.fallDownID);
831 }
832 if (this.forceMoveDownID) {
833 clearTimeout(this.forceMoveDownID);
834 }
835 this.type = this.nextType;
836 this.nextType = random(this.puzzles.length);
837 this.position = 0;
838 this.speed = 80 + (700 / this.tetris.stats.getLevel());
839 this.running = false;
840 this.stopped = false;
841 this.board = [];
842 this.elements = [];
843 for (var i = 0; i < this.nextElements.length; i++) {
844 document.getElementById("tetris-nextpuzzle").removeChild(this.nextElements[i]);
845 }
846 this.nextElements = [];
847 this.x = null;
848 this.y = null;
849 };
850
851 this.nextType = random(this.puzzles.length);
852 this.reset();
853
854 /**
855 * Check whether puzzle is running.
856 * @return bool
857 * @access public
858 */
859 this.isRunning = function()
860 {
861 return this.running;
862 };
863
864 /**
865 * Check whether puzzle has been stopped by user. It happens when user clicks
866 * "down" when puzzle is already at the bottom of area. The puzzle may still
867 * be running with event fallDown(). When puzzle is stopped, no actions will be
868 * performed when user press a key.
869 * @return bool
870 * @access public
871 */
872 this.isStopped = function()
873 {
874 return this.stopped;
875 };
876
877 /**
878 * Get X position of puzzle (top-left)
879 * @return int
880 * @access public
881 */
882 this.getX = function()
883 {
884 return this.x;
885 };
886
887 /**
888 * Get Y position of puzzle (top-left)
889 * @return int
890 * @access public
891 */
892 this.getY = function()
893 {
894 return this.y;
895 };
896
897 /**
898 * Check whether new puzzle may be placed on the area.
899 * Find (x,y) in area where beginning of the puzzle will be placed.
900 * Check if first puzzle line (checking from the bottom) can be placed on the area.
901 * @return bool
902 * @access public
903 */
904 this.mayPlace = function()
905 {
906 var puzzle = this.puzzles[this.type];
907 var areaStartX = parseInt((this.area.x - puzzle[0].length) / 2);
908 var areaStartY = 1;
909 var lineFound = false;
910 var lines = 0;
911 for (var y = puzzle.length - 1; y >= 0; y--) {
912 for (var x = 0; x < puzzle[y].length; x++) {
913 if (puzzle[y][x]) {
914 lineFound = true;
915 if (this.area.getBlock(areaStartY, areaStartX + x)) { return false; }
916 }
917 }
918 if (lineFound) {
919 lines++;
920 }
921 if (areaStartY - lines < 0) {
922 break;
923 }
924 }
925 return true;
926 };
927
928 /**
929 * Create empty board, create blocks in area - html objects, update puzzle board.
930 * Check puzzles on current level, increase level if needed.
931 * @return void
932 * @access public
933 */
934 this.place = function()
935 {
936 // stats
937 this.tetris.stats.setPuzzles(this.tetris.stats.getPuzzles() + 1);
938 if (this.tetris.stats.getPuzzles() >= (10 + this.tetris.stats.getLevel() * 2)) {
939 this.tetris.stats.setLevel(this.tetris.stats.getLevel() + 1);
940 this.tetris.stats.setPuzzles(0);
941 }
942 // init
943 var puzzle = this.puzzles[this.type];
944 var areaStartX = parseInt((this.area.x - puzzle[0].length) / 2);
945 var areaStartY = 1;
946 var lineFound = false;
947 var lines = 0;
948 this.x = areaStartX;
949 this.y = 1;
950 this.board = this.createEmptyPuzzle(puzzle.length, puzzle[0].length);
951 // create puzzle
952 for (var y = puzzle.length - 1; y >= 0; y--) {
953 for (var x = 0; x < puzzle[y].length; x++) {
954 if (puzzle[y][x]) {
955 lineFound = true;
956 var el = document.createElement("div");
957 el.className = "block" + this.type;
958 el.style.left = (areaStartX + x) * this.area.unit + "px";
959 el.style.top = (areaStartY - lines) * this.area.unit + "px";
960 this.area.el.appendChild(el);
961 this.board[y][x] = el;
962 this.elements.push(el);
963 }
964 }
965 if (lines) {
966 this.y--;
967 }
968 if (lineFound) {
969 lines++;
970 }
971 }
972 this.running = true;
973 this.fallDownID = setTimeout(this.fallDown, this.speed);
974 // next puzzle
975 var nextPuzzle = this.puzzles[this.nextType];
976 for (var y = 0; y < nextPuzzle.length; y++) {
977 for (var x = 0; x < nextPuzzle[y].length; x++) {
978 if (nextPuzzle[y][x]) {
979 var el = document.createElement("div");
980 el.className = "block" + this.nextType;
981 el.style.left = (x * this.area.unit) + "px";
982 el.style.top = (y * this.area.unit) + "px";
983 document.getElementById("tetris-nextpuzzle").appendChild(el);
984 this.nextElements.push(el);
985 }
986 }
987 }
988 };
989
990 /**
991 * Remove puzzle from the area.
992 * Clean some other stuff, see reset()
993 * @return void
994 * @access public
995 */
996 this.destroy = function()
997 {
998 for (var i = 0; i < this.elements.length; i++) {
999 this.area.el.removeChild(this.elements[i]);
1000 }
1001 this.elements = [];
1002 this.board = [];
1003 this.reset();
1004 };
1005
1006 /**
1007 * @param int y
1008 * @param int x
1009 * @return array
1010 * @access private
1011 */
1012 this.createEmptyPuzzle = function(y, x)
1013 {
1014 var puzzle = [];
1015 for (var y2 = 0; y2 < y; y2++) {
1016 puzzle.push(new Array());
1017 for (var x2 = 0; x2 < x; x2++) {
1018 puzzle[y2].push(0);
1019 }
1020 }
1021 return puzzle;
1022 };
1023
1024 /**
1025 * Puzzle fall from the top to the bottom.
1026 * After placing a puzzle, this event will be called as long as the puzzle is running.
1027 * @see place() stop()
1028 * @return void
1029 * @access event
1030 */
1031 this.fallDown = function()
1032 {
1033 if (self.isRunning()) {
1034 if (self.mayMoveDown()) {
1035 self.moveDown();
1036 self.fallDownID = setTimeout(self.fallDown, self.speed);
1037 } else {
1038 // move blocks into area board
1039 for (var i = 0; i < self.elements.length; i++) {
1040 self.area.addElement(self.elements[i]);
1041 }
1042 // stats
1043 var lines = self.area.removeFullLines();
1044 if (lines) {
1045 self.tetris.stats.setLines(self.tetris.stats.getLines() + lines);
1046 self.tetris.stats.setScore(self.tetris.stats.getScore() + (1000 * self.tetris.stats.getLevel() * lines));
1047 }
1048 // reset puzzle
1049 self.reset();
1050 if (self.mayPlace()) {
1051 self.place();
1052 } else {
1053 self.tetris.gameOver();
1054 }
1055 }
1056 }
1057 };
1058
1059 /**
1060 * After clicking "space" the puzzle is forced to move down, no user action is performed after
1061 * this event is called. this.running must be set to false. This func is similiar to fallDown()
1062 * Also update score & actions - like Tetris.down()
1063 * @see fallDown()
1064 * @return void
1065 * @access public event
1066 */
1067 this.forceMoveDown = function()
1068 {
1069 if (!self.isRunning() && !self.isStopped()) {
1070 if (self.mayMoveDown()) {
1071 // stats: score, actions
1072 self.tetris.stats.setScore(self.tetris.stats.getScore() + 5 + self.tetris.stats.getLevel());
1073 self.tetris.stats.setActions(self.tetris.stats.getActions() + 1);
1074 self.moveDown();
1075 self.forceMoveDownID = setTimeout(self.forceMoveDown, 30);
1076 } else {
1077 // move blocks into area board
1078 for (var i = 0; i < self.elements.length; i++) {
1079 self.area.addElement(self.elements[i]);
1080 }
1081 // stats: lines
1082 var lines = self.area.removeFullLines();
1083 if (lines) {
1084 self.tetris.stats.setLines(self.tetris.stats.getLines() + lines);
1085 self.tetris.stats.setScore(self.tetris.stats.getScore() + (1000 * self.tetris.stats.getLevel() * lines));
1086 }
1087 // reset puzzle
1088 self.reset();
1089 if (self.mayPlace()) {
1090 self.place();
1091 } else {
1092 self.tetris.gameOver();
1093 }
1094 }
1095 }
1096 };
1097
1098 /**
1099 * Stop the puzzle falling
1100 * @return void
1101 * @access public
1102 */
1103 this.stop = function()
1104 {
1105 this.running = false;
1106 };
1107
1108 /**
1109 * Check whether puzzle may be rotated.
1110 * Check down, left, right, rotate
1111 * @return bool
1112 * @access public
1113 */
1114 this.mayRotate = function()
1115 {
1116 for (var y = 0; y < this.board.length; y++) {
1117 for (var x = 0; x < this.board[y].length; x++) {
1118 if (this.board[y][x]) {
1119 var newY = this.getY() + this.board.length - 1 - x;
1120 var newX = this.getX() + y;
1121 if (newY >= this.area.y) { return false; }
1122 if (newX < 0) { return false; }
1123 if (newX >= this.area.x) { return false; }
1124 if (this.area.getBlock(newY, newX)) { return false; }
1125 }
1126 }
1127 }
1128 return true;
1129 };
1130
1131 /**
1132 * Rotate the puzzle to the left.
1133 * @return void
1134 * @access public
1135 */
1136 this.rotate = function()
1137 {
1138 var puzzle = this.createEmptyPuzzle(this.board.length, this.board[0].length);
1139 for (var y = 0; y < this.board.length; y++) {
1140 for (var x = 0; x < this.board[y].length; x++) {
1141 if (this.board[y][x]) {
1142 var newY = puzzle.length - 1 - x;
1143 var newX = y;
1144 var el = this.board[y][x];
1145 var moveY = newY - y;
1146 var moveX = newX - x;
1147 el.style.left = el.offsetLeft + (moveX * this.area.unit) + "px";
1148 el.style.top = el.offsetTop + (moveY * this.area.unit) + "px";
1149 puzzle[newY][newX] = el;
1150 }
1151 }
1152 }
1153 this.board = puzzle;
1154 };
1155
1156 /**
1157 * Check whether puzzle may be moved down.
1158 * - is any other puzzle on the way ?
1159 * - is it end of the area ?
1160 * If false, then true is assigned to variable this.stopped - no user actions will be performed to this puzzle,
1161 * so this func should be used carefully, only in Tetris.down() and Tetris.puzzle.fallDown()
1162 * @return bool
1163 * @access public
1164 */
1165 this.mayMoveDown = function()
1166 {
1167 for (var y = 0; y < this.board.length; y++) {
1168 for (var x = 0; x < this.board[y].length; x++) {
1169 if (this.board[y][x]) {
1170 if (this.getY() + y + 1 >= this.area.y) { this.stopped = true; return false; }
1171 if (this.area.getBlock(this.getY() + y + 1, this.getX() + x)) { this.stopped = true; return false; }
1172 }
1173 }
1174 }
1175 return true;
1176 };
1177
1178 /**
1179 * Move the puzzle down by 1 unit.
1180 * @return void
1181 * @access public
1182 */
1183 this.moveDown = function()
1184 {
1185 for (var i = 0; i < this.elements.length; i++) {
1186 this.elements[i].style.top = this.elements[i].offsetTop + this.area.unit + "px";
1187 }
1188 this.y++;
1189 };
1190
1191 /**
1192 * Check whether puzzle may be moved left.
1193 * - is any other puzzle on the way ?
1194 * - is the end of the area
1195 * @return bool
1196 * @access public
1197 */
1198 this.mayMoveLeft = function()
1199 {
1200 for (var y = 0; y < this.board.length; y++) {
1201 for (var x = 0; x < this.board[y].length; x++) {
1202 if (this.board[y][x]) {
1203 if (this.getX() + x - 1 < 0) { return false; }
1204 if (this.area.getBlock(this.getY() + y, this.getX() + x - 1)) { return false; }
1205 }
1206 }
1207 }
1208 return true;
1209 };
1210
1211 /**
1212 * Move the puzzle left by 1 unit
1213 * @return void
1214 * @access public
1215 */
1216 this.moveLeft = function()
1217 {
1218 for (var i = 0; i < this.elements.length; i++) {
1219 this.elements[i].style.left = this.elements[i].offsetLeft - this.area.unit + "px";
1220 }
1221 this.x--;
1222 };
1223
1224 /**
1225 * Check whether puzle may be moved right.
1226 * - is any other puzzle on the way ?
1227 * - is the end of the area
1228 * @return bool
1229 * @access public
1230 */
1231 this.mayMoveRight = function()
1232 {
1233 for (var y = 0; y < this.board.length; y++) {
1234 for (var x = 0; x < this.board[y].length; x++) {
1235 if (this.board[y][x]) {
1236 if (this.getX() + x + 1 >= this.area.x) { return false; }
1237 if (this.area.getBlock(this.getY() + y, this.getX() + x + 1)) { return false; }
1238 }
1239 }
1240 }
1241 return true;
1242 };
1243
1244 /**
1245 * Move the puzzle right by 1 unit.
1246 * @return void
1247 * @access public
1248 */
1249 this.moveRight = function()
1250 {
1251 for (var i = 0; i < this.elements.length; i++) {
1252 this.elements[i].style.left = this.elements[i].offsetLeft + this.area.unit + "px";
1253 }
1254 this.x++;
1255 };
1256 }
1257
1258 /**
1259 * Generates random number that is >= 0 and < i
1260 * @return int
1261 * @access private
1262 */
1263 function random(i)
1264 {
1265 return Math.floor(Math.random() * i);
1266 }
1267
1268 /**
1269 * Store highscores in cookie.
1270 */
1271 function Highscores(maxscores)
1272 {
1273 this.maxscores = maxscores;
1274 this.scores = [];
1275
1276 /**
1277 * Load scores from cookie.
1278 * Note: it is automatically called when creating new instance of object Highscores.
1279 * @return void
1280 * @access public
1281 */
1282 this.load = function()
1283 {
1284 var cookie = new Cookie();
1285 var s = cookie.get("tetris-highscores");
1286 this.scores = [];
1287 if (s.length) {
1288 var scores = s.split("|");
1289 for (var i = 0; i < scores.length; ++i) {
1290 var a = scores[i].split(":");
1291 this.scores.push(new Score(a[0], Number(a[1])));
1292 }
1293 }
1294 };
1295
1296 /**
1297 * Save scores to cookie.
1298 * Note: it is automatically called after adding new score.
1299 * @return void
1300 * @access public
1301 */
1302 this.save = function()
1303 {
1304 var cookie = new Cookie();
1305 var a = [];
1306 for (var i = 0; i < this.scores.length; ++i) {
1307 a.push(this.scores[i].name+":"+this.scores[i].score);
1308 }
1309 var s = a.join("|");
1310 cookie.set("tetris-highscores", s, 3600*24*1000);
1311 };
1312
1313 /**
1314 * Is the score high enough to be able to add ?
1315 * @return bool
1316 * @access public
1317 */
1318 this.mayAdd = function(score)
1319 {
1320 if (this.scores.length < this.maxscores) { return true; }
1321 for (var i = this.scores.length - 1; i >= 0; --i) {
1322 if (this.scores[i].score < score) { return true; }
1323 }
1324 return false;
1325 };
1326
1327 /**
1328 * @param string name
1329 * @param int score
1330 * @return void
1331 * @access public
1332 */
1333 this.add = function(name, score)
1334 {
1335 name = name.replace(/[;=:|]/g, "?");
1336 name = name.replace(/</g, "<").replace(/>/g, ">");
1337 if (this.scores.length < this.maxscores) {
1338 this.scores.push(new Score(name, score));
1339 } else {
1340 for (var i = this.scores.length - 1; i >= 0; --i) {
1341 if (this.scores[i].score < score) {
1342 this.scores.removeByIndex(i);
1343 this.scores.push(new Score(name, score));
1344 break;
1345 }
1346 }
1347 }
1348 this.sort();
1349 this.save();
1350 };
1351
1352 /**
1353 * Get array of scores.
1354 * @return array [Score, Score, ..]
1355 * @access public
1356 */
1357 this.getScores = function()
1358 {
1359 return this.scores;
1360 };
1361
1362 /**
1363 * All highscores returned in html friendly format.
1364 * @return string
1365 * @access public
1366 */
1367 this.toHtml = function()
1368 {
1369 var s = '<table cellspacing="0" cellpadding="2"><tr><th></th><th>Name</th><th>Score</th></tr>';
1370 for (var i = 0; i < this.scores.length; ++i) {
1371 s += '<tr><td>?.</td><td>?</td><td>?</td></tr>'.format(i+1, this.scores[i].name, this.scores[i].score);
1372 }
1373 s += '</table>';
1374 return s;
1375 };
1376
1377 /**
1378 * Sort table with scores.
1379 * @return void
1380 * @access private
1381 */
1382 this.sort = function()
1383 {
1384 var scores = this.scores;
1385 var len = scores.length;
1386 this.scores = [];
1387 for (var i = 0; i < len; ++i) {
1388 var el = null, index = null;
1389 for (var j = 0; j < scores.length; ++j) {
1390 if (!el || (scores[j].score > el.score)) {
1391 el = scores[j];
1392 index = j;
1393 }
1394 }
1395 scores.removeByIndex(index);
1396 this.scores.push(el);
1397 }
1398 };
1399
1400 /* Simple score object. */
1401 function Score(name, score)
1402 {
1403 this.name = name;
1404 this.score = score;
1405 }
1406
1407 this.load();
1408 }
1409
1410 /**
1411 * Managing cookies.
1412 */
1413 function Cookie()
1414 {
1415 /**
1416 * @param string name
1417 * @return string
1418 * @access public
1419 */
1420 this.get = function(name)
1421 {
1422 var cookies = document.cookie.split(";");
1423 for (var i = 0; i < cookies.length; ++i) {
1424 var a = cookies[i].split("=");
1425 if (a.length == 2) {
1426 a[0] = a[0].trim();
1427 a[1] = a[1].trim();
1428 if (a[0] == name) {
1429 return unescape(a[1]);
1430 }
1431 }
1432 }
1433 return "";
1434 };
1435
1436 /**
1437 * @param string name
1438 * @param string value (do not use special chars like ";" "=")
1439 * @param int seconds
1440 * @param string path
1441 * @param string domain
1442 * @param bool secure
1443 * @return void
1444 * @access public
1445 */
1446 this.set = function(name, value, seconds, path, domain, secure)
1447 {
1448 this.del(name);
1449 if (!path) path = '/';
1450
1451 var cookie = (name + "=" + escape(value));
1452 if (seconds) {
1453 var date = new Date(new Date().getTime()+seconds*1000);
1454 cookie += ("; expires="+date.toGMTString());
1455 }
1456 cookie += (path ? "; path="+path : "");
1457 cookie += (domain ? "; domain="+domain : "");
1458 cookie += (secure ? "; secure" : "");
1459 document.cookie = cookie;
1460 };
1461
1462 /**
1463 * @param name
1464 * @return void
1465 * @access public
1466 */
1467 this.del = function(name)
1468 {
1469 document.cookie = name + "=; expires=Thu, 01-Jan-70 00:00:01 GMT";
1470 };
1471 }
1472 }
1473
1474 if (!String.prototype.trim) {
1475 String.prototype.trim = function() {
1476 return this.replace(/^\s*|\s*$/g, "");
1477 };
1478 }
1479
1480 if (!Array.prototype.removeByIndex) {
1481 Array.prototype.removeByIndex = function(index) {
1482 this.splice(index, 1);
1483 };
1484 }
1485
1486 if (!String.prototype.format) {
1487 String.prototype.format = function() {
1488 if (!arguments.length) { throw "String.format() failed, no arguments passed, this = "+this; }
1489 var tokens = this.split("?");
1490 if (arguments.length != (tokens.length - 1)) { throw "String.format() failed, tokens != arguments, this = "+this; }
1491 var s = tokens[0];
1492 for (var i = 0; i < arguments.length; ++i) {
1493 s += (arguments[i] + tokens[i + 1]);
1494 }
1495 return s;
1496 };
1497 }