March 10th, 2010

Global High Score Lists

Games have had high score lists for a very long time. In fact, for many past games high score lists were the main reason to keep playing. While this idea worked well for arcade games where strangers could compete asynchronously against each-other it didn’t initially translate to console and other single player games. Clearly, it’s not as much fun to beat your past high score as it is to beat someone else’s. That’s where the idea of a global high score list comes in; The high score list lives on a server instead of next to the game code. This is a simple way to re-ignite competition between players and extend a game’s replay value.

Obviously, placing the high score list on a server introduces some new complications. Where before you could simply read and write to a local file you now must perform some sort of web service call! Don’t worry, while implementing a client-server model in your next game may sound challenging it’s really straightforward. I’ll show you how below.

Server

First let’s look at the server side of the equation. I’m going to implement a very simple system which spans two web pages and operates solely via HTTP. I’m going to assume the following:

  1. You have access to a running server (can be local)
  2. You are able (or willing to learn) to read PHP

Don’t worry the code is incredibly straightforward in this example, so it should be easy to understand if you are familiar with Java. To start, create a file post.php. Your game will call this page when trying to save a score. It should return “success” when it writes the score and “failure” when it doesn’t.

<?php
/** Read in a username and score and save them to a file. */
// read variables from POST data.
$username = @$_POST['name'];
$score = @$_POST['score'];

// verify the username is set and not empty
if (!isset($username) || $username == "") {
    echo "failure";
    exit;
}

// verify the score is set and a number
if (!isset($score) || !is_numeric($score)) {
    echo "failure";
    exit;
}

// format the username and score as a comma delimited row
$entry = $username . "," . $score . "\n";

// append entry to the score file
if (!file_put_contents("scores.csv", $entry, FILE_APPEND)) {
    echo "failure"; // failed to write to file
    exit;
}

echo "success";
?>

See, I told you it was straightforward. As you can see we’re saving everything in a comma delimited file. If you anticipate a massive number of scores then it might make more sense to refactor the above code to use a database. To register a high score your game will call http://server/post.php and pass name=foobar&score=1234 as POST data.

Next let’s create a page to retrieve a list of high scores. Start by creating board.php. This should return some number of top scores. Since CSV is a pretty neutral format we’ll keep using it here.

<?php
/** Return a list of top scores. */
// read variables from GET data
$num_scores = $_GET['num_scores'];

// read each line in scores.csv as a string into an array
$scores = file("scores.csv");

// define a comparator to sort items by score
function compare($s1, $s2) {
    // split the strings by their delimiter
    $a1 = explode(",", $s1);
    $a2 = explode(",", $s2);
    // compare the scores
    return $a2[1] - $a1[1];
}

// sort the array of scores
usort($scores, "compare");

// output the requested number of top scores
for ($i = 0; $i < $num_scores && $i < count($scores); $i++) {
    echo $scores[$i];
}
?>

So your game will call http://server/board.php?num_scores=10 to retrieve the top ten high scores. The server will respond with a CSV file containing each player-score pair. That’s it! Now that the server is implemented let’s turn to the client (read game).

Client

Fortunately, things are even easier on the game side. Since we’ve been leveraging HTTP we can simply use Java’s built in URL class! Here’s an example call to post.php.

public static boolean post(String name, int score) {
    // target URL to post to
    String target = "http://server/post.php";
    // data to write to the server
    String content = "name=" + URLEncoder.encode(name, "US-ASCII");
    content += "&score=" + URLEncoder.encode(score, "US-ASCII");
    // open connection to write
    URL url = new URL(target);
    URLConnection c = url.openConnection();
    c.setConnectionTimeout(1000); // timeout after 1 second
    c.setDoOutput(true); // we're going to write to the server
    DataOutputStream o = new DataOutputStream(c.getOutputStream());
    // write the content
    o.writeBytes(content);
    o.flush();
    o.close();
    // read response and check for success
    DataInputStream i = new DataInputStream(c.getInputStream());
    String response = i.readLine();
    return response.equals("success");
}

Alright, so Java doesn’t win any points for conciseness. Still I think this is pretty straightforward. Notice that I use URLEncoder to encode the parameters I’m sending to the server. This is a necessary step if you intend to allow special characters to be used in, say, the user’s name. For example, if you want to support names such as “John Doe”.

Next let’s read the scores back in from the server. This is simple after the last example:

public static String[] fetch(int num) {
    // target URL to post to
    String target = "http://server/board.php";
    String target += "?num_scores=" + URLEncoder.encode(num, "US_ASCII");
    // open connection to read 
    URL url = new URL(target);
    URLConnection c = url.openConnection();
    c.setConnectionTimeout(1000); // timeout after 1 second
    DataInputStream i = new DataInputStream(c.getInputStream());
    // read lines into an array and return
    LinkedList<String> lines = new LinkedList<String>();
    String line;
    while ((line = i.readLine()) != null) {
        lines.add(line);
    }
    return lines.toArray(new String[0]);
}

See, easy. Notice that this time we’re performing a GET request meaning that we pass num_scores in through the url. Also note that I do not transform the data from CSV format to something suitable for printing on-screen. I’ll leave that as an exercise for you!

Conclusion

That’s that, you now have no good reason not to have a global high score list in your next game. If you do end up using this implementation let me know; I’m especially interested in what limitations you’ve found. Before you run off and start making the next killer shooter, though, let me leave you with one warning.

There is no security in this approach

You should worry that someone will enter in fake information or spam your server. Right now my approach doesn’t have any sort of security in place to prevent that. I would start by adding a password of some sort to prevent people from calling post.php. Of course, I don’t believe it’s possible to ship said password with your game code without compromising it, but at least you’ll stop most people. If there’s interest I can make a second post on this topic to look at different ways to combat spam and cheaters.

Found this post interesting? Consider sharing it:
  • Facebook
  • Digg
  • Reddit
  • StumbleUpon
  • del.icio.us
  • Twitter

comments

  • Hey, not a problem I'm glad we're covering this topic, and I'm sure this is a common problem.


    You're right that switching to GET data will allow you to enter data through a URL, but I would advice you to keep it as POST data which has certain properties which fit well with the action you're trying to accomplish. The only downside is you can't simply type in the URL and hit enter to see the thing take effect. You'll either need to write a form or write code which posts back to the server. If you are using PHP it's as simple as calling file_put_contents.

  • systat

    Hehe, sorry for spamming your website but I fixed it, you need to use


    @$GET instead of @$POST in your post.php script.

  • Systat, to answer your first question, that won't work because you're passing in GET data. You need to pass POST data to post.php in order for the values to be set. See this page for an explanation of the two.


    As for your second problem, I believe that though you were getting "success" from post.php you were actually only inserting empty values. Please try correcting your usage for post.php and then board.php should work.


    Hope that helps, let me know if it still isn't working.

  • systat

    I commented first 2 errors.
    Now I get succes with post.php
    but When I go to board.php?num_scores=10
    I only get this
    , , , , ,

  • systat

    Hey, when I run post.php?name=something&score=1234


    I always get the first error


    // verify the username is set and not empty
    if (!isset($username) || $username == "") {
    echo "failure0";
    exit;
    }


    Do you know what is causing it?

  • aleix
    Great! But I get this error

    Parse error: syntax error, unexpected T_VARIABLE, expecting ';' in board.php on line 22
  • Sorry about that, there are some minor errors with the script. I’ve updated things now such that they should work. Thanks for bringing this to my attention! Also if there is interest I can post a new method for doing global high score lists which allows you to:

    * Read/Write to a server
    * Read/Write locally
    * Cache scores offline when no internet is available
    * Push local scores to the server when internet is available

    Let me know and I’ll prepare things and publish them.
blog comments powered by Disqus