├── .gitignore ├── HighScoreService.js ├── README.md ├── bower.json ├── cloud └── main.js └── test.html /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components -------------------------------------------------------------------------------- /HighScoreService.js: -------------------------------------------------------------------------------- 1 | 2 | ; (function (window) { 3 | 'use strict'; 4 | /** 5 | * The HighScoreService implements a cloud based leaderboard 6 | * system to track your gamers high scores and achivements. 7 | * The implementation is based on parse.com, so you will have to create a free account at https://parse.com/ to use this class. 8 | * The parse.com js api must be avaulable at runtime: 9 | * 10 | * 11 | * Create an instance of HighScoreService: 12 | * 13 | * var highScoreService = new HighScoreService({ 14 | * appId: "PARSE_APP_ID", 15 | * key: "PARSE_JS_KEY", 16 | * debug:true, 17 | * updateUserDetails:function(details, user){ 18 | * highScoreService.logDebug("updateUserDetails called width", details, user); 19 | * }, 20 | * updateScoreDetails:function(details, highScore){ 21 | * highScoreService.logDebug("updateScoreDetails called width", details, highScore); 22 | * } 23 | * }); 24 | * 25 | * @param {object} config 26 | * @returns {HighScoreService} 27 | */ 28 | function HighScoreService(config) { 29 | this.assertRequired(config, "config"); 30 | this.assertRequired(config.appId, "config.appId"); 31 | this.assertRequired(config.key, "config.key"); 32 | 33 | this.config = config; 34 | 35 | this.resetPlayTime(); 36 | 37 | // Authenticate to parse platform 38 | Parse.initialize(this.config.appId, this.config.key); 39 | } 40 | 41 | HighScoreService.prototype.resetPlayTime = function () { 42 | this.playTimeReference = new Date().getTime(); 43 | }; 44 | 45 | HighScoreService.prototype.getPlayTime = function () { 46 | return new Date().getTime() - this.playTimeReference; 47 | }; 48 | 49 | HighScoreService.prototype.assertRequired = function (condition, name) { 50 | if (!condition) { 51 | throw new Error(name + " parameter is required"); 52 | } 53 | }; 54 | 55 | HighScoreService.prototype.errorHandler = function (object, error) { 56 | console.log("Parse Operation failed", object, error); 57 | }; 58 | 59 | HighScoreService.prototype.logDebug = function () { 60 | if (console && this.config.debug) { 61 | console.log.apply(console, arguments); 62 | } 63 | }; 64 | 65 | HighScoreService.prototype.logError = function () { 66 | if (console) { 67 | console.error.apply(console, arguments); 68 | } 69 | }; 70 | 71 | HighScoreService.prototype.login = function (credentials, loginSuccess, loginError) { 72 | this.assertRequired(loginSuccess, "loginSuccess"); 73 | this.assertRequired(credentials.username, "credentials.username"); 74 | 75 | if (!loginError) { 76 | loginError = this.errorHandler; 77 | } 78 | 79 | this.loginSuccess = loginSuccess; 80 | this.loginError = loginError; 81 | var self = this; 82 | this.userExists(credentials.username, function (exists) { 83 | if (exists) { 84 | self.loginUser(credentials); 85 | } else { 86 | self.createUser(credentials); 87 | } 88 | }); 89 | }; 90 | 91 | HighScoreService.prototype.userExists = function (username, callback) { 92 | var self = this; 93 | var query = new Parse.Query(Parse.User); 94 | query.equalTo("username", username); 95 | query.find({ 96 | success: function (user) { 97 | if (user && user.length > 0) { 98 | self.logDebug("User exists", username); 99 | callback(true); 100 | } else { 101 | self.logDebug("User does not exist", username); 102 | callback(false); 103 | } 104 | }, 105 | error: function (user, error) { 106 | self.logError("User does not exist", username); 107 | callback(false); 108 | } 109 | }); 110 | }; 111 | 112 | HighScoreService.prototype.loginUser = function (credentials) { 113 | var self = this; 114 | this.processCredentials(credentials); 115 | Parse.User.logIn(credentials.username, credentials.password, { 116 | success: function (user) { 117 | self.logDebug("Sucessfully logged in user", user); 118 | self.loginSuccess(user) 119 | }, 120 | error: this.loginError 121 | }); 122 | }; 123 | 124 | HighScoreService.prototype.logoutUser = function () { 125 | Parse.User.logOut(); 126 | }; 127 | 128 | HighScoreService.prototype.processCredentials = function (credentials) { 129 | if (!credentials.password || credentials.password==="") { 130 | credentials.password = credentials.username; 131 | } 132 | if (!credentials.email || credentials.email==="") { 133 | credentials.email = credentials.username + "@" + credentials.username + ".com"; 134 | } 135 | }; 136 | 137 | HighScoreService.prototype.createUser = function (credentials) { 138 | var self = this; 139 | var user = new Parse.User(); 140 | this.processCredentials(credentials); 141 | user.set("username", credentials.username); 142 | user.set("password", credentials.password); 143 | user.set("email", credentials.email); 144 | user.set("timePlayed", 0); 145 | user.set("userDetails", {}); 146 | if (this.config.updateUserDetails){ 147 | this.config.updateUserDetails(user.get("userDetails"), user); 148 | } 149 | user.set("rank", 0); 150 | 151 | user.signUp(null, { 152 | success: function (user) { 153 | self.logDebug("Created user", user); 154 | self.createHighScore(function (highScore) { 155 | self.loginUser(credentials); 156 | }); 157 | }, 158 | error: this.errorHandler 159 | }); 160 | }; 161 | 162 | HighScoreService.prototype.getUser = function () { 163 | return Parse.User.current(); 164 | }; 165 | 166 | HighScoreService.prototype.updateUser = function () { 167 | var self = this; 168 | var user = Parse.User.current(); 169 | user.set("timePlayed", user.get("timePlayed")+this.getPlayTime()); 170 | if (this.config.updateUserDetails){ 171 | this.config.updateUserDetails(user.get("userDetails"), user); 172 | } 173 | user.save(null, { 174 | success: function(user) { 175 | self.logDebug("Play time set to", user.get("timePlayed")); 176 | self.resetPlayTime(); 177 | }, 178 | error: this.errorHandler 179 | }); 180 | }; 181 | 182 | HighScoreService.prototype.getAchievements = function (successCallback) { 183 | this.assertRequired(successCallback, "successCallback"); 184 | var self = this; 185 | var query = new Parse.Query("Achievement"); 186 | query.equalTo("user", Parse.User.current()); 187 | query.find({ 188 | success: function (achievements) { 189 | self.logDebug("Loaded achievements", achievements); 190 | successCallback(achievements); 191 | }, 192 | error: this.errorHandler 193 | }); 194 | }; 195 | 196 | HighScoreService.prototype.addAchievement = function (name, description, score, details, callback) { 197 | var self = this; 198 | 199 | var Achievement = Parse.Object.extend("Achievement"); 200 | var achievement = new Achievement(); 201 | achievement.set("name", name); 202 | achievement.set("description", description); 203 | achievement.set("score", score); 204 | achievement.set("user", Parse.User.current()); 205 | achievement.set("details", details); 206 | 207 | var acl = new Parse.ACL(Parse.User.current()); 208 | acl.setPublicReadAccess(false); 209 | achievement.setACL(acl); 210 | 211 | achievement.save(null, { 212 | success: function (achievement) { 213 | self.logDebug("Saved achievement with id", achievement); 214 | self.setScore(score, true, function(highScore){ 215 | if (callback) { 216 | callback(achievement); 217 | } 218 | }); 219 | }, 220 | error: this.errorHandler 221 | }); 222 | }; 223 | 224 | HighScoreService.prototype.getHighScore = function (successCallback) { 225 | this.assertRequired(successCallback, "successCallback"); 226 | var self = this; 227 | // Create a query object 228 | var query = new Parse.Query("HighScore"); 229 | query.equalTo("user", Parse.User.current()); 230 | query.find({ 231 | success: function (highScore) { 232 | self.logDebug("Loaded high score", highScore); 233 | successCallback(highScore[0]); 234 | }, 235 | error: this.errorHandler 236 | }); 237 | }; 238 | 239 | HighScoreService.prototype.createHighScore = function (callback) { 240 | var HighScore = Parse.Object.extend("HighScore"); 241 | var highScore = new HighScore(); 242 | highScore.set("score", 0); 243 | highScore.set("lastScore", 0); 244 | highScore.set("scoreDetails", {}); 245 | highScore.set("username", Parse.User.current().get("username")); 246 | highScore.set("user", Parse.User.current()); 247 | this.saveHighScore(highScore, callback); 248 | }; 249 | 250 | HighScoreService.prototype.saveHighScore = function (highScore, callback) { 251 | var self = this; 252 | var acl = new Parse.ACL(Parse.User.current()); 253 | acl.setPublicReadAccess(true); 254 | highScore.setACL(acl); 255 | highScore.save(null, { 256 | success: function (highScore) { 257 | self.updateUser(); 258 | self.logDebug("Saved highscore with id", highScore); 259 | if (callback) { 260 | callback(highScore); 261 | } 262 | }, 263 | error: this.errorHandler 264 | }); 265 | }; 266 | 267 | HighScoreService.prototype.getLeaderBoard = function (limit, centerPlayer, successCallback) { 268 | var self = this; 269 | var query = new Parse.Query("HighScore"); 270 | query.ascending("rank"); 271 | query.limit(limit); 272 | 273 | if (centerPlayer === null || centerPlayer === undefined){ 274 | centerPlayer = false; 275 | } 276 | 277 | if (centerPlayer){ 278 | this.getHighScore(function(highScore){ 279 | var start = highScore.get("rank")-(Math.floor(limit/2)); 280 | if (start > 1){ 281 | query.greaterThanOrEqualTo("rank", start); 282 | } 283 | query.find({ 284 | success: function (leaderBoard) { 285 | self.logDebug("Loaded centered leaderboard", leaderBoard); 286 | successCallback(leaderBoard); 287 | }, 288 | error: self.errorHandler 289 | }); 290 | }); 291 | } else { 292 | query.find({ 293 | success: function (leaderBoard) { 294 | self.logDebug("Loaded leaderboard", leaderBoard); 295 | successCallback(leaderBoard); 296 | }, 297 | error: this.errorHandler 298 | }); 299 | } 300 | 301 | }; 302 | 303 | HighScoreService.prototype.setScore = function (score, add, callback) { 304 | var self = this; 305 | this.getHighScore(function (highScore) { 306 | if (add) { 307 | score = score + highScore.get("score"); 308 | } 309 | highScore.set("lastScore", highScore.get("score")); 310 | highScore.set("score", score); 311 | if (self.config.updateScoreDetails){ 312 | self.config.updateScoreDetails(highScore.get("scoreDetails"), highScore); 313 | } 314 | self.saveHighScore(highScore, callback); 315 | }); 316 | }; 317 | 318 | /** 319 | * Add HighScoreService to global namespace 320 | */ 321 | window.HighScoreService = HighScoreService; 322 | 323 | })(window); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # parse-highscore-service 2 | JS implementation of a highscore and leaderboard service based on [parse.com](http://parse.com) 3 | 4 | 5 | ## Objective 6 | The HighScoreService Class can be used for HTML5 games, where a simple cloud based highscore and leaderboard system is needed. For smaller games it may not be an option to depend on Google's or Apple's game services. 7 | The implementation has no other dependencies than the parse.com api client. 8 | 9 | 10 | ## Description 11 | The functionality for the highscore service divides in two parts. The first one is the cloud code (```main.js```) which runs as a parse.com application. 12 | The second one is the HighScoreService class (```HighScoreService.js```) as a client. It connects to the parse backend and handles data via Parse JavaScript SDK. 13 | The [parse backend](https://dashboard.parse.com/apps/) gives you some very nice functionality. Just take a look to the analytics section of your app to have an idea about the usage of your game or browse the complete data via core section. 14 | 15 | 16 | ## Features 17 | * Manage players (Users) 18 | * Player specific highscores 19 | * Track players game time 20 | * Manage achievements for players 21 | * Leaderboard 22 | * Store game specific values to the detail objects of achievement and highscore entities 23 | 24 | 25 | ## Getting started 26 | 27 | ### 1. Create a free account at parse.com 28 | Go to parse.com [signup page](https://www.parse.com/signup) to create a free account and choose an app name for your games highscore backend. 29 | 30 | ### 2. Install parse.com command line tool 31 | Because we need some Server Code that does our ranking, we have to deploy the cloud application part of the highscore service via parse.com cloud code option. 32 | Therefore we need to install the parse.com command line tool as described [here](https://parse.com/docs/cloudcode/guide#command-line-installation). 33 | 34 | ### 3. Create your parse app 35 | Go to your terminal and type `parse new`. Choose the existing app from step 1 and follow the instructions on the screen. 36 | After that you will have a parse.com project on your workstation that makes it easy to modify and deploy the cloud code. 37 | Now copy the main.js file from form the cloud folder of this repository to the ```cloud``` folder of the created parse application. It contains the code to 38 | Then you can deploy the cloud code via `parse deploy` command. 39 | 40 | ### 4. Test your backend 41 | The git repository contains a `test.html` that you can use to test your highscore backend. 42 | Your Application ID (`PARSE_APP_ID`) and the JavaScript key (`PARSE_APP_ID`) have to be configured in the application panel so that the test app can connect to your backend. 43 | You can get the two keys from `parse.com > Applications > Your App > App Settings > Security & Keys`. 44 | 45 | After that you can choose a user/player name and optionally a password and an email address and hit _Create or Login_. 46 | If the user does not exist a new user is created, otherwise the existing user is logged in. If you did not specify a password the username is taken as password, which is easy, but less secure. 47 | After you logged in a player, a panel with game actions comes up. You can add score and achievements from there. The leaderboard data is refreshed on every action and you can see the most important properties of the player in the player stats panel. If you can add scores and you see the leaderboard your Backend should be ready to go, now let's focus on your game. 48 | 49 | [Go to Test App](http://peermedia.de/parse-highscore-service/test.html) 50 | 51 | ### 5. Integrate highscore service in your game 52 | 53 | #### 5.1 Install using Bower 54 | You can install highscore service via bower. 55 | 56 | `bower install parse-highscore-service` 57 | 58 | #### 5.2 Load dependencies 59 | 60 | ``` 61 | 62 | 63 | ``` 64 | 65 | #### 5.3 Create an instance of highscore service for use in your game 66 | Define the highscore service instance at your games bootstrap. 67 | 68 | ``` 69 | var highScoreService = new HighScoreService({ 70 | appId: "PARSE_APP_ID", 71 | key: "PARSE_JS_KEY", 72 | debug:true, 73 | updateUserDetails:function(details, user){ 74 | highScoreService.logDebug("updateUserDetails called width", details, user); 75 | }, 76 | updateScoreDetails:function(details, highScore){ 77 | highScoreService.logDebug("updateScoreDetails called width", details, highScore); 78 | } 79 | }); 80 | ``` 81 | 82 | You have to configure the Application ID (`PARSE_APP_ID`) and the JavaScript key (`PARSE_APP_ID`) so that the your client side game can connect to your backend. 83 | You can get the two keys from `parse.com > Applications > Your App > App Settings > Security & Keys`. 84 | If you set the debug property to `true` the service will log it's actions to the console. 85 | You can optionally configure callbacks to do your stuff when user is persisted (`updateUserDetails`) or when a highScore object is persisted (`updateScoreDetails`). 86 | 87 | 88 | ### 6. Use highscore service in your game 89 | 90 | #### 6.1 Login or create user 91 | The first call you have to do is the login call, because everything works on data of the user. 92 | 93 | ``` 94 | highScoreService.login({username: 'username', password: 'password', email: 'test@test.com'}, function(user){ 95 | console.log("Got login callback for", user); 96 | }); 97 | ``` 98 | 99 | #### 6.2 Modifying players score 100 | 101 | ``` 102 | highScoreService.setScore(500, true, function(highScore){ 103 | console.log("Score set to", highScore.get("score")); 104 | }); 105 | ``` 106 | 107 | #### 6.3 Adding an achievement 108 | 109 | ``` 110 | highScoreService.addAchievement('name', 'description', 200, {myDetailProperty:'...'}), function(achievement){ 111 | ... 112 | }); 113 | ``` 114 | 115 | #### 6.4 Get the current highscore 116 | 117 | ``` 118 | highScoreService.getHighScore(function(highScore){ 119 | ... 120 | }); 121 | ``` 122 | 123 | #### 6.5 Get leaderboard 124 | ##### 6.5.1 Top 10 Example 125 | Returns the top 10 ranks regardless of the current players rank. 126 | 127 | ``` 128 | highScoreService.getLeaderBoard(10, false, function(leaderBoard){ 129 | for (var i = 0;i < leaderBoard.length; i++){ 130 | // leaderBoard[i].get("rank") leaderBoard[i].get("username") leaderBoard[i].get("score") ... 131 | } 132 | }); 133 | ``` 134 | 135 | ##### 6.5.2 Centered to player 136 | Returns 5 ranks centered around the current players rank. 137 | 138 | ``` 139 | highScoreService.getLeaderBoard(5, true, function(leaderBoard){ 140 | for (var i = 0;i < leaderBoard.length; i++){ 141 | ... 142 | } 143 | }); 144 | ``` 145 | #### 6.6 Get the current players achievements 146 | 147 | ``` 148 | highScoreService.getAchievements(function(achievements){ 149 | for (var i = 0;i < achievements.length; i++){ 150 | //achievements[i].get("name") achievements[i].get("description") achievements[i].get("score") ... 151 | } 152 | }); 153 | ``` 154 | 155 | ### 7. Security 156 | The parse api helps to secure user data and provides ACL mechanisms for every entity. 157 | But since highscore service is a client side application, some more advanced users could try to cheat and submit hacked scores by using the JS methods. The only way to work against that is to validate the highscore submissions by using cloud code. 158 | 159 | You can place a second event listener in your `main.js` file: 160 | 161 | ``` 162 | /** 163 | * Validate submitted scores 164 | */ 165 | Parse.Cloud.beforeSave("HighScore", function(request, response) { 166 | Parse.Cloud.useMasterKey(); 167 | var highScore = request.object; 168 | var user = request.user; 169 | var valid = ...; //implement your validation logic here 170 | if (!valid) { 171 | response.error("Can not validate submtted score!"); 172 | } 173 | }); 174 | ``` 175 | 176 | Your validation options could be to investigate the details object (`highScore.get("scoreDetails")`) and enforce a correlation between values of the details object and the submitted score value. Details can be passed in from the client using the `updateScoreDetails` callback. 177 | 178 | Another option is to look for the former score state `highScore.get("lastScore")` subject to the current score `highScore.get("score")`. 179 | 180 | A third option could be to correlate the submitted score `highScore.get("score")-highScore.get("lastScore")` to the users time played `user.get("timePlayed")`. 181 | 182 | After modifications of the `main.js` file you have to upload it to the cloud using `parse deploy` command. 183 | 184 | 185 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parse-highscore-service", 3 | "homepage": "https://github.com/peer2p/parse-highscore-service", 4 | "version": "1.0", 5 | "authors": [ 6 | "daniel.peer@gmx.net" 7 | ], 8 | "description": "Highscore and leaderboard service based on parse.com", 9 | "main": "HighScoreService.js", 10 | "moduleType": [ 11 | "globals" 12 | ], 13 | "ignore": [ 14 | "test.html", 15 | "cloud", 16 | "bower_components" 17 | ], 18 | "keywords": [ 19 | "game", 20 | "highscore", 21 | "leaderboard", 22 | "achievements" 23 | ], 24 | "dependencies": { 25 | "parse-sdk": "~1.4.2" 26 | }, 27 | "license": "MIT" 28 | } 29 | -------------------------------------------------------------------------------- /cloud/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create the ranking for high scores after some score has been saved 3 | */ 4 | Parse.Cloud.afterSave("HighScore", function(request) { 5 | Parse.Cloud.useMasterKey(); 6 | var query = new Parse.Query("HighScore"); 7 | query.descending("score"); 8 | query.find({useMasterKey : true}).then(function(results) { 9 | for (var i = 0; i < results.length; ++i) { 10 | if (results[i].get("rank") != (i+1)){ 11 | results[i].set("rank", i+1); 12 | results[i].save(); 13 | } 14 | } 15 | console.log("high score ranking successfully created from "+results.length+" highscores"); 16 | }); 17 | }); -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | parse.com highscore service 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 31 | 32 | 33 | 34 | 35 | 44 | 45 |
46 | 47 | 48 | 49 | 50 |
51 |
52 |

Application

53 |
54 |
55 |
56 |
57 | 58 |
59 |
60 | 61 |
62 |
63 |
64 |
65 | 66 | 67 |
68 |
69 |

Player

70 |
71 |
72 |
73 |
74 | 75 |
76 |
77 | 78 |
79 |
80 | 81 |
82 |
83 | 84 |
85 |
86 |
87 |
88 | 89 | 90 | 137 | 138 | 139 |
140 |
141 |

Player Stats

142 |
143 |
144 |
145 |
146 | 147 | 148 |
149 |
150 | 151 | 152 |
153 |
154 | 155 | 156 |
157 |
158 | 159 | 160 |
161 |
162 |
163 |
164 | 165 | 166 |
167 |
168 |

Results

169 |
170 |
171 | 172 | 173 |
174 |
175 |
176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 |
LeaderBoard
#NameScore
188 |
189 |
190 |
191 | 192 | 193 |
194 |
195 |
196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 |
Achievements
NameDescriptionScoreDetails
209 |
210 |
211 |
212 | 213 | 214 |
215 |
216 | 217 | 218 |
219 |
220 | 221 | 222 |
223 |
224 | 225 |
226 |
227 |
228 | 229 | 338 | 339 | --------------------------------------------------------------------------------