├── 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 | ![Screenshot](screenshot.png) 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 | 43 |
44 | Attacker: 50 |
51 |
52 | Defender: 57 |
58 |

This configuration is known as .

59 |

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 --------------------------------------------------------------------------------