├── drawing ├── rules.json ├── index.html ├── css │ └── drawing.css └── js │ └── drawing.js ├── presence ├── index.html ├── rules.json ├── css │ └── presence.css └── js │ ├── presence.js │ └── idle.js ├── chat ├── index.html ├── rules.json ├── js │ └── chat.js └── css │ └── chat.css ├── tetris ├── index.html ├── css │ └── tetris.css ├── rules.json └── js │ └── tetris.js ├── leaderboard ├── index.html ├── rules.json ├── css │ └── leaderboard.css └── js │ └── leaderboard.js ├── LICENSE └── README.md /drawing/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // Let everyone read and write to the "drawing" node 4 | "drawing": { 5 | ".read": true, 6 | ".write": true, 7 | 8 | "$pixel": { 9 | // The key name must have the format "##:##" and the value must be a string of three characters 10 | ".validate": "$pixel.matches(/^\\d{1,2}:\\d{1,2}$/) && newData.isString() && newData.val().length === 3" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /presence/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Firebase Presence Example 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |

Firebase Realtime Presence

23 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /presence/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // Let everyone read and write to the "presence" node 4 | "presence": { 5 | ".read": true, 6 | ".write": true, 7 | 8 | "$user": { 9 | // Each user must contain a "name" and "status" 10 | ".validate": "newData.hasChildren(['name', 'status'])", 11 | 12 | // "name" must be a string 13 | "name": { 14 | ".validate": "newData.isString()" 15 | }, 16 | 17 | // "status" must be a string 18 | "status": { 19 | ".validate": "newData.isString()" 20 | }, 21 | 22 | // No other children are allowed 23 | "$other": { 24 | ".validate": false 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /chat/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Firebase Basic Chat Example 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

Firebase Realtime Chat

20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /drawing/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Firebase Collaborative Drawing Example 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

Firebase Realtime Collaborative Drawing

20 | 21 |
22 | 23 |
24 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /chat/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // Let everyone read and write to the "chat" node 4 | "chat": { 5 | ".read": true, 6 | ".write": true, 7 | 8 | "$message": { 9 | // Existing messages cannot be overwritten and each message must contain a "name" and "text" 10 | ".validate": "!data.exists() && newData.hasChildren(['name', 'text'])", 11 | 12 | // "name" must be a string 13 | "name": { 14 | ".validate": "newData.isString()" 15 | }, 16 | 17 | // "text" must be a string 18 | "text": { 19 | ".validate": "newData.isString()" 20 | }, 21 | 22 | // No other children are allowed 23 | "$other": { 24 | ".validate": false 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /chat/js/chat.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | // Get a reference to the root of the chat data. 3 | var messagesRef = new Firebase("https://INSTANCE.firebaseio.com/chat"); 4 | 5 | // When the user presses enter on the message input, write the message to firebase. 6 | $("#messageInput").keypress(function (e) { 7 | if (e.keyCode == 13) { 8 | var name = $("#nameInput").val(); 9 | var text = $("#messageInput").val(); 10 | messagesRef.push({name:name, text:text}); 11 | $("#messageInput").val(""); 12 | } 13 | }); 14 | 15 | // Add a callback that is triggered for each chat message. 16 | messagesRef.limitToLast(10).on("child_added", function (snapshot) { 17 | var message = snapshot.val(); 18 | $("
").text(message.text).prepend($("") 19 | .text(message.name + ": ")).appendTo($("#messagesDiv")); 20 | $("#messagesDiv")[0].scrollTop = $("#messagesDiv")[0].scrollHeight; 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tetris/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Firebase Tetris Example 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

Firebase Realtime Tetris

20 | 21 |
22 | 23 | 24 |
25 | 26 | 27 | 28 |

Game is in progress. You will automatically join if either player leaves.

29 | 30 | 31 | -------------------------------------------------------------------------------- /presence/css/presence.css: -------------------------------------------------------------------------------- 1 | /* Global */ 2 | body { 3 | margin-top: 10px; 4 | margin-left: auto; 5 | margin-right: auto; 6 | width: 500px; 7 | background-color: #f8f8f8; 8 | font-size: 24px; 9 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 10 | color: #424547; 11 | text-align: center; 12 | } 13 | 14 | h1 { 15 | font-size: 36px; 16 | font-weight: bold; 17 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 18 | color: #424547; 19 | } 20 | 21 | h3 { 22 | font-size: 24px; 23 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 24 | color: #424547; 25 | } 26 | 27 | p { 28 | font-size: 16px; 29 | } 30 | 31 | input { 32 | font-size: 24px; 33 | } 34 | 35 | input[type=text] { 36 | color: #424547; 37 | border: 1px solid #c2c2c2; 38 | background-color: white; 39 | } 40 | 41 | em { 42 | font-style: normal; 43 | font-weight: bold; 44 | color: black; 45 | } 46 | 47 | /* Presence */ 48 | #presenceDiv { 49 | text-align: center; 50 | } 51 | -------------------------------------------------------------------------------- /leaderboard/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Firebase Leaderboard Example 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

Firebase Realtime Leaderboard

20 | 21 | 22 | 23 | 24 | 25 | 26 |
NameScore
27 | 28 | 29 | 30 | 31 |
32 | Highest score so far: 33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /leaderboard/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // Let everyone read and write to the "leaderboard" node 4 | "leaderboard": { 5 | ".read": true, 6 | ".write": true, 7 | 8 | // The highest score must be a number 9 | "highestScore": { 10 | ".validate": "newData.isNumber()" 11 | }, 12 | 13 | "scoreList": { 14 | "$name": { 15 | // Each score must contain a "name" and "text" 16 | ".validate": "newData.hasChildren(['name', 'score'])", 17 | 18 | // "name" must be a string 19 | "name": { 20 | ".validate": "newData.isString()" 21 | }, 22 | 23 | // "score" must be a number 24 | "score": { 25 | ".validate": "newData.isNumber()" 26 | }, 27 | 28 | // No other children are allowed 29 | "$other": { 30 | ".validate": false 31 | } 32 | } 33 | }, 34 | 35 | // No other children are allowed 36 | "$other": { 37 | ".validate": false 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Firebase 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /leaderboard/css/leaderboard.css: -------------------------------------------------------------------------------- 1 | /* Global */ 2 | body { 3 | margin-top: 10px; 4 | margin-left: auto; 5 | margin-right: auto; 6 | width: 500px; 7 | background-color: #f8f8f8; 8 | font-size: 24px; 9 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 10 | color: #424547; 11 | text-align: center; 12 | } 13 | 14 | h1 { 15 | font-size: 36px; 16 | font-weight: bold; 17 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 18 | color: #424547; 19 | } 20 | 21 | h3 { 22 | font-size: 24px; 23 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 24 | color: #424547; 25 | } 26 | 27 | p { 28 | font-size: 16px; 29 | } 30 | 31 | input { 32 | font-size: 24px; 33 | } 34 | 35 | input[type=text] { 36 | color: #424547; 37 | border: 1px solid #c2c2c2; 38 | background-color: white; 39 | } 40 | 41 | em { 42 | font-style: normal; 43 | font-weight: bold; 44 | color: black; 45 | } 46 | 47 | 48 | /* Leaderboard */ 49 | #leaderboardTable { 50 | margin: 0 auto 20px auto; 51 | text-align: center; 52 | } 53 | 54 | #nameInput { 55 | width: 26%; 56 | } 57 | 58 | #messageInput { 59 | width: 68%; 60 | } 61 | 62 | #highestScore { 63 | margin-top: 20px; 64 | } 65 | -------------------------------------------------------------------------------- /tetris/css/tetris.css: -------------------------------------------------------------------------------- 1 | /* Global */ 2 | body { 3 | margin-top: 10px; 4 | margin-left: auto; 5 | margin-right: auto; 6 | width: 600px; 7 | background-color: #f8f8f8; 8 | font-size: 24px; 9 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 10 | color: #424547; 11 | text-align: center; 12 | } 13 | 14 | h1 { 15 | font-size: 36px; 16 | font-weight: bold; 17 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 18 | color: #424547; 19 | } 20 | 21 | h3 { 22 | font-size: 24px; 23 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 24 | color: #424547; 25 | } 26 | 27 | p { 28 | font-size: 16px; 29 | } 30 | 31 | input { 32 | font-size: 24px; 33 | } 34 | 35 | input[type=text] { 36 | color: #424547; 37 | border: 1px solid #c2c2c2; 38 | background-color: white; 39 | } 40 | 41 | em { 42 | font-style: normal; 43 | font-weight: bold; 44 | color: black; 45 | } 46 | 47 | 48 | /* Tetris */ 49 | #canvas0, #canvas1 { 50 | display: inline-block; 51 | border: 4px solid #424547; 52 | } 53 | 54 | #canvas0 { 55 | margin-right: 50px; 56 | } 57 | 58 | #restartButton { 59 | margin-top: 5px; 60 | } 61 | 62 | #gameInProgress { 63 | font-size: 14px; 64 | } 65 | 66 | .hide { 67 | display: none; 68 | } 69 | -------------------------------------------------------------------------------- /chat/css/chat.css: -------------------------------------------------------------------------------- 1 | /* Global */ 2 | body { 3 | margin-top: 10px; 4 | margin-left: auto; 5 | margin-right: auto; 6 | width: 500px; 7 | background-color: #f8f8f8; 8 | font-size: 24px; 9 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 10 | color: #424547; 11 | text-align: center; 12 | } 13 | 14 | h1 { 15 | font-size: 36px; 16 | font-weight: bold; 17 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 18 | color: #424547; 19 | } 20 | 21 | h3 { 22 | font-size: 24px; 23 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 24 | color: #424547; 25 | } 26 | 27 | p { 28 | font-size: 16px; 29 | } 30 | 31 | input { 32 | font-size: 24px; 33 | } 34 | 35 | input[type=text] { 36 | color: #424547; 37 | border: 1px solid #c2c2c2; 38 | background-color: white; 39 | } 40 | 41 | em { 42 | font-style: normal; 43 | font-weight: bold; 44 | color: black; 45 | } 46 | 47 | 48 | /* Chat */ 49 | #messagesDiv { 50 | background-color: white; 51 | overflow: auto; 52 | height: 230px; 53 | width: 100%; 54 | padding: 10px; 55 | border: 8px solid #424547; 56 | margin-bottom: 5px; 57 | text-align: left; 58 | } 59 | 60 | #nameInput { 61 | width: 26%; 62 | } 63 | 64 | #messageInput { 65 | width: 68%; 66 | } 67 | -------------------------------------------------------------------------------- /drawing/css/drawing.css: -------------------------------------------------------------------------------- 1 | /* Global */ 2 | body { 3 | margin-top: 10px; 4 | margin-left: auto; 5 | margin-right: auto; 6 | width: 500px; 7 | background-color: #f8f8f8; 8 | font-size: 24px; 9 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 10 | color: #424547; 11 | text-align: center; 12 | } 13 | 14 | h1 { 15 | font-size: 36px; 16 | font-weight: bold; 17 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 18 | color: #424547; 19 | } 20 | 21 | h3 { 22 | font-size: 24px; 23 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 24 | color: #424547; 25 | } 26 | 27 | p { 28 | font-size: 16px; 29 | } 30 | 31 | input { 32 | font-size: 24px; 33 | } 34 | 35 | input[type=text] { 36 | color: #424547; 37 | border: 1px solid #c2c2c2; 38 | background-color: white; 39 | } 40 | 41 | em { 42 | font-style: normal; 43 | font-weight: bold; 44 | color: black; 45 | } 46 | 47 | 48 | /* Drawing */ 49 | #colorholder { 50 | width: 480px; 51 | height: 30px; 52 | border: 2px solid #424547; 53 | margin-top: 5px; 54 | margin-left: auto; 55 | margin-right: auto; 56 | } 57 | 58 | #drawing-canvas { 59 | border: 3px solid #999 60 | } 61 | 62 | .colorbox { 63 | width: 22px; 64 | height: 22px; 65 | margin: 1px; 66 | display: inline-block; 67 | border: 3px solid black; 68 | } 69 | -------------------------------------------------------------------------------- /tetris/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // Let everyone read and write to the "tetris" node 4 | "tetris": { 5 | ".read": true, 6 | ".write": true, 7 | 8 | "$player": { 9 | // The parent key name must be "player0" or "player1" and each user must contain "board", "piece", and "restart" 10 | ".validate": "($player === 'player0' || $player === 'player1')", 11 | 12 | // "online" must be a boolean 13 | "online": { 14 | ".validate": "newData.isBoolean()" 15 | }, 16 | 17 | // "board" must be a list of strings representing each line 18 | "board": { 19 | "$lineNum": { 20 | ".validate": "$lineNum.length === 2 && newData.isString() && newData.val().length === 10" 21 | } 22 | }, 23 | 24 | // "piece" must contain a bunch of numeric fields 25 | "piece": { 26 | "pieceNum": { 27 | ".validate": "newData.isNumber()" 28 | }, 29 | "rotation": { 30 | ".validate": "newData.isNumber()" 31 | }, 32 | "x": { 33 | ".validate": "newData.isNumber()" 34 | }, 35 | "y": { 36 | ".validate": "newData.isNumber()" 37 | }, 38 | 39 | // No other children are allowed 40 | "$other": { 41 | ".validate": false 42 | } 43 | }, 44 | 45 | // "restart" must be a number 46 | "restart": { 47 | ".validate": "newData.isNumber()" 48 | }, 49 | 50 | // No other children are allowed 51 | "$other": { 52 | ".validate": false 53 | } 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Status: Archived 2 | This repository has been archived and is no longer maintained. 3 | 4 | ![status: inactive](https://img.shields.io/badge/status-inactive-red.svg) 5 | 6 | # These are legacy Firebase example (for SDK 2.x.x). You probably want to use one of the up-to-date examples at https://firebase.google.com/docs/samples 7 | 8 | --- 9 | 10 | 11 | # Firebase Examples 12 | 13 | A set of single-page examples demonstrating the use of [Firebase](https://www.firebase.com?utm_source=examples). 14 | The following examples are included: 15 | 16 | * [Drawing](https://www.firebase.com/tutorial/#example/drawing) - share a canvas with other users and draw together 17 | * [Chat](https://www.firebase.com/tutorial/#example/chat) - an easy-to-integrate chatroom for your site 18 | * [Presence](https://www.firebase.com/tutorial/#example/presence) - show who is available, who is idle, and who is gone 19 | * [Leaderboard](https://www.firebase.com/tutorial/#example/leaderboard) - see who is winning and how it changes as scores come in 20 | * [Multiplayer Tetris](https://www.firebase.com/tutorial/#example/tetris) - play head-to-head Tetris in your browser 21 | 22 | 23 | ## Getting Started with Firebase 24 | 25 | These examples use Firebase to store and sync data. You can [sign up here for a free 26 | account](https://www.firebase.com/signup/?utm_source=examples). 27 | 28 | 29 | ## Live Demos 30 | 31 | All of these examples can be downloaded and run a live website in a few seconds using 32 | [Firebase Hosting](https://www.firebase.com/hosting.html) and the [Firebase 33 | Tools](https://github.com/firebase/firebase-tools). First, you need to download `firebase-tools`: 34 | 35 | ```bash 36 | npm install -g firebase-tools 37 | ``` 38 | 39 | Then, choose a template from the bootstrap list: 40 | 41 | ```bash 42 | firebase bootstrap 43 | ``` 44 | 45 | Once the template is downloaded, `cd` to the created directory and deploy to Firebase Hosting: 46 | 47 | ```bash 48 | firebase deploy 49 | ``` 50 | 51 | You can then view the example on your web browser by running: 52 | 53 | ```bash 54 | firebase open 55 | ``` 56 | -------------------------------------------------------------------------------- /presence/js/presence.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | // Prompt the user for a name to use. 3 | var name = prompt("Your name?", "Guest"), 4 | currentStatus = "\u2605 online"; 5 | 6 | var firebaseRef = new Firebase("https://INSTANCE.firebaseio.com"); 7 | 8 | // Get a reference to the presence data in Firebase. 9 | var userListRef = firebaseRef.child("presence"); 10 | 11 | // Generate a reference to a new location for my user with push. 12 | var myUserRef = userListRef.push(); 13 | 14 | // Get a reference to my own presence status. 15 | var connectedRef = firebaseRef.child(".info/connected"); 16 | connectedRef.on("value", function(isOnline) { 17 | if (isOnline.val()) { 18 | // If we lose our internet connection, we want ourselves removed from the list. 19 | myUserRef.onDisconnect().remove(); 20 | 21 | // Set our initial online status. 22 | setUserStatus("\u2605 online"); 23 | } else { 24 | 25 | // We need to catch anytime we are marked as offline and then set the correct status. We 26 | // could be marked as offline 1) on page load or 2) when we lose our internet connection 27 | // temporarily. 28 | setUserStatus(currentStatus); 29 | } 30 | }); 31 | 32 | // A helper function to let us set our own state. 33 | function setUserStatus(status) { 34 | // Set our status in the list of online users. 35 | currentStatus = status; 36 | myUserRef.set({ name: name, status: status }); 37 | } 38 | 39 | // Update our GUI to show someone"s online status. 40 | userListRef.on("child_added", function(snapshot) { 41 | var user = snapshot.val(); 42 | $("#presenceDiv").append($("
").attr("id", snapshot.key())); 43 | $("#" + snapshot.key()).text(user.name + " is currently " + user.status); 44 | }); 45 | 46 | // Update our GUI to remove the status of a user who has left. 47 | userListRef.on("child_removed", function(snapshot) { 48 | $("#" + snapshot.key()).remove(); 49 | }); 50 | 51 | // Update our GUI to change a user"s status. 52 | userListRef.on("child_changed", function(snapshot) { 53 | var user = snapshot.val(); 54 | $("#" + snapshot.key()).text(user.name + " is currently " + user.status); 55 | }); 56 | 57 | // Use idle/away/back events created by idle.js to update our status information. 58 | document.onIdle = function () { 59 | setUserStatus("\u2606 idle"); 60 | } 61 | document.onAway = function () { 62 | setUserStatus("\u2604 away"); 63 | } 64 | document.onBack = function (isIdle, isAway) { 65 | setUserStatus("\u2605 online"); 66 | } 67 | 68 | setIdleTimeout(5000); 69 | setAwayTimeout(10000); 70 | }); 71 | -------------------------------------------------------------------------------- /drawing/js/drawing.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | //Set up some globals 3 | var pixSize = 8, lastPoint = null, currentColor = "000", mouseDown = 0; 4 | 5 | //Create a reference to the pixel data for our drawing. 6 | var pixelDataRef = new Firebase("https://INSTANCE.firebaseio.com/drawing"); 7 | 8 | // Set up our canvas 9 | var myCanvas = document.getElementById("drawing-canvas"); 10 | var myContext = myCanvas.getContext ? myCanvas.getContext("2d") : null; 11 | if (myContext == null) { 12 | alert("You must use a browser that supports HTML5 Canvas to run this demo."); 13 | return; 14 | } 15 | 16 | //Setup each color palette & add it to the screen 17 | var colors = ["fff","000","f00","0f0","00f","88f","f8d","f88","f05","f80","0f8","cf0","08f","408","ff8","8ff"]; 18 | for (c in colors) { 19 | var item = $("
").css("background-color", "#" + colors[c]).addClass("colorbox"); 20 | item.click((function () { 21 | var col = colors[c]; 22 | return function () { 23 | currentColor = col; 24 | }; 25 | })()); 26 | item.appendTo("#colorholder"); 27 | } 28 | 29 | //Keep track of if the mouse is up or down 30 | myCanvas.onmousedown = function () {mouseDown = 1;}; 31 | myCanvas.onmouseout = myCanvas.onmouseup = function () { 32 | mouseDown = 0, lastPoint = null; 33 | }; 34 | 35 | //Draw a line from the mouse's last position to its current position 36 | var drawLineOnMouseMove = function(e) { 37 | if (!mouseDown) return; 38 | 39 | // Bresenham's line algorithm. We use this to ensure smooth lines are drawn 40 | var offset = $("canvas").offset(); 41 | var x1 = Math.floor((e.pageX - offset.left) / pixSize - 1), 42 | y1 = Math.floor((e.pageY - offset.top) / pixSize - 1); 43 | var x0 = (lastPoint == null) ? x1 : lastPoint[0]; 44 | var y0 = (lastPoint == null) ? y1 : lastPoint[1]; 45 | var dx = Math.abs(x1 - x0), dy = Math.abs(y1 - y0); 46 | var sx = (x0 < x1) ? 1 : -1, sy = (y0 < y1) ? 1 : -1, err = dx - dy; 47 | while (true) { 48 | //write the pixel into Firebase, or if we are drawing white, remove the pixel 49 | pixelDataRef.child(x0 + ":" + y0).set(currentColor === "fff" ? null : currentColor); 50 | 51 | if (x0 == x1 && y0 == y1) break; 52 | var e2 = 2 * err; 53 | if (e2 > -dy) { 54 | err = err - dy; 55 | x0 = x0 + sx; 56 | } 57 | if (e2 < dx) { 58 | err = err + dx; 59 | y0 = y0 + sy; 60 | } 61 | } 62 | lastPoint = [x1, y1]; 63 | } 64 | $(myCanvas).mousemove(drawLineOnMouseMove); 65 | $(myCanvas).mousedown(drawLineOnMouseMove); 66 | 67 | // Add callbacks that are fired any time the pixel data changes and adjusts the canvas appropriately. 68 | // Note that child_added events will be fired for initial pixel data as well. 69 | var drawPixel = function(snapshot) { 70 | var coords = snapshot.key().split(":"); 71 | myContext.fillStyle = "#" + snapshot.val(); 72 | myContext.fillRect(parseInt(coords[0]) * pixSize, parseInt(coords[1]) * pixSize, pixSize, pixSize); 73 | } 74 | var clearPixel = function(snapshot) { 75 | var coords = snapshot.key().split(":"); 76 | myContext.clearRect(parseInt(coords[0]) * pixSize, parseInt(coords[1]) * pixSize, pixSize, pixSize); 77 | } 78 | 79 | pixelDataRef.on("child_added", drawPixel); 80 | pixelDataRef.on("child_changed", drawPixel); 81 | pixelDataRef.on("child_removed", clearPixel); 82 | }); 83 | -------------------------------------------------------------------------------- /leaderboard/js/leaderboard.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | var LEADERBOARD_SIZE = 5; 3 | 4 | // Build some firebase references. 5 | var rootRef = new Firebase("https://INSTANCE.firebaseio.com/leaderboard"); 6 | var scoreListRef = rootRef.child("scoreList"); 7 | var highestScoreRef = rootRef.child("highestScore"); 8 | 9 | // Keep a mapping of firebase locations to HTML elements, so we can move / remove elements as necessary. 10 | var htmlForPath = {}; 11 | 12 | // Helper function that takes a new score snapshot and adds an appropriate row to our leaderboard table. 13 | function handleScoreAdded(scoreSnapshot, prevScoreName) { 14 | var newScoreRow = $(""); 15 | newScoreRow.append($("").text(scoreSnapshot.val().name)); 16 | newScoreRow.append($("").text(scoreSnapshot.val().score)); 17 | 18 | // Store a reference to the table row so we can get it again later. 19 | htmlForPath[scoreSnapshot.key()] = newScoreRow; 20 | 21 | // Insert the new score in the appropriate place in the table. 22 | if (prevScoreName === null) { 23 | $("#leaderboardTable").append(newScoreRow); 24 | } 25 | else { 26 | var lowerScoreRow = htmlForPath[prevScoreName]; 27 | lowerScoreRow.before(newScoreRow); 28 | } 29 | } 30 | 31 | // Helper function to handle a score object being removed; just removes the corresponding table row. 32 | function handleScoreRemoved(scoreSnapshot) { 33 | var removedScoreRow = htmlForPath[scoreSnapshot.key()]; 34 | removedScoreRow.remove(); 35 | delete htmlForPath[scoreSnapshot.key()]; 36 | } 37 | 38 | // Create a view to only receive callbacks for the last LEADERBOARD_SIZE scores 39 | var scoreListView = scoreListRef.limitToLast(LEADERBOARD_SIZE); 40 | 41 | // Add a callback to handle when a new score is added. 42 | scoreListView.on("child_added", function (newScoreSnapshot, prevScoreName) { 43 | handleScoreAdded(newScoreSnapshot, prevScoreName); 44 | }); 45 | 46 | // Add a callback to handle when a score is removed 47 | scoreListView.on("child_removed", function (oldScoreSnapshot) { 48 | handleScoreRemoved(oldScoreSnapshot); 49 | }); 50 | 51 | // Add a callback to handle when a score changes or moves positions. 52 | var changedCallback = function (scoreSnapshot, prevScoreName) { 53 | handleScoreRemoved(scoreSnapshot); 54 | handleScoreAdded(scoreSnapshot, prevScoreName); 55 | }; 56 | scoreListView.on("child_moved", changedCallback); 57 | scoreListView.on("child_changed", changedCallback); 58 | 59 | // When the user presses enter on scoreInput, add the score, and update the highest score. 60 | $("#scoreInput").keypress(function (e) { 61 | if (e.keyCode == 13) { 62 | var newScore = Number($("#scoreInput").val()); 63 | var name = $("#nameInput").val(); 64 | $("#scoreInput").val(""); 65 | 66 | if (name.length === 0) 67 | return; 68 | 69 | var userScoreRef = scoreListRef.child(name); 70 | 71 | // Use setWithPriority to put the name / score in Firebase, and set the priority to be the score. 72 | userScoreRef.setWithPriority({ name:name, score:newScore }, newScore); 73 | 74 | // Track the highest score using a transaction. A transaction guarantees that the code inside the block is 75 | // executed on the latest data from the server, so transactions should be used if you have multiple 76 | // clients writing to the same data and you want to avoid conflicting changes. 77 | highestScoreRef.transaction(function (currentHighestScore) { 78 | if (currentHighestScore === null || newScore > currentHighestScore) { 79 | // The return value of this function gets saved to the server as the new highest score. 80 | return newScore; 81 | } 82 | // if we return with no arguments, it cancels the transaction. 83 | return; 84 | }); 85 | } 86 | }); 87 | 88 | // Add a callback to the highest score in Firebase so we can update the GUI any time it changes. 89 | highestScoreRef.on("value", function (newHighestScore) { 90 | $("#highestScoreDiv").text(newHighestScore.val()); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /presence/js/idle.js: -------------------------------------------------------------------------------- 1 | /** @license 2 | // idle.js (c) Alexios Chouchoulas 2009 http://www.bedroomlan.org/coding/detecting-%E2%80%98idle%E2%80%99-and-%E2%80%98away%E2%80%99-timeouts-javascript 3 | // Released under the terms of the GNU Public License version 2.0 (or later). 4 | */http://www.bedroomlan.org/coding/detecting-%E2%80%98idle%E2%80%99-and-%E2%80%98away%E2%80%99-timeouts-javascript 5 | 6 | var _API_JQUERY = 1; 7 | var _API_PROTOTYPE = 2; 8 | var _api; 9 | 10 | var _idleTimeout = 5000; 11 | var _awayTimeout = 10000; 12 | 13 | var _idleNow = false; 14 | var _idleTimestamp = null; 15 | var _idleTimer = null; 16 | var _awayNow = false; 17 | var _awayTimestamp = null; 18 | var _awayTimer = null; 19 | 20 | function setIdleTimeout(ms) 21 | { 22 | _idleTimeout = ms; 23 | _idleTimestamp = new Date().getTime() + ms; 24 | if (_idleTimer != null) { 25 | clearTimeout (_idleTimer); 26 | } 27 | _idleTimer = setTimeout(_makeIdle, ms + 50); 28 | //console.log('idle in ' + ms + ', tid = ' + _idleTimer); 29 | } 30 | 31 | function setAwayTimeout(ms) 32 | { 33 | _awayTimeout = ms; 34 | _awayTimestamp = new Date().getTime() + ms; 35 | if (_awayTimer != null) { 36 | clearTimeout (_awayTimer); 37 | } 38 | _awayTimer = setTimeout(_makeAway, ms + 50); 39 | //console.log('away in ' + ms); 40 | } 41 | 42 | function _makeIdle() 43 | { 44 | var t = new Date().getTime(); 45 | if (t < _idleTimestamp) { 46 | //console.log('Not idle yet. Idle in ' + (_idleTimestamp - t + 50)); 47 | _idleTimer = setTimeout(_makeIdle, _idleTimestamp - t + 50); 48 | return; 49 | } 50 | //console.log('** IDLE **'); 51 | _idleNow = true; 52 | 53 | try { 54 | if (document.onIdle) document.onIdle(); 55 | } catch (err) { 56 | } 57 | } 58 | 59 | function _makeAway() 60 | { 61 | var t = new Date().getTime(); 62 | if (t < _awayTimestamp) { 63 | //console.log('Not away yet. Away in ' + (_awayTimestamp - t + 50)); 64 | _awayTimer = setTimeout(_makeAway, _awayTimestamp - t + 50); 65 | return; 66 | } 67 | //console.log('** AWAY **'); 68 | _awayNow = true; 69 | 70 | try { 71 | if (document.onAway) document.onAway(); 72 | } catch (err) { 73 | } 74 | } 75 | 76 | 77 | function _initPrototype() 78 | { 79 | _api = _API_PROTOTYPE; 80 | } 81 | 82 | function _active(event) 83 | { 84 | var t = new Date().getTime(); 85 | _idleTimestamp = t + _idleTimeout; 86 | _awayTimestamp = t + _awayTimeout; 87 | //console.log('not idle.'); 88 | 89 | if (_idleNow) { 90 | setIdleTimeout(_idleTimeout); 91 | } 92 | 93 | if (_awayNow) { 94 | setAwayTimeout(_awayTimeout); 95 | } 96 | 97 | try { 98 | //console.log('** BACK **'); 99 | if ((_idleNow || _awayNow) && document.onBack) document.onBack(_idleNow, _awayNow); 100 | } catch (err) { 101 | } 102 | 103 | _idleNow = false; 104 | _awayNow = false; 105 | } 106 | 107 | function _initJQuery() 108 | { 109 | _api = _API_JQUERY; 110 | var doc = $(document); 111 | doc.ready(function(){ 112 | doc.mousemove(_active); 113 | try { 114 | doc.mouseenter(_active); 115 | } catch (err) { } 116 | try { 117 | doc.scroll(_active); 118 | } catch (err) { } 119 | try { 120 | doc.keydown(_active); 121 | } catch (err) { } 122 | try { 123 | doc.click(_active); 124 | } catch (err) { } 125 | try { 126 | doc.dblclick(_active); 127 | } catch (err) { } 128 | }); 129 | } 130 | 131 | function _initPrototype() 132 | { 133 | _api = _API_PROTOTYPE; 134 | var doc = $(document); 135 | Event.observe (window, 'load', function(event) { 136 | Event.observe(window, 'click', _active); 137 | Event.observe(window, 'mousemove', _active); 138 | Event.observe(window, 'mouseenter', _active); 139 | Event.observe(window, 'scroll', _active); 140 | Event.observe(window, 'keydown', _active); 141 | Event.observe(window, 'click', _active); 142 | Event.observe(window, 'dblclick', _active); 143 | }); 144 | } 145 | 146 | // Detect the API 147 | try { 148 | if (Prototype) _initPrototype(); 149 | } catch (err) { } 150 | 151 | try { 152 | if (jQuery) _initJQuery(); 153 | } catch (err) { } 154 | 155 | // End of file. 156 | -------------------------------------------------------------------------------- /tetris/js/tetris.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | var canvas = $("#canvas0").get(0); 3 | if (!canvas || !canvas.getContext || !canvas.getContext("2d")) 4 | alert("You must use a browser that supports HTML5 Canvas to run this demo."); 5 | 6 | function start() { 7 | var tetrisRef = new Firebase("https://INSTANCE.firebaseio.com/tetris"); 8 | var tetrisController = new Tetris.Controller(tetrisRef); 9 | } 10 | 11 | var Tetris = { }; 12 | 13 | 14 | /** 15 | * Various constants related to board size / drawing. 16 | */ 17 | Tetris.BOARD_WIDTH = 10; // (in "blocks", not pixels) 18 | Tetris.BOARD_HEIGHT = 20; 19 | 20 | Tetris.BLOCK_SIZE_PIXELS = 25; 21 | Tetris.BOARD_HEIGHT_PIXELS = Tetris.BOARD_HEIGHT * Tetris.BLOCK_SIZE_PIXELS; 22 | Tetris.BOARD_WIDTH_PIXELS = Tetris.BOARD_WIDTH * Tetris.BLOCK_SIZE_PIXELS; 23 | 24 | Tetris.BLOCK_BORDER_COLOR = "#484848"; 25 | Tetris.BLOCK_COLORS = { "X": "black", "b": "cyan", "B": "blue", "O": "orange", 26 | "Y": "yellow", "G": "green", "P": "#9370D8", "R": "red" }; 27 | 28 | Tetris.GRAVITY_DELAY = 300; // 300ms 29 | 30 | Tetris.EMPTY_LINE = " "; 31 | Tetris.FILLED_LINE = "XXXXXXXXXX"; 32 | Tetris.COMPLETE_LINE_PATTERN = /[^ ]{10}/; 33 | 34 | // Pieces. (Indexed by piece rotation (0-3), row (0-3), piece number (0-6)) 35 | Tetris.PIECES = []; 36 | for (var i = 0; i < 4; i++) { Tetris.PIECES[i] = []; } 37 | Tetris.PIECES[0][0] = [ " ", " ", " ", " ", " ", " ", " " ]; 38 | Tetris.PIECES[0][1] = [ " ", "B ", " O ", " YY ", " GG ", " P ", "RR " ]; 39 | Tetris.PIECES[0][2] = [ "bbbb", "BBB ", "OOO ", " YY ", "GG ", "PPP ", " RR " ]; 40 | Tetris.PIECES[0][3] = [ " ", " ", " ", " ", " ", " ", " " ]; 41 | Tetris.PIECES[1][0] = [ " b ", " ", " ", " ", " ", " ", " R " ]; 42 | Tetris.PIECES[1][1] = [ " b ", " B ", "OO ", " YY ", " G ", " P ", " RR " ]; 43 | Tetris.PIECES[1][2] = [ " b ", " B ", " O ", " YY ", " GG ", " PP ", " R " ]; 44 | Tetris.PIECES[1][3] = [ " b ", "BB ", " O ", " ", " G ", " P ", " " ]; 45 | Tetris.PIECES[2][0] = [ " ", " ", " ", " ", " ", " ", " " ]; 46 | Tetris.PIECES[2][1] = [ " ", " ", " ", " YY ", " GG ", " ", "RR " ]; 47 | Tetris.PIECES[2][2] = [ "bbbb", "BBB ", "OOO ", " YY ", "GG ", "PPP ", " RR " ]; 48 | Tetris.PIECES[2][3] = [ " ", " B ", "O ", " ", " ", " P ", " " ]; 49 | Tetris.PIECES[3][0] = [ " b ", " ", " ", " ", " ", " ", " R " ]; 50 | Tetris.PIECES[3][1] = [ " b ", " BB ", " O ", " YY ", " G ", " P ", " RR " ]; 51 | Tetris.PIECES[3][2] = [ " b ", " B ", " O ", " YY ", " GG ", "PP ", " R " ]; 52 | Tetris.PIECES[3][3] = [ " b ", " B ", " OO ", " ", " G ", " P ", " " ]; 53 | 54 | 55 | 56 | /** 57 | * Stores the state of a tetris board and handles drawing it. 58 | */ 59 | Tetris.Board = function (canvas, playerRef) { 60 | this.context = canvas.getContext("2d"); 61 | this.playerRef = playerRef; 62 | this.snapshot = null; 63 | this.isMyBoard = false; 64 | 65 | // Listen for changes to our board. 66 | var self = this; 67 | playerRef.on("value", function(snapshot) { 68 | self.snapshot = snapshot; 69 | self.draw(); 70 | }); 71 | }; 72 | 73 | 74 | /** 75 | * Draws the contents of the board as well as the current piece. 76 | */ 77 | Tetris.Board.prototype.draw = function () { 78 | // Clear canvas. 79 | this.context.clearRect(0, 0, Tetris.BOARD_WIDTH_PIXELS, Tetris.BOARD_HEIGHT_PIXELS); 80 | 81 | // Iterate over columns / rows in board data and draw each non-empty block. 82 | for (var x = 0; x < Tetris.BOARD_WIDTH; x++) { 83 | for (var y = 0; y < Tetris.BOARD_HEIGHT; y++) { 84 | var colorValue = this.getBlockVal(x, y); 85 | if (colorValue != " ") { 86 | // Calculate block position and draw a correctly-colored square. 87 | var left = x * Tetris.BLOCK_SIZE_PIXELS; 88 | var top = y * Tetris.BLOCK_SIZE_PIXELS; 89 | this.context.fillStyle = Tetris.BLOCK_COLORS[colorValue]; 90 | this.context.fillRect(left, top, Tetris.BLOCK_SIZE_PIXELS, Tetris.BLOCK_SIZE_PIXELS); 91 | this.context.lineWidth = 1; 92 | this.context.strokeStyle = Tetris.BLOCK_BORDER_COLOR; 93 | this.context.strokeRect(left, top, Tetris.BLOCK_SIZE_PIXELS, Tetris.BLOCK_SIZE_PIXELS); 94 | } 95 | } 96 | } 97 | 98 | // If there's a falling piece, draw it. 99 | if (this.snapshot !== null && this.snapshot.hasChild("piece")) { 100 | var piece = Tetris.Piece.fromSnapshot(this.snapshot.child("piece")); 101 | this.drawPiece(piece); 102 | } 103 | 104 | // If this isn't my board, dim it out with a 25% opacity black rectangle. 105 | if (!this.isMyBoard) { 106 | this.context.fillStyle = "rgba(0, 0, 0, 0.25)"; 107 | this.context.fillRect(0, 0, Tetris.BOARD_WIDTH_PIXELS, Tetris.BOARD_HEIGHT_PIXELS); 108 | } 109 | }; 110 | 111 | 112 | /** 113 | * Draw the currently falling piece. 114 | */ 115 | Tetris.Board.prototype.drawPiece = function (piece) { 116 | var self = this; 117 | this.forEachBlockOfPiece(piece, 118 | function (x, y, colorValue) { 119 | var left = x * Tetris.BLOCK_SIZE_PIXELS; 120 | var top = y * Tetris.BLOCK_SIZE_PIXELS; 121 | 122 | self.context.fillStyle = Tetris.BLOCK_COLORS[colorValue]; 123 | self.context.fillRect(left, top, Tetris.BLOCK_SIZE_PIXELS, Tetris.BLOCK_SIZE_PIXELS); 124 | self.context.lineWidth = 1; 125 | self.context.strokeStyle = Tetris.BLOCK_BORDER_COLOR; 126 | self.context.strokeRect(left, top, Tetris.BLOCK_SIZE_PIXELS, Tetris.BLOCK_SIZE_PIXELS); 127 | }); 128 | }; 129 | 130 | 131 | /** 132 | * Clear the board contents. 133 | */ 134 | Tetris.Board.prototype.clear = function () { 135 | for (var row = 0; row < Tetris.BOARD_HEIGHT; row++) { 136 | this.setRow(row, Tetris.EMPTY_LINE); 137 | } 138 | }; 139 | 140 | 141 | /** 142 | * Given a Tetris.Piece, returns true if it has collided with the board (i.e. its current position 143 | * and rotation causes it to overlap blocks already on the board). 144 | */ 145 | Tetris.Board.prototype.checkForPieceCollision = function (piece) { 146 | var collision = false; 147 | var self = this; 148 | this.forEachBlockOfPiece(piece, 149 | function (x, y, colorValue) { 150 | // NOTE: we explicitly allow y < 0 since pieces can be partially visible. 151 | if (x < 0 || x >= Tetris.BOARD_WIDTH || y >= Tetris.BOARD_HEIGHT) { 152 | collision = true; 153 | } 154 | else if (y >= 0 && self.getBlockVal(x, y) != " ") { 155 | collision = true; // collision with board contents. 156 | } 157 | }, /*includeInvalid=*/ true); 158 | 159 | return collision; 160 | }; 161 | 162 | 163 | /** 164 | * Given a Tetris.Piece that has landed, add it to the board contents. 165 | */ 166 | Tetris.Board.prototype.addLandedPiece = function (piece) { 167 | var self = this; 168 | // We go out of our way to set an entire row at a time just so the rows show up as 169 | // child_added in the graphical debugger, rather than child_changed. 170 | var rowY = -1, rowContents = null; 171 | this.forEachBlockOfPiece(piece, 172 | function (x, y, val) { 173 | if (y != rowY) { 174 | if (rowY !== -1) 175 | self.setRow(rowY, rowContents); 176 | 177 | rowContents = self.getRow(y); 178 | rowY = y; 179 | } 180 | rowContents = rowContents.substring(0, x).concat(val).concat(rowContents.substring(x + 1, Tetris.BOARD_WIDTH)); 181 | }); 182 | 183 | if (rowY !== -1) 184 | self.setRow(rowY, rowContents); 185 | }; 186 | 187 | 188 | /** 189 | * Check for any completed lines (no gaps) and remove them, then return the number 190 | * of removed lines. 191 | */ 192 | Tetris.Board.prototype.removeCompletedRows = function () { 193 | // Start at the bottom of the board, working up, removing completed lines. 194 | var copyFrom = Tetris.BOARD_HEIGHT - 1; 195 | var copyTo = copyFrom; 196 | 197 | var completedRows = 0; 198 | while (copyFrom >= 0) { 199 | var fromContents = this.getRow(copyFrom); 200 | 201 | // See if the line is complete (if so, we'll skip it) 202 | if (fromContents.match(Tetris.COMPLETE_LINE_PATTERN)) { 203 | copyFrom--; 204 | completedRows++; 205 | } else { 206 | // Copy the row down (to fill the gap from any removed rows) and continue on. 207 | this.setRow(copyTo, fromContents); 208 | copyFrom--; 209 | copyTo--; 210 | } 211 | } 212 | 213 | return completedRows; 214 | }; 215 | 216 | 217 | /** 218 | * Generate the specified number of junk rows at the bottom of the board. Return true if the added 219 | * rows overflowed the board (in which case the player loses). 220 | */ 221 | Tetris.Board.prototype.addJunkRows = function (numRows) { 222 | var overflow = false; 223 | // First, check if any blocks are going to overflow off the top of the screen. 224 | var topRowContents = this.getRow(numRows - 1); 225 | overflow = topRowContents.match(/[^ ]/); 226 | 227 | // Shift rows up to make room for the new rows. 228 | for (var i = 0; i < Tetris.BOARD_HEIGHT - numRows; i++) { 229 | var moveLineContents = this.getRow(i + numRows); 230 | this.setRow(i, moveLineContents); 231 | } 232 | 233 | // Fill the bottom with junk rows that are full except for a single random gap. 234 | var gap = Math.floor(Math.random() * Tetris.FILLED_LINE.length); 235 | var junkRow = Tetris.FILLED_LINE.substring(0, gap) + " " + Tetris.FILLED_LINE.substring(gap + 1); 236 | for (i = Tetris.BOARD_HEIGHT - numRows; i < Tetris.BOARD_HEIGHT; i++) { 237 | this.setRow(i, junkRow); 238 | } 239 | 240 | return overflow; 241 | }; 242 | 243 | 244 | /** 245 | * Helper to enumerate the blocks that make up a particular piece. Calls fn() for each block, 246 | * passing the x and y position of the block and the color value. If includeInvalid is true, it 247 | * includes blocks that would fall outside the bounds of the board. 248 | */ 249 | Tetris.Board.prototype.forEachBlockOfPiece = function (piece, fn, includeInvalid) { 250 | for (var blockY = 0; blockY < 4; blockY++) { 251 | for (var blockX = 0; blockX < 4; blockX++) { 252 | var colorValue = Tetris.PIECES[piece.rotation][blockY][piece.pieceNum].charAt(blockX); 253 | if (colorValue != " ") { 254 | var x = piece.x + blockX, y = piece.y + blockY; 255 | if (includeInvalid || (x >= 0 && x < Tetris.BOARD_WIDTH && y >= 0 && y < Tetris.BOARD_HEIGHT)) { 256 | fn(x, y, colorValue); 257 | } 258 | } 259 | } 260 | } 261 | }; 262 | 263 | 264 | Tetris.Board.prototype.getRow = function (y) { 265 | var row = (y < 10) ? ("0" + y) : ("" + y); // Pad row so they sort nicely in debugger. :-) 266 | 267 | var rowContents = this.snapshot === null ? null : this.snapshot.child("board/" + row).val(); 268 | return rowContents || Tetris.EMPTY_LINE; 269 | }; 270 | 271 | 272 | Tetris.Board.prototype.getBlockVal = function (x, y) { 273 | return this.getRow(y).charAt(x); 274 | }; 275 | 276 | 277 | Tetris.Board.prototype.setRow = function (y, rowContents) { 278 | var row = (y < 10) ? ("0" + y) : ("" + y); // Pad row so they sort nicely in debugger. :-) 279 | 280 | if (rowContents === Tetris.EMPTY_LINE) 281 | rowContents = null; // delete empty lines so we get remove / added events in debugger. :-) 282 | 283 | this.playerRef.child("board").child(row).set(rowContents); 284 | }; 285 | 286 | 287 | Tetris.Board.prototype.setBlockVal = function (x, y, val) { 288 | var rowContents = this.getRow(y); 289 | rowContents = rowContents.substring(0, x) + val + rowContents.substring(x+1); 290 | this.setRow(y, rowContents); 291 | }; 292 | 293 | 294 | /** 295 | * Immutable object representing a falling piece along with its rotation and board position. 296 | * Has helpers for generating mutated Tetris.Piece objects (e.g. rotated or dropped). 297 | */ 298 | Tetris.Piece = function (pieceNum, x, y, rotation) { 299 | if (arguments.length > 0) { 300 | this.pieceNum = pieceNum; 301 | this.x = x; 302 | this.y = y; 303 | this.rotation = rotation; 304 | } else { 305 | // Initialize new random piece. 306 | this.pieceNum = Math.floor(Math.random() * 7); 307 | this.x = 4; // "center" it. 308 | this.y = -2; // this will make the bottom line of the piece visible. 309 | this.rotation = 0; 310 | } 311 | }; 312 | 313 | 314 | /** 315 | * Create a piece from a Firebase snapshot representing a piece. 316 | */ 317 | Tetris.Piece.fromSnapshot = function (snapshot) { 318 | var piece = snapshot.val(); 319 | return new Tetris.Piece(piece.pieceNum, piece.x, piece.y, piece.rotation); 320 | }; 321 | 322 | 323 | /** 324 | * Writes the current piece data into Firebase. 325 | */ 326 | Tetris.Piece.prototype.writeToFirebase = function (pieceRef) { 327 | pieceRef.set({pieceNum: this.pieceNum, x: this.x, y: this.y, rotation: this.rotation}); 328 | }; 329 | 330 | 331 | Tetris.Piece.prototype.drop = function () { 332 | return new Tetris.Piece(this.pieceNum, this.x, this.y + 1, this.rotation); 333 | }; 334 | 335 | 336 | Tetris.Piece.prototype.rotate = function () { 337 | return new Tetris.Piece(this.pieceNum, this.x, this.y, (this.rotation + 1) % 4); 338 | }; 339 | 340 | 341 | Tetris.Piece.prototype.moveLeft = function () { 342 | return new Tetris.Piece(this.pieceNum, this.x - 1, this.y, this.rotation); 343 | }; 344 | 345 | 346 | Tetris.Piece.prototype.moveRight = function () { 347 | return new Tetris.Piece(this.pieceNum, this.x + 1, this.y, this.rotation); 348 | }; 349 | 350 | 351 | 352 | /** 353 | * Manages joining the game, responding to keypresses, making the piece drop, etc. 354 | */ 355 | Tetris.PlayingState = { Watching: 0, Joining: 1, Playing: 2 }; 356 | Tetris.Controller = function (tetrisRef) { 357 | this.tetrisRef = tetrisRef; 358 | this.createBoards(); 359 | 360 | this.playingState = Tetris.PlayingState.Watching; 361 | this.waitToJoin(); 362 | }; 363 | 364 | 365 | Tetris.Controller.prototype.createBoards = function () { 366 | this.boards = []; 367 | for(var i = 0; i <= 1; i++) { 368 | var playerRef = this.tetrisRef.child("player" + i); 369 | var canvas = $("#canvas" + i).get(0); 370 | this.boards.push(new Tetris.Board(canvas, playerRef)); 371 | } 372 | }; 373 | 374 | 375 | Tetris.Controller.prototype.waitToJoin = function() { 376 | var self = this; 377 | 378 | // Listen on "online" location for player0 and player1. 379 | this.tetrisRef.child("player0/online").on("value", function(onlineSnap) { 380 | if (onlineSnap.val() === null && self.playingState === Tetris.PlayingState.Watching) { 381 | self.tryToJoin(0); 382 | } 383 | }); 384 | 385 | this.tetrisRef.child("player1/online").on("value", function(onlineSnap) { 386 | if (onlineSnap.val() === null && self.playingState === Tetris.PlayingState.Watching) { 387 | self.tryToJoin(1); 388 | } 389 | }); 390 | }; 391 | 392 | 393 | /** 394 | * Try to join the game as the specified playerNum. 395 | */ 396 | Tetris.Controller.prototype.tryToJoin = function(playerNum) { 397 | // Set ourselves as joining to make sure we don't try to join as both players. :-) 398 | this.playingState = Tetris.PlayingState.Joining; 399 | 400 | // Use a transaction to make sure we don't conflict with other people trying to join. 401 | var self = this; 402 | this.tetrisRef.child("player" + playerNum + "/online").transaction(function(onlineVal) { 403 | if (onlineVal === null) { 404 | return true; // Try to set online to true. 405 | } else { 406 | return; // Somebody must have beat us. Abort the transaction. 407 | } 408 | }, function(error, committed) { 409 | if (committed) { // We got in! 410 | self.playingState = Tetris.PlayingState.Playing; 411 | self.startPlaying(playerNum); 412 | } else { 413 | self.playingState = Tetris.PlayingState.Watching; 414 | } 415 | }); 416 | }; 417 | 418 | 419 | /** 420 | * Once we've joined, enable controlling our player. 421 | */ 422 | Tetris.Controller.prototype.startPlaying = function (playerNum) { 423 | this.myPlayerRef = this.tetrisRef.child("player" + playerNum); 424 | this.opponentPlayerRef = this.tetrisRef.child("player" + (1 - playerNum)); 425 | this.myBoard = this.boards[playerNum]; 426 | this.myBoard.isMyBoard = true; 427 | this.myBoard.draw(); 428 | 429 | // Clear our "online" status when we disconnect so somebody else can join. 430 | this.myPlayerRef.child("online").onDisconnect().remove(); 431 | 432 | // Detect when other player pushes rows to our board. 433 | this.watchForExtraRows(); 434 | 435 | // Detect when game is restarted by other player. 436 | this.watchForRestart(); 437 | 438 | $("#gameInProgress").hide(); 439 | 440 | var self = this; 441 | $("#restartButton").removeAttr("disabled"); 442 | $("#restartButton").click(function () { 443 | self.restartGame(); 444 | }); 445 | 446 | this.initializePiece(); 447 | this.enableKeyboard(); 448 | this.resetGravity(); 449 | }; 450 | 451 | 452 | Tetris.Controller.prototype.initializePiece = function() { 453 | this.fallingPiece = null; 454 | var pieceRef = this.myPlayerRef.child("piece"); 455 | var self = this; 456 | 457 | // Watch for changes to the current piece (and initialize it if it's null). 458 | pieceRef.on("value", function(snapshot) { 459 | if (snapshot.val() === null) { 460 | var newPiece = new Tetris.Piece(); 461 | newPiece.writeToFirebase(pieceRef); 462 | } else { 463 | self.fallingPiece = Tetris.Piece.fromSnapshot(snapshot); 464 | } 465 | }); 466 | }; 467 | 468 | 469 | /** 470 | * Sets up handlers for all keyboard commands. 471 | */ 472 | Tetris.Controller.prototype.enableKeyboard = function () { 473 | var self = this; 474 | $(document).on("keydown", function (evt) { 475 | if (self.fallingPiece === null) 476 | return; // piece isn't initialized yet. 477 | 478 | var keyCode = evt.which; 479 | var key = { space:32, left:37, up:38, right:39, down:40 }; 480 | 481 | var newPiece = null; 482 | switch (keyCode) { 483 | case key.left: 484 | newPiece = self.fallingPiece.moveLeft(); 485 | break; 486 | case key.up: 487 | newPiece = self.fallingPiece.rotate(); 488 | break; 489 | case key.right: 490 | newPiece = self.fallingPiece.moveRight(); 491 | break; 492 | case key.down: 493 | newPiece = self.fallingPiece.drop(); 494 | break; 495 | case key.space: 496 | // Drop as far as we can. 497 | var droppedPiece = self.fallingPiece; 498 | do { 499 | newPiece = droppedPiece; 500 | droppedPiece = droppedPiece.drop(); 501 | } while (!self.myBoard.checkForPieceCollision(droppedPiece)); 502 | break; 503 | } 504 | 505 | if (newPiece !== null) { 506 | // If the new piece position / rotation is valid, update self.fallingPiece and firebase. 507 | if (!self.myBoard.checkForPieceCollision(newPiece)) { 508 | // If the keypress moved the piece down, reset gravity. 509 | if (self.fallingPiece.y != newPiece.y) { 510 | self.resetGravity(); 511 | } 512 | 513 | newPiece.writeToFirebase(self.myPlayerRef.child("piece")); 514 | } 515 | return false; // handled 516 | } 517 | 518 | return true; 519 | }); 520 | }; 521 | 522 | 523 | /** 524 | * Sets a timer to make the piece repeatedly drop after GRAVITY_DELAY ms. 525 | */ 526 | Tetris.Controller.prototype.resetGravity = function () { 527 | // If there's a timer already active, clear it first. 528 | if (this.gravityIntervalId !== null) { 529 | clearInterval(this.gravityIntervalId); 530 | } 531 | 532 | var self = this; 533 | this.gravityIntervalId = setInterval(function() { 534 | self.doGravity(); 535 | }, Tetris.GRAVITY_DELAY); 536 | }; 537 | 538 | 539 | Tetris.Controller.prototype.doGravity = function () { 540 | if (this.fallingPiece === null) 541 | return; // piece isn't initialized yet. 542 | 543 | var newPiece = this.fallingPiece.drop(); 544 | 545 | // If we've hit the bottom, add the (pre-drop) piece to the board and create a new piece. 546 | if (this.myBoard.checkForPieceCollision(newPiece)) { 547 | this.myBoard.addLandedPiece(this.fallingPiece); 548 | 549 | // Check for completed lines and if appropriate, push extra rows to our opponent. 550 | var completedRows = this.myBoard.removeCompletedRows(); 551 | var rowsToPush = (completedRows === 4) ? 4 : completedRows - 1; 552 | if (rowsToPush > 0) 553 | this.opponentPlayerRef.child("extrarows").push(rowsToPush); 554 | 555 | // Create new piece (it'll be initialized to a random piece at the top of the screen). 556 | newPiece = new Tetris.Piece(); 557 | 558 | // Is the board full? 559 | if (this.myBoard.checkForPieceCollision(newPiece)) 560 | this.gameOver(); 561 | } 562 | 563 | newPiece.writeToFirebase(this.myPlayerRef.child("piece")); 564 | }; 565 | 566 | 567 | /** 568 | * Detect when our opponent pushes extra rows to us. 569 | */ 570 | Tetris.Controller.prototype.watchForExtraRows = function () { 571 | var self = this; 572 | var extraRowsRef = this.myPlayerRef.child("extrarows"); 573 | extraRowsRef.on("child_added", function(snapshot) { 574 | var rows = snapshot.val(); 575 | extraRowsRef.child(snapshot.key()).remove(); 576 | 577 | var overflow = self.myBoard.addJunkRows(rows); 578 | if (overflow) 579 | self.gameOver(); 580 | 581 | // Also move piece up to avoid collisions. 582 | if (self.fallingPiece) { 583 | self.fallingPiece.y -= rows; 584 | self.fallingPiece.writeToFirebase(self.myPlayerRef.child("piece")); 585 | } 586 | }); 587 | }; 588 | 589 | 590 | /** 591 | * Detect when our opponent restarts the game. 592 | */ 593 | Tetris.Controller.prototype.watchForRestart = function () { 594 | var self = this; 595 | var restartRef = this.myPlayerRef.child("restart"); 596 | restartRef.on("value", function(snap) { 597 | if (snap.val() === 1) { 598 | restartRef.set(0); 599 | self.resetMyBoardAndPiece(); 600 | } 601 | }); 602 | }; 603 | 604 | 605 | Tetris.Controller.prototype.gameOver = function () { 606 | this.restartGame(); 607 | }; 608 | 609 | 610 | Tetris.Controller.prototype.restartGame = function () { 611 | this.opponentPlayerRef.child("restart").set(1); 612 | this.resetMyBoardAndPiece(); 613 | }; 614 | 615 | 616 | Tetris.Controller.prototype.resetMyBoardAndPiece = function () { 617 | this.myBoard.clear(); 618 | var newPiece = new Tetris.Piece(); 619 | newPiece.writeToFirebase(this.myPlayerRef.child("piece")); 620 | }; 621 | 622 | 623 | start(); 624 | }); 625 | --------------------------------------------------------------------------------