Implementing the High Score table for My Champion

Image

This post explains how to implement the leaderboard I used for my game My Champion. At the time, I was looking for a premade leaderboard that would be available on any portals. I first looked at the Mochi leaderboard but that one seem to be too resource intensive. So instead, I decided to implement my own. Since I didn’t want to support a server on my end, I had to find a free service for the backend. I ended up using Gamejolt’s API because it had a few features I was looking for, such as the ability to display a score that is not just a numeric value (like 1-0, 3-4…), and an “extra” field for putting whatever information I want.

A few interesting features of the resulting leaderboard are:
– Shows the flag of the player’s country, linking to a google search combining that country and the world “football”.
– Showing the profile picture if the player is logged into one of the major portals
– Linking to the player’s profile on that portal
– Showing an icon of the portal through which the game is played

1. Gamejolt’s Score API

Gamejolt uses a REST api which consist of simply sending HTTP requests to update a score or read the score table. The request is protected by a MD5 signature with a secret key as the salt. You can find the doc for the API here, but the following explains the steps to use the API in a Flash game.

– First, you have to create your game on Gamejolt. Sign-up to the website or log in using your account, then click on [Developer] tab > [Upload a game]. You don’t need to have a game ready at this point. Simply fill up the information about the game and mark it as “in progress”. It won’t be public.

– The game can later be accessed from your Developer dashboard. Next you have to create the leaderboard. Click on it, and go to the tab [Achievement] > [High Scores] > [Add a High Score table]. Set “Guest Scoring” to enabled. I would also set “Unique Scores” to enabled so that it shows only one score per player.

– On the [Achievement] page, you will also note the section on the right that shows your GameID and private key:
Screen Shot 2014-02-12 at 1.59.00 PM

Write down the Game ID and private key, you will need those to call the API. Also write down the table ID of the leaderboard you just created.

– You will now test the API by calling the “fetch score” request, documented here. All API’s on Gamejolt use the same signature system documented here. The following AS3 code will retrieve the list of scores as an array:

var url:String = "http://gamejolt.com/api/game/v1/scores/?game_id="+GAME_ID;
url += "&format=json";
url += "&limit=100";
url += "&table_id="+TABLE_ID;
url += "&time="+new Date().time;
url += "&signature="+MD5.hash(url + PRIVATE_KEY);
var urlloader:URLLoader = new URLLoader();
urlloader.addEventListener(IOErrorEvent.IO_ERROR,onError);
urlloader.addEventListener(Event.COMPLETE,
   function(e:Event):void {
      var obj:Object = JSON.parse(urlloader.data);
      var scores:Array = obj.response.scores;
      updateScores(scores);
   });

Note that we don’t pass the private key, instead we produce an MD5 that combines the url request and uses the private key as salt, and pass the result as a signature. To create an MD5, use one of the available AS3 library for doing encryption. My favorite is http://www.blooddy.by/en/crypto/.

– The next step is to implement the request for adding a score. The documentation is here. The following AS3 code will add a score.

var params:URLVariables = new URLVariables();
params.game_id = GAME_ID;
params.score = getScoreDisplay();
params.sort = getScoreValue();
params.table_id = TABLE_ID;
var extraData:URLVariables = new URLVariables();
extraData.country_name = getCountryName();
extraData.avatar = getAvatar();
extraData.profile = getProfile();
params.extra_data = extraData.toString();
if(!isGuest()) {
    params.username = getGamejoltUserName();
    params.user_token = getGamejoltToken();
}
else {
    params.guest = getPlayerName() +" ("+ getHostName() +")";
}
var url:String = "http://gamejolt.com/api/game/v1/scores/add/?"+params;
url += "&signature=" + MD5.hash(url + PRIVATE_KEY);
var urlloader:URLLoader = new URLLoader();
urlloader.addEventListener(IOErrorEvent.IO_ERROR,onError);
urlloader.addEventListener(Event.COMPLETE,
   function(e:Event):void {
       var obj:Object = JSON.parse(urlloader.data);
       trace(obj.response.success ? "Score entered." : "Score not entered." );
   });
urlloader.load(new URLRequest(url));

For preliminary testing, I advise you to hardcode all the highlighted functions. You can always delete the score entries. In the next section, I’m going to explain the implementation of each of those functions.

– The highlighted functions in the previous section  retrieve different information regarding the user.

function getScoreDisplay():String {
  // this is how the score should be displayed. ex: 1-0, 3-1 or something
  // like 5 coins collected, 10 min... This will depend on your game
  return heroScore + "-" + foeScore; // This will show something like 1-0
}

function getScoreValue():uint {
  // this is a positive non/zero integer that will determine how the scores
  // will be sorted. For My Champion, I wanted the highest score difference
  // to show first, but I needed to make sure the value is positive. I also
  // have a special algorithm to distinguish the same score differences
  // depending on the number of goals, but I won't get into details for that.
  return heroScore-foeScore + 100; // Make sure to return a positive integer.
}

function getCountryName():String {
  // here, you return the country name, like France, United States, Korea...
  // The country is determined at the beginning of the game, so here you will
  // simply return the public global variable that stores it
  return Game.country_name;
}

function getAvatar():String {
  // this stores the user's profile picture URL. This gets calculated at the
  // beginning of the game, depending on the user's portal. Simply return the
  // global variable for it
  return Game.avatar;
}

function getProfile():String {
  // again, the link to the profile is calculated at the beginning. Here you
  // return the global variable
  return Game.profile;
}

function isGuest():Boolean {
  // this only applies to Gamejolt. On other portals, everyone is a guest
  return !getGamejoltUsername() && !getGameJoltToken();
}

function getGamejoltUsername():String {
  return root.loaderInfo.parameters.gjapi_username;
}

function getGamejoltToken():String {
  return root.loaderInfo.parameters.gjapi_token;
}

function getPlayerName():String {
  // this is the player's name or username. For My Champion, I try to detect the
  // username depending on the portal. If the detection is not successful, I
  // let the user type their name. This is an example for some portals:
  if(getHostName()=="gamejolt.com") return getGamejoltUsername();
  if(getHostName()=="newgrounds.com") return com.newgrounds.API.username;
  if(getHostName()=="kongregate.com") return root.loaderInfo.parameters.kongregate_username;
}

function getHostName():String {
  // to determine the hostname, I would try to look at differences between
  // hosts in terms of parameters. The lazy approach, which is what I used and
  // it's really not the best way, is to look at the site is hosted
  var swfPath:String = root.loaderInfo.url;
  var hostName:String =  swfPath.split("?")[0].split("#")[0].split("://")[1].split("/")[0].toLowerCase();
  if(hostName.indexOf("gamejolt")>=0) hostName="gamejolt.com";
  if(hostName.indexOf("mochiads")>=0) hostName="mochimedia.com";
  if(hostName.indexOf("ungrounded")>=0) hostName="newgrounds.com";
  if(hostName.indexOf("kongregate")>=0) hostName="kongregate.com";
  return hostName;
}

// the reason it is not the best approach is because the hosting location of the
// swf could possibly change if the host decides to, in which case your 
// highscore system is broken. This works for now.

So far, we’re able to send and retrieve the scores via Gamejolt’s API. However, there are still a few unknown parameters. In the next section, we’ll be getting the country name using ip-api.com.

2. Getting the country name

We use a free IP geolocation service to get the country name. As you are using their free service, make sure to mention their website in the credit. The call to get the country name is as follow:

var urlloader:URLLoader = new URLLoader();
urlloader.addEventListener(Event.COMPLETE,
   function(e:Event):void {
      var obj:Object = JSON.parse(urlloader.data);
      Game.country_name = obj.country;
   });
urlloader.load(new URLRequest("http://ip-api.com/json/?fields=1"));

For the API call, I could have also used http://ip-api.com/json/?fields=country. I used fields=1 to make the call a tiny bit shorter. If you look at the doc, you’ll realize that you can get a bit more information than just the country (like the city, ect.). I do advise to simply stick with the country if you don’t want to run into privacy issues. That’s really all you need.

3. Getting the avatar and profile

The code for getting the avatar picture and profile depends on the portal. Most of the code will be asynchronous, so it’s better to run it once in the beginning and store the value in global variables. Below are the implementation for some of those portals:

function fetchNewgroundsProfile():void {
   API.addEventListener(APIEvent.API_CONNECTED,
      function(e:APIEvent):void {
          if(API.userId) {
             Game.avatar = "http://uimg.ngfiles.com/icons/"+API.userId.toString().substr(0,4)+"/"+API.userId+"_small.jpg";
             Game.profile = "http://"+API.username+".newgrounds.com";
          }
      });
}
function fetchKongregateProfile():void {
   var username:String = root.loaderInfo.parameters.kongregate_username;
   var urlloader:URLLoader = new URLLoader();
   var url:String = "http://api.kongregate.com/api/user_info.json?username="+username;
   urlloader.addEventListener(Event.COMPLETE,
      function(e:Event):void {
          var obj:Object = JSON.parse(urlloader.data);
          if(obj.user_vars) {
             Game.avatar = obj.user_vars.chat_avatar_url;
             Game.profile = "http://www.kongregate.com/accounts/"+username;
          }
      });
   urlloader.load(new URLRequest(url));
}

function fetchGamejoltProfile():void {
   var username:String = getGamejoltUsername();
   var urlloader:URLLoader = new URLLoader();
   var url:String = "http://gamejolt.com/api/game/v1/users/?username="+username
             +"&game_id="+GAMEJOLT_GAME_ID+"&format=json";
   url += "&signature="+MD5.hash(url+GAMEJOLT_PRIVATE_KEY);
   urlloader.addEventListener(Event.COMPLETE,
       function(e:Event):void {
          var obj:Object = JSON.parse(urlloader.data);
          if(obj.response.success) {
             Game.avatar = obj.response.users[0].avatar_url;
             Game.profile = "http://gamejolt.com/profile/"+username+"/"+obj.response.users[0].id;
          }
	});
   urlloader.load(new URLRequest(url));
}

function fetchMochiProfile():void {
   MochiSocial.addEventListener(MochiSocial.LOGGED_IN, 
      function (event:Object):void {
         Game.profile = "http://www.mochigames.com/"+event.name;
         Game.avatar = event.profileImgURL;
      });
}

4. Display the results

Going back to the fetch code, we had the following function

      var scores:Array = obj.response.scores;
      updateScores(scores);

The updateScores function will display the array of scores in a nice fashion. The array should be 100 at most, so the display should be paginated to show only 10 scores at the time.

// each ScoreEntry is a UI element manually placed into a MovieClip.
var entries:Array = [];
for(var i:int=0;i<numChildren;i++) {
    var child:ScoreEntry = getChildAt(i) as ScoreEntry;
    if(child) {
       entries.push(child);
    }
}
entries.sortOn("y",Array.NUMERIC);
for(i=0;i<entries.length;i++) {
    var scoreObj:Object = scores[i+page*10];
    addEntry(entries[i],i,page,scoreObj);
}

Screen Shot 2014-02-02 at 4.43.16 PM

Each entry is composed of several elements: country flag, rank, profile picture, name, score and portal. The scoreObj object has all the information needed to display all of it.

To retrieve the extra_data from the scoreObj, use the following code:

if(scoreObj.extra_data) {
   var extra_data:URLVariables = new URLVariables(scoreObj.extra_data);
   country = extra_data.country;
   avatar = extra_data.avatar;
   profile = extra_data.profile;
   ...
}

5. Country Flag 

The image of the country flag is fetched from Wikipedia. The location of a flag  looks like this:

French Flag   http://upload.wikimedia.org/wikipedia/en/thumb/c/c3/Flag_of_France.svg/25px-Flag_of_France.svg.png

The location is pretty standard. The part that is hard to guess is the 3 letters after thumb (c/c3). To calculate that, you take the first two letters of the MD5 hash of the filename. The AS3 code is as follow:

var filename:String = "Flag_of_"+country_name.split(",")[0]
      .split(" ").join("_")+".svg";
var md5:String = MD5.hash(filename);
var path:String = "http://upload.wikimedia.org/wikipedia/commons/thumb/"
      +md5.charAt(0)+"/"+md5.substr(0,2) 
      +"/"+filename+"/"+WIDTH+"px-"+filename+".png";
loader.load(new URLRequest(path));

6. Ranking 

The ranking is calculated depending on the page looked at and the index of the ScoreEntry.

rank = 1+i+page*10

7. Profile 

The profile picture should have been stored within the scoreObj.extra_data object along with the link to the profile. Simply display the image and link to profile using the “avatar” and “profile” variables.

8. Score 

To show the score, use the scoreObj.score property.

9. Username and Portal

The portal is actually stored within the guest name (or if the user is not a guest, you know the portal is Gamejolt.com).

if(scoreObj.user && scoreObj.user.length) {
    playerName = scoreObj.user;
    portal = "gamejolt.com";
}
else if(scoreObj.guest) {
    portal = scoreObj.guest.split(" (")[1].split(")")[0];
    playerName = decodeURIComponent(scoreObj.guest.split(" (")[0]);
}

Once you get the portal, you can retrieve its image using Google’s API for turning domain favicon into a png: https://www.google.com/s2/favicons?domain=

ex: dobuki.com https://www.google.com/s2/favicons?domain=dobuki.com

For known portals like newgrounds.com, gamejolt.com, kongregate.com and mochigames.com I advise you to display a custom image rather than fetching them from Google’s API.

This ends the tutorial on implementing the high score system used in My Champion. I know it’s a bit technical. My goal is that you, as a developer, should be able to pick bits and pieces of this tutorial to implement the needed components for your own leaderboard. I will be posting an open source library shortly so that game developers can just import it into the code and easily without knowing all the details.

Good luck on developing your next game!

Advertisements

About jacklehamster

Technocraft and internet developer

Posted on February 3, 2014, in Flash, Game Development, Making Games and tagged , , , , , , , , , , , . Bookmark the permalink. 8 Comments.

  1. The beauty of the method is it is so accurate that you will find you have a lot of free time.
    You want a system that allows you to lose at
    least half your trades and still break even or make a small profit.
    If you want to make money in Forex, avoid people telling you that you can
    get rich with no effort and do what all successful Forex traders do and that’s learn skills.

  2. Your way of explaining everything in this piece of writing is truly fastidious, all
    can easily be aware of it, Thanks a lot.

  3. If some one needs to be updated with most recent
    technologies therefore he must be visit this web page and be up to date every day.

  4. Good day! I know this is kind of off topic but I was wondering which blog platform are you
    using for this site? I’m getting tired of WordPress because I’ve
    had issues with hackers and I’m looking at options for another platform.

    I would be awesome if you could point me in the direction of a good platform.

  5. Excellent goods from you, man. I’ve understand your stuff previous to and you’re just too wonderful.
    I actually like what you’ve acquired here, really like what you are saying
    and the way in which you say it. You make it entertaining and you
    still take care of to keep it sensible. I can not wait to read
    much more from you. This is actually a tremendous website.

  6. Many wise buyers often use their local bike shops to
    try out the bikes before purchasing. You are not going to find sportbike boots at the
    “Harley Dealer”. Tea hats come in white color with a ribbon in black on the ladies
    hats itself.

  7. It’s awesome in support of me to have a website, which is good
    designed for my knowledge. thanks admin

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: