├── README.md
├── css
├── gallery.css
├── setup.css
└── style.css
├── favicon.ico
├── index.html
├── js
├── ai.js
├── app.js
├── attacker.ai.js
├── attacker.human.js
├── attacker.random.js
├── cell.js
├── classList.js
├── defender.ai.js
├── defender.human.js
├── engine.js
├── engine.network.js
├── gallery.js
├── game.js
├── piece.js
├── pit.js
├── player.js
└── xy.js
├── license.txt
└── screenshot.png
/README.md:
--------------------------------------------------------------------------------
1 | # Custom Tetris
2 |
3 | Play the classic Tetris game **the way you like it!** Adjust the rules, *change* the sides!
4 | Playable version at http://ondras.github.io/custom-tetris/.
5 |
6 | 
7 |
8 | ## Instructions
9 |
10 | Configure the game by choosing an attacker (the one who picks pieces) and a defender (the one who positions pieces).
11 | The attacker is given a stash of pieces to pick from; once they are depleted, the stash completely refreshes.
12 |
13 | When playing human attacker, either click the pieces or hit number keys to pick them.
14 |
15 | When playing human defender, use four arrow keys to position, rotate and drop the current piece.
16 |
17 | ## Technologies
18 |
19 | * Written in vanilla JS, no libraries used
20 | * Polyfill for `classList`
21 | * Uses CSS transforms and transitions
22 | * Uses [Firebase](https://www.firebase.com/) as a networking backend
23 | * Works in FF, Chrome, Opera, Safari, IE10
24 |
25 | ## DevLog
26 |
27 | * Day 1: forked
28 | * Days 2 to 3: looking for ideas
29 | * Day 4: got an idea, started first API draft
30 | * Day 5: experiments with the main Engine object, still no visuals
31 | * Day 6: working engine, visuals, human defender, ai defender
32 | * Day 7: bugfixing async issues, basic set of pieces, readonly gallery
33 | * Day 8: human (working) and ai (unusable) attackers, bonus pieces
34 | * Day 9: refactored attacker rules, UI tuning, Game.App draft
35 | * Day 10: setup texts and UI
36 | * Day 11: finalizing setup UI, basic network experiments
37 | * Days 12 to 13: tuning network synchronization
38 | * Days 14 to 17: idle, frustrated by poor networking experience
39 | * Day 18: refactoring networking code
40 | * Day 19: rejoicing at the fixed networking experience, tuning, testing
41 | * Day 20: release
42 | * Day 28: final fixes and testing
43 |
44 | ## License
45 |
46 | This game is distributed under the terms of the New BSD license.
47 |
--------------------------------------------------------------------------------
/css/gallery.css:
--------------------------------------------------------------------------------
1 | .gallery {
2 | position: relative;
3 | border: 3px solid #999;
4 | border-radius: 15px;
5 | float: left;
6 | margin: 0.5em;
7 | text-align: center;
8 | }
9 |
10 | .gallery.disabled {
11 | opacity: 0.5;
12 | }
13 |
14 | .gallery.next {
15 | box-shadow: 0 0 4px 2px #f00;
16 | border-color: #ccc;
17 | }
18 |
19 | .attacker-human .gallery:hover:not(.disabled) {
20 | cursor: pointer;
21 | border-color: #ccc;
22 | }
23 |
--------------------------------------------------------------------------------
/css/setup.css:
--------------------------------------------------------------------------------
1 | #setup {
2 | -moz-box-sizing: border-box;
3 | box-sizing: border-box;
4 | width: 100%;
5 | height: 100%;
6 | position: absolute;
7 | left: 0;
8 | top: 0;
9 | -webkit-transition: all 0.5s;
10 | transition: all 0.5s;
11 | background-color: #444;
12 | padding: 0 20%;
13 | font-size: 120%;
14 | }
15 |
16 | #setup #attacker span, #setup #defender span {
17 | display: inline-block;
18 | width: 6em;
19 | }
20 |
21 | #play {
22 | float: right;
23 | font-size: 200%;
24 | border-radius: 1em;
25 | padding: 0.2em 0.5em;
26 | }
27 |
28 | #play:not([disabled]) {
29 | cursor: pointer;
30 | }
31 |
32 | #setup.playing {
33 | -webkit-transform: translate(0, -100%);
34 | transform: translate(0, -100%);
35 | }
36 |
37 | #setup #description {
38 | color: #6f6;
39 | font-variant: small-caps;
40 | }
41 |
42 | #setup #network {
43 | font-family: monospace;
44 | }
45 |
46 | #setup #connect {
47 | cursor: pointer;
48 | }
49 |
50 | #setup #connect.connected:before {
51 | color: #0f0;
52 | content: "✓ ";
53 | }
54 |
55 | .local #setup #network {
56 | display: none;
57 | }
58 |
59 | footer {
60 | position: absolute;
61 | bottom: 2px;
62 | right: 2px;
63 | font-size: 80%;
64 | }
65 |
66 | footer a {
67 | color: #fff;
68 | }
69 |
--------------------------------------------------------------------------------
/css/style.css:
--------------------------------------------------------------------------------
1 | @import url(http://fonts.googleapis.com/css?family=Lato:400,700);
2 | @import url(gallery.css);
3 | @import url(setup.css);
4 |
5 | html, body {
6 | margin: 0;
7 | height: 100%;
8 | background-color: #666;
9 | color: #fff;
10 | text-shadow: 1px 1px 2px #000;
11 | }
12 |
13 | body, select, button {
14 | font-family: lato;
15 | font-size: 100%;
16 | }
17 |
18 | h1 {
19 | text-align: center;
20 | }
21 |
22 | #left, #right {
23 | width: 50%;
24 | height: 100%;
25 | float: left;
26 | -moz-box-sizing: border-box;
27 | box-sizing: border-box;
28 | position: relative;
29 | }
30 |
31 | #left {
32 | padding-top: 4em;
33 | border-right: 2px solid #ccc;
34 | }
35 |
36 | #right {
37 | border-left: 2px solid #ccc;
38 | padding-top: 8em;
39 | padding-left: 0.5em;
40 | }
41 |
42 | #info {
43 | position: absolute;
44 | top: 0;
45 | left: 50%;
46 | -webkit-transform: translate(-50%, 0);
47 | transform: translate(-50%, 0);
48 | border: 4px solid #ccc;
49 | border-top: none;
50 | text-align: center;
51 | width: 250px;
52 | border-radius: 0 0 50px 50px;
53 | background-color: #666;
54 | font-size: 130%;
55 | }
56 |
57 | #score {
58 | color: #66f;
59 | }
60 |
61 | #status {
62 | color: #f66;
63 | }
64 |
65 | #left #defender, #right #attacker {
66 | position: absolute;
67 | top: 0;
68 | padding-top: 1em;
69 | left: 50%;
70 | -webkit-transform: translate(-50%, 0);
71 | transform: translate(-50%, 0);
72 | }
73 |
74 | .pit {
75 | position: relative;
76 | overflow: hidden;
77 | border: 4px solid #ccc;
78 | border-top: none;
79 | margin: auto;
80 | }
81 |
82 | .piece {
83 | position: absolute;
84 | -webkit-transition: all 150ms;
85 | transition: all 150ms;
86 | }
87 |
88 | .cell {
89 | position: absolute;
90 | border-radius: 3px;
91 | box-shadow: 1px 1px 1px #ddd inset, -1px -1px 1px #222 inset;
92 | -webkit-transition: all 150ms;
93 | transition: all 150ms;
94 | }
95 |
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ondras/custom-tetris/0e7101ab2c80a872c64152e5b60b949e7b0c978b/favicon.ico
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Custom Tetris
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
Score:
31 |
Status:
32 |
33 |
34 |
Custom Tetris
35 |
Change the way Tetris is played!
36 |
In a traditional Tetris setup, you play as a defender ,
37 | trying to position blocks correctly and remain alive as long as possible. This version
38 | allows you to customize both roles: defending (as a human or AI) and
39 | attacking (picking blocks as a human, AI or randomly). You can even change them during the gameplay.
40 |
All of these possibilities can be used in a networked multiplayer game as well,
41 | resulting in a total of 12 games in 1!
42 |
Play!
43 |
44 | Attacker:
45 | Human
46 | AI
47 | Random
48 | Network
49 |
50 |
51 |
52 | Defender:
53 | Human
54 | AI
55 | Network
56 |
57 |
58 |
This configuration is known as .
59 |
Connect https:// .firebaseio.com/tetris/
60 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/js/ai.js:
--------------------------------------------------------------------------------
1 | Game.AI = {}
2 |
3 | Game.AI.findBestPosition = function(pit, piece) {
4 | pit = pit.clone();
5 | piece = piece.clone();
6 | piece.center();
7 |
8 | /* shift leftwards */
9 | var left = new XY(-1, 0);
10 | while (piece.fits(pit)) { piece.xy = piece.xy.plus(left); }
11 | piece.xy = piece.xy.minus(left);
12 |
13 | /* move rightwards, test scores */
14 | var bestScore = Infinity;
15 | var bestPositions = [];
16 |
17 | while (piece.fits(pit)) {
18 | var tmpPit = pit.clone();
19 | tmpPit.drop(piece.clone());
20 | var score = tmpPit.getScore();
21 |
22 | if (score < bestScore) {
23 | bestScore = score;
24 | bestPositions = [];
25 | }
26 |
27 | if (score == bestScore) {
28 | bestPositions.push(piece.xy.x);
29 | }
30 |
31 | piece.xy = piece.xy.minus(left);
32 | }
33 |
34 | var x = bestPositions.random();
35 |
36 | return {
37 | score: bestScore,
38 | x: x
39 | }
40 | }
41 |
42 | Game.AI.findBestPositionRotation = function(pit, piece) {
43 | var bestScore = Infinity;
44 | var bestRotations = [];
45 |
46 | for (var i=0;i<4;i++) {
47 | var tmpPiece = piece.clone();
48 | for (var j=0;j -1 && avail.length > 1) { avail.splice(index, 1); }
24 |
25 | var pit = this._engine.pit;
26 | var current = this._engine.getPiece();
27 |
28 | if (current) { /* drop current piece based on its expected position/rotation */
29 | pit = pit.clone();
30 | current = current.clone();
31 |
32 | var best = Game.AI.findBestPositionRotation(pit, current);
33 | for (var i=0;i worstScore) {
45 | worstScore = score;
46 | worstTypes = [];
47 | }
48 | if (score == worstScore) { worstTypes.push(type); }
49 | }
50 |
51 | var type = worstTypes.random();
52 | this._lastType = type;
53 | this._engine.setNextType(type);
54 | }
55 |
--------------------------------------------------------------------------------
/js/attacker.human.js:
--------------------------------------------------------------------------------
1 | Game.Attacker.Human = function(engine) {
2 | Game.Player.call(this, engine);
3 | document.body.classList.add("attacker-human");
4 | document.body.addEventListener("click", this);
5 | window.addEventListener("keydown", this);
6 | }
7 | Game.Attacker.Human.prototype = Object.create(Game.Player.prototype);
8 |
9 | Game.Attacker.Human.prototype.destroy = function() {
10 | document.body.classList.remove("attacker-human");
11 | document.body.removeEventListener("click", this);
12 | window.removeEventListener("keydown", this);
13 | Game.Player.prototype.destroy.call(this);
14 | }
15 |
16 | Game.Attacker.Human.prototype.handleEvent = function(e) {
17 | switch (e.type) {
18 | case "click":
19 | var node = e.target;
20 | var type = null;
21 | while (node != document.body) {
22 | if (node.hasAttribute("data-type")) { type = node.getAttribute("data-type"); }
23 | node = node.parentNode;
24 | }
25 | if (type) { this._engine.setNextType(type); }
26 | break;
27 |
28 | case "keydown":
29 | var index = e.keyCode - "1".charCodeAt(0);
30 | if (index == -1) { index = 9; }
31 | var def = Object.keys(Game.Piece.DEF);
32 | var type = def[index];
33 | var avail = this._engine.getAvailableTypes();
34 | if (avail[type]) { this._engine.setNextType(type); }
35 | break;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/js/attacker.random.js:
--------------------------------------------------------------------------------
1 | Game.Attacker.Random = function(engine) {
2 | Game.Player.call(this, engine);
3 | this._interval = setInterval(this._poll.bind(this), Game.INTERVAL_ATTACKER);
4 | }
5 |
6 | Game.Attacker.Random.prototype = Object.create(Game.Player.prototype);
7 |
8 | Game.Attacker.Random.prototype.destroy = function() {
9 | clearInterval(this._interval);
10 | this._interval = null;
11 | Game.Player.prototype.destroy.call(this);
12 | }
13 |
14 | Game.Attacker.Random.prototype._poll = function() {
15 | var next = this._engine.getNextType();
16 | if (next) { return; }
17 |
18 | var type = Object.keys(Game.Piece.DEF).random();
19 | this._engine.setNextType(type);
20 | }
21 |
--------------------------------------------------------------------------------
/js/cell.js:
--------------------------------------------------------------------------------
1 | Game.Cell = function(xy, type) {
2 | this.xy = xy;
3 | this.type = type;
4 | this.node = null;
5 | }
6 |
7 | Object.defineProperty(Game.Cell.prototype, "xy", {
8 | get: function() {
9 | return this._xy;
10 | },
11 |
12 | set: function(xy) {
13 | this._xy = xy;
14 | if (this.node) { this._position(); }
15 | }
16 | });
17 |
18 | Game.Cell.prototype.build = function(parent) {
19 | this.node = document.createElement("div");
20 | this.node.classList.add("cell");
21 | this.node.style.width = Game.CELL + "px";
22 | this.node.style.height = Game.CELL + "px";
23 | this.node.style.backgroundColor = Game.Piece.DEF[this.type].color;
24 | this._position();
25 | parent.appendChild(this.node);
26 | return this;
27 | }
28 |
29 | Game.Cell.prototype.clone = function() {
30 | return new Game.Cell(this.xy, this.type);
31 | }
32 |
33 | Game.Cell.prototype._position = function() {
34 | this.node.style.left = (this.xy.x * Game.CELL) + "px";
35 | this.node.style.bottom = (this.xy.y * Game.CELL) + "px";
36 | return this;
37 | }
38 |
--------------------------------------------------------------------------------
/js/classList.js:
--------------------------------------------------------------------------------
1 | if (!("classList" in document.documentElement) && window.Element) {
2 | (function () {
3 | var prototype = Array.prototype,
4 | indexOf = prototype.indexOf,
5 | slice = prototype.slice,
6 | push = prototype.push,
7 | splice = prototype.splice,
8 | join = prototype.join;
9 |
10 | function DOMTokenList(elm) {
11 | this._element = elm;
12 | if (elm.className == this._classCache) { return; }
13 | this._classCache = elm.className;
14 | if (!this._classCache) { return; }
15 |
16 | var classes = this._classCache.replace(/^\s+|\s+$/g,'').split(/\s+/);
17 | for (var i = 0; i < classes.length; i++) {
18 | push.call(this, classes[i]);
19 | }
20 | }
21 | window.DOMTokenList = DOMTokenList;
22 |
23 | function setToClassName(el, classes) {
24 | el.className = classes.join(" ");
25 | }
26 |
27 | DOMTokenList.prototype = {
28 | add: function(token) {
29 | if (this.contains(token)) { return; }
30 | push.call(this, token);
31 | setToClassName(this._element, slice.call(this, 0));
32 | },
33 | contains: function(token) {
34 | return (indexOf.call(this, token) != -1);
35 | },
36 | item: function(index) {
37 | return this[index] || null;
38 | },
39 | remove: function(token) {
40 | var i = indexOf.call(this, token);
41 | if (i == -1) { return; }
42 | splice.call(this, i, 1);
43 | setToClassName(this._element, slice.call(this, 0));
44 | },
45 | toString: function() {
46 | return join.call(this, " ");
47 | },
48 | toggle: function(token) {
49 | if (indexOf.call(this, token) == -1) {
50 | this.add(token);
51 | return true;
52 | } else {
53 | this.remove(token);
54 | return false;
55 | }
56 | }
57 | };
58 |
59 | function defineElementGetter (obj, prop, getter) {
60 | if (Object.defineProperty) {
61 | Object.defineProperty(obj, prop, {
62 | get: getter
63 | });
64 | } else {
65 | obj.__defineGetter__(prop, getter);
66 | }
67 | }
68 |
69 | defineElementGetter(Element.prototype, "classList", function() {
70 | return new DOMTokenList(this);
71 | });
72 | })();
73 | }
74 |
--------------------------------------------------------------------------------
/js/defender.ai.js:
--------------------------------------------------------------------------------
1 | Game.Defender.AI = function(engine) {
2 | Game.Player.call(this, engine);
3 | this._interval = setInterval(this._poll.bind(this), Game.INTERVAL_DEFENDER);
4 | this._currentPiece = null;
5 | this._currentTarget = null;
6 | }
7 | Game.Defender.AI.prototype = Object.create(Game.Player.prototype);
8 |
9 | Game.Defender.AI.prototype.destroy = function() {
10 | clearInterval(this._interval);
11 | this._interval = null;
12 | Game.Player.prototype.destroy.call(this);
13 | }
14 |
15 | Game.Defender.AI.prototype._poll = function() {
16 | var piece = this._engine.getPiece();
17 | if (!piece) { return; }
18 |
19 | if (piece != this._currentPiece) {
20 | this._currentPiece = piece;
21 |
22 | var pit = this._engine.pit;
23 | this._currentTarget = Game.AI.findBestPositionRotation(pit, piece);
24 | }
25 |
26 | if (this._currentTarget.rotation) {
27 | this._currentTarget.rotation--;
28 | this._engine.rotate();
29 | return;
30 | }
31 |
32 | var diff = (this._currentTarget.x - this._currentPiece.xy.x);
33 | if (!diff) {
34 | this._engine.drop();
35 | return;
36 | }
37 |
38 | this._engine.shift(diff > 0 ? 1 : -1)
39 | }
40 |
--------------------------------------------------------------------------------
/js/defender.human.js:
--------------------------------------------------------------------------------
1 | Game.Defender.Human = function(engine) {
2 | Game.Player.call(this, engine);
3 | window.addEventListener("keydown", this);
4 | }
5 | Game.Defender.Human.prototype = Object.create(Game.Player.prototype);
6 |
7 | Game.Defender.Human.prototype.destroy = function() {
8 | window.removeEventListener("keydown", this);
9 | Game.Player.prototype.destroy.call(this);
10 | }
11 |
12 | Game.Defender.Human.prototype.handleEvent = function(e) {
13 | switch (e.keyCode) {
14 | case 37: /* left */
15 | e.preventDefault();
16 | this._engine.shift(-1);
17 | break;
18 |
19 | case 39: /* right */
20 | e.preventDefault();
21 | this._engine.shift(+1);
22 | break;
23 |
24 | case 38: /* top */
25 | e.preventDefault();
26 | this._engine.rotate();
27 | break;
28 |
29 | case 40: /* bottom */
30 | e.preventDefault();
31 | this._engine.drop();
32 | break;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/js/engine.js:
--------------------------------------------------------------------------------
1 | Game.Engine = function() {
2 | this._status = {
3 | score: 0,
4 | playing: true
5 | }
6 |
7 | this._interval = null;
8 | this._dropping = false;
9 | this._availableTypes = {};
10 |
11 | this._setScore(0);
12 | this._setPlaying(true);
13 |
14 | this.gallery = new Game.Gallery(this);
15 | this.pit = new Game.Pit();
16 | this.pit.build();
17 |
18 | document.querySelector("#left").appendChild(this.pit.node);
19 | document.querySelector("#right").appendChild(this.gallery.node);
20 |
21 | this._piece = null;
22 | this._nextType = "";
23 | this._refreshAvailable();
24 | this.gallery.sync();
25 | }
26 |
27 | Game.Engine.prototype.setNextType = function(nextType) {
28 | var avail = this._availableTypes[nextType] || 0;
29 | if (avail < 1) { return; }
30 |
31 | this._nextType = nextType;
32 | if (!this._piece) {
33 | this._useNextType();
34 | } else {
35 | this.gallery.sync();
36 | }
37 | return this;
38 | }
39 |
40 | Game.Engine.prototype.getAvailableTypes = function() {
41 | return this._availableTypes;
42 | }
43 |
44 | Game.Engine.prototype.getPiece = function() {
45 | return this._piece;
46 | }
47 |
48 | Game.Engine.prototype.getStatus = function() {
49 | return this._status;
50 | }
51 |
52 | Game.Engine.prototype.getNextType = function() {
53 | return this._nextType;
54 | }
55 |
56 | Game.Engine.prototype.drop = function() {
57 | if (!this._piece || this._dropping) { return; }
58 |
59 | var gravity = new XY(0, -1);
60 | while (this._piece.fits(this.pit)) {
61 | this._piece.xy = this._piece.xy.plus(gravity);
62 | }
63 | this._piece.xy = this._piece.xy.minus(gravity);
64 |
65 | this._stop();
66 | this._dropping = true;
67 | setTimeout(this._drop.bind(this), Game.INTERVAL_DROP);
68 | return this;
69 | }
70 |
71 | Game.Engine.prototype.rotate = function() {
72 | if (!this._piece || this._dropping) { return; }
73 | this._piece.rotate(+1);
74 | if (!this._piece.fits(this.pit)) { this._piece.rotate(-1); }
75 | return this;
76 | }
77 |
78 | Game.Engine.prototype.shift = function(direction) {
79 | if (!this._piece || this._dropping) { return; }
80 | var xy = new XY(direction, 0);
81 | this._piece.xy = this._piece.xy.plus(xy);
82 | if (!this._piece.fits(this.pit)) { this._piece.xy = this._piece.xy.minus(xy); }
83 | return this;
84 | }
85 |
86 | /**
87 | * After drop timeout
88 | */
89 | Game.Engine.prototype._drop = function() {
90 | this._dropping = false;
91 | var removed = this.pit.drop(this._piece);
92 | this._piece = null;
93 | this._setScore(this._status.score + this._computeScore(removed));
94 | if (this._nextType) { this._useNextType(); }
95 | }
96 |
97 |
98 | Game.Engine.prototype._refreshAvailable = function() {
99 | for (var type in Game.Piece.DEF) {
100 | this._availableTypes[type] = Game.Piece.DEF[type].avail;
101 | }
102 | }
103 |
104 | Game.Engine.prototype._useNextType = function() {
105 | var avail = this._availableTypes[this._nextType]-1;
106 | if (avail) {
107 | this._availableTypes[this._nextType] = avail;
108 | } else {
109 | delete this._availableTypes[this._nextType];
110 | }
111 | if (!Object.keys(this._availableTypes).length) { this._refreshAvailable(); }
112 |
113 | var nextPiece = new Game.Piece(this._nextType);
114 | nextPiece.center();
115 | nextPiece.build(this.pit.node);
116 |
117 | if (nextPiece.fits(this.pit)) {
118 | this._piece = nextPiece;
119 | this._nextType = "";
120 | this._start();
121 | } else { /* game over */
122 | this._setPlaying(false);
123 | }
124 |
125 | this.gallery.sync();
126 | }
127 |
128 | Game.Engine.prototype._setScore = function(score) {
129 | this._status.score = score;
130 | document.querySelector("#score").innerHTML = score;
131 | }
132 |
133 | Game.Engine.prototype._tick = function() {
134 | var gravity = new XY(0, -1);
135 | this._piece.xy = this._piece.xy.plus(gravity);
136 | if (!this._piece.fits(this.pit)) {
137 | this._piece.xy = this._piece.xy.minus(gravity);
138 | this.drop();
139 | }
140 | }
141 |
142 | Game.Engine.prototype._computeScore = function(removed) {
143 | if (!removed) { return 0; }
144 | return 100 * (1 << (removed-1));
145 | }
146 |
147 | Game.Engine.prototype._setPlaying = function(playing) {
148 | this._status.playing = playing;
149 | document.querySelector("#status").innerHTML = (playing ? "Playing" : "GAME OVER");
150 | }
151 |
152 | Game.Engine.prototype._start = function() {
153 | if (this._interval) { return; }
154 | this._interval = setInterval(this._tick.bind(this), Game.INTERVAL_ENGINE);
155 | Game.INTERVAL_ENGINE -= 5;
156 | }
157 |
158 | Game.Engine.prototype._stop = function() {
159 | if (!this._interval) { return; }
160 | clearInterval(this._interval);
161 | this._interval = null;
162 | }
163 |
--------------------------------------------------------------------------------
/js/engine.network.js:
--------------------------------------------------------------------------------
1 | Game.Engine.Network = function(firebase, master) {
2 | this._firebase = firebase;
3 | this._master = master;
4 | this._firebase.on("value", this._change.bind(this));
5 |
6 | if (this._master) { this._firebase.set(null); }
7 | Game.Engine.call(this);
8 | }
9 | Game.Engine.Network.prototype = Object.create(Game.Engine.prototype);
10 |
11 | Game.Engine.Network.prototype.setNextType = function(nextType) {
12 | Game.Engine.prototype.setNextType.call(this, nextType);
13 |
14 | if (!this._master) {
15 | if (this._nextType) { /* waiting, propagate upwards */
16 | this._send("next", "avail");
17 | } else { /* next piece got transformed into piece */
18 | this._send("next", "piece", "avail");
19 | }
20 | }
21 | }
22 |
23 | Game.Engine.Network.prototype.drop = function() {
24 | Game.Engine.prototype.drop.call(this);
25 | if (this._master) { this._send("piece"); }
26 | return this;
27 | }
28 |
29 | Game.Engine.Network.prototype.rotate = function() {
30 | Game.Engine.prototype.rotate.call(this);
31 | if (this._master) { this._send("piece"); }
32 | return this;
33 | }
34 |
35 | Game.Engine.Network.prototype.shift = function(direction) {
36 | Game.Engine.prototype.shift.call(this, direction);
37 | if (this._master) { this._send("piece"); }
38 | return this;
39 | }
40 |
41 | Game.Engine.Network.prototype._drop = function() {
42 | Game.Engine.prototype._drop.call(this);
43 | if (this._master) { this._send("pit", "piece", "next", "avail", "status"); }
44 | }
45 |
46 | Game.Engine.Network.prototype._tick = function() {
47 | Game.Engine.prototype._tick.call(this);
48 | if (this._master) { this._send("piece"); }
49 | }
50 |
51 | Game.Engine.Network.prototype._change = function(snap) {
52 | var data = snap.val();
53 | if (!data) { return; }
54 |
55 | if (data.pit) { this._syncPit(data.pit); }
56 | this._syncPiece(data.piece || null);
57 | this._syncNextType(data.next || "");
58 | if (data.avail) { this._syncAvailablePieces(data.avail); }
59 | if (data.status) { this._syncStatus(data.status); }
60 | }
61 |
62 | Game.Engine.Network.prototype._send = function() {
63 | var data = {};
64 | for (var i=0;i= Game.WIDTH) { return false; }
131 | if (xy.y < 0) { return false; }
132 | if (pit.cells[xy]) { return false; }
133 | }
134 |
135 | return true;
136 | }
137 |
138 | Game.Piece.prototype.rotate = function(direction) {
139 | var sign = (direction > 0 ? new XY(-1, 1) : new XY(1, -1));
140 | var newCells = {};
141 |
142 | for (var p in this.cells) {
143 | var cell = this.cells[p];
144 | var xy = cell.xy;
145 | var nxy = new XY(xy.y*sign.x, xy.x*sign.y);
146 | cell.xy = nxy;
147 | newCells[nxy] = cell;
148 | }
149 | this.cells = newCells;
150 |
151 | return this;
152 | }
153 |
154 | Game.Piece.prototype.center = function() {
155 | this.xy = new XY(Game.WIDTH/2, Game.DEPTH-1);
156 | return this;
157 | }
158 |
159 | Game.Piece.prototype.clone = function() {
160 | var clone = new Game.Piece(this.type);
161 |
162 | clone.xy = this.xy;
163 | clone.cells = {};
164 | for (var p in this.cells) {
165 | clone.cells[p] = this.cells[p].clone();
166 | }
167 |
168 | return clone;
169 | }
170 |
171 | Game.Piece.prototype._position = function() {
172 | this.node.style.left = (this.xy.x * Game.CELL) + "px";
173 | this.node.style.bottom = (this.xy.y * Game.CELL) + "px";
174 | return this;
175 | }
176 |
--------------------------------------------------------------------------------
/js/pit.js:
--------------------------------------------------------------------------------
1 | Game.Pit = function() {
2 | this.cells = {};
3 | this.cols = []; /* maximum values per-column */
4 | this.rows = []; /* non-empty cells per-row */
5 | this.node = null;
6 |
7 | for (var i=0;i= 0 && !(xy in this.cells)) { holes++; }
74 | }
75 |
76 | for (var i=0;i j) { xy = new XY(xy.x, xy.y-1); } /* lower xy */
148 |
149 | cell.xy = xy;
150 | cells[xy] = cell;
151 | this.cols[xy.x] = Math.max(this.cols[xy.x], xy.y+1);
152 | }
153 | this.cells = cells;
154 |
155 | result++;
156 | j--;
157 | }
158 |
159 | return result;
160 | }
161 |
--------------------------------------------------------------------------------
/js/player.js:
--------------------------------------------------------------------------------
1 | Game.Player = function(engine) {
2 | this._engine = engine;
3 | }
4 |
5 | Game.Player.prototype.destroy = function() {
6 | }
7 |
8 | Game.Attacker.Network = Game.Defender.Network = Game.Player;
9 |
--------------------------------------------------------------------------------
/js/xy.js:
--------------------------------------------------------------------------------
1 | var XY = function(x, y) {
2 | this.x = x || 0;
3 | this.y = y || 0;
4 | }
5 |
6 | XY.fromString = function(str) {
7 | var parts = str.split(",");
8 | return new this(parseInt(parts[0]), parseInt(parts[1]));
9 | }
10 |
11 | XY.prototype.toString = function() {
12 | return this.x+","+this.y;
13 | }
14 |
15 | XY.prototype.plus = function(xy) {
16 | return new XY(this.x+xy.x, this.y+xy.y);
17 | }
18 |
19 | XY.prototype.minus = function(xy) {
20 | return new XY(this.x-xy.x, this.y-xy.y);
21 | }
22 |
23 | XY.prototype.clone = function() {
24 | return new XY(this.x, this.y);
25 | }
26 |
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2005-2012, Ondrej Zara
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice,
8 | this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above copyright notice,
10 | this list of conditions and the following disclaimer in the documentation
11 | and/or other materials provided with the distribution.
12 | * Neither the name of Ondrej Zara nor the names of its contributors may be used
13 | to endorse or promote products derived from this software without specific
14 | prior written permission.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
19 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
20 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
21 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
23 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
25 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ondras/custom-tetris/0e7101ab2c80a872c64152e5b60b949e7b0c978b/screenshot.png
--------------------------------------------------------------------------------