<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.0/js/bootstrap.min.js"></script>
<script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
<!------ Include the above in your HEAD tag ---------->
<html>
<head><base href="https://www.firebase.com/tutorial/#session/xuu8u3i0qru" />
<script src="https://cdn.firebase.com/js/client/2.0.4/firebase.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>
<link rel="stylesheet" type="text/css" href="/resources/tutorial/css/example.css">
</head>
<body class="tetris-body example-base">
<div>
<canvas id="canvas0" width="250" height="500"></canvas>
<canvas id="canvas1" width="250" height="500"></canvas>
</div>
<input type="button" id="restartButton" value="Restart Game" class="hide" />
<div id="gameInProgress">Game is in progress. You will automatically join if either player leaves.</div>
<script>
var canvas = $("#canvas0").get(0);
if (!canvas || !canvas.getContext || !canvas.getContext('2d'))
alert("You must use a browser that supports HTML5 Canvas to run this demo.");
function start() {
var tetrisRef = new Firebase('https://xuu8u3i0qru.firebaseio-demo.com/');
var tetrisController = new Tetris.Controller(tetrisRef);
}
var Tetris = { };
/**
* Various constants related to board size / drawing.
*/
Tetris.BOARD_WIDTH = 10; // (in "blocks", not pixels)
Tetris.BOARD_HEIGHT = 20;
Tetris.BLOCK_SIZE_PIXELS = 25;
Tetris.BOARD_HEIGHT_PIXELS = Tetris.BOARD_HEIGHT * Tetris.BLOCK_SIZE_PIXELS;
Tetris.BOARD_WIDTH_PIXELS = Tetris.BOARD_WIDTH * Tetris.BLOCK_SIZE_PIXELS;
Tetris.BLOCK_BORDER_COLOR = "#484848";
Tetris.BLOCK_COLORS = { 'X': 'black', 'b': 'cyan', 'B': 'blue', 'O': 'orange',
'Y': 'yellow', 'G': 'green', 'P': '#9370D8', 'R': 'red' };
Tetris.GRAVITY_DELAY = 300; // 300ms
Tetris.EMPTY_LINE = " ";
Tetris.FILLED_LINE = "XXXXXXXXXX";
Tetris.COMPLETE_LINE_PATTERN = /[^ ]{10}/;
// Pieces. (Indexed by piece rotation (0-3), row (0-3), piece number (0-6))
Tetris.PIECES = [];
for (var i = 0; i < 4; i++) { Tetris.PIECES[i] = []; }
Tetris.PIECES[0][0] = [ " ", " ", " ", " ", " ", " ", " " ];
Tetris.PIECES[0][1] = [ " ", "B ", " O ", " YY ", " GG ", " P ", "RR " ];
Tetris.PIECES[0][2] = [ "bbbb", "BBB ", "OOO ", " YY ", "GG ", "PPP ", " RR " ];
Tetris.PIECES[0][3] = [ " ", " ", " ", " ", " ", " ", " " ];
Tetris.PIECES[1][0] = [ " b ", " ", " ", " ", " ", " ", " R " ];
Tetris.PIECES[1][1] = [ " b ", " B ", "OO ", " YY ", " G ", " P ", " RR " ];
Tetris.PIECES[1][2] = [ " b ", " B ", " O ", " YY ", " GG ", " PP ", " R " ];
Tetris.PIECES[1][3] = [ " b ", "BB ", " O ", " ", " G ", " P ", " " ];
Tetris.PIECES[2][0] = [ " ", " ", " ", " ", " ", " ", " " ];
Tetris.PIECES[2][1] = [ " ", " ", " ", " YY ", " GG ", " ", "RR " ];
Tetris.PIECES[2][2] = [ "bbbb", "BBB ", "OOO ", " YY ", "GG ", "PPP ", " RR " ];
Tetris.PIECES[2][3] = [ " ", " B ", "O ", " ", " ", " P ", " " ];
Tetris.PIECES[3][0] = [ " b ", " ", " ", " ", " ", " ", " R " ];
Tetris.PIECES[3][1] = [ " b ", " BB ", " O ", " YY ", " G ", " P ", " RR " ];
Tetris.PIECES[3][2] = [ " b ", " B ", " O ", " YY ", " GG ", "PP ", " R " ];
Tetris.PIECES[3][3] = [ " b ", " B ", " OO ", " ", " G ", " P ", " " ];
/**
* Stores the state of a tetris board and handles drawing it.
*/
Tetris.Board = function (canvas, playerRef) {
this.context = canvas.getContext('2d');
this.playerRef = playerRef;
this.snapshot = null;
this.isMyBoard = false;
// Listen for changes to our board.
var self = this;
playerRef.on('value', function(snapshot) {
self.snapshot = snapshot;
self.draw();
});
};
/**
* Draws the contents of the board as well as the current piece.
*/
Tetris.Board.prototype.draw = function () {
// Clear canvas.
this.context.clearRect(0, 0, Tetris.BOARD_WIDTH_PIXELS, Tetris.BOARD_HEIGHT_PIXELS);
// Iterate over columns / rows in board data and draw each non-empty block.
for (var x = 0; x < Tetris.BOARD_WIDTH; x++) {
for (var y = 0; y < Tetris.BOARD_HEIGHT; y++) {
var colorValue = this.getBlockVal(x, y);
if (colorValue != ' ') {
// Calculate block position and draw a correctly-colored square.
var left = x * Tetris.BLOCK_SIZE_PIXELS;
var top = y * Tetris.BLOCK_SIZE_PIXELS;
this.context.fillStyle = Tetris.BLOCK_COLORS[colorValue];
this.context.fillRect(left, top, Tetris.BLOCK_SIZE_PIXELS, Tetris.BLOCK_SIZE_PIXELS);
this.context.lineWidth = 1;
this.context.strokeStyle = Tetris.BLOCK_BORDER_COLOR;
this.context.strokeRect(left, top, Tetris.BLOCK_SIZE_PIXELS, Tetris.BLOCK_SIZE_PIXELS);
}
}
}
// If there's a falling piece, draw it.
if (this.snapshot !== null && this.snapshot.hasChild('piece')) {
var piece = Tetris.Piece.fromSnapshot(this.snapshot.child('piece'));
this.drawPiece(piece);
}
// If this isn't my board, dim it out with a 25% opacity black rectangle.
if (!this.isMyBoard) {
this.context.fillStyle = "rgba(0, 0, 0, 0.25)";
this.context.fillRect(0, 0, Tetris.BOARD_WIDTH_PIXELS, Tetris.BOARD_HEIGHT_PIXELS);
}
};
/**
* Draw the currently falling piece.
*/
Tetris.Board.prototype.drawPiece = function (piece) {
var self = this;
this.forEachBlockOfPiece(piece,
function (x, y, colorValue) {
var left = x * Tetris.BLOCK_SIZE_PIXELS;
var top = y * Tetris.BLOCK_SIZE_PIXELS;
self.context.fillStyle = Tetris.BLOCK_COLORS[colorValue];
self.context.fillRect(left, top, Tetris.BLOCK_SIZE_PIXELS, Tetris.BLOCK_SIZE_PIXELS);
self.context.lineWidth = 1;
self.context.strokeStyle = Tetris.BLOCK_BORDER_COLOR;
self.context.strokeRect(left, top, Tetris.BLOCK_SIZE_PIXELS, Tetris.BLOCK_SIZE_PIXELS);
});
};
/**
* Clear the board contents.
*/
Tetris.Board.prototype.clear = function () {
for (var row = 0; row < Tetris.BOARD_HEIGHT; row++) {
this.setRow(row, Tetris.EMPTY_LINE);
}
};
/**
* Given a Tetris.Piece, returns true if it has collided with the board (i.e. its current position
* and rotation causes it to overlap blocks already on the board).
*/
Tetris.Board.prototype.checkForPieceCollision = function (piece) {
var collision = false;
var self = this;
this.forEachBlockOfPiece(piece,
function (x, y, colorValue) {
// NOTE: we explicitly allow y < 0 since pieces can be partially visible.
if (x < 0 || x >= Tetris.BOARD_WIDTH || y >= Tetris.BOARD_HEIGHT) {
collision = true;
}
else if (y >= 0 && self.getBlockVal(x, y) != ' ') {
collision = true; // collision with board contents.
}
}, /*includeInvalid=*/ true);
return collision;
};
/**
* Given a Tetris.Piece that has landed, add it to the board contents.
*/
Tetris.Board.prototype.addLandedPiece = function (piece) {
var self = this;
// We go out of our way to set an entire row at a time just so the rows show up as
// child_added in the graphical debugger, rather than child_changed.
var rowY = -1, rowContents = null;
this.forEachBlockOfPiece(piece,
function (x, y, val) {
if (y != rowY) {
if (rowY !== -1)
self.setRow(rowY, rowContents);
rowContents = self.getRow(y);
rowY = y;
}
rowContents = rowContents.substring(0, x).concat(val)
.concat(rowContents.substring(x + 1, Tetris.BOARD_WIDTH));
});
if (rowY !== -1)
self.setRow(rowY, rowContents);
};
/**
* Check for any completed lines (no gaps) and remove them, then return the number
* of removed lines.
*/
Tetris.Board.prototype.removeCompletedRows = function () {
// Start at the bottom of the board, working up, removing completed lines.
var copyFrom = Tetris.BOARD_HEIGHT - 1;
var copyTo = copyFrom;
var completedRows = 0;
while (copyFrom >= 0) {
var fromContents = this.getRow(copyFrom);
// See if the line is complete (if so, we'll skip it)
if (fromContents.match(Tetris.COMPLETE_LINE_PATTERN)) {
copyFrom--;
completedRows++;
} else {
// Copy the row down (to fill the gap from any removed rows) and continue on.
this.setRow(copyTo, fromContents);
copyFrom--;
copyTo--;
}
}
return completedRows;
};
/**
* Generate the specified number of junk rows at the bottom of the board. Return true if the added
* rows overflowed the board (in which case the player loses).
*/
Tetris.Board.prototype.addJunkRows = function (numRows) {
var overflow = false;
// First, check if any blocks are going to overflow off the top of the screen.
var topRowContents = this.getRow(numRows - 1);
overflow = topRowContents.match(/[^ ]/);
// Shift rows up to make room for the new rows.
for (var i = 0; i < Tetris.BOARD_HEIGHT - numRows; i++) {
var moveLineContents = this.getRow(i + numRows);
this.setRow(i, moveLineContents);
}
// Fill the bottom with junk rows that are full except for a single random gap.
var gap = Math.floor(Math.random() * Tetris.FILLED_LINE.length);
var junkRow = Tetris.FILLED_LINE.substring(0, gap) + ' ' + Tetris.FILLED_LINE.substring(gap + 1);
for (i = Tetris.BOARD_HEIGHT - numRows; i < Tetris.BOARD_HEIGHT; i++) {
this.setRow(i, junkRow);
}
return overflow;
};
/**
* Helper to enumerate the blocks that make up a particular piece. Calls fn() for each block,
* passing the x and y position of the block and the color value. If includeInvalid is true, it
* includes blocks that would fall outside the bounds of the board.
*/
Tetris.Board.prototype.forEachBlockOfPiece = function (piece, fn, includeInvalid) {
for (var blockY = 0; blockY < 4; blockY++) {
for (var blockX = 0; blockX < 4; blockX++) {
var colorValue = Tetris.PIECES[piece.rotation][blockY][piece.pieceNum].charAt(blockX);
if (colorValue != ' ') {
var x = piece.x + blockX, y = piece.y + blockY;
if (includeInvalid || (x >= 0 && x < Tetris.BOARD_WIDTH && y >= 0 && y < Tetris.BOARD_HEIGHT)) {
fn(x, y, colorValue);
}
}
}
}
};
Tetris.Board.prototype.getRow = function (y) {
var row = (y < 10) ? ('0' + y) : ('' + y); // Pad row so they sort nicely in debugger. :-)
var rowContents = this.snapshot === null ? null : this.snapshot.child('board/' + row).val();
return rowContents || Tetris.EMPTY_LINE;
};
Tetris.Board.prototype.getBlockVal = function (x, y) {
return this.getRow(y).charAt(x);
};
Tetris.Board.prototype.setRow = function (y, rowContents) {
var row = (y < 10) ? ('0' + y) : ('' + y); // Pad row so they sort nicely in debugger. :-)
if (rowContents === Tetris.EMPTY_LINE)
rowContents = null; // delete empty lines so we get remove / added events in debugger. :-)
this.playerRef.child('board').child(row).set(rowContents);
};
Tetris.Board.prototype.setBlockVal = function (x, y, val) {
var rowContents = this.getRow(y);
rowContents = rowContents.substring(0, x) + val + rowContents.substring(x+1);
this.setRow(y, rowContents);
};
/**
* Immutable object representing a falling piece along with its rotation and board position.
* Has helpers for generating mutated Tetris.Piece objects (e.g. rotated or dropped).
*/
Tetris.Piece = function (pieceNum, x, y, rotation) {
if (arguments.length > 0) {
this.pieceNum = pieceNum;
this.x = x;
this.y = y;
this.rotation = rotation;
} else {
// Initialize new random piece.
this.pieceNum = Math.floor(Math.random() * 7);
this.x = 4; // "center" it.
this.y = -2; // this will make the bottom line of the piece visible.
this.rotation = 0;
}
};
/**
* Create a piece from a Firebase snapshot representing a piece.
*/
Tetris.Piece.fromSnapshot = function (snapshot) {
var piece = snapshot.val();
return new Tetris.Piece(piece.pieceNum, piece.x, piece.y, piece.rotation);
};
/**
* Writes the current piece data into Firebase.
*/
Tetris.Piece.prototype.writeToFirebase = function (pieceRef) {
pieceRef.set({pieceNum: this.pieceNum, x: this.x, y: this.y, rotation: this.rotation});
};
Tetris.Piece.prototype.drop = function () {
return new Tetris.Piece(this.pieceNum, this.x, this.y + 1, this.rotation);
};
Tetris.Piece.prototype.rotate = function () {
return new Tetris.Piece(this.pieceNum, this.x, this.y, (this.rotation + 1) % 4);
};
Tetris.Piece.prototype.moveLeft = function () {
return new Tetris.Piece(this.pieceNum, this.x - 1, this.y, this.rotation);
};
Tetris.Piece.prototype.moveRight = function () {
return new Tetris.Piece(this.pieceNum, this.x + 1, this.y, this.rotation);
};
/**
* Manages joining the game, responding to keypresses, making the piece drop, etc.
*/
Tetris.PlayingState = { Watching: 0, Joining: 1, Playing: 2 };
Tetris.Controller = function (tetrisRef) {
this.tetrisRef = tetrisRef;
this.createBoards();
this.playingState = Tetris.PlayingState.Watching;
this.waitToJoin();
};
Tetris.Controller.prototype.createBoards = function () {
this.boards = [];
for(var i = 0; i <= 1; i++) {
var playerRef = this.tetrisRef.child('player' + i);
var canvas = $('#canvas' + i).get(0);
this.boards.push(new Tetris.Board(canvas, playerRef));
}
};
Tetris.Controller.prototype.waitToJoin = function() {
var self = this;
// Listen on 'online' location for player0 and player1.
this.tetrisRef.child('player0/online').on('value', function(onlineSnap) {
if (onlineSnap.val() === null && self.playingState === Tetris.PlayingState.Watching) {
self.tryToJoin(0);
}
});
this.tetrisRef.child('player1/online').on('value', function(onlineSnap) {
if (onlineSnap.val() === null && self.playingState === Tetris.PlayingState.Watching) {
self.tryToJoin(1);
}
});
};
/**
* Try to join the game as the specified playerNum.
*/
Tetris.Controller.prototype.tryToJoin = function(playerNum) {
// Set ourselves as joining to make sure we don't try to join as both players. :-)
this.playingState = Tetris.PlayingState.Joining;
// Use a transaction to make sure we don't conflict with other people trying to join.
var self = this;
this.tetrisRef.child('player' + playerNum + '/online').transaction(function(onlineVal) {
if (onlineVal === null) {
return true; // Try to set online to true.
} else {
return; // Somebody must have beat us. Abort the transaction.
}
}, function(error, committed) {
if (committed) { // We got in!
self.playingState = Tetris.PlayingState.Playing;
self.startPlaying(playerNum);
} else {
self.playingState = Tetris.PlayingState.Watching;
}
});
};
/**
* Once we've joined, enable controlling our player.
*/
Tetris.Controller.prototype.startPlaying = function (playerNum) {
this.myPlayerRef = this.tetrisRef.child('player' + playerNum);
this.opponentPlayerRef = this.tetrisRef.child('player' + (1 - playerNum));
this.myBoard = this.boards[playerNum];
this.myBoard.isMyBoard = true;
this.myBoard.draw();
// Clear our 'online' status when we disconnect so somebody else can join.
this.myPlayerRef.child('online').onDisconnect().remove();
// Detect when other player pushes rows to our board.
this.watchForExtraRows();
// Detect when game is restarted by other player.
this.watchForRestart();
$('#gameInProgress').hide();
var self = this;
$('#restartButton').show();
$("#restartButton").click(function () {
self.restartGame();
});
this.initializePiece();
this.enableKeyboard();
this.resetGravity();
};
Tetris.Controller.prototype.initializePiece = function() {
this.fallingPiece = null;
var pieceRef = this.myPlayerRef.child('piece');
var self = this;
// Watch for changes to the current piece (and initialize it if it's null).
pieceRef.on('value', function(snapshot) {
if (snapshot.val() === null) {
var newPiece = new Tetris.Piece();
newPiece.writeToFirebase(pieceRef);
} else {
self.fallingPiece = Tetris.Piece.fromSnapshot(snapshot);
}
});
};
/**
* Sets up handlers for all keyboard commands.
*/
Tetris.Controller.prototype.enableKeyboard = function () {
var self = this;
$(document).on('keydown', function (evt) {
if (self.fallingPiece === null)
return; // piece isn't initialized yet.
var keyCode = evt.which;
var key = { space:32, left:37, up:38, right:39, down:40 };
var newPiece = null;
switch (keyCode) {
case key.left:
newPiece = self.fallingPiece.moveLeft();
break;
case key.up:
newPiece = self.fallingPiece.rotate();
break;
case key.right:
newPiece = self.fallingPiece.moveRight();
break;
case key.down:
newPiece = self.fallingPiece.drop();
break;
case key.space:
// Drop as far as we can.
var droppedPiece = self.fallingPiece;
do {
newPiece = droppedPiece;
droppedPiece = droppedPiece.drop();
} while (!self.myBoard.checkForPieceCollision(droppedPiece));
break;
}
if (newPiece !== null) {
// If the new piece position / rotation is valid, update self.fallingPiece and firebase.
if (!self.myBoard.checkForPieceCollision(newPiece)) {
// If the keypress moved the piece down, reset gravity.
if (self.fallingPiece.y != newPiece.y) {
self.resetGravity();
}
newPiece.writeToFirebase(self.myPlayerRef.child('piece'));
}
return false; // handled
}
return true;
});
};
/**
* Sets a timer to make the piece repeatedly drop after GRAVITY_DELAY ms.
*/
Tetris.Controller.prototype.resetGravity = function () {
// If there's a timer already active, clear it first.
if (this.gravityIntervalId !== null) {
clearInterval(this.gravityIntervalId);
}
var self = this;
this.gravityIntervalId = setInterval(function() {
self.doGravity();
}, Tetris.GRAVITY_DELAY);
};
Tetris.Controller.prototype.doGravity = function () {
if (this.fallingPiece === null)
return; // piece isn't initialized yet.
var newPiece = this.fallingPiece.drop();
// If we've hit the bottom, add the (pre-drop) piece to the board and create a new piece.
if (this.myBoard.checkForPieceCollision(newPiece)) {
this.myBoard.addLandedPiece(this.fallingPiece);
// Check for completed lines and if appropriate, push extra rows to our opponent.
var completedRows = this.myBoard.removeCompletedRows();
var rowsToPush = (completedRows === 4) ? 4 : completedRows - 1;
if (rowsToPush > 0)
this.opponentPlayerRef.child('extrarows').push(rowsToPush);
// Create new piece (it'll be initialized to a random piece at the top of the screen).
newPiece = new Tetris.Piece();
// Is the board full?
if (this.myBoard.checkForPieceCollision(newPiece))
this.gameOver();
}
newPiece.writeToFirebase(this.myPlayerRef.child('piece'));
};
/**
* Detect when our opponent pushes extra rows to us.
*/
Tetris.Controller.prototype.watchForExtraRows = function () {
var self = this;
var extraRowsRef = this.myPlayerRef.child('extrarows');
extraRowsRef.on('child_added', function(snapshot) {
var rows = snapshot.val();
extraRowsRef.child(snapshot.key()).remove();
var overflow = self.myBoard.addJunkRows(rows);
if (overflow)
self.gameOver();
// Also move piece up to avoid collisions.
if (self.fallingPiece) {
self.fallingPiece.y -= rows;
self.fallingPiece.writeToFirebase(self.myPlayerRef.child('piece'));
}
});
};
/**
* Detect when our opponent restarts the game.
*/
Tetris.Controller.prototype.watchForRestart = function () {
var self = this;
var restartRef = this.myPlayerRef.child('restart');
restartRef.on('value', function(snap) {
if (snap.val() === 1) {
restartRef.set(0);
self.resetMyBoardAndPiece();
}
});
};
Tetris.Controller.prototype.gameOver = function () {
this.restartGame();
};
Tetris.Controller.prototype.restartGame = function () {
this.opponentPlayerRef.child('restart').set(1);
this.resetMyBoardAndPiece();
};
Tetris.Controller.prototype.resetMyBoardAndPiece = function () {
this.myBoard.clear();
var newPiece = new Tetris.Piece();
newPiece.writeToFirebase(this.myPlayerRef.child('piece'));
};
start();
</script>
</body>
</html>