├── .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 |